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

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 as einstein_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; }
    }
}

You've successfully subscribed to inteygrate
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Success! Your account is fully activated, you now have access to all content.