CHAPTER 3. Building Conversations: The Essentials

At its core, a chatbot is a conversational user interface (CUI). This is different from traditional graphical user interface (GUI) development in that the primary interface for a chatbot is text. The elements of the interface aren’t a page layout and styles as much as they are about users and the conversations that users and chatbots engage in. This approach shifts the concept of an interface from graphical to conversational and this chapter focuses on CUI essentials.

While covering conversation essentials, this chapter explains how to track users and conversations. You’ll learn how to manage conversation state with the Bot State Service and how to read and respond to user messages. You’ll also learn about activities, which are any type of information that flows from and to your chatbot.

The Rock, Paper, Scissors Game Bot

The demo chatbot for this chapter is a game called Rock, Paper, Scissors. The game is based on a two person interaction where both players count one, two, three and, on the count of three, show their hand in a gesture of a fist (Rock), flat (Paper), or two fingers (Scissors). The winner is either a rock smashes scissors, paper covers rock, or scissors cut paper. A tie occurs when both players display the same gesture. The Rock, Paper, Scissors chatbot (refered to as just chatbot in following paragraphs) plays with the user via text. Listing 3-1 though 3-3 shows the entire program. This is the RockPaperScissors1 project in the accompanying source code.

The PlayType Enum

The chatbot has a PlayType enum, a Game class, and logic in the MessagesController class that manages a game and responds to the user. Listing 3-1 though 3-3 shows each of these types, starting with Listing 3-1, showing the PlayType enum.

LISTING 3-1 The Rock, Paper, Scissors Game - PlayType Enum

namespace RockPaperScissors1.Models
{
    public enum PlayType
    {
        Rock, Paper, Scissors
    }
}

PlayType has a value for each of the choices a user or chatbot can make. The program uses PlayType to represent a user’s play, a chatbot’s play, and as part of the logic to determine who won.

The Game Class

The Game class, shown in Listing 3-2, has game result state and methods for managing a single play.

LISTING 3-2 The Rock, Paper, Scissors Game – Game Class

using System;
using System.Collections.Generic;

namespace RockPaperScissors1.Models
{
    public class Game
    {
        readonly Dictionary<PlayType, string> rockPlays =
            new Dictionary<PlayType, string>
            {
                [PlayType.Paper] = “Paper covers rock - You lose!”,
                [PlayType.Scissors] = “Rock crushes scissors - You win!”
            };
        readonly Dictionary<PlayType, string> paperPlays =
            new Dictionary<PlayType, string>
            {
                [PlayType.Rock] = “Paper covers rock - You win!”,
                [PlayType.Scissors] = “Scissors cuts paper - You lose!”
            };
        readonly Dictionary<PlayType, string> scissorsPlays =
            new Dictionary<PlayType, string>
            {
                [PlayType.Rock] = “Rock crushes scissors - You lose!”,
                [PlayType.Paper] = “Scissors cut paper - You win!”
            };

        public string Play(string userText)
        {
            string message = “”;

            PlayType userPlay;
            bool isValidPlay = Enum.TryParse(
                userText, ignoreCase: true, result: out userPlay);

            if (isValidPlay)
            {
                PlayType botPlay = GetBotPlay();
                message = Compare(userPlay, botPlay);
            }
            else
            {
                message = “Type ”Rock”, ”Paper”, or ”Scissors” to play.”;
            }

            return message;
        }

        public PlayType GetBotPlay()
        {
            long seed = DateTime.Now.Ticks;
            var rnd = new Random(unchecked((int) seed) );
            int position = rnd.Next(maxValue: 3);

            return (PlayType) position;
        }

        public string Compare(PlayType userPlay, PlayType botPlay)
        {
            string plays = $”You: {userPlay}, Bot: {botPlay}”;
            string result = “”;

            if (userPlay == botPlay)
                result = “Tie.”;
            else
                switch (userPlay)
                {
                    case PlayType.Rock:
                        result = rockPlays[botPlay];
                        break;
                    case PlayType.Paper:
                        result = paperPlays[botPlay];
                        break;
                    case PlayType.Scissors:
                        result = scissorsPlays[botPlay];
                        break;
                }

            return $”{plays}. {result}”;
        }
    }
}

In the Game class, the Dictionary<PlayType, string> dictionaries hold the rules for each play. The Play accepts a PlayType enum for both the user and the chatbot and compares them to see who won. GetBotPlay implements the chatbot’s choice, and Compare matches the user’s choice with the chatbot’s choice to indicate who the winner is. Here’s the implementation of GetBotPlay:

        public PlayType GetBotPlay()
        {
            long seed = DateTime.Now.Ticks;
            var rnd = new Random(unchecked((int) seed) );
            int position = rnd.Next(maxValue: 3);

            return (PlayType) position;
        }

GetBotPlay shows how the chatbot determines its next play. The .NET Random class generates a pseudo-random number, within the range of the underlying values of the PlayType enum, then converts that number to a PlayType enum to return to the caller.

The Compare method, repeated below, uses the Game class dictionaries to determine who won:

        public string Compare(PlayType userPlay, PlayType botPlay)
        {
            string plays = $”You: {userPlay}, Bot: {botPlay}”;
            string result = “”;

            if (userPlay == botPlay)
                result = “Tie.”;
            else
                switch (userPlay)
                {
                    case PlayType.Rock:
                        result = rockPlays[botPlay];
                        break;
                    case PlayType.Paper:
                        result = paperPlays[botPlay];
                        break;
                    case PlayType.Scissors:
                        result = scissorsPlays[botPlay];
                        break;
                }

            return $”{plays}. {result}”;
        }

The first task of Compare is to determine if both the user and chatbot played the same choices. If so, the game is a tie. Otherwise, it uses the switch statement to see who won. In this case, the userPlay determines which dictionary to use and the botPlay represents the member of that dictionary. The chosen dictionary represents the user’s choice and the index into the dictionary represents the chatbot’s choice. The result is the indexed value from the chosen dictionary.

The Play method, repeated below, pulls all of this together by obtaining the user’s PlayType, then the chatbot’s PlayType, and calling Compare to learn who won, shown below:

        public string Play(string userText)
        {
            string message = “”;

            PlayType userPlay;
            bool isValidPlay = Enum.TryParse(
                userText, ignoreCase: true, result: out userPlay);

            if (isValidPlay)
            {
                PlayType botPlay = GetBotPlay();
                message = Compare(userPlay, botPlay);
            }
            else
            {
                message = “Type ”Rock”, ”Paper”, or ”Scissors” to play.”;
            }

            return message;
        }

The Play method uses TryParse because the user could potentially type in anything, possibly not matching a value in the PlayType enum. If the user input is valid, Play calls GetBotPlay to obtain a random PlayType for the chatbot, as discussed previously. Then Play calls Compare with both plays and returns the resulting message to the caller.

When the userText doesn’t match a value in the PlayList enum, that means the chatbot can’t understand what the user wants. In that case, the else clause prints out a help message to the user to let them know what the proper plays are.


Images Note

Designing conversations is sometimes like designing algorithms. If you write your code to be correct for the happy path, then there won’t be a problem. However, when user input differs from what you expect, you’ll encounter errors and exceptions. Just like any other input, conversation text can be something you didn’t expect. In the PaperRockScissors program, the code expects a response that matches the PlayType enum. The code is also written to handle the situation where user input is something that isn’t expected and responds with a simple help message. While the PaperRockScissors program is simple, this situation highlights the type of thinking that you need to do when designing your own chatbot conversations. e.g. Would it be acceptable to attempt to interpret misspellings or abbreviations? Throughout this book, you learn different techniques for managing conversation flow. Because the conversation is the user interface, you should keep the concept of stray input on your mind while designing the chatbot conversation.


The MessagesController Class

This program delegates all of its logic into a single class, Game. The Post method, from the MessagesController class, uses Game to make a play and return the result to the user, shown in Listing 3-3.

LISTING 3-3 The Rock, Paper, Scissors Game – MessagesController Class

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using RockPaperScissors1.Models;

namespace RockPaperScissors1
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                var connector = new ConnectorClient(new Uri(activity.ServiceUrl));

                var game = new Game();

                string message = game.Play(activity.Text);

                Activity reply = activity.CreateReply(message);
                await connector.Conversations.ReplyToActivityAsync(reply);
            }

            HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }
    }
}

The MessagesController class has the Post method, described in Chapter 2, which is the HTTP endpoint that the Bot Connector calls when the user sends a message from the chat window. After instantiating the ConnectorClient, the code instantiates a Game instance, calls Play with the user’s message, activity.Text, and responds to the user with the string returned from Play.

In practice, you don’t want Post to contain any business logic, but rather act as a controller for accepting user response, passing parameters to the business logic for processing, and responding to the user. This helps manage separation of concerns between the various layers of your application.

If you run the RockPaperScissors application and run the emulator, addressing the chatbot endpoint, as described in Chapter 2, you should see a similar experience to Figure 3-1. Remember that the port number specified in the emulator must match the port number from the address of the chatbot, which you can find in the browser address bar, such as how. the port number in Figure 3.1 is 3979. You also need the api/messages suffix to properly identify the Web API endpoint where the chatbot resides.

Images

FIGURE 3-1 Playing the Rock, Paper, Scissors Game.

Figure 3-1 shows that typing a case insensitive member of the PlayType enum results in a play and anything else results in a help message. In the next section, you learn more about the participants in the conversation and ways to work with conversation state.

Conversation State Management

In the Bot Framework, you can track both users and conversations. This section explains how to read user and conversation data. There’s also a Bot Connector service that allows you to save user and conversation state, which is covered here too.

Elements of a Conversation

Conversations have identifiers for users, chatbots, and the conversation between the user and chatbot. The following sections discuss this and show how to access these values with code.

The Conversation

A conversation is a set of request and response message activities between a user and a chatbot. You can track a conversation by a conversation identifier associated with a message. The first message in a conversation is always initiated by a user, when they invite the chatbot into their channel and send their first message. That message arrives with a conversation identifier. In Chapter 4, you’ learn how a chatbot can initiate a conversation, but that can only happen if the chatbot knows which user it wants to communicate with, which can only happen if the user has previously communicated with the chatbot.

Activity State

The basic element of a conversation is an Activity. The Bot Connector populates Activity properties to make it convenient for a chatbot to reason about the state of the conversation on a single interaction with the user. Here’s an excerpt of the Activity class, with properties relevant to conversation state:

    public partial class Activity
    {
        public string Type { get; set; }
        public string Id { get; set; }
        public DateTime? Timestamp { get; set; }
        public DateTimeOffset? LocalTimestamp { get; set; }
        public string ServiceUrl { get; set; }
        public string ChannelId { get; set; }
        public ChannelAccount From { get; set; }
        public ConversationAccount Conversation { get; set; }
        public ChannelAccount Recipient { get; set; }
    }

As shown in Listing 3-3, the Type property indicates the purpose of the Activity, which checked for ActivityType.Message in that case. Later sections of this chapter examine other ActivityType members. ServiceUrl is useful because you don’t need to remember the URL of the Bot Connector, which might not always be the same. The Id property is useful for logging the identity of that particular Activity or help with debugging and you’ll learn how important it is in building replies to a message. Timestamp is the UTC time of the message, but LocalTimestamp is the local time of the user. These might help approximate the user’s timezone in addition to other logic that might be useful for a chatbot. The ChannelId tells you which channel the user is communicating from. The next section explains From and Recipient properties.

User and Chatbot Identities

To personalize chatbots, you need to identify the user, which is what the From property allows. Similarly the Recipient property identifies the chatbot. There might be future scenarios, like support for group conversations, but this chapter assumes a single conversation between chatbot and user, meaning that the Recipient is the chatbot. Both the From and Recipient properties are type ChannelAccount, shown here:

    public partial class ChannelAccount
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

Images Note

This section describes Recipient as the chatbot and From as the user. However, in the Building a Custom Message Activity section of this chapter, you’ll see how the From becomes the chatbot and the Recipient becomes the user when the chatbot needs to send a response to the user.


The channel that the user is communicating on assigns the Id property, which uniquely identifies the user. The channel also populates the Name property, which you might want to use to address the user by their name. Many messaging clients allow users to change their name, so the Name property can change, while the Id property is constant for that channel.


Images Note

The From.Id property is unique to a channel, identified by Activity.ChannelId. If you publish to different channels, it’s possible that the same user communicates with a chatbot over different channels. In that case, it’s the same user, but ActivityChannelId and From.Id are different.


Now that you can find state associated with users and chatbots, the next section discusses the Conversation property.

Conversation Identity

All communication between users and chatbots occur in the scope of a conversation. The Conversation property, shown below, contains properties to represent details of the conversation:

    public partial class ConversationAccount
    {
        public bool? IsGroup { get; set; }
        public string Id { get; set; }
        public string Name { get; set; }
    }

Each conversation has a unique Id and Name. The IsGroup property indicates that a channel supports groups. Only a limited number of channels support groups, so this doesn’t always apply.


Images Note

The Bot Connector doesn’t define the lifetime of a conversation. Rather, channels define when conversations start and end. A new conversation starts when a user first communicates with your chatbot, but when that conversation ends and a new conversation begins is undefined. Unless a channel specifically states what the rules are for a conversation beginning and endings, you shouldn’t make any assumptions about conversation lifetime.


The user, chatbot, and conversation identification just discussed help you track information and state associated with conversations, and the next section discusses how the Bot Framework can help with this.

Saving and Retrieving State

One of the services of the Bot Connector is state management, called the Bot Framwork State Service. For Bot Framework purposes, state is any data or information you need to save to support the operation of your chatbot. You can save state for a user, a conversation, or a user in a conversation (aka private).

The Bot Connector state management service capacity is 32kb for either user, conversation, or private states. The largest amount of data you can have in user state is 32kb. Additionally, this state is scoped by channel. For example, you can have 32kb for user state on channel A, 32kb for user state on channel B, 32kb for conversation state on channel A. The following sections describe how to use the Bot Connector state management service.


Images Tip

The Bot Connector state management service isn’t meant to be a generalized store of all user and conversation data. If you need more space for tracking user and conversation data, it’s best to use your own database solution.



Caching with Multiple Chatbots in the Same Project

There are times when you might have solutions with more than one chatbot project. This can confound testing because the default setup with the Visual Studio development environment and IIS Express uses aggressive web page caching. The result is that once a chatbot is running, it’s cached by the server. The problem occurs when running the second chatbot because the first is cached and you never see the second. There might be multiple fixes to this situation such as disabling browser link, closing IIS Express applications, and even clearing browser cache. However, a quick fix is to change the port number for the chatbot’s URL by double-clicking the project’s Properties folder, going to the Web tab, and changing the port number in the Project URL box, as shown in Figure 3-2.

Images

FIGURE 3-2 Change the port number of the Project URL to avoid page caching problems when testing multiple chatbots in the same project.

In Figure 3-2, you can see that the port for the Project URL is changed from the default 3979 to 3978. The reason this works is because the web page caching strategy affecting this is based on the page URL. Changing the port number changes the URL and ensures the browser displays the proper page.


Bot State Service Capabilities and Organization

As mentioned earlier, the Bot State Service allows you to manage state for either a single user, a conversation, or private (user in a conversation). The difference between user and private is that, for a single user, the user state is maintained across all conversations, whereas private is user data in only that one conversation, but not in other conversations. Conversation state holds data for only one conversation and is available for all users in that conversation.

You’ll see code for how to use this service in more detail in the Using the Bot State Service section of this chapter, but first you’ll see an overview of the types required to make this happen and their relationships. The first type you’ll need is an instance of the Bot Framework’s StateClient, which implements IStateClient, shown below:

    public partial interface IStateClient : IDisposable
    {
        Uri BaseUri { get; set; }
        JsonSerializerSettings SerializationSettings { get; }
        JsonSerializerSettings DeserializationSettings { get; }
        ServiceClientCredentials Credentials { get; }
        IBotState BotState { get; }
    }

The BaseUri and Credentials are the address and username/password, respectively, for communicating with the Bot State Service. Fortunately, you won’t have to set up these details because the Activity type has a convenience method that you’ll learn about in Responding to Conversations section of this chapter. The Bot Framework uses the popular open-source Json.NET package for managing data payloads, so the SerializationSettings and DeserializationSettings are types belonging to Json.NET, which is where you can find guidance on how to configure those. Generally, using default serialization as you’ll see in this chapter, is sufficient for serialization and you won’t need to work with the StateClient serialization settings yourself.

When working with state, you always use the BotState, which allows you to choose between user, conversation, and private state. BotState implements IBotState, shown below:

    public partial interface IBotState
    {
        public static async Task<BotData> GetUserDataAsync(
            this IBotState operations, string channelId, string userId, 
            CancellationToken cancellationToken = default(CancellationToken));
               
        public static async Task<BotData> SetUserDataAsync(
            this IBotState operations, string channelId, string userId, BotData botData,
            CancellationToken cancellationToken = default(CancellationToken));
               
        public static async Task<string[]> DeleteStateForUserAsync(
            this IBotState operations, string channelId, string userId, 
            CancellationToken cancellationToken = default(CancellationToken));
               
        public static async Task<BotData> GetConversationDataAsync(
            this IBotState operations, string channelId, string conversationId, 
            CancellationToken cancellationToken = default(CancellationToken));
               
        public static async Task<BotData> SetConversationDataAsync(
            this IBotState operations, string channelId, 
            string conversationId, BotData botData, 
            CancellationToken cancellationToken = default(CancellationToken));
               
        public static async Task<BotData> GetPrivateConversationDataAsync(
            this IBotState operations, string channelId, 
            string conversationId, string userId, 
            CancellationToken cancellationToken = default(CancellationToken));
               
        public static async Task<BotData> SetPrivateConversationDataAsync(
            this IBotState operations, string channelId, 
            string conversationId, string userId, BotData botData, 
            CancellationToken cancellationToken = default(CancellationToken));
    }

The IBotState shows set and get methods for user, conversation, and private data. The DeleteStateForUserAsync method deletes all user data across all conversations for the specified channel. You might have noticed that each of the methods shown in the IBotState interface are async.

There are also non-async methods that mirror each of the async methods. The non-async methods use the same implementation as async methods, so either is technically correct. Our preference of the two is async because we believe it hints at the fact that there is an out-of-process Internet call happening, which might help with design and maintenance for performance and scalability.


Images Note

The IBotState code in this chapter shows methods that you won’t see on the IBotState in the Bot Framework source code. The physical implementation is that these methods are really extension methods of the BotStateExtensions class that extend IBotState. In this chapter, we depict the abstraction, rather than the more complex implementation, which we believe is more practical.


The response type from DeleteStateForUserAsync is string[], which would hold any messages that the server responded with. It the server doesn’t return response messages, the return values is null. For all IBotState methods, if the server returns an error, the method throws an HttpOperationException with Request, Response, and Body properties for code to examine and handle as it sees fit.

Besides DeleteStateForUserAsync, all other IBotState methods return a BotData, shown below.

    public partial class BotData
    {
        public string ETag { get; set; }
        public object Data { get; set; }
    }

The Bot State Service uses ETag for optimistic concurrency. If a first chatbot reads, changes, and writes data back to the state service, while a second chatbot reads that same data after the first, but writes its changes before the first chatbot, the first chatbot’s write throws an HttpOperationException. This indicates to the first chatbot that it’s operating on stale data.

Remembering that the BotData instance came from a method call to get state in either user, conversation, or private context, the Data property holds the JSON-formatted data for the requested context. BotData also has GetProperty, SetProperty, and RemoveProperty methods, which do exactly what their names imply. The next section shows you how to use the types and methods just presented to manage chatbot state.

Using the Bot State Service

The examples in this section, showing how to work with the Bot Framework State Service, build on the RockPaperScissors program from Listings 3-1 to 3-3. In this case, you’ll see a new class that sets and reads state in addition to modifications to the Post method in Listing 3-3. This program is in the RockPaperScissors2 project of the accompanying source code.

The goal is to use the Bot State Service to hold scores. The scores show the tallies of the 10 most recent plays with how many user wins, how many chatbot wins, and how many ties. Listing 3-4 shows the GameState class for managing state and Listing 3-5 (shown later in this chapter) presents the new Post method for handling score requests and keeping the score updated.

LISTING 3-4 The Rock, Paper, Scissors Game – GameState Class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;

namespace RockPaperScissors2.Models
{
    public class GameState
    {
        [Serializable]
        class PlayScore
        {
            public DateTime Date { get; set; } = DateTime.Now;
            public bool UserWin { get; set; }
        }

        public async Task<string> GetScoresAsync(Activity activity)
        {
            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.”;
            }
        }

        public async Task UpdateScoresAsync(Activity activity, bool userWin)
        {
            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)
                    scoreQueue = new Queue<PlayScore>();

                if (scoreQueue.Count >= 10)
                    scoreQueue.Dequeue();

                scoreQueue.Enqueue(new PlayScore { UserWin = userWin });

                chatbotData.SetProperty<Queue<PlayScore>>(property: “scores”, data: scoreQueue);
                await chatbotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, chatbotData);
            }
        }

        public async Task<string> DeleteScoresAsync(Activity activity)
        {
            using (StateClient stateClient = activity.GetStateClient())
            {
                IBotState chatbotState = stateClient.BotState;

                await chatbotState.DeleteStateForUserAsync(activity.ChannelId, activity.From.Id);

                return “All scores deleted.”;
            }
        }

        public async Task AddTieAsync(Activity activity)
        {
            using (StateClient stateClient = activity.GetStateClient())
            {
                IBotState chatbotState = stateClient.BotState;
                BotData chatbotData = await chatbotState.GetUserDataAsync(
                    activity.ChannelId, activity.From.Id);

                int ties = chatbotData.GetProperty<int>(property: “ties”);

                chatbotData.SetProperty<int>(property: “ties”, data: ++ties);

                await chatbotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, chatbotData);
            }
        }
    }
}

The GameState class has a nested PlayScore class that holds the result of a play, where either the user or chatbot won, shown here:

        [Serializable]
        class PlayScore
        {
            public DateTime Date { get; set; } = DateTime.Now;
            public bool UserWin { get; set; }
        }

The Date tracks when the play occurred and a bool UserWin property, indicating if the user won that game. Notice the Serializable attribute decorating PlayScore. Because the chatbot communicates over the Internet, all types must be serializable, allowing them to be formatted properly for transmission.

The GetScoresAsync method, shown below, reads scores from the Bot State Service. It uses the types and methods you learned about in the previous section.

        public async Task<string> GetScoresAsync(Activity activity)
        {
            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.”;
            }
        }

If you recall from the previous section, the StateClient implements IStateClient. The using statement in the code above shows how to create a StateClient instance, using the CreateStateClient factory method of the Activity instance parameter, activity.

The chatbotState variable holds the IBotState from stateClient. From chatbotState, the code can get a BotData instance for either the user, conversation, or private context, but calls GetUserDataAsync for user context. Also, observe that the arguments indicate which channel this state is associated, activity.ChannelId, with and the user’s id, activity.From.Id.

At this point, the code has an instance of BotData, chatbotData. This is what you will always do in preparing to get, set, or remove data. You’ll see the exact same pattern in all of the GameState methods.

The Queue of PlayScore helps manage the top 10 scores. The code calls chatbotData.GeProperty to get a reference to the scores property. The first time a user plays, the Data property of BotData is null because there isn’t any data. The code could check that too, but because Data can have more than one property, and it does in this program, that wouldn’t indicate whether the Queue was missing. The Queue collection is serializable, so it works fine as a state service property.


Images Tip

By design, the example in this chapter only keeps track of the last 10 plays. While this might not be the exact strategy everyone would use, it demonstrates one way to manage resources. Remember, there are resource considerations, like storage space and bandwidth, leading to the intentional choices made in the Rock, Paper, Scissors program.


The next statements collect number of plays, userWins, and chatbotWins, where a chatbotWin is defined as the user not winning. One thing you might be wondering is how the program keeps track of ties. That’s handled in the chatbotData.GetProperty call for the ties property, which is the second property, besides scores, in chatbotData. After that, we return a message to the user. Before examining the code that calls GetScoresAsync, let’s look at the other GameState methods.

You’ve seen how to read state and the UpdateScoresAsync shows how to write state. Essentially, it gets the scores Queue, updates the Queue with a new score, and writes the Queue back to the Bot State Service, as shown below.

        public async Task UpdateScoresAsync(Activity activity, bool userWin)
        {
            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)
                    scoreQueue = new Queue<PlayScore>();

                if (scoreQueue.Count >= 10)
                    scoreQueue.Dequeue();

                scoreQueue.Enqueue(new PlayScore { UserWin = userWin });

                chatbotData.SetProperty<Queue<PlayScore>>(property: “scores”, data: scoreQueue);
                await chatbotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, chatbotData);
            }
        }

As mentioned in the GetScoresAsync walkthrough, the code to get BotData follows the same pattern. Similarly, this code calls chatbotData.GetProperty on the scores property. If the user hasn’t ever played this game or deleted their data, as explained in a later part of this section, then the scoreQueue will be null, indicating that the code must instantiate a new Queue of PlayScore. Since the program only manages the 10 most recent scores, it calls scoreQueue.Dequeue to remove the oldest. Then it calls scoreQueue.Enqueue to add the new, most recent, score.

The program uses chatbotData.SetProperty to assign scoreQueue to the scores property in BotData. At this point, you could set multiple properties if it made sense for a chatbot. Calling chatbotState.SetUserDataAsync posts the property changes back to the Bot State Service for the user on the channel they’re communicating on.

In addition to reading and writing, chatbots can delete state. The following DeleteScoresAsync Method, from Listing 3-4, shows how.

        public async Task<string> DeleteScoresAsync(Activity activity)
        {
            using (StateClient stateClient = activity.GetStateClient())
            {
                IBotState chatbotState = stateClient.BotState;

                await chatbotState.DeleteStateForUserAsync(activity.ChannelId, activity.From.Id);

                return “All scores deleted.”;
            }
        }

The DeleteScoresAsync method doesn’t need BotData because it uses chatbotState, to call the IBotState.DeleteStateForUserAsync. This deletes all of the user’s state in user, conversation, and private contexts.


Images Tip

The IBotState.DeleteStateForUserAsync method removes all properties from a user’s data. If you needed a more surgical technique for removing only one property, get a reference to BotData, using the typical pattern shown in multiple code listings in this section, and call chatbotData.RemoveProperty().


Most of the examples so far show how to work with complex objects, which is the Queue of PlayScore in this case. The only exception was the GetScoresAsync method, reading the ties property, which is a primitive type object. The AddTieAsync method, below, shows how to write a primitive type property:

        public async Task AddTieAsync(Activity activity)
        {
            using (StateClient stateClient = activity.GetStateClient())
            {
                IBotState chatbotState = stateClient.BotState;
                BotData chatbotData = await chatbotState.GetUserDataAsync(
                    activity.ChannelId, activity.From.Id);

                int ties = chatbotData.GetProperty<int>(property: “ties”);

                chatbotData.SetProperty<int>(property: “ties”, data: ++ties);

                await chatbotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, chatbotData);
            }
        }

After the standard pattern for getting BotData, the method calls chatbotData.GetProperty to read the ties property. Then it calls chatbotData.SetProperty, also using the pre-increment operator on ties to increase the number of ties to its new value. Just as in the UpdateScoresAsync method, the AddTieAsync method calls chatbotState.SetUserDataAsync to post the new ties property value back to the Bot State Service.

Now you know how to use the Bot State Service, so the only thing left to do is tie it together to see how the user can interact with the chatbot to interact with scores. Figure 3-3 shows a user playing and using new score and delete commands. After playing a while, the user types score and receives a message indicating what the score is. Then the user types delete and receives a message that user information has been deleted. Finally, the user types score again and since the system doesn’t have a score, it suggests that the user play to create a score first.

Images

FIGURE 3-3 The score and delete commands show scores or removes scores, respectivly.


Images Warning

The Bot Emulator only stores state for the current session. If you shut down and restart the emulator, state is cleared. It will be as if you’ve starting from scratch, which you are. To work around this for testing state operations while working with code, leave the emulator running and only close the chatbot application (browser window). Then you can re-run your chatbot, which starts a new browser instance, and then begin using the emulator again, with the same state intact.


To make these commands work, the chatbot must read the user’s message and execute the proper logic. Listing 3-5 shows the modifications to the Post method to make score and delete commands work.

LISTING 3-5 The Rock, Paper, Scissors Game – MessagesController Class With Score and Delete Commands

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using RockPaperScissors2.Models;

namespace RockPaperScissors2
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                var connector = new ConnectorClient(new Uri(activity.ServiceUrl));

                string message = await GetMessage(activity);

                Activity reply = activity.CreateReply(message);
                await connector.Conversations.ReplyToActivityAsync(reply);
            }

            HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }

        async Task<string> GetMessage(Activity activity)
        {
            var state = new GameState();

            string userText = activity.Text.ToLower();
            string message = string.Empty;

            if (userText.Contains(value: “score”))
            {
                message = await state.GetScoresAsync(activity);
            }
            else if (userText.Contains(value: “delete”))
            {
                message = await state.DeleteScoresAsync(activity);
            }
            else
            {
                var game = new Game();
                message = game.Play(userText);

                bool isValidInput = !message.StartsWith(“Type”);
                if (isValidInput)
                {
                    if (message.Contains(value: “Tie”))
                    {
                        await state.AddTieAsync(activity);
                    }
                    else
                    {
                        bool userWin = message.Contains(value: “win”);
                        await state.UpdateScoresAsync(activity, userWin);
                    } 
                }
            }

            return message;
        }
    }
}

The first change in Listing 3-5 is that the logic is moved from Post to a GetMessage method. The GameState instance, state, is the same as the GameState class in Listing 3-4. The code first looks to see if the user typed score or delete and calls state.GetScoresAsync or DeleteScoresAsync, respectively. Otherwise, the program handles the user message as a game play, using the same Game class from Listing 3-2.

When the response starts with Type, that means the user entered something the chatbot doesn’t recognize and the message is “Type ”Rock”, ”Paper”, or ”Scissors” to play.” If the game was a tie, the code calls state.AddTieAsync to increment the tie count. Otherwise, it determines if the user won and calls state.UpdateScoresAsync to record the new score.

That covers the Bot State Service. The next section goes deeper into the Activity type to help understand Activity internals.

Participating in Conversations

Previous sections of this chapter discussed ways of listening and interpreting the meaning of an Activity, but only briefly addressed the response, which was a CreatReply method that did all the work of building a response object. This section discusses the other side of the conversation, when a chatbot sends a message to the user. Here, we’ll look deeper into a message Activity.

Responding to Conversations

When an Activity arrives at a chatbot, as the parameter to the Post method, the Bot Connector already constructed the details. Similarly, the Bot Framework supports the common case of preparing an Activity for a reply via convenience methods. To help you understand the nature of an Activity, this section breaks an Activity down and shows how it’s manually created in case you need to perform your own customization of responses in the future.

Previous listings use the CreateReply convenience method, shown below, to prepare a reply message:

        Activity reply = activity.CreateReply(message);

The CreateReply method also offers an optional locale parameter to let the user’s client software know which language the chatbot is communicating in. The default locale is en-US:

    Activity reply = activity.CreateReply(message , locale: “en-US”);

Building a Custom Message Activity

While most of your work can use CreateReply to save time, you might have a need to build your own Activity. Listing 3-6 shows how it’s done. This is from the RockPaperScissors3 project in the source code for this chapter.

LISTING 3-6 The Rock, Paper, Scissors Game – BuildMessageActivity Extension Method

using Microsoft.Bot.Connector;

namespace RockPaperScissors3.Models
{
    public static class ActivityExtensions
    {
        public static Activity BuildMessageActivity(
            this Activity userActivity, string message, string locale = “en-US”)
        {
            IMessageActivity replyActivity = new Activity(ActivityTypes.Message)
            {
                From = new ChannelAccount
                {
                    Id = userActivity.Recipient.Id,
                    Name = userActivity.Recipient.Name
                },
                Recipient = new ChannelAccount
                {
                    Id = userActivity.From.Id,
                    Name = userActivity.From.Name
                },
                Conversation = new ConversationAccount
                {
                    Id = userActivity.Conversation.Id,
                    Name = userActivity.Conversation.Name,
                    IsGroup = userActivity.Conversation.IsGroup
                },
                ReplyToId = userActivity.Id,
                Text = message,
                Locale = locale
            };

            return (Activity)replyActivity;
        }
    }
}

The ActivityExtensions class, in Listing 3-6 holds the BuildMessageActivity extension method for the Activity class. The BuildMessageActivity has a message and optional locale parameter. The userActivity is the Activity that this message is being built to reply to.

This example uses object initialization syntax to create a new Activity instance. The Activity class also has CreateXxx factory methods that produce activities of various types, where Xxx is the type, which would have been Activity.CreateMessageActivity() in this case. This is another way to do the same thing.

Because the message is built to reply to the original message, the From ChannelAccount values populate from the userActivity.Recipient values, which is the chatbot. Similarly, the Recipient ChannelAccount values populate from the userActivity.From values, which is the user who sent the original message. The code builds the Activity to come from the chatbot to the user.

The Conversation is the same conversation as the user’s. Text is the message parameter and Locale is the locale parameter.

Notice the ReplyToId parameter, being set with the userActivity.Id. This indicates to the Bot Connector that this Activity, from the chatbot, is a reply to the user’s Activity.


Images Warning

Forgetting to set the ReplyToId results in a ValidationException thrown for activityId cannot be null. That means the code must set the ReplyToId of the reply Activity with the value of the Id property of the user’s Activity.


Using a Custom Message Activity

Now that you know how to create a custom Activity from scratch, you can see how it works. Listing 3-7 shows modifications to the Post method that calls BuildMessageActivity.

LISTING 3-7 The Rock, Paper, Scissors Game – Post Method Changes for BuildMessageActivity

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
using RockPaperScissors3.Models;

namespace RockPaperScissors3
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                var connector = new ConnectorClient(new Uri(activity.ServiceUrl));

                string message = await GetMessage(connector, activity);

                Activity reply = activity.BuildMessageActivity(message);

                await connector.Conversations.ReplyToActivityAsync(reply);
            }

            HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }

        async Task<string> GetMessage(ConnectorClient connector, Activity activity)
        {
            var state = new GameState();

            string userText = activity.Text.ToLower();
            string message = “”;

            if (userText.Contains(value: “score”))
            {
                message = await state.GetScoresAsync(activity);
            }
            else if (userText.Contains(value: “delete”))
            {
                message = await state.DeleteScoresAsync(activity);
            }
            else
            {
                var game = new Game();
                message = game.Play(userText);

                bool isValidInput = !message.StartsWith(“Type”);
                if (isValidInput)
                {
                    if (message.Contains(value: “Tie”))
                    {
                        await state.AddTieAsync(activity);
                    }
                    else
                    {
                        bool userWin = message.Contains(value: “win”);
                        await state.UpdateScoresAsync(activity, userWin);
                    } 
                }
            }

            return message;
        }
    }
}

The code in Listing 3-7 is equivalent to Listing 3-5 with one exception. In the Post method, the call to activity.CreateReply(message) is replaced with activity.BuildMessageActivity(message).

Summary

This chapter introduced the Rock, Paper, Scissors chatbot. Different sections of this chapter modified that program to show various concepts and ways to manage conversations. The Conversation, Activity, and identity types are core elements of conversations and you saw their properties and relationships between them. You learned how to use the Bot State Service and how it supports user, conversation, and private state. The explanation covered the essential types and their relationships and the code showed how to work with complex and primitive state. Finally, you built a custom message activity, showing some of the internals of the Activity type.

The next chapter builds on this one by discussing Activities and other ways to participate in conversations. The difference will be in illustrating how the Bot Emulator supports debugging and testing chatbots.

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

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