CHAPTER 5. Building Dialogs

Previous chapters built a game chatbot named Rock, Paper, Scissors. It was a simple chatbot in that it mostly responded to single word commands. Interactions were largely limited to a single request and response pair. That worked well for that situation, but there are times when a user needs a more in-depth conversation with a chatbot. For example, filling out forms, getting help, or ordering a product or service can often lead to a series of interactions to share information. A more sophisticated set of tools for conversations is Bot Builder dialogs.

This chapter shows how to use dialogs for conversations. You’ll learn the essential parts of a dialog class and the reasons each part exists. You’ll learn how to manage a conversation. You’ll also learn about convenience methods for prompting the user and receiving their responses.

Introducing WineBot

This chapter shifts away from a game theme to a type of chatbot that supports ordering or storefront interfaces. The particular example is a chatbot named WineBot. The purpose of WineBot is to let the user perform searches for a wine that they’re interested in. To perform searches, the chatbot needs to ask the user several questions to learn what they want. WineBot serves as an example of how you can use Bot Builder dialogs to manage a conversation.

Figure 5-1 shows the beginning of a typical session with WineBot. After an initial welcome message, the user kicks off the conversation and answers a series of questions, resulting in a list of wines matching the criteria based on the user’s answers to the questions.

Images

FIGURE 5-1 Chatting with WineBot.

The menu options, shown in Figure 5-1, and search results come from the Wine.com API. This API allows retrieving catalogs, performing searches for wine, and more. WineBot uses a subset of the Wine.com API, which the next section discusses.

Using the Wine.com API

We’ll get to how the chatbot works soon, but let’s first look at how WineBot gets its data. The Wine.com API uses a REST interface and WineBot communicates with it through HTTP GET requests. Listing 5-1 through Listing 5-4 show the Status, WineCategories, WineProducts, and WineApi classes that handle all of this communication.


Images Note

To run this program, you need an API key from Wine.com. You can find the Wine.com API documentation at https://api.wine.com/.


LISTING 5-1 WineBot – Status Class

namespace WineBot
{
    public class Status
    {
        public object[] Messages { get; set; }
        public int ReturnCode { get; set; }
    } 
}

LISTING 5-2 WineBot – WineCategories Class

using System;

namespace WineBot
{
    public class WineCategories
    {
        public Status Status { get; set; }
        public Category[] Categories { get; set; }
    }

    public class Category
    {
        public string Description { get; set; }
        public int Id { get; set; }
        public string Name { get; set; }
        public Refinement[] Refinements { get; set; }
    }

    [Serializable]
    public class Refinement
    {
        public string Description { get; set; }
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }
}

LISTING 5-3 WineBot – WineProducts Class

namespace WineBot
{
    public class WineProducts
    {
        public Status Status { get; set; }
        public Products Products { get; set; }
    }

    public class Products
    {
        public List[] List { get; set; }
        public int Offset { get; set; }
        public int Total { get; set; }
        public string Url { get; set; }
    }

    public class List
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public Appellation Appellation { get; set; }
        public Label[] Labels { get; set; }
        public string Type { get; set; }
        public Varietal Varietal { get; set; }
        public Vineyard Vineyard { get; set; }
        public string Vintage { get; set; }
        public Community Community { get; set; }
        public string Description { get; set; }
        public Geolocation1 GeoLocation { get; set; }
        public float PriceMax { get; set; }
        public float PriceMin { get; set; }
        public float PriceRetail { get; set; }
        public Productattribute[] ProductAttributes { get; set; }
        public Ratings Ratings { get; set; }
        public object Retail { get; set; }
        public Vintages Vintages { get; set; }
    }

    public class Appellation
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public Region Region { get; set; }
    }

    public class Region
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public object Area { get; set; }
    }

    public class Varietal
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public Winetype WineType { get; set; }
    }

    public class Winetype
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }

    public class Vineyard
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public string ImageUrl { get; set; }
        public Geolocation GeoLocation { get; set; }
    }

    public class Geolocation
    {
        public int Latitude { get; set; }
        public int Longitude { get; set; }
        public string Url { get; set; }
    }

    public class Community
    {
        public Reviews Reviews { get; set; }
        public string Url { get; set; }
    }

    public class Reviews
    {
        public int HighestScore { get; set; }
        public object[] List { get; set; }
        public string Url { get; set; }
    }

    public class Geolocation1
    {
        public int Latitude { get; set; }
        public int Longitude { get; set; }
        public string Url { get; set; }
    }

    public class Ratings
    {
        public int HighestScore { get; set; }
        public object[] List { get; set; }
    }

    public class Vintages
    {
        public object[] List { get; set; }
    }

    public class Label
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }

    public class Productattribute
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public string ImageUrl { get; set; }
    }
}

LISTING 5-4 WineBot – WineApi Class

using System;
using System.Configuration;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace WineBot
{
    public class WineApi
    {
        const string BaseUrl = “http://services.wine.com/api/beta2/service.svc/json/”;

        static HttpClient http;

        public WineApi()
        {
            http = new HttpClient();
        }

        string ApiKey => ConfigurationManager.AppSettings[“WineApiKey”];

        public async Task<Refinement[]> GetWineCategoriesAsync()
        {
            const int WineTypeID = 4;
            string url = BaseUrl + “categorymap?filter=categories(490+4)&apikey=” + ApiKey;

            string result = await http.GetStringAsync(url);

            var wineCategories = JsonConvert.DeserializeObject<WineCategories>(result);

            var categories =
                (from cat in wineCategories.Categories
                 where cat.Id == WineTypeID
                 from attr in cat.Refinements
                 where attr.Id != WineTypeID
                 select attr)
                .ToArray();

            return categories;
        }

        public async Task<List[]> SearchAsync(int wineCategory, long rating, bool inStock, string searchTerms)
        {
            string url = 
                $”{BaseUrl}catalog” +
                $”?filter=categories({wineCategory})” +
                $”+rating({rating}|100)” +
                $”&inStock={inStock.ToString().ToLower()}” +
                $”&apikey={ApiKey}”;

            if (searchTerms != “none”)
                url += $”&search={Uri.EscapeUriString(searchTerms)}”;

            string result = await http.GetStringAsync(url);

            var wineProducts = JsonConvert.DeserializeObject<WineProducts>(result);
            return wineProducts?.Products?.List ?? new List[0];
        }

        public async Task<byte[]> GetUserImageAsync(string url)
        {
            var responseMessage = await http.GetAsync(url);
            return await responseMessage.Content.ReadAsByteArrayAsync();
        }
    }
}

To use the Wine.com API, you need a key, which is what the ApiKey reads from the Web.config file, shown below. You can obtain a key by visiting https://api.wine.com.

  <appSettings>
    <add key=”WineApiKey” value=”YourWineDotComApiKey”/>
    
    <!-- update these with your BotId, Microsoft App Id and your Microsoft App Password-->
    <add key=”BotId” value=”YourBotId” />
    <add key=”MicrosoftAppId” value=”” />
    <add key=”MicrosoftAppPassword” value=”” />
  </appSettings>

Images Note

To make the demo code simple, we added keys to the Web.config file. However, this isn’t recommended practice and you should visit https://docs.microsoft.com/en-us/aspnet/ and review more secure techniques for working with code secrets.


The Wine.com API returns JSON object responses and Listing 5-1 through Listing 5-3 are C# class representations of those objects. WineBot uses Newtonsoft Json.NET to deserialize the JSON objects into C# classes. The Microsoft.Bot.Builder NuGet package has a dependency on Json.NET, which is included with the Bot Application template. Anywhere you see JsonConvert.DeserializeObject, that’s Json.NET transforming JSON into a C# class.


Images Tip

You might notice that the WineApi class in Listing 5-4 contains a static HttpClient instance, http. At first glance, this might seem odd because HttpClient implements IDisposable and we’ve all had the mantra of wrapping IDisposable objects with a using statement burned into our brains. In the case of HttpClient, wrapping in a using statement hurts performance and scalability. For more information, visit the Microsoft Patterns and Practices Guidance at https://github.com/mspnp/performance-optimization/blob/master/ImproperInstantiation/docs/ImproperInstantiation.md to learn more on why wrapping HttpClient in a using statement is more of an anti-pattern.


The WineApi class handles all the communication with the Wine.com API. It uses the .NET HttpClient library to make HTTP GET requests. GetWineCategoriesAsync adds the proper URL segment and parameters to the BaseUrl, performs the GET request, and deserializes the results into an instance of WineCategories. As Listing 5-2 shows, WineCategories contains an array of Category and each Category contains an array of Refinement. WineBot only cares about the Wine category, which is represented by WineTypeID. The Refinements array inside of the Wine category contains the different types of Wine that WineBot shows to the user. GetWineCategoriesAsync reads those Refinements via the SelectMany LINQ query, and returns them to the caller.

After the user answers all questions, the chatbot calls SearchAsync to get a list of wines that match the given criteria, represented by the wineCategory, rating, inStock, and searchTerms parameters. Just like GetWineCategoriesAsync, SearchAsync builds the URL, performs the GET request, and deserializes the results – this time into an instance of WineProducts as defined in Listing 5-3. Then SearchAsync returns the List array, containing the wines found so the chatbot can display those wines to the user.

Though GetUserImageAsync doesn’t interact with the Wine.com API, it uses HttpClient to perform some utility work. When a user uploads an image or document to the chatbot, it contains an Attachment, which you’ll learn about in the upcoming Dialog Prompt Options section of this chapter. The Attachment contains a URL for where the file resides. The purpose of GetUserImageAsync is to perform an HTTP Get request to obtain an image located at a URL and return a byte array of that image to the caller.

Implementing a Dialog

A dialog is a class that contains state and a set of methods that guide a conversation. The chatbot hands over control to a dialog and the dialog manages the entire conversation. In this section, we’ll examine WineSearchDialog – a dialog class that collects information from a user and then does a search of the wines matching the criteria that the user provided in their answers. Listing 5-5 shows WineSearchDialog.

Listing 5-5 WineBot – WineSearchDialog Class

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System.Web.Hosting;

namespace WineBot
{
    [Serializable]
    class WineSearchDialog : IDialog<object>
    {
        public Refinement[] WineCategories { get; set; }
        public string WineType { get; set; }
        public long Rating { get; set; }
        public bool InStock { get; set; }
        public string SearchTerms { get; private set; }

        public async Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);
        }

        async Task MessageReceivedAsync(
            IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;

            if (activity.Text.Contains(“catalog”))
            {
                WineCategories = await new WineApi().GetWineCategoriesAsync();
                var categoryNames = WineCategories.Select(c => c.Name).ToList();

                PromptDialog.Choice(
                    context: context, 
                    resume: WineTypeReceivedAsync, 
                    options: categoryNames,
                    prompt: “Which type of wine?”,
                    retry: “Please select a valid wine type: “,
                    attempts: 4,
                    promptStyle: PromptStyle.AutoText);
            }
            else
            {
                await context.PostAsync(
                    “Currently, the only thing I can do is search the catalog. “ +
                    “Type ”catalog” if you would like to do that”);
            }
        }

        async Task WineTypeReceivedAsync(
            IDialogContext context, IAwaitable<string> result)
        {
            WineType = await result;

            PromptDialog.Number(
                context: context,
                resume: RatingReceivedAsync,
                prompt: “What is the minimum rating?”,
                retry: “Please enter a number between 1 and 100.”,
                attempts: 4);
        }

        async Task RatingReceivedAsync(
            IDialogContext context, IAwaitable<long> result)
        {
            Rating = await result;

            PromptDialog.Confirm(
                context: context,
                resume: InStockReceivedAsync,
                prompt: “Show only wines in stock?”,
                retry: “Please reply with either Yes or No.”);
        }

        async Task InStockReceivedAsync(
            IDialogContext context, IAwaitable<bool> result)
        {
            InStock = await result;

            PromptDialog.Text(
                context: context, 
                resume: SearchTermsReceivedAsync, 
                prompt: “Which search terms (type ”none” if you don’t want to add search terms)?”);
        }


        async Task SearchTermsReceivedAsync(
            IDialogContext context, IAwaitable<string> result)
        {
            SearchTerms = (await result)?.Trim().ToLower() ?? “none”;

            PromptDialog.Confirm(
                context: context,
                resume: UploadConfirmedReceivedAsync,
                prompt: “Would you like to upload your favorite wine image?”,
                retry: “Please reply with either Yes or No.”);
        }

        async Task UploadConfirmedReceivedAsync(
            IDialogContext context, IAwaitable<bool> result)
        {
            bool shouldUpload = await result;

            if (shouldUpload)
                PromptDialog.Attachment(
                    context: context,
                    resume: AttachmentReceivedAsync,
                    prompt: “Please upload your image.”);
            else
                await DoSearchAsync(context);
        }

        async Task AttachmentReceivedAsync(
            IDialogContext context, IAwaitable<IEnumerable<Attachment>> result)
        {
            Attachment attachment = (await result).First();

            byte[] imageBytes = 
                await new WineApi().GetUserImageAsync(attachment.ContentUrl);

            string hostPath = HostingEnvironment.MapPath(@”~/”);
            string imagePath = Path.Combine(hostPath, “images”);
            if (!Directory.Exists(imagePath))
                Directory.CreateDirectory(imagePath);

            string fileName = context.Activity.From.Name;
            string extension = Path.GetExtension(attachment.Name);
            string filePath = Path.Combine(imagePath, $”{fileName}{extension}”);

            File.WriteAllBytes(filePath, imageBytes);

            await DoSearchAsync(context);
        }

        async Task DoSearchAsync(IDialogContext context)
        {
            await context.PostAsync(
                $”You selected Wine Type: {WineType}, “ +
                $”Rating: {Rating}, “ +
                $”In Stock: {InStock}, and “ +
                $”Search Terms: {SearchTerms}”);

            int wineTypeID =
                (from cat in WineCategories
                 where cat.Name == WineType
                 select cat.Id)
                .FirstOrDefault();

            List[] wines = 
                await new WineApi().SearchAsync(
                    wineTypeID, Rating, InStock, SearchTerms);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ + 
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);

            context.Wait(MessageReceivedAsync);
        }
    }
}

As Listing 5-5 shows, WineSearchDialog has several members and the following sections drill down and explain what each member means.

Creating a Dialog Class

A dialog class is serializable, has state, and implements IDialog<T>. We’ll refer to this type of dialog throughout the book as IDialog<T> to make it clear what type of dialog we’re discussing. The following snippet, from Listing 5-5 shows how WineSearchDialog implements these things:

    [Serializable]
    class WineSearchDialog : IDialog<object>
    {
        public Refinement[] WineCategories { get; set; }
        public string WineType { get; set; }
        public long Rating { get; set; }
        public bool InStock { get; set; }
        public string SearchTerms { get; private set; }
    }

Dialogs must be serializable, which is why the Serializable attribute decorates WineSearchDialog. Alternatively, you could implement the ISerializable interface and define a serialization constructor, which is a way for .NET developers to perform custom serialization. You can visit the .NET Framework documentation for more information on serialization because it isn’t a feature specific to the Bot Framework.

The requirement for a dialog to be serializable is interesting because it highlights the fact that the Bot Framework transfers dialog state across the Internet to the Bot State Service. This allows a dialog to not only keep track of your custom data state, but also keep track of where the user is in the conversation, conversation state, that the dialog is managing.

On data state, notice that WineSearchDialog has several public properties. Bot Builder persists these properties in the Bot State Service. Remember, this is a web API, which is inherently stateless, and this is the mechanism dialogs use to re-populate state when receiving the next message from the user.

Later, you’ll see how WineCategories helps read the category ID for a selected wine type, so the code populates WineCategories and then re-uses it again later to find the ID associated with a category. WineType, Rating, InStock, and SearchTerms all hold answers from the user and are arguments to guide the search for matching wines.

WineSearchDialog implements IDialog<object>, which is a Bot Framework type, shown below:

    public interface IDialog<out TResult>
    {
        Task StartAsync(IDialogContext context);
    }

IDialog has an out TResult type parameter, indicating the object type it will return. The return value is used in some advanced scenarios, that you’ll learn more about in Chapter 9, Managing Advanced Conversation, where code can call this dialog and receive a result of its operation, which will be type TResult. For WineSearchDialog, the implemented IDialog type is object because WineBot doesn’t require a return value.

This section explained the details of serialization, state, and implementing IDialog. You’ll also notice that IDialog has a StartAsync member and you’ll learn about that in the next section.

Dialog Initialization and Workflow

Implementing IDialog, WineSearchDialog has a StartAsync method. You can think of StartAsync as the entry point for a dialog because it’s the first method the Bot Builder calls when starting a dialog. Here’s the StartAsync method:

        public async Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);
        }

StartAsync is async, doesn’t return a value and has an IDialogContext parameter. The call to context.Wait suspends the StartAsync method and sets MessageReceivedAsync as the next method to call in the dialog.

The call to context.Wait is important because it tells the Bot Framwork which method to call next. Every scenario doesn’t involve passing an IMessageActivity to the dialog and that means StartAsync might have more logic. In other cases, like WineSearchDialog, the dialog always receives an IMessageActivity and it’s important to call context.Wait on a method, MessageReceivedAsync, with the logic to handle that input. In the upcoming Dialog Conversation Flow section, you’ll see how MessageReceivedAsync handles the dialog input. Chapter 9, Managing Advanced Conversation, discusses the dialog stack in more detail, illuminating some of the internal behaviors of how the Bot Framework manages flow of control through dialogs.

The next section drills down into the StartAsync parameter type IDialogContext.

Examining IDialogContext

The IDialogContext parameter is important because it implements interfaces with members you need during various steps of a conversation, as shown below:

    public interface IDialogContext : IDialogStack, IBotContext
    {
    }

IDialogContext doesn’t have it’s own members, but derives from other interfaces, IDialogStack and IBotContext, which do. Figure 5-2 shows the relationship between IDialogContext and the interfaces it derives from.

Images

FIGURE 5-2 The IDialogContext Interface Hierarchy

Here’s IDialogStack:

    public interface IDialogStack
    {
        void Wait<R>(ResumeAfter<R> resume);
    }

IDialogStack has many members, not shown here, to manage a stack of dialogs, which I’ll discuss in more detail in Chapter 9, Managing Advanced Conversation. The Wait method is relevant to the current discussion because StartAsync calls context.Wait. This suspends the dialog conversation and sets MessageReceivedAsync as the method to resume on.

    public interface IBotContext : IBotData, IBotToUser
    {
        CancellationToken CancellationToken { get; }
        IActivity Activity { get; }
    }

IBotContext and the interfaces it derives from are part of Bot Builder. It has a CancellationToken for async cancellation support and a reference to the current IActivity. You’ve worked with Activities in previous chapters, and this is the same Activity that is the parameter to MessagesController Post method.

The IBotData has convenience members for accessing the Bot State Service and we discuss this more in Chapter 9, Managing Advanced Communication. IBotToUser has members supporting chatbot communication to the user, as shown below:

    public interface IBotToUser
    {
        Task PostAsync(IMessageActivity message, CancellationToken cancellationToken = default(CancellationToken));
        IMessageActivity MakeMessage();
    }

IBotToUser lets you post a message to the user with PostAsync and you’ll see an example of using it in the next section. MakeMessage is a convenience method for creating a new Activity that implements IMessageActivity.

Dialog Conversation Flow

Dialogs move a conversation forward by setting the current state of a conversation to the next method to call when the next IMessageActivity arrives from the user. The state machine in Figure 5-3 shows how this works.

Images

FIGURE 5-3 The IDialogContext Interface Hierarchy.

As shown in Figure 5-3, when the dialog starts, Bot Builder calls StartAsync, which suspends by calling Wait on the IDialogContext instance, context. At that point, the dialog is in the Suspended state. When a new IMessageActivity arrives, Bot Builder moves the dialog state to Resumed and executes the next method, which would be MessageReceivedAsync. The exception is that if StartAsync is called with an Activity to process, it immediately resumes on the method it called Wait upon – MessageReceivedAsync in this example. Each dialog method that is part of a conversation either leaves itself as the next Resumed method, or sets another method to be the next Resumed method.

The following list shows how conversation state management works with WineBot. The sequence describes when each method, from Listing 5-5, executes and how the conversation flows. We explain PromptDialog in the upcoming Dialog Prompt Options section of this chapter, but the primary bit of information you need to know is that PromptDialog methods have a parameter that sets the next method to resume on.

1. Bot Builder calls StartAsync, which calls context.Wait, setting the next method to MessageReceivedAsync, and suspends.

2. Message arrives to resume on MessageReceivedAsync. When message isn’t catalog, code sends message to user, does not change next method, leaving MessageReceivedAsync as the next method to resume on, and suspends. When message is catalog, calls PromptDialog.Choice, setting WineTypeReceivedAsync as the next method to resume on, and suspends. Notice that not setting the next message to resume on, as in the case of user input not being catalog, the current method remains as the next method to resume on.

3. Message arrives to resume on WineTypeReceivedAsync, which calls PromptDialog.Number to set RatingReceivedAsync as the next method to resume on, and suspends.

4. Message arrives to resume on RatingReceivedAsync, which calls PromptDialog.Confirm to set InStockReceivedAsync as the next method to resume on, and suspends.

5. Message arrives to resume on InStockReceivedAsync, which calls PromptDialog.Text to set SearchTermsReceivedAsync as the next method to resume on, and suspends.

6. Message arrives to resume on SearchTermsReceivedAsync, which calls PromptDialog.Confirm to set UploadConfirmedReceivedAsync as the next method to resume on, and suspends.

7. Message arrives to resume on UploadConfirmedReceivedAsync and goes straight to DoSearchAsync if the user doesn’t want to upload an image. If the user does want to upload an image, call PromptDialog.Attachment to set AttachmentReceivedAsync as the next method to resume on, and suspends.

8. If the user wanted to upload an image, message arrives to resume on AttachmentReceivedAsync and calls DoSearchAsync.

9. Either UploadConfirmedReceivedAsync or AttachmentReceivedAsync calls DoSearchAsync. When DoSearchAsync completes its work, it calls context.Wait to set MessageReceivedAsync as the next method to resume on, and suspends. This is how the conversation re-starts from the beginning.

The point of all of this is that by knowing how a dialog works, via its state machine, you can understand how to design and implement the chatbot conversation flow. In the current discussion, you’ve seen the StartAsync method, it called Wait to suspend, and then the dialog resumes with the next message, calling the MessageReceivedAsync method, from Listing 5-5, below:

        async Task MessageReceivedAsync(
            IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;

            if (activity.Text.Contains(“catalog”))
            {
                WineCategories = await new WineApi().GetWineCategoriesAsync();
                var categoryNames = WineCategories.Select(c => c.Name).ToList();

                PromptDialog.Choice(
                    context: context, 
                    resume: WineTypeReceivedAsync, 
                    options: categoryNames,
                    prompt: “Which type of wine?”,
                    retry: “Please select a valid wine type: “,
                    attempts: 4,
                    promptStyle: PromptStyle.AutoText);
            }
            else
            {
                await context.PostAsync(
                    “Currently, the only thing I can do is search the catalog. “ +
                    “Type ”catalog” if you would like to do that”);
            }
        }

As discussed in the previous section, all dialog methods participating in the conversation have an IDialogContext parameter. Additionally, The IAwaitable parameter contains the message that the user sent. The IAwaitable is a Bot Builder type that has a type parameter specifying the type of the parameter sent to the method. Notice in the first line of MessageReceivedAsync that awaits result. The IAwaitable type is an async awaitable type and awaiting it makes a call to the Bot State Service to read the value.

When the user sends a message saying catalog, MessageReceivedAsync starts a conversation to request information from the user that eventually results in a search for wine. It calls GetWineCategoriesAsync, as discussed in the previous section and creates a list of wine types for the user to choose from.

When the user types something other than catalog, the chatbot responds with a method with more clear instructions on how to get started. As discussed in the previous section, PostToUser is a method from the IBotToUser interface that sends to the user.

PromptDialog.Choice is one of many prompt methods for interacting with the user and setting the method to resume when the user responds, and we discuss that in detail in the next section.

Dialog Prompt Options

The PromptDialog class offers serveral convenience methods, supporting question and answer conversations with the user. Listing 5-5 shows PromptDialog in action and we’ve briefly discussed PromptDialog.Choice in the previous section. In this section, we cover all the PromptDialog methods. Before doing so, let’s review Table 5-1, outlining several common PromptDialog method parameters.

TABLE 5-1 Common PromptDialog Method Parameters

Parameter Name

Description

context

IDialogContext that was passed into the method. Discussed in a previous section of this chapter.

resume

ResumeAfter<T> delegate specifying the method to resume on when the next IMessageActivity arrives.

prompt

Text (string) to show the user. Typically a question to show the user before displaying options, if any.

retry

Text (string) to display when a user enters an invalid option. Defaults to prompt message.

attempts

Number of times to retry prompt. Bot Builder will show the retry message until the number of attempts occur. Defaults to 3.

When thinking about how a chatbot interacts with a user, the prompt, retry, and attempts parameters come into play. Done right, these can reduce friction for the user. One example might be to define a retry message that is worded differently than the prompt, assuming the user didn’t fully understand what the prompt message really wanted. Another option is the attempts, which defaults to 3. In some cases, you might want to give the user several more chances to try, but an excessive number of retries might make them walk away. On the other hand, what if you needed a Confirm response that needed a yes or no and any response other than yes would mean no, meaning that you just want to read the user’s response with 0 retries.


Images Tip

When all attempts expire, Bot Builder resumes on the method specified by the resume parameter. When awaiting the IAwaitable parameter, result in Listing 5-5, Bot Builder throws a TooManyAttemptsException. Wrap await result in a try/catch block to handle the situation. You can learn what the user typed in by examining context.Activity, which is the IMessageActivity from the user.


A couple of the PromptDialog methods have a promptOptions parameter with a constructor that takes all of the parameters described in Table 5-1. So, you could instantiate a PromptOptions instance, shown below, with those parameters and pass that instance as a parameter to one of the PromptDialog methods that accept it instead of coding the parameters to the method itself.

    public class PromptOptions<T>
    {
        public readonly string Prompt;
        public readonly string Retry;
        public readonly IReadOnlyList<T> Options;
        public readonly IReadOnlyList<string> Descriptions;
        public readonly string TooManyAttempts;
        public readonly PromptStyler PromptStyler;
        public int Attempts { get; set; }
        public string DefaultRetry { get; set; }
        protected string DefaultTooManyAttempts { get; }

        public PromptOptions(
            string prompt, string retry = null, string tooManyAttempts = null, 
            IReadOnlyList<T> options = null, int attempts = 3, 
            PromptStyler promptStyler = null, IReadOnlyList<string> descriptions = null);
    }

In the normal case, just using PromptDialog parameters works fine, but there is one parameter in PromptOptions that you won’t find in any of the PromptDialog method overloads and that is tooManyAttempts, which you can use to specify the message to show the user when they’ve exceeded the number of specified attempts. PromptOptions also has public properties that are synonymous with its constructor parameters.

The following sections follow the conversation flow in WineBot and drill down on PromptDialog usage from Listing 5.5.

Choice

When StartAsync calls context.Wait with its ResumeAfter<IMessageActivity> delegate parameter set to MessageReceivedAsync; it starts the conversation. MessageReceivedAsync handles the condition, where the user sends catalog, which a PromptDialog.Choice method. The goal is to learn the type of wine the user wants to search for, which is one of several options, and the code below shows how to do this:

        async Task MessageReceivedAsync(
            IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;

            if (activity.Text.Contains(“catalog”))
            {
                WineCategories = await new WineApi().GetWineCategoriesAsync();
                var categoryNames = WineCategories.Select(c => c.Name).ToList();

                PromptDialog.Choice(
                    context: context, 
                    resume: WineTypeReceivedAsync, 
                    options: categoryNames,
                    prompt: “Which type of wine?”,
                    retry: “Please select a valid wine type: “,
                    attempts: 4,
                    promptStyle: PromptStyle.AutoText);
            }
            else
            {
                await context.PostAsync(
                    “Currently, the only thing I can do is search the catalog. “ +
                    “Type ”catalog” if you would like to do that”);
            }
        }

The PromptDialog.Choice has all of the common parameters, discussed in the previous section. It also has a promptStyle parameter, of type PromptStyle enum, specifying how each item in the list of choices appears. Table 5-2 describes what the PromptStyle enum members mean.

TABLE 5-2 PromptStyle Enum Members

Enum Member

Description

Auto

Select a style based on channel capabilities. Can vary by channel based on the channel’s preferences. Bullet list in Bot Emulator.

Keyboard

Displays as keyboard card, mapping to a Hero Card, which you’ll learn about in Chapter 10, Attaching Cards.

AutoText

Shows options as text on either separate lines or in the same line, depending on number of choices.

Inline

Show all options as text on a single line. Unlike AutoText, this is regardless of number of choices.

PerLine

Show all options as text with each option on a separate line. Unlike AutoText, this is regardless of number of choices.

None

Don’t show any choices. Might be useful if you felt it made more sense to add options to prompt/retry parameter text. E.g. two or three choices.

The method naming convention in Listing 5-5 is TReceivedAsync, where T is the name of the answer received from the user in the result. After StartAsync, the chatbot waits for the next message and that goes to MessageReceivedAsync. When MessageReceivedAsync prompts for a wine type, it specifies WineTypeReceivedAsync for its resume parameter, where it performs a Number prompt that we discuss in the next section.

Number

After the user answers with the type of wine they want, the next step is to ask for the minimum rating, which is an integer. PromptDialog has a Number method that works for this, as shown below:

        async Task WineTypeReceivedAsync(
            IDialogContext context, IAwaitable<string> result)
        {
            WineType = await result;

            PromptDialog.Number(
                context: context,
                resume: RatingReceivedAsync,
                prompt: “What is the minimum rating?”,
                retry: “Please enter a number between 1 and 100.”,
                attempts: 4);
        }

Notice how the first statement of WineTypeReceivedAsync awaits the result parameter, assigning the string value to the WineType property. The result parameter is a Bot Builder IAwaitable<T>, allowing you to asynchronously request the result of the previous dialog. Since the PromptDialog.Choice, in MessageReceivedAsync, resulted in type string from the user, the IAwaitable<T> type parameter, T, must also be type string. You could also use an IAwaitable<object>, but then the code would need to perform the conversion from object to string and IAwaitable<string> lets Bot Builder do it for us.

WineType is a property of WineSearchDialog. If you recall, WineSearchDialog is serializable, as all dialogs must be. That means their properties must also be serializable. Further, when Bot Builder serializes WineSearchDialog, it also serializes instance state (properties and fields). When a new user message arrives for WineSearchDialog, Bot Builder deserializes WineSearchDialog, including all of its state. The benefit is that by assigning user input, when received, to properties, as WineTypeReceivedAsync does for WineType, that state persists for subsequent operations inside the dialog instance. You’ll see this later in the Performing the Search section.

PromptDialog.Number accepts a ResumeAfter delegate with a type parameter of either double or long. In this example, we need an integer, so the resume delegate, RatingReceivedAsync has an IAwaitable<long> parameter, as you’ll see in the next section.

Confirm

Yes or no answers are another common type of question, which are supported. In this case, you would use a PromptDialog.Confirm, as shown below:

        async Task RatingReceivedAsync(
            IDialogContext context, IAwaitable<long> result)
        {
            Rating = await result;

            PromptDialog.Confirm(
                context: context,
                resume: InStockReceivedAsync,
                prompt: “Show only wines in stock?”,
                retry: “Please reply with either Yes or No.”);
        }

PromptDialog.Confirm accepts a bool value, where a Yes answer is true and a No answer is false, as described in the next section.

Text

Some answers require text – it’s a chatbot after all. In the current situation, WineBot needs to collect some search terms from the user so that the search can filter by those terms. So, if the user types cabernet, the search will likely return Cabernet Sauvignon that also matches the other criteria. The following shows how PromptDialog.Text works:

        async Task InStockReceivedAsync(
            IDialogContext context, IAwaitable<bool> result)
        {
            InStock = await result;

            PromptDialog.Text(
                context: context, 
                resume: SearchTermsReceivedAsync, 
                prompt: “Which search terms (type ”none” if you don’t want to add search terms)?”);
        }

PromptDialog.Text expects an answer of type string, which is what it’s resume parameter, SearchTermsReceivedAsync handles, shown below:

        async Task SearchTermsReceivedAsync(
            IDialogContext context, IAwaitable<string> result)
        {
            SearchTerms = (await result)?.Trim().ToLower() ?? “none”;

            PromptDialog.Confirm(
                context: context,
                resume: UploadConfirmedReceivedAsync,
                prompt: “Would you like to upload your favorite wine image?”,
                retry: “Please reply with either Yes or No.”);
        }

For the search terms, the PromptDialog asked the user to type none if they didn’t have any terms. You can see how SearchTermsReceivedAsync processes the result, trimming, lower casing, and setting to none if the input doesn’t contain a valid term.


Images Warning

Users sometimes say anything and everything you never expected when communicating with a chatbot. Notice how SearchTermsReceivedAsync has special handling for the user input. You should code defensively to handle any random input a user provides.


SearchTermsRecievedAsync asks a user if they would like to upload their favorite wine image and sets its resume parameter to UploadConfirmedReceived, which is covered next.

Attachment

During the conversation, WineBot asks the user if they would like to upload a picture of their favorite wine image. While this is an image, the user could also upload any type of file. E.g. what if the chatbot took a formatted file like JSON, Excel, or CSV with data to process? Here’s how WineBot asks for an attachment:

        async Task UploadConfirmedReceivedAsync(
            IDialogContext context, IAwaitable<bool> result)
        {
            bool shouldUpload = await result;

            if (shouldUpload)
                PromptDialog.Attachment(
                    context: context,
                    resume: AttachmentReceivedAsync,
                    prompt: “Please upload your image.”);
            else
                await DoSearchAsync(context);
        }

When the user confirms that they want to upload, WineBot calls PromptDialog.Attachment to let the user know that they should send their attachment. Otherwise, WineBot performs the search.

As explained in Chapter 4, Bot Emulator has an attachment button on the left of the text input box that the user can click to select an image (or any other file) to upload. When the user does this, Bot Builder calls the AttachmentReceivedAsync method, shown below, specified as the PromptDialog.Attachment resume parameter:

        async Task AttachmentReceivedAsync(
            IDialogContext context, IAwaitable<IEnumerable<Attachment>> result)
        {
            Attachment attachment = (await result).First();

            byte[] imageBytes = 
                await new WineApi().GetUserImageAsync(attachment.ContentUrl);

            string hostPath = HostingEnvironment.MapPath(@”~/”);
            string imagePath = Path.Combine(hostPath, “images”);
            if (!Directory.Exists(imagePath))
                Directory.CreateDirectory(imagePath);

            string fileName = context.Activity.From.Name;
            string extension = Path.GetExtension(attachment.Name);
            string filePath = Path.Combine(imagePath, $”{fileName}{extension}”);

            File.WriteAllBytes(filePath, imageBytes);

            await DoSearchAsync(context);
        }

The PromptDialog.Attachment resume parameter takes a ResumeAfter delegate with a IEnumerable<Attachment> type parameter. Because it’s a single attachment, AttachmentReceivedAsync takes the first item. Attachment has several properties, shown below:

    public class Attachment
    {
        public Attachment();
        public Attachment(
            string contentType = null, string contentUrl = null, object content = null, 
            string name = null, string thumbnailUrl = null);
        public string ContentType { get; set; }
        public string ContentUrl { get; set; }
        public object Content { get; set; }
        public string Name { get; set; }
        public string ThumbnailUrl { get; set; }
    }

An Attachment has various properties that are populated according to the context in which the Attachment is used. You’ll learn more about Attachment in Chapter 10, Attaching Cards. In the current scenario, the Attachment has a ContentUrl, specifying the location of where the file resides.

As discussed when examining Listing 5-4, WineApi has a GetUserImageAsync method that accepts a URL and returns the byte[] with the file data, which AttachmentReceivedAsync uses. The code then builds a file path located under the current web API instance and names the file after the user with the file extension. Then it saves the file in that location. This program doesn’t do anything significant with that file, but you might imagine saving that file in a folder or database of your choice or processing it in some way that makes sense to your chatbot.

After processing the file, WineBot has all the information it needs to perform the search, which the next section discusses.

Performing the Search

To this point, WineBot hasn’t done any extra work to save the answers from the user, other than to populate WineSearchDialog properties. It didn’t have to because, as described in a previous section, WineSearchDialog, like all dialogs must be, is serializable and this allows Bot Builder to save dialog state in the Bot State Service. That means the properties are populated and ready when we need them, like in the DoSearchAsync method shown below:

        async Task DoSearchAsync(IDialogContext context)
        {
            await context.PostAsync(
                $”You selected Wine Type: {WineType}, “ +
                $”Rating: {Rating}, “ +
                $”In Stock: {InStock}, and “ +
                $”Search Terms: {SearchTerms}”);

            int wineTypeID =
                (from cat in WineCategories
                 where cat.Name == WineType
                 select cat.Id)
                .FirstOrDefault();

            List[] wines = 
                await new WineApi().SearchAsync(
                    wineTypeID, Rating, InStock, SearchTerms);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ + 
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);

            context.Wait(MessageReceivedAsync);
        }

The first thing DoSearchAsyc does is send a message to the user with the choices they made. This might be another opportunity for a PromptDialog.Confirm to make sure this is what they wanted and this implementation skips that because you’ve already seen all of the PromptDialog options in previous sections.

If you recall MessageReceivedAsync stored the wine choices in WineCategories. This is useful because now we can take the text WineType description and query for it’s matching ID. The code calls the SearchAsync method from the WineApi class in Listing 5-4 and displays the wine list to the user.

Finally, calling context.Wait sets the dialog state to MessageReceivedAsync as the next method to resume upon. This starts the conversation over again, at the beginning, but the user is still in the same conversation. An important point here is that the code doesn’t call context.Wait on StartAsync. That’s because StartAsync is only called when a brand new conversation starts. After StartAsync calls context.Wait, the Bot Builder serializes dialog state, including where the dialog is to resume upon the next message arriving from the user. Bot Builder sends the serialized state to the Bot State Service and the Bot State Service uses private state for the dialog. As you recall, from Chapter 3, private state is for a user in a conversation. So, when the same user continues a conversation, Bot Builder can deserialize and set the next method to resume on from wherever the dialog left off. StartAsync doesn’t work as a method to resume on because, unlike MessageReceivedAsync, it doesn’t have an IAwaitable<T> to handle the message result – it’s only the entry point to a new conversation. That’s why MessageReceivedAsync is the first method to resume on.

This completes the description of how a dialog works and the next section shows you how to use this dialog in a chatbot.

Calling a Dialog

The last step in using a dialog is telling a chatbot which dialog to use. WineBot does this in the MessagesController, shown in Listing 5-6.

LISTING 5-6 WineBot – 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.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace WineBot
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
                await Conversation.SendAsync(activity, () => new WineSearchDialog());
            else
                await HandleSystemMessageAsync(activity);

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

        async Task HandleSystemMessageAsync(Activity message)
        {
            if (message.Type == ActivityTypes.ConversationUpdate)
            {
                const string WelcomeMessage =
                    “Welcome to WineBot! You can type ”catalog” to search wines.”;

                Func<ChannelAccount, bool> isChatbot =
                    channelAcct => channelAcct.Id == message.Recipient.Id;

                if (message.MembersAdded?.Any(isChatbot) ?? false)
                {
                    Activity reply = message.CreateReply(WelcomeMessage);

                    var connector = new ConnectorClient(new Uri(message.ServiceUrl));
                    await connector.Conversations.ReplyToActivityAsync(reply);
                }
            }
        }
    }
}

If the incoming activity is ActivityType.Message, WineBot calls Conversation.SendAsync. The second parameter to SendAsync is a lambda with a new instance of WineSearchDialog. This is essentially handing off all processing of that message to WineSearchDialog. As you’ve seen throughout this chapter, WineSearchDialog manages all data state and conversation state to keep track of what data it has and where it’s at in the conversation.

Of additional note is the HandleSystemMessageAsync method that handles ActivityType.ConversationUpdate, which we discussed in Chapter 4. When the user first connects with their chatbot, it’s polite to send them a Hello or Welcome message, letting them know how to interact with the chatbot. If you wanted to be clever, you could save the user’s ID to compare against subsequent connections and vary the message, e.g. welcome back User1. This is a best practice and you’ll see this regularly throughout the book.

Summary

This chapter was all about building a dialog, henceforth referred to as IDialog<T>. The example was WineBot, a chatbot that lets users answer questions that result in a search for wines matching the given answers. We used the Wine.com API as a data source.

You learned how to specify an IDialog<T> and how the IDialog<T> manages data state via serialization and interaction with the Bot State Service. This chapter covered the IDialog<T> entry point and how a dialog manages conversation state.

The Implementing a Dialog section explained parameters to IDialog<T> methods, including the IDialogContext hierarchy and capabilities. You can use several PromptDialog methods to ask the user questions resulting in answers from a list of options, yes or no, text, and numeric. You can even accept file attachments from the user.

Finally you saw how easy it is to use an IDialog<T>, instructing MessagesController to hand control to the IDialog<T> anytime it received a message from 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.142.200.226