Fine-Tuning Your Chatbot
- 1/18/2018
- Reviewing Bot Emulator Details
- Handling Activities
- Advanced Conversation Messages
- Summary
Advanced Conversation Messages
Chapter 3 went into depth on how to construct a reply to a user’s Message Activity. That was for a text response, but this section covers a couple of other scenarios for communicating with the user: Sending Typing Activities to the user and sending a notification or alert to the user.
Sending Typing Activities
Occasionally, a chatbot needs to perform some action that might take longer than normal. Rather than make the user wait and wonder, it’s polite to let the user know that the chatbot is busy preparing their response. You could send a text message to let the user know, but a common way to do this is by sending the user a Typing Activity. Listing 4-3 shows shows how to build a new Typing Activity.
LISTING 4-3 The Rock, Paper, Scissors Game – BuildTypingActivity Method
using Microsoft.Bot.Connector; namespace RockPaperScissors4.Models { public static class ActivityExtensions { public static Activity BuildTypingActivity(this Activity userActivity) { ITypingActivity replyActivity = Activity.CreateTypingActivity(); replyActivity.ReplyToId = userActivity.Id; replyActivity.From = new ChannelAccount { Id = userActivity.Recipient.Id, Name = userActivity.Recipient.Name }; replyActivity.Recipient = new ChannelAccount { Id = userActivity.From.Id, Name = userActivity.From.Name }; replyActivity.Conversation = new ConversationAccount { Id = userActivity.Conversation.Id, Name = userActivity.Conversation.Name, IsGroup = userActivity.Conversation.IsGroup }; return (Activity) replyActivity; } } }
Each of the Activity types has a factory method, prefixed with Create to create a new instance of that Activity type. The BuildTypingActivity takes advantage of that and calls the CreateTypingActivity factory method on the Activity class. For the resulting instance that implements ITypingActivity, you must populate ReplyToId, From, Recipient, and Conversation.
ReplyToId is the Id of the Activity instance being replied to. Notice that the From and Recipient populate from the Recipient and From userActivity properties, saying that the Activity is now From the chatbot and being sent to the user Recipient. You also need to populate the Conversation so the Bot Connector knows which conversation the Activity belongs to.
In this example, the return type is cast to Activity, rather than returning ITypingActivity for the convenience of the caller, which is the GetScoresAsync method in Listing 4-4.
LISTING 4-4 The Rock, Paper, Scissors Game – GetScoresAsync Method
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Bot.Connector; namespace RockPaperScissors4.Models { public class GameState { [Serializable] class PlayScore { public DateTime Date { get; set; } = DateTime.Now; public bool UserWin { get; set; } } public async Task<string> GetScoresAsync(ConnectorClient connector, Activity activity) { Activity typingActivity = activity.BuildTypingActivity(); await connector.Conversations.ReplyToActivityAsync(typingActivity); await Task.Delay(millisecondsDelay: 10000); using (StateClient stateClient = activity.GetStateClient()) { IBotState chatbotState = stateClient.BotState; BotData chatbotData = await chatbotState.GetUserDataAsync( activity.ChannelId, activity.From.Id); Queue<PlayScore> scoreQueue = chatbotData.GetProperty<Queue<PlayScore>>(property: “scores”); if (scoreQueue == null) return “Try typing Rock, Paper, or Scissors to play first.”; int plays = scoreQueue.Count; int userWins = scoreQueue.Where(q => q.UserWin).Count(); int chatbotWins = scoreQueue.Where(q => !q.UserWin).Count(); int ties = chatbotData.GetProperty<int>(property: “ties”); return $”Out of the last {plays} contests, “ + $”you scored {userWins} and “ + $”Chatbot scored {chatbotWins}. “ + $”You’ve also had {ties} ties since playing.”; } } } }
You can read about the GetScoresAsync method in Chapter 3 and the changes are the first three lines in the method, repeated below for convenience:
Activity typingActivity = activity.BuildTypingActivity(); await connector.Conversations.ReplyToActivityAsync(typingActivity); await Task.Delay(millisecondsDelay: 10000);
This method calls the BuildTypingActivity extension method on the activity parameter. The code also passes in the ConnectorClient instance, connector, that the Post method of MessagesController instantiated. ReplyToActivityAsync sends the response back to the user, exactly like all the previous replies. Task.Delay simulates a longer running process because GetScoresAsync is too quick to see the Typing Activity in the Bot Emulator and that gives you a chance to see the Typing message before it goes away. After Task.Delay completes, the rest of the GetScoresAsync method runs as normal. Figure 4-4 shows how the Bot Emulator displays a Typing Acivity.
FIGURE 4.4 The Bot Emulator Receiving Typing Activity.
The Typing activity appears in Figure 4-4 as an ellipses animation, pointed to by the ITypingActivity label. In the emulator, the animation continues for a few seconds and goes away. The appearance and duration of the animation is dependent upon the channel the chatbot appears in.
Sending Independent Messages
Most of the time, a chatbot waits passively for the user to talk to it and then only replies. The following sections describe two scenarios where a chatbot might want to proactively send messages to the user on its own.
For the demos in this section, we created a new Console application that you can see in Listing 4-5. If you do this yourself, remember to add a reference to the Microsoft.Bot.Builder NuGet package. You’ll also need a reference to the System.Configuration assembly. The project name for this demo is RockPaperScissorsNotifier1.
LISTING 4-5 Proactive Communication from Chatbot to User – Program.cs
using Newtonsoft.Json; using System; using System.Configuration; using System.IO; namespace RockPaperScissorsNotifier1 { class Program { public static string MicrosoftAppId { get; set; } = ConfigurationManager.AppSettings[“MicrosoftAppId”]; public static string MicrosoftAppPassword { get; set; } = ConfigurationManager.AppSettings[“MicrosoftAppPassword”]; static void Main() { ConversationReference convRef = GetConversationReference(); var serviceUrl = new Uri(convRef.ServiceUrl); var connector = new ConnectorClient(serviceUrl, MicrosoftAppId, MicrosoftAppPassword); Console.Write(value: “Choose 1 for existing conversation or 2 for new conversation: “); ConsoleKeyInfo response = Console.ReadKey(); if (response.KeyChar == ‘1’) SendToExistingConversation(convRef, connector.Conversations); else StartNewConversation(convRef, connector.Conversations); } static void SendToExistingConversation(ConversationReference convRef, IConversations conversations) { var existingConversationMessage = convRef.GetPostToUserMessage(); existingConversationMessage.Text = $”Hi, I’ve completed that long-running job and emailed it to you.”; conversations.SendToConversation(existingConversationMessage); } static void StartNewConversation(ConversationReference convRef, IConversations conversations) { ConversationResourceResponse convResponse = conversations.CreateDirectConversation(convRef.Bot, convRef.User); var notificationMessage = convRef.GetPostToUserMessage(); notificationMessage.Text = $”Hi, I haven’t heard from you in a while. Want to play?”; notificationMessage.Conversation = new ConversationAccount(id: convResponse.Id); conversations.SendToConversation(notificationMessage); } static ConversationReference GetConversationReference() { string convRefJson = File.ReadAllText(path: @”..\..\ConversationReference.json”); ConversationReference convRef = JsonConvert.DeserializeObject<ConversationReference>(convRefJson); return convRef; } } }
To run, this program requires data for the current conversation, user, and chatbot. You would normally use some type of database to share this information that the chatbot stores and this program reads. However, this program simulates the data store by copying a JSON message from the current conversation. Here are the steps for making this happen:
Run the RockPaperScissors4 chatbot.
Run the Bot Emulator, select the chatbot URL, and click Connect.
Play at least one round of the RockPaperScissors game. e.g. type scissors.
This created a ConversationReference.json file, described later, which you can find in the base folder of the RockPaperScissors4 project through Windows File Explorer. Open this file and copy its contents.
Open the ConversationReference.json file in the RockPaperScissorsNotifier1 project and replace the entire contents of that file with the JSON copied from the RockPaperScissors4 project. Note: Make sure you have the opening and closing curly braces and that you didn’t accidentally copy extra text.
Save the ConversationReference.json file.
Right-click the RockPaperScissorsNotifier1 project and select Debug | Start new instance.
You have a choice to select 1 or 2. Select 1 and the program will close.
Open the Bot Emulator and observe that there is a new message on the screen, as shown in Figure 4-5.
FIGURE 4.5 Sending a Message to a Conversation.
Figure 4-5 shows what the Bot Emulator might look like after the previous steps. Clicking the Hello message shows the JSON in Details. When running the project and selecting 1, a new message appears below the Hello message.
Step #4 discussed a ConversationReference.json file created by playing a round of the game. MessagesController created this file when handling the user Message activity, shown below. It uses a ConversationReference, which is a Bot Builder type to help with saving and resuming conversations.
if (activity.Type == ActivityTypes.Message) { string message = await GetMessage(connector, activity); Activity reply = activity.BuildMessageActivity(message); await connector.Conversations.ReplyToActivityAsync(reply); SaveActivity(activity); }
The first few lines handle the user message and reply. What’s most interesting here is the call to SaveActivity, which creates the`ConversationReference.json file, shown below.
void SaveActivity(Activity activity) { ConversationReference convRef = activity.ToConversationReference(); string convRefJson = JsonConvert.SerializeObject(convRef); string path = HttpContext.Current.Server.MapPath(@”..\ConversationReference.json”); File.WriteAllText(path, convRefJson); }
SaveActivity creates a ConversationReference instance, convRef, using the activity.ToConversationReference method. Then the code serializes convRef into a string and saves the string into the ConversationReference.json file, described in Step #4 above. Here’s what the file contents look like.
{ “user”: { “id”: “default-user”, “name”: “User” }, “bot”: { “id”: “jn125aajg2ljbg4gc”, “name”: “Bot” }, “conversation”: { “id”: “35hn6jf29di2” }, “channelId”: “emulator”, “serviceUrl”: “http://localhost:31750” }
This is a JSON formatted file, with various fields that are identical to what you would find in the JSON representation of an Activity class. That was how the file is created.
Next, let’s look at how the RockPaperScissorsNotifier1 program uses ConversationReference. The following GetConversationReference, from Listing 4-5, shows how to do that:
static ConversationReference GetConversationReference() { string convRefJson = File.ReadAllText(path: @”..\..\ConversationReference.json”); ConversationReference convRef = JsonConvert.DeserializeObject<ConversationReference>(convRefJson); return convRef; }
The first line of GetConversationReference reads all of the text from that file. Then it deserializes the string into a ConversationReference, convRef. The following Main method, from Listing 4-5, shows what the program does with the results of GetConversationParameters:
public static string MicrosoftAppId { get; set; } = ConfigurationManager.AppSettings[“MicrosoftAppId”]; public static string MicrosoftAppPassword { get; set; } = ConfigurationManager.AppSettings[“MicrosoftAppPassword”]; static void Main() { ConversationReference convRef = GetConversationReference(); var serviceUrl = new Uri(convRef.ServiceUrl); var connector = new ConnectorClient(serviceUrl, MicrosoftAppId, MicrosoftAppPassword); Console.Write(value: “Choose 1 for existing conversation or 2 for new conversation: “); ConsoleKeyInfo response = Console.ReadKey(); if (response.KeyChar == ‘1’) SendToExistingConversation(convRef, connector.Conversations); else StartNewConversation(convRef, connector.Conversations); }
As in previous examples, you need a ConnectorClient instance to communicate with the Bot Connector. This time, the ConnectorClient constructor overload includes the MicrosoftAppId and MicrosoftAppPassword, which are read from the *.config file appSettings, just like Web.config for the chatbot. The ConnectorClient constructor also uses the ServiceUrl as its first parameter, which comes from the call to GetConversationReference, discussed earlier.
With the convRef and connector, the program asks the user what they would like to do and passes those as arguments to subsequent methods. The next sections explains how to use those parameters in sending messages to existing and new conversations.
Continuing a Conversation
Normally, there’s a continuous back and forth interaction in a conversation where the user sends a message, the chatbot responds, and this repeats until the user stops communicating or you’ve established a protocol to indicate an end to a given session. There are times though when you might want to continue a conversation later because of a need to do some processing or wait on an event. For these situations, the chatbot sends a message to the user later on, continuing the original conversation. The following SendToExistingConversation method, from Listing 4-5, shows how to send a message when you want to participate in an existing conversation.
static void SendToExistingConversation(ConversationReference convRef, IConversations conversations) { var existingConversationMessage = convRef.GetPostToUserMessage(); existingConversationMessage.Text = $”Hi, I’ve completed that long-running job and emailed it to you.”; conversations.SendToConversation(existingConversationMessage); }
SendToExistingConversation, as its name implies, sends a message to an existing conversation. It uses the convRef parameter, calling GetPostToUserMessage to convert from a ConversationReference to an activity, existingConversationMessage. The Text indicates a hypothetical scenario where the user might have requested a long running task that has now been completed.
Use the conversations parameter, from connector.Conversations, to send that Message Activity from the chatbot to the user. Figure 4-5 shows how the Bot Emulator might look after calling SendToConversation. The next section is very similar, but the semantics are different because instead sending a message to an existing conversation, it explains how the chatbot can initiate a new conversation.
Starting a New Conversation
Another scenario for a conversation is when a chatbot wants to initiate a brand new conversation. This could happen if the purpose of the chatbot is to notify the user when something happens. e.g. What if an appointment chatbot notified a user of a meeting and the user wanted to order a taxi to get to the place of meeting, could ask for an agenda, or might want to reschedule. Another scenario might be if the chatbot notified the user of an event, like breaking news or a daily quote. The following code, from Listing 4-5 shows the StartNewConversation and how it can originate a new conversation with the user:
static void StartNewConversation(ConversationReference convRef, IConversations conversations) { ConversationResourceResponse convResponse = conversations.CreateDirectConversation(convRef.Bot, convRef.User); var notificationMessage = convRef.GetPostToUserMessage(); notificationMessage.Text = $”Hi, I haven’t heard from you in a while. Want to play?”; notificationMessage.Conversation = new ConversationAccount(id: convResponse.Id); conversations.SendToConversation(notificationMessage); }
At first look, you might notice that the new Activity, notificationMessage, is built very much the same as the previous section. One difference is the Text, that suggests this is a notification that you might send after a period of inactivity from the user. The other difference is the fact that this Message Activity is sent to a brand new conversation.
At the top of the method, the call to CreateDirectConversation on the conversations parameter receives a ConversationResourceResponse return value. The convResponse supplies the Id for a new ConversationAccount instance that is passed to the Conversation property of notificationMessage. Starting a new conversation clears out the existing conversation in the Bot Emulator and only shows this message, but the actual behavior is dependent on the channel the chatbot communicates with.