Tweet sentiment analysis using Salesforce Einstein Language
Sentiment Analysis of a text as we know is the interpretation and classification of emotions (as positive, negative and neutral). In this article we see how Einstein Language API provides out-of-the-box and pre-built model (Community Sentiment Model) that can be leveraged to identify the tone of any text by simply making an API call. We will see the simple steps required to setup Einstein Language and a sample code to analyze the sentiment of a text.
Why Perform Sentiment Analysis?
Sentiment analysis allows businesses to identify customer sentiment towards products, brands and/or services in online conversations and feedback. For a use case and demo of sentiment analysis read this article on Twitter Salesforce Integration - where we identify the tone of the customer tweet to set the priority of the Case being created.
Community Sentiment Model
Einstein Language offers a pre-built sentiment model that you can use as long as you have a valid JWT token (coming up next are the class details to generate a JWT token). This model has three classes and was created from data that comes from multiple sources. The data is short snippets of text, about one or two sentences, and similar to what you would find in a public community or Chatter group, a review/feedback forum, or enterprise social media.
- positive
- negative
- neutral
Use the community sentiment model to classify text without building your own custom model.
curl -X POST -H "Authorization: Bearer " -H "Cache-Control: no-cache" -H "Content-Type: multipart/form-data" -F "modelId=CommunitySentiment" -F "document=the presentation was great and I learned a lot" https://api.einstein.ai/v2/language/sentiment
The above sample cURL command demonstrates how we can send in a text string to the API that returns a prediction from the model. You call the pre-built model the same way you call a custom model, but instead of passing in your own modelId
, you pass in a modelId
of CommunitySentiment
The model returns a result similar to the following JSON with probability for each label. Based on the highest probability you can identify under which class/label the sentiment of the text falls.
{
"probabilities": [
{
"label": "positive",
"probability": 0.77945083
},
{
"label": "negative",
"probability": 0.18806243
},
{
"label": "neutral",
"probability": 0.032486767
}
]
}
Steps to setup and start with Einstein Language
We will need to carry out the following steps before we move forward to the actual implementation. These steps will help us to sign up for a Salesforce Einstein account and also create Apex Classes that are required for token generation.
Step-1 Get an Einstein Platform Services Account
- Navigate to the sign-up page for Einstein API Account.
Be sure you’re logged out of Salesforce before you go through the steps to get an account. That way, you can be sure you’re giving access to the right Salesforce org when you sign up.
- Click Sign Up Using Salesforce.
- On the Salesforce login page, type your Salesforce Org username and password and click Log In.
- Click Allow so the page can access basic information, such as your email address, and perform requests.
- On the activation page, click Download Key to save the key locally. The key file is named
einstein_platform.pem
. If you can’t download it, cut and paste your key from the browser into a text file and save it aseinstein_platform.pem
.
Step-2 Upload the Key obtained in Step 1 into Salesforce Content
Follow this link to know more details – https://metamind.readme.io/docs/upload-your-key
Step 3 Apex Classes for token generation
Below two classes are required for generating the token that will be utilized for calling the Einstein Language API
JWT.cls
public class JWT {
public String alg {get;set;}
public String iss {get;set;}
public String sub {get;set;}
public String aud {get;set;}
public String exp {get;set;}
public String iat {get;set;}
public Map<String,String> claims {get;set;}
public Integer validFor {get;set;}
public String cert {get;set;}
public String pkcs8 {get;set;}
public String privateKey {get;set;}
public static final String HS256 = 'HS256';
public static final String RS256 = 'RS256';
public static final String NONE = 'none';
public JWT(String alg) {
this.alg = alg;
this.validFor = 300;
}
public String issue() {
String jwt = '';
JSONGenerator header = JSON.createGenerator(false);
header.writeStartObject();
header.writeStringField('alg', this.alg);
header.writeEndObject();
String encodedHeader = base64URLencode(Blob.valueOf(header.getAsString()));
JSONGenerator body = JSON.createGenerator(false);
body.writeStartObject();
body.writeStringField('iss', this.iss);
body.writeStringField('sub', this.sub);
body.writeStringField('aud', this.aud);
Long rightNow = (dateTime.now().getTime()/1000)+1;
body.writeNumberField('iat', rightNow);
body.writeNumberField('exp', (rightNow + validFor));
if (claims != null) {
for (String claim : claims.keySet()) {
body.writeStringField(claim, claims.get(claim));
}
}
body.writeEndObject();
jwt = encodedHeader + '.' + base64URLencode(Blob.valueOf(body.getAsString()));
if ( this.alg == HS256 ) {
Blob key = EncodingUtil.base64Decode(privateKey);
Blob signature = Crypto.generateMac('hmacSHA256',Blob.valueof(jwt),key);
jwt += '.' + base64URLencode(signature);
} else if ( this.alg == RS256 ) {
Blob signature = null;
if (cert != null ) {
signature = Crypto.signWithCertificate('rsa-sha256', Blob.valueOf(jwt), cert);
} else {
Blob privateKey = EncodingUtil.base64Decode(pkcs8);
signature = Crypto.sign('rsa-sha256', Blob.valueOf(jwt), privateKey);
}
jwt += '.' + base64URLencode(signature);
} else if ( this.alg == NONE ) {
jwt += '.';
}
return jwt;
}
public String base64URLencode(Blob input){
String output = encodingUtil.base64Encode(input);
output = output.replace('+', '-');
output = output.replace('/', '_');
while ( output.endsWith('=')){
output = output.subString(0,output.length()-1);
}
return output;
}
}
JWTBearerFlow.cls
public class JWTBearerFlow {
public static String getAccessToken(String tokenEndpoint, JWT jwt) {
String access_token = null;
String body = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=' + jwt.issue();
HttpRequest req = new HttpRequest();
req.setMethod('POST');
req.setEndpoint(tokenEndpoint);
req.setHeader('Content-type', 'application/x-www-form-urlencoded');
req.setBody(body);
Http http = new Http();
HTTPResponse res = http.send(req);
if ( res.getStatusCode() == 200 ) {
System.JSONParser parser = System.JSON.createParser(res.getBody());
while (parser.nextToken() != null) {
if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) && (parser.getText() == 'access_token')) {
parser.nextToken();
access_token = parser.getText();
break;
}
}
}
return access_token;
}
}
Make sure you create a Remote Site Setting for Einstein API following the steps mentioned on the given link https://metamind.readme.io/docs/apex-qs-create-remote-site
The Code
Calling the Einstein Language API is a two step process which is encapsulated in the following classes that in-turn utilizes JWT
and JWTBearerFLow
classes mentioned above. The final call would simply be as below:
String sentimentAnalysisResponse =EinsteinAPI.findSentiment(<your text>);
EinsteinAPI - Calling class
/**
* This class is created to make requests to
* various Einstein Endpoints.
*
* @author Mahesh Peruri
* @since 03/29/2020
**/
public class EinsteinAPI {
public static String tokenEndpoint {
get {
Einstein_API_Settings__c settings = Einstein_API_Settings__c.getInstance( UserInfo.getOrganizationId() );
return settings.Token_Endpoint__c;
}
}
public static Decimal tokenExpirationSeconds {
get {
Einstein_API_Settings__c settings = Einstein_API_Settings__c.getInstance( UserInfo.getOrganizationId() );
return settings.Token_Expiration_Seconds__c;
}
}
public static String registeredEmail {
get {
Einstein_API_Settings__c settings = Einstein_API_Settings__c.getInstance( UserInfo.getOrganizationId() );
return settings.Registered_Email__c;
}
}
public static String sentimentEndpoint {
get {
Einstein_API_Settings__c settings = Einstein_API_Settings__c.getInstance( UserInfo.getOrganizationId() );
return settings.Sentiment_Endpoint__c;
}
}
public static String sentimentModelId {
get {
Einstein_API_Settings__c settings = Einstein_API_Settings__c.getInstance( UserInfo.getOrganizationId() );
return settings.Sentiment_Model_Id__c;
}
}
/**
* This method is created to make a call
* to the Token Endpoint and get the token
* which will help us to make request to
* other Endpoints of Einstein Services.
*
* @return String Returns the access token of the Org
*/
public Static String getAccessToken() {
ContentVersion base64Content = [
SELECT Title
,VersionData
FROM ContentVersion
WHERE Title = 'einstein_platform'
OR Title = 'predictive_services'
ORDER BY Title
LIMIT 1
];
String keyContents = base64Content.VersionData.tostring();
keyContents = keyContents.replace( '-----BEGIN RSA PRIVATE KEY-----', '' );
keyContents = keyContents.replace( '-----END RSA PRIVATE KEY-----', '' );
keyContents = keyContents.replace( '\n', '' );
JWT jwt = new JWT( 'RS256' );
jwt.pkcs8 = keyContents;
jwt.iss = 'developer.force.com';
jwt.sub = registeredEmail;
jwt.aud = tokenEndpoint;
jwt.exp = String.valueOf( tokenExpirationSeconds );
String access_token = JWTBearerFlow.getAccessToken( tokenEndpoint, jwt );
system.debug('access_token'+access_token);
return access_token;
}
/**
* This method is created to make call
* to the Sentiment Endpoint and get
* the Sentiment of the block of text.
*
* @param text Block of text whose Sentiment has to be analysed
*
* @return SentimentAnalysisResponse Returns an instance of SentimentAnalysisResponse class
*/
public static String findSentiment( String text ) {
String key = getAccessToken();
//system.debug('key'+key);
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setMethod( 'POST' );
req.setEndpoint( sentimentEndpoint );
req.setHeader( 'Authorization', 'Bearer ' + key );
req.setHeader( 'Content-type', 'application/json' );
String body = '{\"modelId\":\"'+ sentimentModelId + '\",\"document\":\"' + text + '\"}';
req.setBody( body );
HTTPResponse res = http.send( req );
SentimentAnalysisResponse resp = ( SentimentAnalysisResponse ) JSON.deserialize( res.getBody(), SentimentAnalysisResponse.class );
system.debug('res.getBody()'+res.getBody());
return res.getBody();
}
}
Wrapper class used for request serialization
public class EinsteinCalloutWrapper {
public String modelId;
public string document;
public EinsteinCalloutWrapper(String modelId, String desp){
this.modelId = modelId;
this.document = desp;
}
public class EinsteinCalloutResponseWrapper{
@AuraEnabled
public List<EinsteinCalloutProbablityWrapper> probabilities;
public String object_x;
}
public class EinsteinCalloutProbablityWrapper{
@AuraEnabled
public String label;
@AuraEnabled
public Decimal probability;
}
}
Wrapper class used for response deserialization
global class SentimentAnalysisResponse {
webservice List<Probabilities> probabilities { get; set; }
global class Probabilities {
webservice String label { get; set; }
webservice Double probability { get; set; }
}
}