Sentiment Analysis in USD with Cognitive Services Text Analytics


As noted in an earlier post, Microsoft’s Cognitive Services offer an array of intelligent APIs that allow developers to build new apps, and enhance existing ones, with the power of machine-based AI. These services enable users to interact with applications and tools in a natural and contextual manner. These types of intelligent application interactions have significant applicability in the realm of Service, to augment the experiences of both customers and agents.

In this post, we will focus on the Text Analytics API, and how it can be leveraged within the agent’s experience, to automatically detect the sentiment of activities and information during customer interactions, and to enable automated actions based on that sentiment within the Unified Service Desk. We will build a custom Unified Service Desk hosted control that will allow us to determine and visualize the sentiment of any text-based information available in USD, and automatically trigger actions based on the sentiment score determined.

 

Concept and Value

As agents interact with customers in an omni-channel customer care environment, two-way communication will take place between the customer and the agent. The communication may take place by voice, email, live chat, or other channels. Regardless of the channel used, the messages sent back and forth will contain sentiments, and being able to programmatically determine whether the sentiment being expressed is positive or negative in real-time can offer significant value to the efficiency and effectiveness of a customer engagement strategy.

Being able to detect the sentiment of customer and agent communications can offer possibilities including:

  • Automatically flagging or escalating live chats and phone-based contacts in the event of ongoing negative sentiment expressed by either customer or agent
  • Routing inbound emails, web submissions, phone calls, and live chats based on customer sentiment; for example, automatically routing contacts that contain negative sentiment to a Tier 2 queue
  • Capturing and recording the customer sentiment expressed during contacts, to allow for reporting and trend analysis by customer, product, or service
  • Capturing and recording the agent sentiment expressed during contacts, to assist with ongoing agent performance evaluation

The custom control we build in this post will allow us to call an action with any string of text, have the sentiment score calculated and displayed to the agent, give a visualization of positive or negative sentiment via an updating image, and raise an event in USD to allow automation of actions. The control we create is shown in the image below, showing the sentiment expressed in a case submitted via a CRM Portal:

GoodSentiment

 

In this post, we will build on the sample code available in these two resources:

 

Pre-requisites

The pre-requisites for building and deploying our custom hosted control include:

 

Building our Custom Control

In Visual Studio 2015, we will first create a new project using the Visual C# USD Custom Hosted Control template, and will name our project USDSentiment:

NewProject

 

We set the Platform Target for our project build to the appropriate CPU (x64 or x86).

 

Adding References

We add references to our project by right-clicking the USDSentiment project in the Solution Explorer, and adding the following:

  • System.Net
  • System.Net.Http
  • System.Runtime.Serialization
  • System.Web

 

Adding a Data Contract for the Text Analytics API

To facilitate interacting with the Text Analytics API, we add a new item to our project: a Visual C# class which we will name TextAnalytics.JSON.cs. We will populate this file with the JSON data contracts for the REST Services outlined in the Text Analytics API documentation:

using System.Runtime.Serialization;

namespace TextAnalytics.JSON
{
    [DataContract]
    public class Response
    {
        [DataMember(Name = "documents")]
        public Document[] Documents { get; set; }

        [DataMember(Name = "errors")]
        public Error[] Errors { get; set; }
    }
    [DataContract]
    public class Document
    {
        [DataMember(Name = "score")]
        public double Score { get; set; }

        [DataMember(Name = "id")]
        public string ID { get; set; }

    }

    [DataContract]
    public class Error
    {
        [DataMember(Name = "id")]
        public string ID { get; set; }

        [DataMember(Name = "message")]
        public string Message { get; set; }
    }

}

 

Laying Out our UI

For our custom control, we will design it to sit in the LeftPanelFill display group of the default USD layout. In our XAML code, we add:

  • A Checkbox, to allow the agent to choose whether the sentiment detection is enabled
  • An Image, to allow us to give a quick and simple visualization of the most recently detected sentiment to the agent
  • A TextBlock , to allow us to render the actual score of the most recently detected sentiment to the agent
    • Note that the Text Analytics API will return sentiment scores between 0 and 1, with 0 indicating very negative sentiment, and 1 indicating very positive sentiment

 
Our final markup should appear as shown below:

<USD:DynamicsBaseHostedControl x:Class="USDSentiment.USDControl"
             x:Name="_usdHostedControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:USD="clr-namespace:Microsoft.Crm.UnifiedServiceDesk.Dynamics;assembly=Microsoft.Crm.UnifiedServiceDesk.Dynamics"
             mc:Ignorable="d" 
             d:DesignHeight="125" d:DesignWidth="370">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition Height="80"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="100"/>
            </Grid.ColumnDefinitions>
            <Label x:Name="lbl_SentimentTitle" Content="SENTIMENT INDICATOR"  FontSize="13" FontWeight="Bold" 
                   HorizontalAlignment="Left" Grid.Column="0" VerticalAlignment="Top"/>
            <CheckBox x:Name="chk_SentimentEnabled" Content="Enabled" HorizontalAlignment="Center"  
                      Grid.Column="1" VerticalAlignment="Center"  
                      IsChecked="{Binding ElementName=_usdHostedControl, Path=IsSentimentEnabled, Mode=TwoWay}" />
        </Grid>
        <Border Grid.Row="1" BorderBrush="LightGray" BorderThickness="1" Background="GhostWhite">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="100"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Image x:Name="img_Sentiment" Width="65" Height="65" Grid.Column="0" 
                       Source="images\neutral.png" Opacity="0.2" Margin="0,0,0,0" VerticalAlignment="Center"/>
                <TextBlock x:Name="txt_SentimentScore" TextWrapping="Wrap" Text="N/A" FontSize="30" 
                           Opacity="0.2" TextAlignment="Center" VerticalAlignment="Center" Grid.Column="1" />
            </Grid>
        </Border>
    </Grid>
</USD:DynamicsBaseHostedControl>

 

Our custom control should appear as shown below:

 

USDSentimentControl

 

Adding Our Sentiment Indicator Images

We will add three images to our project (happy, neutral, and sad), to give quick visualizations of the calculated sentiment. In our Solution Explorer, we Add a New Folder to our USDSentiment project, and into that folder, we copy three PNG images, which we also add to the project:

 

SentimentImages

 

Adding Our C# Code

In our USDControl.xaml.cs code-behind, we add several using statements. We also:

  • use a private a variable for our Text Analytics API Subscription Key that we obtained as one of our pre-requisites
  • set variables for each of the images we will use to render sentiment
  • set other variables and events that we will use in our code

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Media.Imaging;
using Microsoft.Crm.UnifiedServiceDesk.Dynamics;
using Microsoft.Crm.UnifiedServiceDesk.Dynamics.Utilities;
using Microsoft.Uii.Desktop.SessionManager;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization.Json;
using System.Web;

namespace USDSentiment
{
    /// <summary>
    /// Interaction logic for USDControl.xaml
    /// This is a base control for building Unified Service Desk Aware add-ins
    /// See USD API documentation for full API Information available via this control.
    /// </summary>
    public partial class USDControl : DynamicsBaseHostedControl
    {
        #region Vars
        // the Text Analytics subscription key can alternately be pulled from USD Options:
        private string _textAnalyticsSubscriptionKey = "InsertYourKeyHere";
        // Configure images to allow visualization of sentiment:
        private BitmapImage imgNeutral = new BitmapImage(new Uri(@"images/neutral.png", UriKind.Relative));
        private BitmapImage imgHappy = new BitmapImage(new Uri(@"images/happy.png", UriKind.Relative));
        private BitmapImage imgSad = new BitmapImage(new Uri(@"images/sad.png", UriKind.Relative));

        public bool IsSentimentEnabled { get; set; }

        #endregion

 

To our UII Constructor, we add a call to our InitUIBinding method, which configures some initial bound variables:

/// <summary>
/// UII Constructor 
/// </summary>
/// <param name="appID">ID of the application</param>
/// <param name="appName">Name of the application</param>
/// <param name="initString">Initializing XML for the application</param>
public USDControl(Guid appID, string appName, string initString)
    : base(appID, appName, initString)
{
    InitializeComponent();
    Init();
}

private void Init()
{
    IsSentimentEnabled = true;
    // Set default in UI:
    chk_SentimentEnabled.IsChecked = true;
}

 

We now adapt the DoAction method of the custom hosted control project template, to handle an action named GetSentiment. When this action is called, we will ensure the ‘Enabled’ checkbox is clicked, then extract two parameters from the action data:

  • Text – this contains the string of text that we will detect the sentiment of
  • Source – this contains a string that identifies the source of the text, in case we are detecting the sentiment of multiple sources; we can then take specific actions after the sentiment has been detected, depending on the source

We then call the GetSentiment method, passing in the text, the source, and the API subscription key as parameters:

/// <summary>
/// Raised when an action is sent to this control
/// </summary>
/// <param name="args">args for the action</param>
protected override void DoAction(Microsoft.Uii.Csr.RequestActionEventArgs args)
{
    #region Process Actions
    if (args.Action.Equals("GetSentiment", StringComparison.OrdinalIgnoreCase))
    {
        // Check to ensure the control is enabled in the UI:
        if (IsSentimentEnabled)
        {
            try
            {
                List<KeyValuePair<string, string>> actionDataList = Utility.SplitLines(args.Data, CurrentContext, localSession);
                string sentimentText = Utility.GetAndRemoveParameter(actionDataList, "Text");
                string sentimentSource = Utility.GetAndRemoveParameter(actionDataList, "Source");
                if (sentimentText != null && sentimentText != "")
                {
                    GetSentiment(sentimentText, sentimentSource, _textAnalyticsSubscriptionKey);

                }
            }
            catch (Exception ex)
            {
                // place error handling here
            }
        }
    }
    #endregion

    base.DoAction(args);

}

 

In the GetSentiment method, we receive the three incoming parameters, and build and issue our request to the Text Analytics API via the HttpClient object.

We add our API subscription key to the headers of the request, and create some basic JSON data to send in our request body. Note that the API is capable of receiving multiple ‘documents’ with the request, but we will send just a single one.

We receive our response, and use our data contract to serialize the response. We check for errors, and determine whether we have received any results in our response. Depending on the results returned, we use the overloaded DisplaySentiment method to either display the sentiment score, or to gray-out the results display if no score is returned, depending on the parameters passed.

If we have a score returned, we also call AddSentimentToContext to allow the sentiment results to trigger other actions in USD, if applicable:

private async void GetSentiment(string docSentiment, string source, string subscriptionKey)
{
    var client = new HttpClient();

    // Request headers
    client.DefaultRequestHeaders
            .Accept
            .Add(new MediaTypeWithQualityHeaderValue("application/json"));
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);

    // Request parameters
    var uri = "https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment";

    HttpResponseMessage response;

    // Create a basic JSON request body, with a single text 'document'
    byte[] byteData = Encoding.UTF8.GetBytes("{\"documents\": [{\"id\": \"1\",\"text\": \"" + docSentiment + "\"}]}");

    using (var content = new ByteArrayContent(byteData))
    {
        content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        response = await client.PostAsync(uri, content);

        using (var stream = await response.Content.ReadAsStreamAsync())
        {
            DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(TextAnalytics.JSON.Response));
            TextAnalytics.JSON.Response jsonResponse = ser.ReadObject(stream) as TextAnalytics.JSON.Response;

            // Check for errors in the response:
            if (jsonResponse.Errors != null && jsonResponse.Errors.Length > 0)
            {
                // Gray out the sentiment display:
                DisplaySentiment();
            }
            // Ensure we have some results:
            else if (jsonResponse.Documents != null && jsonResponse.Documents.Length > 0 && jsonResponse.Documents[0].Score > 0)
            {
                // Render the sentiment:
                DisplaySentiment(Math.Round(jsonResponse.Documents[0].Score, 2));
                // Add sentiment to the context, and trigger an event
                //  This will allow us to initiate actions in USD based on the sentiment:
                AddSentimentToContext(Math.Round(jsonResponse.Documents[0].Score, 2).ToString(), source);
            }
            else
            {
                // Gray out the sentiment display:
                DisplaySentiment();
            }
        }


    }

}

 

We have two definitions for the overloaded DisplaySentiment method. When a score is passed, the method will use some arbitrarily defined tiers to determine whether to display a happy, neutral, or sad face as a visualizing image. The score itself is also displayed, and the opacity set to full.

We also have a definition that takes no parameters, and when called, the sentiment display is effectively disabled, with the visualization opacity set to partial, to indicate no score:

private void DisplaySentiment(double sentiment)
{

    // Determine which image to display, based on the sentiment score:
    BitmapImage imgSentiment;

    if (sentiment < 0.35)
    {
        imgSentiment = imgSad;
    }
    else if (sentiment > 0.65)
    {
        imgSentiment = imgHappy;
    }
    else
    {
        imgSentiment = imgNeutral;
    }

    // Update sentiment display:
    img_Sentiment.Source = imgSentiment;
    img_Sentiment.Opacity = 1.0;
    txt_SentimentScore.Opacity = 1.0;
    txt_SentimentScore.Text = "Score: " + sentiment.ToString();

}

private void DisplaySentiment()
{

    // Gray out sentiment display;
    img_Sentiment.Source = imgNeutral;
    img_Sentiment.Opacity = 0.2;
    txt_SentimentScore.Opacity = 0.2;
    txt_SentimentScore.Text = "Score: N/A";

}

 

In the AddSentimentToContext method, we add the score and the source of the text to the session context. We then fire the SentimentFound event, to allow for the sentiment determination to initiate other actions in USD:

private void AddSentimentToContext(string sentimentScore, string source)
{
    Dictionary<string, CRMApplicationData> sentimentAttributes = new Dictionary<string, CRMApplicationData>();
    // add strings to context: 
    sentimentAttributes.Add("SentimentScore", new CRMApplicationData() { name = "SentimentScore", type = "string", value = sentimentScore});
    sentimentAttributes.Add("SentimentSource", new CRMApplicationData() { name = "SentimentSource", type = "string", value = source });
    ((DynamicsCustomerRecord)((AgentDesktopSession)localSessionManager.ActiveSession).Customer.DesktopCustomer).MergeReplacementParameter(this.ApplicationName, sentimentAttributes, true);
            
    //raise appropriate event, to allow USD actions based on sentiment:
    this.FireEvent("SentimentFound");

}

 

In our code-behind, we add in some additional methods to cover some other basics. These additional code snippets are found in the downloadable solution at the end of this post.

 

We can now Build our solution in Visual Studio.

Upon successful build, we can retrieve the resulting DLL, which should be available in a subdirectory of our project folder:

.\USDSentiment\bin\Debug\USDSentiment.dll

We can copy this DLL into the USD directory on our client machine, which by default will be:

C:\Program Files\Microsoft Dynamics CRM USD\USD
 

Note that instead of copying the files manually, we could also take advantage of the Customization Files feature, as outlined here, to manage the deployment of our custom DLLs.

 

Configuring Unified Service Desk

We now need to configure Unified Service Desk, by adding our custom hosted control, and the associated actions and events to allow it to detect and display sentiment.

To start, we log in to the CRM web client as a user with System Administrator role.

Next, we will add a new Hosted Control, which is our new custom control that we have created. By navigating to Settings > Unified Service Desk > Hosted Controls, we add in a Hosted Control with inputs as shown below. Note that the USD Component Type is USD Hosted Control, and the Assembly URI and Assembly Type values incorporate the name of our DLL:

HostedControl

 

We will add a new UII Action to our USDSentiment hosted control. While viewing our USDSentiment hosted control, we can select the chevron in our navigation, and selecting UII Actions. We then click ‘Add New UII Action’, and create our new UII Action, which we name GetSentiment, to match the action as defined in our hosted control code:

GetSentimentUIIAction

 

Next, we will add a new Action Call, by navigating to Settings > Unified Service Desk > Action Calls, and adding a new one, as shown below. We use the default action of our newly added hosted control, which will instantiate the control when the action is called:

ActionCallOpenControl

 

Next, we will add our new Action Call to the SessionNew event of the CRM Global Manager, which will cause our control to be instantiated when a new session is initiated. We can do this by navigating to Settings > Unified Service Desk > Events, and opening the SessionNew event of the CRM Global Manager. We add our new action to the Active Actions for the event:

AddActionToNewSession

 

Next, we will add a new event, to make it possible for us to take specific actions when sentiment is detected. We navigate to Settings > Unified Service Desk > Events, and add a New Event. Our Event is called SentimentFound, to match the name of the event our custom USD control raises. We add this event to the USDSentiment Hosted Control:

SentimentFoundEvent

 

By attaching Action Calls to this event, we can take specific actions when sentient is detected, such as:

  • Updating case records to store the sentiment
  • Automatically escalating chats or phone calls based on negative sentiment

In our example, we will use our custom hosted control to determine the sentiment a customer has expressed in a case submitted by email or through web portal submission. To do this, we will add a new Action Call, by navigating to Settings > Unified Service Desk > Action Calls, and adding a new one, as shown below. We pass in our case description as the text which we will use to calculate the sentiment, and we pass in an identifier of the source of the sentiment:

CaseDescActionCall

 

We can also add a Condition to our Action Call so that the search is only conducted when we have a non-empty search term, with a condition like this: "[[incident.description]]" != ""

ActionCallCaseDescCondition

 

Next, we will add our new Action Call to the DataReady event of the Incident hosted control, which will cause the sentiment of cases to be determined and displayed when the case is loaded. We can do this by navigating to Settings > Unified Service Desk > Events, and opening the DataReady event of the Incident control. We add our new action to the Active Actions for the event:

DataReadyEvent

 

Testing our Custom Hosted Control

We are now ready to launch the Unified Service Desk on our client machine, and test the hosted control. After we log into USD, we can open cases that contain a Description, and have the sentiment expressed in the case description automatically determined and displayed, as shown in the examples below:

Positive Sentiment Captured from Case Description
GoodSentiment

 

Negative Sentiment Captured from Case Description
BadSentiment

 

Visualizing the sentiment of a case as we open it is just a basic example of how we can leverage sentiment. The video below gives a few examples of how we can combine the Cognitive Services Speech Recognition from our earlier post, along with the Sentiment Analysis in this post, to augment agent efficiency in a number of ways, including:

  • Chat Channel in USD:
    • Speech-to-Chat to augment agent chat efficiency
    • Speech-to-Chat to augment agent chat efficiency
    • Automatic sentiment analysis on all chat messages
    • Voice-powered search for knowledge during chat
  • Phone Channel in USD:
    • Automated capture of call transcript
    • Sentiment analysis on call transcript as it is captured
  • Web and Email Cases in USD:
    • Automated sentiment analysis on submitted case content

 

 

The solution source code can be downloaded here. You will need to insert your Text Analytics API subscription key into the placeholder in the code-behind.

Comments (0)

Skip to main content