Creating an Outlook Add-In with Sentiment Analysis

A few months ago, I was reading an email that had a snippy tone to it and several people didn’t take too kindly to it.  Despite the tone, I don’t believe that the person who composed the email intended it to come off that way.  It got me thinking that it would be cool to have an Outlook Add-In that would use the Sentiment Analysis api made available by the Azure Cognitive Services to score an email.  I finally got around to putting this together this weekend and this is a quick overview of the setup.

Setting up Sentiment Analysis in Azure

Start by logging into your Azure portal and searching for Cognitive Services

cognitive services search.PNG

On the cognitive services blade, click the Add button which will show the “AI + Machine Learning” blade where you can search for “Text Analytics”.

cognitive services - add.PNG

text analytics search.PNG

After you select Text Analytics, you can provide you service name, subscription, location, etc as shown below.

create form.PNG

Your new service will have some basic information for getting started.  The keys that your service will use, links to additional information and tutorials, etc.

text analytics - configure.PNG

There isn’t anything else to configure.  The keys needed are in the link under Step 1.  You can regenerate the keys if you need to but this is where you’ll get the keys that you need when you call the api.

step 1 - manage keys.PNG

Building the Outlook Add-In

For this demo, I’m using a circular progress bar sample by Anders Ingemann.  I start by creating a Outlook Add-In project in Visual Studio 2017.  I’ll show how I created an Add-In that is available in the composer and scores your email message.vs - outlook add-in.PNG

Similar to SharePoint Add-Ins, the Outlook Add-In creates 2 projects; one that creates a manifest and the other is a web application.

vs- project files.PNG

If you run the project without making any changes, you’ll notice that the Add-In is available when reading an email, but I want it to be visible on compose.  You configure this via the manifest file found in the first project.  Look for the following lines (I apologize for the code images.  Posting code was causing issues):

ReadXML.PNG

 

Replace the first line with:

composexml.PNG

Now, since this is just a demo that I was playing with, I didn’t bother to change the file names and I left the MessageRead.css, MessageRead.html, and MessageRead.js files as is.  If this were an actual project, I would probably create new files for Compose. Open MessageRead.html and find the div with id “content-main.”  This is where the main body of your Add-In is rendered from.  Using the sample from the link above with some minor tweaks, I came up with the following:

compose html.PNG

The main change is the div with the class “numbers”, where I removed the list of spans (1 for each available percentage) and replaced it with a span where I will pass in the score provided by the sentiment analysis results. I created a new styles.less file and added the following contents, straight from the link above with one minor change.  I removed the inset class which is no longer needed since I’m passing in the score.

<pre>/*@import&nbsp;url(http://fonts.googleapis.com/css?family=Lato:100,300,400,700,900,100italic,300italic,400italic,700italic,900italic);*/
 
.radial-progress&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;@circle-size:&nbsp;120px;
&nbsp;&nbsp;&nbsp;&nbsp;@circle-background:&nbsp;#d6dadc;
&nbsp;&nbsp;&nbsp;&nbsp;@circle-color:&nbsp;#97a71d;
&nbsp;&nbsp;&nbsp;&nbsp;@inset-size:&nbsp;90px;
&nbsp;&nbsp;&nbsp;&nbsp;@inset-color:&nbsp;#fbfbfb;
&nbsp;&nbsp;&nbsp;&nbsp;@transition-length:&nbsp;1s;
&nbsp;&nbsp;&nbsp;&nbsp;@shadow:&nbsp;6px&nbsp;6px&nbsp;10px&nbsp;rgba(0,0,0,0.2);
&nbsp;&nbsp;&nbsp;&nbsp;@percentage-color:&nbsp;#97a71d;
&nbsp;&nbsp;&nbsp;&nbsp;@percentage-font-size:&nbsp;22px;
&nbsp;&nbsp;&nbsp;&nbsp;@percentage-text-width:&nbsp;57px;
&nbsp;&nbsp;&nbsp;&nbsp;margin:&nbsp;50px;
&nbsp;&nbsp;&nbsp;&nbsp;width:&nbsp;@circle-size;
&nbsp;&nbsp;&nbsp;&nbsp;height:&nbsp;@circle-size;
&nbsp;&nbsp;&nbsp;&nbsp;background-color:&nbsp;@circle-background;
&nbsp;&nbsp;&nbsp;&nbsp;border-radius:&nbsp;50%;
&nbsp;&nbsp;&nbsp;&nbsp;.circle
 
{
&nbsp;&nbsp;&nbsp;&nbsp;.mask,&nbsp;.fill,&nbsp;.shadow
 
{
&nbsp;&nbsp;&nbsp;&nbsp;width:&nbsp;@circle-size;
&nbsp;&nbsp;&nbsp;&nbsp;height:&nbsp;@circle-size;
&nbsp;&nbsp;&nbsp;&nbsp;position:&nbsp;absolute;
&nbsp;&nbsp;&nbsp;&nbsp;border-radius:&nbsp;50%;
}
 
.shadow&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;box-shadow:&nbsp;@shadow&nbsp;inset;
}
 
.mask,&nbsp;.fill&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;-webkit-backface-visibility:&nbsp;hidden;
&nbsp;&nbsp;&nbsp;&nbsp;transition:&nbsp;-webkit-transform&nbsp;@transition-length;
&nbsp;&nbsp;&nbsp;&nbsp;transition:&nbsp;-ms-transform&nbsp;@transition-length;
&nbsp;&nbsp;&nbsp;&nbsp;transition:&nbsp;transform&nbsp;@transition-length;
&nbsp;&nbsp;&nbsp;&nbsp;border-radius:&nbsp;50%;
}
 
.mask&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;clip:&nbsp;rect(0px,&nbsp;@circle-size,&nbsp;@circle-size,&nbsp;@circle-size/2);
&nbsp;&nbsp;&nbsp;&nbsp;.fill
 
{
&nbsp;&nbsp;&nbsp;&nbsp;clip:&nbsp;rect(0px,&nbsp;@circle-size/2,&nbsp;@circle-size,&nbsp;0px);
&nbsp;&nbsp;&nbsp;&nbsp;background-color:&nbsp;@circle-color;
}
 
}
}
 
.inset&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;width:&nbsp;@inset-size;
&nbsp;&nbsp;&nbsp;&nbsp;height:&nbsp;@inset-size;
&nbsp;&nbsp;&nbsp;&nbsp;position:&nbsp;absolute;
&nbsp;&nbsp;&nbsp;&nbsp;margin-left:&nbsp;(@circle-size&nbsp;-&nbsp;@inset-size)/2;
&nbsp;&nbsp;&nbsp;&nbsp;margin-top:&nbsp;(@circle-size&nbsp;-&nbsp;@inset-size)/2;
&nbsp;&nbsp;&nbsp;&nbsp;background-color:&nbsp;@inset-color;
&nbsp;&nbsp;&nbsp;&nbsp;border-radius:&nbsp;50%;
&nbsp;&nbsp;&nbsp;&nbsp;box-shadow:&nbsp;@shadow;
&nbsp;&nbsp;&nbsp;&nbsp;.percentage
 
{
&nbsp;&nbsp;&nbsp;&nbsp;height:&nbsp;@percentage-font-size;
&nbsp;&nbsp;&nbsp;&nbsp;width:&nbsp;@percentage-text-width;
&nbsp;&nbsp;&nbsp;&nbsp;overflow:&nbsp;hidden;
&nbsp;&nbsp;&nbsp;&nbsp;position:&nbsp;absolute;
&nbsp;&nbsp;&nbsp;&nbsp;top:&nbsp;(@inset-size&nbsp;-&nbsp;@percentage-font-size)&nbsp;/&nbsp;2;
&nbsp;&nbsp;&nbsp;&nbsp;left:&nbsp;(@inset-size&nbsp;-&nbsp;@percentage-text-width)&nbsp;/&nbsp;2;
&nbsp;&nbsp;&nbsp;&nbsp;line-height:&nbsp;1;
&nbsp;&nbsp;&nbsp;&nbsp;.numbers
 
{
&nbsp;&nbsp;&nbsp;&nbsp;margin-top:&nbsp;-@percentage-font-size;
&nbsp;&nbsp;&nbsp;&nbsp;transition:&nbsp;width&nbsp;@transition-length;
&nbsp;&nbsp;&nbsp;&nbsp;span
 
{
&nbsp;&nbsp;&nbsp;&nbsp;width:&nbsp;@percentage-text-width;
&nbsp;&nbsp;&nbsp;&nbsp;display:&nbsp;inline-block;
&nbsp;&nbsp;&nbsp;&nbsp;vertical-align:&nbsp;top;
&nbsp;&nbsp;&nbsp;&nbsp;text-align:&nbsp;center;
&nbsp;&nbsp;&nbsp;&nbsp;font-weight:&nbsp;800;
&nbsp;&nbsp;&nbsp;&nbsp;font-size:&nbsp;@percentage-font-size;
&nbsp;&nbsp;&nbsp;&nbsp;font-family:&nbsp;"Lato",&nbsp;"Helvetica&nbsp;Neue",&nbsp;Helvetica,&nbsp;Arial,&nbsp;sans-serif;
&nbsp;&nbsp;&nbsp;&nbsp;color:&nbsp;@percentage-color;
}
 
}
}
}
 
@i:&nbsp;0;
@increment:&nbsp;180deg&nbsp;/&nbsp;100;
 
.loop&nbsp;(@i)&nbsp;when&nbsp;(@i&nbsp;&lt;=&nbsp;100)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&amp;[data-progress="@{i}"]
 
{
&nbsp;&nbsp;&nbsp;&nbsp;.circle
 
{
&nbsp;&nbsp;&nbsp;&nbsp;.mask.full,&nbsp;.fill
 
{
&nbsp;&nbsp;&nbsp;&nbsp;-webkit-transform:&nbsp;rotate(@increment&nbsp;*&nbsp;@i);
&nbsp;&nbsp;&nbsp;&nbsp;-ms-transform:&nbsp;rotate(@increment&nbsp;*&nbsp;@i);
&nbsp;&nbsp;&nbsp;&nbsp;transform:&nbsp;rotate(@increment&nbsp;*&nbsp;@i);
}
 
.fill.fix&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;-webkit-transform:&nbsp;rotate(@increment&nbsp;*&nbsp;@i&nbsp;*&nbsp;2);
&nbsp;&nbsp;&nbsp;&nbsp;-ms-transform:&nbsp;rotate(@increment&nbsp;*&nbsp;@i&nbsp;*&nbsp;2);
&nbsp;&nbsp;&nbsp;&nbsp;transform:&nbsp;rotate(@increment&nbsp;*&nbsp;@i&nbsp;*&nbsp;2);
}
 
}
 
.inset&nbsp;.percentage&nbsp;.numbers&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;/*width:&nbsp;@i&nbsp;*&nbsp;@percentage-text-width&nbsp;+&nbsp;@percentage-text-width;*/
}
 
}
.loop(@i&nbsp;+&nbsp;1);
}
.loop(@i);
}
</pre>
<pre>

The javascript logic consists of just a few simple steps.  First, we get the message and pass it to the calculateSentiment function.  Then we construct the “document” that the sentiment analysis will score.  For more info on this document schema, visit the how-to page.  We then do an http post to your endpoint.  The Ocp-Apim-Subscription-Key is the key that we get from Azure that we saw at the beginning of this post.  Finally, we get the result if the post succeeds, and we pass it to the function that sets the chart value.

</pre>
<pre>(function&nbsp;()&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;'use&nbsp;strict';
&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;item;
 
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;The&nbsp;Office&nbsp;initialize&nbsp;function&nbsp;must&nbsp;be&nbsp;run&nbsp;each&nbsp;time&nbsp;a&nbsp;new&nbsp;page&nbsp;is&nbsp;loaded.
&nbsp;&nbsp;&nbsp;&nbsp;Office.initialize&nbsp;=&nbsp;function&nbsp;(reason)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;item&nbsp;=&nbsp;Office.context.mailbox.item;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$(document).ready(function&nbsp;()&nbsp;{
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;get&nbsp;the&nbsp;body&nbsp;of&nbsp;the&nbsp;message&nbsp;and&nbsp;calculate&nbsp;the&nbsp;score
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;item.body.getAsync('text',&nbsp;function&nbsp;(async)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;calculateSentiment(async.value)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});
&nbsp;&nbsp;&nbsp;&nbsp;};
 
&nbsp;&nbsp;&nbsp;&nbsp;function&nbsp;calculateSentiment(emailMessage)&nbsp;{
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;build&nbsp;the&nbsp;document
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;documentsVal&nbsp;=&nbsp;[{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"language":&nbsp;"en",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"id":&nbsp;1,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"text":&nbsp;emailMessage
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}];
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;documentsKey&nbsp;=&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"documents":&nbsp;documentsVal
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$.ajax({
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;type:&nbsp;'POST',
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;url:&nbsp;"https://eastus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;headers:&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Ocp-Apim-Subscription-Key":&nbsp;"",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Content-Type":&nbsp;"application/json",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Accept":&nbsp;"application/json"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data:&nbsp;JSON.stringify(documentsKey,&nbsp;null,&nbsp;2),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dataType:&nbsp;"json",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;success:&nbsp;function&nbsp;(result)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;documents&nbsp;=&nbsp;result.documents;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;(var&nbsp;i&nbsp;=&nbsp;0;&nbsp;i&nbsp;&lt;&nbsp;documents.length;&nbsp;i++)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;msg&nbsp;=&nbsp;documents[i];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var&nbsp;score&nbsp;=&nbsp;msg.score;
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setChart(score)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;error:&nbsp;function&nbsp;(xhr,&nbsp;ajaxOptions,&nbsp;thrownError)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;console.log("Error&nbsp;statue:&nbsp;"&nbsp;+&nbsp;xhr.status);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;console.log("Thrown&nbsp;error:&nbsp;"&nbsp;+&nbsp;thrownError);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});
&nbsp;&nbsp;&nbsp;&nbsp;}
 
 
&nbsp;&nbsp;&nbsp;&nbsp;function&nbsp;setChart(pct)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;window.randomize&nbsp;=&nbsp;function&nbsp;()&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$('.radial-progress').attr('data-progress',&nbsp;Math.floor(pct&nbsp;*&nbsp;100));
 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let&nbsp;displayVal&nbsp;=&nbsp;Math.round(pct&nbsp;*&nbsp;100)&nbsp;+&nbsp;'%';
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$('#app-pct').text(displayVal);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setTimeout(window.randomize,&nbsp;200);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$('.radial-progress').click(window.randomize);
&nbsp;&nbsp;&nbsp;&nbsp;}
 
 
})();
 
 
 
</pre>
<pre>

The end result is as follows:

This slideshow requires JavaScript.

2 thoughts on “Creating an Outlook Add-In with Sentiment Analysis

Comments are closed.