hckr.fyi // thoughts

Sentiment Analysis Middleware for the Bot Framework

by Michael Szul on

Gary Pretty has been tearing it up when it comes to adding extensibility to the Bot Framework. The .NET repo for the Bot Builder Community has been thriving, while the Node.JS repo has lagged a bit behind--mostly due to my own busy schedule, and moving houses. Well it languishes no more (at least for this week). I had a fit of inspiration last night that carried into today, and I ported over the sentiment analysis middleware, so that there was a Node.JS package for it. Just an hour or so ago, I decided to rewrite and rename that package, so that it incorporated all of the Text Analytics API services into a single package, accessible from different classes.

You can now work with the Text Analytics API through Bot Framework middleware by installing the package and running with it:

npm install @botbuildercommunity/text-analytics-middleware --save
    

It was a good lesson in how simple it is to both build middleware, and also to work with the Text Analytics API, so I decided to write something up about how to implement sentiment analysis as middleware in the Bot Framework.

First you will need to install a couple of packages to handle the sentiment analysis:

npm install azure-cognitiveservices-textanalytics --save
    npm install ms-rest-azure --save
    

Now, create a file named sentiment.ts, and add the following import statements at the top:

import { Middleware, TurnContext, ActivityTypes } from "botbuilder";
    import { CognitiveServicesCredentials } from "ms-rest-azure";
    import { TextAnalyticsClient } from "azure-cognitiveservices-textanalytics";
    

We are going to create a class that implements the middleware interface from the Bot Framework, and we know that, in doing so, we have to implement an onTurn() asynchronous method that calls next() when it is finished:

export class SentimentAnalysis implments Middleware {
        ...
        public async onTurn(context: TurnContext, next: () => Promise<void>) {
            await next();
        }
    }
    

To use the Text Analytics API, we will need to have a service key to access the service, as well as an endpoint. We will have to add our credentials, and then create a text analytics client. We will do this by creating two public properties, and then pass some information into the class constructor.

export class SentimentAnalysis implements Middleware {
        public credentials: CognitiveServicesCredentials;
        public client: TextAnalyticsClient;
        constructor(public serviceKey: string, public endpoint: string, public options?: any) {
            this.credentials = new CognitiveServicesCredentials(serviceKey);
            this.client = new TextAnalyticsClient(this.credentials, endpoint, options);
        }
        public async onTurn(context: TurnContext, next: () => Promise<void>) {
            ...
            }
            await next();
        }
    }
    

In the above code, credentials is a public property that contains the Cognitive Services credentials. We passed CognitiveServicesCredentials() the service key that we obtained from the Text Analytics API registration (on Azure's Cognitive Services web site). Once we have credentials, we can then create our client with the TextAnalyticsClient() method. This accepts the credentials, the endpoint for the service, and an optional options object. This is a ServiceClientOptions object that we will not need for this example.

Now we just need to call the service and analyze the text, so we add the following code to the onTurn() method:

if(context.activity.type === "message") {
        const input = {
            documents: [
                {
                    "id": "1"
                    , "text": context.activity.text
                }
            ]
        };
        try {
            const result = await this.client.sentiment(input);
            const s = result.documents[0].score;
            context.turnState.set("sentimentScore", s);
        }
        catch(e) {
            throw new Error(`Failed to process sentiment on ${context.activity.text}. Error: ${e}`);
        }
    }
    

If the activity type is a message, we create an input object that contains an array of documents. Since we are only processing a single message, we only have a single document with a hardcoded ID. The text property of that document is the message that the user sent in.

Once we have this object, we can call the sentiment() method on the client, passing in the input constant. The result should contain a list of documents with a score associated with each document. Since we only sent one document in, we receive one document back. We then add a "sentimentScore" property to the turnState so that it is accessible in normal turn processing. We set that property to the sentiment score.

The final class looks like this:

export class SentimentAnalysis implements Middleware {
        public credentials: CognitiveServicesCredentials;
        public client: TextAnalyticsClient;
        constructor(public serviceKey: string, public endpoint: string, public options?: any) {
            this.credentials = new CognitiveServicesCredentials(serviceKey);
            this.client = new TextAnalyticsClient(this.credentials, endpoint, options);
        }
        public async onTurn(context: TurnContext, next: () => Promise<void>) {
            if(context.activity.type === ActivityTypes.Message) {
                const input = {
                    documents: [
                        {
                            "id": "1"
                            , "text": context.activity.text
                        }
                    ]
                };
                try {
                    const result = await this.client.sentiment(input);
                    const s = result.documents[0].score;
                    context.turnState.set("sentimentScore", s);
                }
                catch(e) {
                    throw new Error(`Failed to process sentiment on ${context.activity.text}. Error: ${e}`);
                }
            }
            await next();
        }
    }
    

You can then use this middleware in your chatbot by importing the SentimentAnalysis class:

import { SentimentAnalysis } from "./sentiment";
    

Then you need to attach it to your adapter:

adapter.use(new SentimentAnalysis(YOUR_TEXT_ANALYTICS_KEY, TEXT_ANALYTICS_API_ENDPOINT, SERVICE_CLIENT_OPTIONS));
    

Inside of your chatbot logic, when you need to check the sentiment of a message that the user sent, you can just access it via the turnState with context.turnState.get("sentimentScore").