Fine-Tuning Your Chatbot
- 1/18/2018
- Reviewing Bot Emulator Details
- Handling Activities
- Advanced Conversation Messages
- Summary
Handling Activities
An Activity class is one of a set of notification types that the Bot Connector can send to your chatbot. So far, the only Activity you’ve seen in this book is a Message Activity and there are several more that notify your chatbot of events related to conversations. The following sections explain what activities are available, what they mean, and examples of how to write code for them.
The Activity Class
The Activity class contains members to represent all of the different types of activities there are. Some Activity members are only used in the context of the type of Activity sent to a chatbot. e.g. if the Acitivity is Type Message, you would expect the Text property to contain the user input but an Activity of Type Typing doesn’t use any properties because the Type represents the semantics of the Activity. Here’s an abbreviated Activity class definition, with all members removed, except for Type.
public partial class Activity : IActivity, IConversationUpdateActivity, IContactRelationUpdateActivity, IMessageActivity, ITypingActivity // interfaces for additional activities { public string Type { get; set; } }
As you can see, Activity implements several interfaces. Each of these interfaces specify members to support derived activity types. Activity also has a Type property, indicating the purpose of an Activity instance. Table 4-1 shows common Activity types, matching interfaces, and a quick description (explained in more detail in following sections).
Table 4-1 Activity Types
Activity Type |
Interface |
Description |
---|---|---|
ConversationUpdate |
IConversationUpdateActivity |
User(s) joined or left a conversation. |
ContactRelationshipUpdate |
IContactRelationshipUpdateActivity |
User added or removed your chatbot from their list. |
DeleteUserData |
None |
User wants to remove all Personally Identifiable Information (PII). |
Message |
IMessageActivity |
Chatbot receives or sends a communication. |
Ping |
None |
Sent to determine if bot URL is available. |
Typing |
ITypingActivity |
Busy indicator by either chatbot or user. |
When handling Activity instances, you might care about the interface so you can convert the activity to that interface to access relevant Activity members. You might also notice in Table 4-1 that some Activity Types, like DeleteUserData and Ping, don’t have a matching interface on Activity. In those cases, there aren’t any Activity members to access because the purpose of the Activity Type is for you to take an action, regardless of Activity instance state. The next section discusses the ActivityType class that contains members that define the type of an Activity instance.
The ActivityType Class
In the Bot Framework, an Activity has a purpose that is represented by an ActivityType class, with properties for each activity type, shown below:
public static class ActivityTypes { public const string ContactRelationUpdate = “contactRelationUpdate”; public const string ConversationUpdate = “conversationUpdate”; public const string DeleteUserData = “deleteUserData”; public const string Message = “message”; public const string Ping = “ping”; public const string Typing = “typing
ActivityTypes is a convenient class to help avoid typing strings and you might recall from earlier examples where the code checked for ActivityTypes.Message to ensure the activity it was working with was a Message, like this:
if (activity.Type == ActivityTypes.Message) { // handle message... }
While the names of each ActivityTypes member suggests their purpose, the following sections offer more details and code examples.
Code Design Overview
The code in this chapter continues the Rock, Paper, Scissors game from Chapter 3. Chapter 4 code includes a project named RockPaperScissors4, with a new class, shown in Listing 4-1, for handling Activities.
LISTING 4-1 The Rock, Paper, Scissors Game - SystemMessages Class
using Microsoft.Bot.Connector; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Web; namespace RockPaperScissors4.Models { public class SystemMessages { public async Task Handle(ConnectorClient connector, Activity message) { switch (message.Type) { case ActivityTypes.ContactRelationUpdate: HandleContactRelation(message); break; case ActivityTypes.ConversationUpdate: await HandleConversationUpdateAsync(connector, message); break; case ActivityTypes.DeleteUserData: await HandleDeleteUserDataAsync(message); break; case ActivityTypes.Ping: HandlePing(message); break; case ActivityTypes.Typing: HandleTyping(message); break; default: break; } } void HandleContactRelation(IContactRelationUpdateActivity activity) { if (activity.Action == “add”) { // user added chatbot to contact list } else // activity.Action == “remove” { // user removed chatbot from contact list } } async Task HandleConversationUpdateAsync( ConnectorClient connector, IConversationUpdateActivity activity) { const string WelcomeMessage = “Welcome to the Rock, Paper, Scissors game! “ + “To begin, type \”rock\”, \”paper\”, or \”scissors\”. “ + “Also, \”score\” will show scores and “ + “delete will \”remove\” all your info.”; Func<ChannelAccount, bool> isChatbot = channelAcct => channelAcct.Id == activity.Recipient.Id; if (activity.MembersAdded.Any(isChatbot)) { Activity reply = (activity as Activity).CreateReply(WelcomeMessage); await connector.Conversations.ReplyToActivityAsync(reply); } if (activity.MembersRemoved.Any(isChatbot)) { // to be determined } } async Task HandleDeleteUserDataAsync(Activity activity) { await new GameState().DeleteScoresAsync(activity); } // random methods to test different ping responses bool IsAuthorized(IActivity activity) => DateTime.Now.Ticks % 3 != 0; bool IsForbidden(IActivity activity) => DateTime.Now.Ticks % 7 == 0; void HandlePing(IActivity activity) { if (!IsAuthorized(activity)) throw new HttpException( httpCode: (int)HttpStatusCode.Unauthorized, message: “Unauthorized”); if (IsForbidden(activity)) throw new HttpException( httpCode: (int) HttpStatusCode.Forbidden, message: “Forbidden”); } void HandleTyping(ITypingActivity activity) { // user has started typing, but hasn’t submitted message yet } } }
The Handle method in the Listing 4-1 SystemMessages class receives an Activity and passes it to a method, based on the matching type in the switch statement. Later sections of this chapter explain the Activity handling methods. Listing 4-2 has a modified Post method in MessagesController, showing how to call SystemMessages.Handle.
LISTING 4-2 The Rock, Paper, Scissors Game – MessageController.Post Method
using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Microsoft.Bot.Connector; using RockPaperScissors4.Models; using System.Web; namespace RockPaperScissors4 { [BotAuthentication] public class MessagesController : ApiController { /// <summary> /// POST: api/Messages /// Receive a message from a user and reply to it /// </summary> public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { HttpStatusCode statusCode = HttpStatusCode.OK; var connector = new ConnectorClient(new Uri(activity.ServiceUrl)); if (activity.Type == ActivityTypes.Message) { // code that handles Message Activity type } else { try { await new SystemMessages().Handle(connector, activity); } catch (HttpException ex) { statusCode = (HttpStatusCode) ex.GetHttpCode(); } } HttpResponseMessage response = Request.CreateResponse(statusCode); return response; } } }
The Post method in Listing 4-2 checks to see if the incoming Activity is a Message and handles the Activity, as described in Chapter 3. For other Activity types, the code calls Handle on a new SystemMessages instance, passing the ConnectorClient instance and Activity.
The try/catch block sets statusCode if Handle throws an HttpException. Otherwise, statusCode is a 200 OK. Stay tuned for the upcoming discussion of Ping Activities to see how this fits in.
Those are the essential changes to the Rock, Paper, Scissors game. The following sections describe how SystemMessages methods handle Activity types.
Sending Activities with the Bot Emulator
Clicking the three vertical dots on the Bot Emulator address bar reveals a menu with additional options for testing a chatbot. Figure 4-2 shows this menu, including the options for sending additional Activity types to the chatbot.
FIGURE 4.2 Bot Emulator Send System Activity Menu Options.
If you click on the menu Conversation | Send System Activity | <Activity Type>, the Bot Emulator sends the selected Activity type to your chatbot. These Activities match what you see in Table 4-1 with more granularity for added and removed options on conversationUpdate and contactRelationUpdate. The following sections walk through concepts of handing these Activity types, showing interfaces and handler method code from Listing 4-1.
Relationship Changes
When a user wants to interact with a chatbot, they add that chatbot to their channel. Similarly, when the user no longer wants to interact with a chatbot, they remove that chatbot from their channel. Whenever these channel add and remove events occur, the Bot Connector sends an Activity with Type ContactRelationUpdate to your chatbot. The IContactRelationUpdateActivity interface, below, shows properties relevant to a change in relationship.
public interface IContactRelationUpdateActivity : IActivity { string Action { get; set; } }
The Action property takes on string values of either add or remove. Here’s the code that handles the IContactRelationUpdateActivity:
void HandleContactRelation(IContactRelationUpdateActivity activity) { if (activity.Action == “add”) { // user added chatbot to contact list } else // activity.Action == “remove” { // user removed chatbot from contact list } }
The value of Action is either add or remove, making the logic to figure out which relatively easy. A possible use of this is for a chatbot that sends notifications to either add to a notification list or remove it. Another possibilitiy is to clean out any cached, transient, or un-used data associated with that user. You might even send an email to yourself as an alert when a user removes the chatbot from their user list, giving you the opportunity to review logs, learn why, and improve the chatbot.
As shown in Figure 4-2, you can open the menu and select the contactRelationUpdate item for testing. This sends a ContactRelationUpdate Activity to your chatbot.
Conversation Updates
Whenever a user first communicates with a chatbot, they join a conversation. When the user joins a conversation, the Bot Connector sends an Activity with Type ConversationUpdate, implementing the following IConversationUpdateActivity interface:
public interface IConversationUpdateActivity : IActivity { IList<ChannelAccount> MembersAdded { get; set; } IList<ChannelAccount> MembersRemoved { get; set; } }
Whenever the Bot Connector sends a ConversationUpdate Activity, MembersAdded contains the ChannelAccount information for the added user(s). The Bot Emulator sends a separate ConversationUpdate Activity for each user added – one for the user and another for the chatbot. The following handler code from Listing 4-1 shows one way to handle a ConversationUpdate Activity:
async Task HandleConversationUpdateAsync( ConnectorClient connector, IConversationUpdateActivity activity) { const string WelcomeMessage = “Welcome to the Rock, Paper, Scissors game! “ + “To begin, type \”rock\”, \”paper\”, or \”scissors\”. “ + “Also, \”score\” will show scores and “ + “delete will \”remove\” all your info.”; Func<ChannelAccount, bool> isChatbot = channelAcct => channelAcct.Id == activity.Recipient.Id; if (activity.MembersAdded.Any(isChatbot)) { Activity reply = (activity as Activity).CreateReply(WelcomeMessage); await connector.Conversations.ReplyToActivityAsync(reply); } if (activity.MembersRemoved.Any(isChatbot)) { // to be determined } }
The isChatbot lambda detects whether added/removed user is the chatbot. The Recipient is the chatbot, so comparing its Id to the Id of the current ChannelAccount returns true if the ChannelAccount is also the chatbot.
Because the code works with the IConversationUpdateActivity, it needs to convert activity back to the base Activity type to call CreateReply when the chatbot has been added to the conversation. Then it uses the connector parameter to send that reply back to the user. Figure 4-3 shows the Bot Emulator interaction that results in the ConversationUpdate Activity arriving at your chatbot.
FIGURE 4.3 The Bot Emulator Sending ConversationUpdate Activity.
The three callouts in Figure 4-3 show how to examine the ConversationUpdate Activity. Start at the lower right and observe that at 12:42:12, there are three POST messages. Two POSTs go to the chatbot and one to the Bot Emulator, labeled IConversationUpdateActivity to indicate the Activity implementation sent to the chatbot. The first POST sends a ConversationUpdate Activity with membersAdded set to the user. The second POST occurs when the chatbot receives the ConversationUpdate Activity and replies with the Hello Message. The third post is another ConversationUpdate Activity – this time adding the chatbot to the conversation. Clicking the POST link for the third POST, observe that Details contains the JSON formatted message sent to the chatbot. The type says conversationUpdated to indicate Activity type and membersAdded indicates that the chatbot is added to the conversation.
Any time you click the Connect button in the Connection Details panel, the Bot Emulator sends these ConversationUpdate Activities to your chatbot. You can also do this manually, as shown in Figure 4-2, you can open the menu and select the conversationUpdate item for testing. This sends a ConversationUpdate Activity to your chatbot.
Deleting User Data
A user can request that you delete their data. This request comes to you as an Activity with Type DeleteUserData. This Activity type doesn’t have an interface and the Activity is treated as a command. The following code, from Listing 4-1, shows how you could handle a DeleteUserData Activity:
async Task HandleDeleteUserDataAsync(Activity activity) { await new GameState().DeleteScoresAsync(activity); }
Reusing the GameState class, described in Chapter 3, the HandleDeleteUserDataAsync method calls DeleteScoresAsync. This deletes the user data from the Bot State Service.
Pinging
A channel might want to test whether a chatbot URL is accessible. In those cases, the chatbot receives an Activity of Type Ping. Here’s the code from Listing 4-1, showing how to handle Ping Activities:
// random methods to test different ping responses bool IsAuthorized(IActivity activity) => DateTime.Now.Ticks % 3 != 0; bool IsForbidden(IActivity activity) => DateTime.Now.Ticks % 7 == 0; void HandlePing(IActivity activity) { if (!IsAuthorized(activity)) throw new HttpException( httpCode: (int)HttpStatusCode.Unauthorized, message: “Unauthorized”); if (IsForbidden(activity)) throw new HttpException( httpCode: (int) HttpStatusCode.Forbidden, message: “Forbidden”); }
With a Ping Activity, you have three ways to respond, where the number is the HTTP status code and the text is a short description of that code’s meaning:
200 OK
401 Unauthorized
403 Forbidden
In Listing 4-2, the Post method sets statusCode to OK by default. An HttpException indicates that Ping handling resulted in something other than OK. The IsAuthorized and IsForbidden methods above are demo code to pseudo-randomly support Unauthorized and Forbidden responses. A typical application could implement those by examining the Activity instance and determining whether the user was authorized or allowed to use this chatbot – if those semantics make sense. Currently, there isn’t any guidance from Microsoft or channels for associated Ping response protocols and it’s fine to just return a 200 OK response.
Typing Indications
Sometimes you might not want to send a message to the user if they are typing. e.g. What if they provided incomplete information and the next message was an Activity of Type Typing? You might want to wait a small period of time to receive another message that might have more details. Another scenario might be that a chatbot wants to be polite and avoid sending messages while a user is typing. Here’s the ITypingActivity interface that the Bot Connector sends:
public interface ITypingActivity : IActivity { }
ITypingActivity doesn’t have properties, so you simply need to handle it as a notification that the user, indicated by the Activities From property is preparing to send a message. Here’s the handler, from Listing 4-1, for the TypingActivity:
void HandleTyping(ITypingActivity activity) { // user has started typing, but hasn’t submitted message yet }
The demo code doesn’t do anything with the Typing Activity. While the previous paragraph speculated that you might use this to decide whether to send the next response to the user, the reality might not be so simple. Consider the scenario where you have a Web API REST service scaled out across servers in Azure. The first user Message Activity arrives as one instance of the chatbot and the Typing instance arrives at a second instance of the chatbot because the user is rapidly communicating with the chatbot via multiple messages. That means if you want to react to a Typing activity, you also need to coordinate between running instances of your chatbot. How easy is that? It depends because in a world of distributed cloud computing you have to consider designs that might affect performance, scalability, and time to implement.
A more appropriate use of a Typing Activity might not be in receiving and handling the Activity from the user, but to send a Typing Activity to the user. The next section covers sending Typing Activity responses and other communication scenarios.