CHAPTER 4. Fine-Tuning Your Chatbot

Most software development projects have common tasks for design, coding, and deployment. There are also a common set of tasks surrounding maintenance, quality, security, and user experience that are often not documented, but remain vital considerations for a healthy project. These are continuous tasks, so it’s important to learn about ways to approach them early with the Bot Framework and carry the skillset through the rest of this book and beyond. We are pulling these additional tasks together under a general category of fine-tuning.

You’ve used the Bot Emulator in previous chapters to interact with chatbots, but that was a minimal introduction. This chapter does a deep dive of Bot Emulator features and shows how to perform testing from user to chatbot. This chapter is also a continuation of Chapter 3, “Building Conversations: The Essentials,” where there’s a detailed explanation of Activities, communications between a chatbot and the Bot Connector, and how the Bot Emulator facilitates testing Activities.

Reviewing Bot Emulator Details

In addition to having conversations, the Bot Emulator offers several other features for testing and verifying the operation of a chatbot. At first glance, you can see that the Bot Emulator has sections for Connection Details, Conversation Display, Message Input, Details, and Log. Figure 4-1 shows each section of the Bot Emulator.

Images

FIGURE 4-1 Sections of the Bot Emulator.

Clicking the address bar at the top left causes the Bot Emulator to display the Connection Details area. Chapter 2 explained how to obtain Microsoft App ID and Microsoft App Password from the Registration page on the Microsoft Bot Framework page. The Locale field, containing en-US lets you populate a custom locale for testing localization. In the code, you can read that value via the Activity.Locale field. Clicking Connect hides the Connection Details window and enables the Message Input box.

As shown several times in previous chapters, type any text into the Message Input field and either press Enter or click the arrow button on the right to send a message to the chatbot. Clicking the button on the left of the Message Input, with the picture icon, opens the File dialog so you can attach a file and sent it to the chatbot, which is covered in more detail in Chapter 10.

Sending a message to the chatbot also shows what you typed on the right side of the Conversation Display. Messages from the chatbot show on the left side of the Conversation Display. Notice the dark highlight on the final message in the Figure 4-1 Message Display, ending with You win. This displays the message sent to the chatbot to appear in the Details section.

The Details section shows messages between Bot Emulator and chatbot. Details shows JSON formatted messages. Read the message in Details any time you want to know the exact details of the data transferred between Bot Emulator and chatbot.

Use the Log section to see the type of messages between Bot Emulator and chatbot. This shows time, HTTP status, and a quick description of what transferred. Figure 4-1 shows POST and GET messages for the current conversation. The timings give you some idea of when a message transferred and how long it took between messages and the arrows show the direction of the messages, where pointing right is a message to the chatbot, and pointing left is a message to the Bot Emulator. There is a hyperlink on each status that you click to make the message display in the Details section.

The Bot Emulator also lets you test Activities, which you’ll learn about in the next section.

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; }
}

Images Note

Version 1 of the Bot Framework passed a Message class instance as the single parameter to the Post method. In version 3, the Bot Framework replaced Message with an Activity class instance instead. This makes sense because Message is only a single type of information that the Bot Framework supports and the general semantics of the name, Activity, encompases other notification types.


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

Images Note

As the Bot Framework matures, you might see new activity types introduced. Some members might be added to ActivityTypes to reflect a future possibility, yet still be undocumented until full implementation.


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.

Images

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; }
}

Images Note

There’s Bot Framework support for when a user leaves a conversation, represented by an Activity implementing IConversationUpdateActivity with the MembersRemoved property. However, the Bot Framework doesn’t define when this will happen. While the leaving a conversation scenario might be defined in the future, you might not want to consider the undefined nature of this feature in your design.


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.

Images

FIGURE 4-3 The Bot Emulator Sending ConversationUpdate Activity.


Images Tip

Sending an initial Hello message to a user is regarded as a best practice. The message should contain a greeting with information on how the user can use the chatbot. This lets the user know that the chatbot is alive and helps the user acclimate faster. Handling the ConversationUpdate Activity is the perfect place to do this.


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.


Images Warning

Various governments have laws requiring you to delete user information if the user requests that you do so. This book shows a couple out of many possible technical approaches to deleting user data and does not provide legal advice. You are advised to seek the council of an attorney for more questions.


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:

Images 200 OK

Images 401 Unauthorized

Images 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.

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.

Images

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:

1. Run the RockPaperScissors4 chatbot.

2. Run the Bot Emulator, select the chatbot URL, and click Connect.

3. Play at least one round of the RockPaperScissors game. e.g. type scissors.

4. 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.

5. 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.

6. Save the ConversationReference.json file.

7. Right-click the RockPaperScissorsNotifier1 project and select Debug | Start new instance.

8. You have a choice to select 1 or 2. Select 1 and the program will close.

9. Open the Bot Emulator and observe that there is a new message on the screen, as shown in Figure 4-5.

Images

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.


Images Tip

If you run RockPaperScissorsNotifier1 and first select option #2 to start a new conversation, that will work. However, that also clears the previous conversation and running the program. Thereafter, selecting option #1 for an existing conversation won’t work because the emulator state doesn’t reflect the old conversation. To work around this, go back to Step #1, start the conversation, copy the file and try again. While this is an artificiality of the demo environment, it’s also an opportunity to understand more about the nature of activities and conversations.


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.


Images Note

Another way to send a message is to address multiple recipients for group scenarios. As of this writing, the only channels supporting groups is email and Skype. You can check with other channels in the future to see if they eventually support groups.


Summary

This chapter was a conglomeration of subjects on testing, Activity management, and advanced conversation techniques. You should become familiar with the Bot Emulator because it offers many features for testing, including message details, logging, and simulating Activities.

The section on Activities went in-depth on Activities other than Messages. You learned how to handle when the user adds a chatbot to their friends list, when a user first communicates, and how to detect when the user is typing. Other Activities include Ping for when a channel wants to know if it can communicate with your chatbot and Typing to let a chatbot know that the user is typing.

Finally, you learned about different ways to send messages other than passively waiting and replying to something the user said. This includes the ability to send a Typing message to the user, sending a message to an existing conversation, and starting a new notification/alert style conversation with the user.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.202.187