CHAPTER 9. Managing Advanced Conversation

Earlier chapers in this part of the book detailed the inner workings of IDialog<T>, FormFlow, and LuisDialog<T>. Individually, these dialog types are powerful and allow for useful chatbots–with chatbots, simplicity is often the essence of success. Yet, there will be times when your requirements are sophisticated and require more advanced communication techniques. This chapter introduces several techniques for moving beyond the times you might feel limited by the functionality of a single dialog type.

An important concept in navigation is the dialog stack and we’ll start off by explaining how this works, moving from one dialog to another. Because real life doesn’t follow a script, your chatbot shouldn’t either and you’ll learn how to accept out-of-band information that you otherwise wouldn’t be able to handle. You’ll learn about chaining in this chapter, which supports complex dialog communication and navigation. You’ll also learn how to format message text, thus improving the user experience.

Managing the Dialog Stack

All of the chatbots developed in previous chapters were simple, in that the developer needed to only code a single dialog type instance. The chatbot logic was to hand off control to a dialog, which would do the work and the underlying implementation of that wasn’t a design factor. When working with multiple dialogs, however, this handoff logic between dialogs becomes more important, so you need to know about the dialog stack.

What is the Dialog Stack?

Before discussing the dialog stack, let’s discuss stacks in general. A common metaphor is a stack of dishes. What makes this stack special is how people place dishes on and off the stack. When putting dishes in the cupboard, people place each dish on top of the other, in a stack. When retrieving dishes, they pull the dish from the top of the stack. There’s a pattern here where the last dish placed in the stack will be the first dish to be removed.

This pattern is a primary consideration for computer stacks that is called Last-In First Out (LIFO). In computing, software is based on stacks. When a program starts, it sets the entry point to the first item in the stack. As the program calls methods, those methods get pushed to the top of the stack, so you can imagine methods calling methods as the stack grows. When those methods return, they pop the old method off the stack, which is how the program knows how to return to its caller.

The dialog stack is based on this same concept. When the Bot Framework calls a dialog, that dialog is pushed onto the dialog stack. Then, when the current dialog is done, it returns and is popped from the stack, letting its caller resume.

Designing Bot Framework dialog navigation around a stack is a well-worn path that’s been proven for other technologies. While stack-based navigation handles a lot of common scenarios, you still aren’t stuck because the Bot Framework has support for scorables, which is a way to break out of the single path of a stack to handle random paths that accommodate the unpredictability of human conversation and you’ll see how that works later in this chapter.


Images Note

If you’ve ever written a Windows Phone App, you might already be familiar with an application stack. Essentially, opening a new page puts that page at the top of the stack. Tapping the back button pops that page from the application stack, bringing the user either back to the previous page or closing the app.


The next section shows how to build a chatbot that navigates between dialogs, using the dialog stack.

Navigating to Other Dialogs

Using the dialog stack, you can build chatbots that navigate from one dialog to another and back. The example here builds on on the WineBot chatbots from earlier chapters in this part of the book. This time, searching for wine is only a part of what a user can do and you’ll see another feature for managing a profile. Listings 9-1 and 9-2, from the WineBotDialogStack project in the accompanying source code, show how to manage navigation between multiple dialogs with the dialog stack.

Listing 9-1 is the typical Post method, invoking SendAsync to call the first dialog the user interacts with, RootDialog. Behind the scenes, SendAsync pushes RootDialog on the stack, making it the current dialog.

LISTING 9-1 Using the Dialog Stack to Navigate Between Dialogs – MessagesController.cs

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;

namespace WineBotDialogStack
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                try
                {
                    await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
                }
                catch (InvalidOperationException ex)
                {
                    var client = new ConnectorClient(new Uri(activity.ServiceUrl));
                    var reply = activity.CreateReply($”Reset Message: {ex.Message}”);
                    client.Conversations.ReplyToActivity(reply);
                }
            }

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

RootDialog, from Listing 9-2 is an IDialog<T>, which you’ve seen in previous chapters. Its StartAsync sets MessageReceivedAsync as the next method to handle input from the user. After SendAsync calls RootDialog, it sends the IMessageActivity to RootDialog, which forwards the call to MessageReceivedAsync.

LISTING 9-2 Using the Dialog Stack to Navigate Between Dialogs – RootDialog.cs

using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using WineBotLib;
using System.Linq;

namespace WineBotDialogStack.Dialogs
{
    [Serializable]
    public class RootDialog : IDialog<object>
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            string prompt = “What would you like to do?”;
            var options = new[]
            {
                “Search Wine”,
                “Manage Profile”
            };

            PromptDialog.Choice(context, ResumeAfterChoiceAsync, options, prompt);

            return Task.CompletedTask;
        }

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

            if (choice.StartsWith(“Search”))
                await context.Forward(
                    FormDialog.FromForm(new WineForm().BuildForm),
                    ResumeAfterWineSearchAsync,
                    context.Activity.AsMessageActivity());
            if (choice.StartsWith(“Manage”))
                context.Call(new ProfileDialog(), ResumeAfterProfileAsync);
            else
                await context.PostAsync($”’{choice}’ isn’t implemented.”);

        }

        async Task ResumeAfterWineSearchAsync(
            IDialogContext context, IAwaitable<WineForm> result)
        {
            WineForm wineResults = await result;

            List[] wines =
                await new WineApi().SearchAsync(
                    (int)wineResults.WineType,
                    (int)wineResults.Rating,
                    wineResults.InStock == StockingType.InStock,
                    “”);

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

        async Task ResumeAfterProfileAsync(IDialogContext context, IAwaitable<string> result)
        {
            try
            {
                string email = await result;

                await context.PostAsync($”Your profile email is now {email}”);
            }
            catch (ArgumentException ex)
            {
                await context.PostAsync($”Fail Message: {ex.Message}”);
            }

            context.Wait(MessageReceivedAsync);
        }
    }
}

Inside of MessageReceivedAsync, notice the call to PromptDialog.Choice. While you’ve seen this in Chapter 5, Building Dialogs, it’s important to point out the PromptDialog sets the ResumeAfterChoiceAsync method as the next method in RootDialog to run and then pushes itself as a new dialog on the stack. When the user responds, the PromptDialog.Choice dialog pops from the stack and returns control to RootDialog, the previous dialog on the stack, to resume at ResumeAfterChoiceAsync.

The following sections detail more options for the dialog stack and navigation, starting where we left off here at ResumeAfterChoiceAsync.

Navigating via Forward

One of the ways you can navigate to another dialog is by forwarding the IMessageActivity so the new dialog can handle the user’s input. Listing 9-2 shows how to use the IDialogContext’s Forward method, repeated next, to accomplish this:

                    await context.Forward(
                        FormDialog.FromForm(new WineForm().BuildForm),
                        ResumeAfterWineSearchAsync,
                        context.Activity.AsMessageActivity());

This example uses three parameters from forward, taking the default for the fourth , which is an async CancellationToken. The first parameter is an IDialog<T> instance and you can see that it’s using FormFlow. The second parameter refers to the method to return to when the WineForm dialog returns. The third parameter is the activity containing the user’s method–it uses the AsMessageActivity method to convert Activity and pass an IMessageActivity instance.

Forward pushes the new WineForm dialog on the stack and starts it, which passes the user’s IMessageActivity. Listing 9-3 shows the WineForm dialog.

Listing 9-3 Forwarding IMessageActivity to a Dialog – WineForm.cs

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Linq;
using System.Threading.Tasks;
using WineBotLib;

namespace WineBotDialogStack.Dialogs
{
    [Serializable]
    public class WineForm
    {
        public WineType WineType { get; set; }
        public RatingType Rating { get; set; }
        public StockingType InStock { get; set; }

        public IForm<WineForm> BuildForm()
        {
            return new FormBuilder<WineForm>()
                .Message(
                    “I have a few questions on your wine search. “ +
                    “You can type ”help” at any time for more info.”)
                .Build();
        }
    }
}

WineForm is intentionally implemented minimally to demonstrate the behavior of calling a FormFlow form. When complete, WineForm holds all of the user’s values in properties, which the caller can access, as shown in ResumeAfterWineSearchAsync, repeated next from Listing 9-2:

        async Task ResumeAfterWineSearchAsync(IDialogContext context, IAwaitable<WineForm> result)
        {
            WineForm wineResults = await result;

            List[] wines =
                await new WineApi().SearchAsync(
                    (int)wineResults.WineType,
                    (int)wineResults.Rating,
                    wineResults.InStock == StockingType.InStock,
                    “”);

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

After WineForm completes and returns, it pops from the stack and resumes on RootDialog.ResumeAfterWineSearchAsync. The awaited result parameter is the instance of WineForm that is now done and has properties holding values from the user’s responses. As seen in previous chapters, the SearchAsync call returns results based on the WineForm instance, wineResults, properties and responds to the user.

Navigating via Call

Sometimes you don’t need to send the IMessageActivity to the called dialog, as demonstrated in the previous discussion on Forward. Maybe you just need to start a dialog and let it interact with the user, regardless of what the user’s initial communication was. In that case, you can use the IDialogContext’s Call method, repeated here from the ResumeAfterChoiceAsync method in Listing 9-2:

                context.Call(new ProfileDialog(), ResumeAfterProfileAsync);

Call has two parameters: the new dialog and a resume method. The new dialog in this example is ProfileDialog. Similar to how resume methods in Forward and PromptDialog work, the second parameter refers to ResumeAfterProfileAsync as the method to call after ProfileDialog, shown in Listing 9-4, completes.

LISTING 9-4 Calling a Dialog – ProfileDialog.cs

using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Threading.Tasks;

namespace WineBotDialogStack.Dialogs
{
    [Serializable]
    public class ProfileDialog : IDialog<string>
    {
        public Task StartAsync(IDialogContext context)
        {
            string prompt = “What would you like to do?”;
            var options = new[]
            {
                “Change Email”,
                “Reset”,
                “Fail”,
            };

            PromptDialog.Choice(context, MessageReceivedAsync, options, prompt);

            return Task.CompletedTask;
        }

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

            switch (choice)
            {
                case “Change Email”:
                    string prompt = “What is your email address?”;
                    PromptDialog.Text(context, ResumeAfterEmailAsync, prompt);
                    break;
                case “Fail”:
                    context.Fail(new ArgumentException(“Testing Fail.”));
                    break;
                case “Reset”:
                    context.Reset();
                    break;
                default:
                    await context.PostAsync($”’{choice}’ isn’t implemented.”);
                    break;
            }
        }

        async Task ResumeAfterEmailAsync(IDialogContext context, IAwaitable<string> result)
        {
            string email = await result;
            context.Done(email);
        }
    }
}

As with all IDialog<T> implementations, StartAsync is the entry point. This example is different from previous StartAsync implementations because instead of calling Wait on MessageReceivedAsync, it calls PromptDialog.Choice to give the user a menu for what to do next. This was necessary because after calling ProfileDialog, StartAsync runs and then waits until the next user input. The user needs some indication of what they should do. This differs from SendAsync and Forward, both of which not only call the dialog, but subsequently send the user’s IMessageActivity, which then invokes the next method, which would have been MessageReceivedAsync if StartAsync called the Wait method. In this case, Forward doesn’t make sense and Call is the more logical choice.

After the user responds to the PromptDialog.Choice in StartAsync, the dialog resumes on the MessageReceivedAsync method. The switch statement handles all three choices, discussed next.

Finishing a Dialog

The choices handled in the switch statement in MessageReceivedAsync, from Listing 9-4, represent the three ways to handle finishing a dialog: returning results, resetting the stack, or failing. The next three sections discuss these options.

The Done Method

The Change Email case shows how to handle the Done method. It uses PromptDialog.Text to get the user’s email:

                case “Change Email”:
                    string prompt = “What is your email address?”;
                    PromptDialog.Text(context, ResumeAfterEmailAsync, prompt);
                    break;

PromptDialog.Text specifies ResumeAfterEmailAsync to handle the user’s response:

        async Task ResumeAfterEmailAsync(IDialogContext context, IAwaitable<string> result)
        {
            string email = await result;
            context.Done(email);
        }

Notice the call to IDialogContext’s Done method. This is what transfers control back to the calling dialog. The email parameter is type string, corresponding with the fact that PromptDialog implements IDialog<string>. The IDialog<T> type parameter T is the return type of the dialog. Earlier examples just set this to object because they didn’t return any values. Since this example needs to return a string through its Done method, the IDialog<T> type must also be string. Both the T type in IDialog<T> and the type returned by Done must be the same, or at least assignable.

The Done method pops the current dialog from the dialog stack and returns its argument to the caller. The RootDialog, from Listing 9-2 has a ResumeFromProfileAsync method that handles the return result from the call to ProfileDialog, repeated here:

        async Task ResumeAfterProfileAsync(IDialogContext context, IAwaitable<string> result)
        {
            try
            {
                string email = await result;

                await context.PostAsync($”Your profile email is now {email}”);
            }
            catch (ArgumentException ex)
            {
                await context.PostAsync($”Fail Message: {ex.Message}”);
            }

            context.Wait(MessageReceivedAsync);
        }
    }

ResumeAfterProfileAsync awaits the result parameter, which is an IAwaitable<T>, where T must be the same or an assignable type. There’s also a try/catch handler for the case when a called dialog fails, discussed next.

The Fail Method

The Fail method is a way for a chatbot to indicate that it is unable to complete the dialog. The Fail case, from MessageReceivedAsync in Listing 9-4, calls the Fail method:

                case “Fail”:
                    context.Fail(new ArgumentException(“Testing Fail.”));
                    break;

The Fail method argument is an Exception type. This case uses an ArgumentException, but you can choose whatever exception you feel is appropriate. When the Fail method executes, it takes care of internal Bot Builder record keeping logic and throws the exception parameter for the resume method of the calling dialog.

The Fail method pops the current dialog from the dialog stack and throws its exception argument to the call chain of its caller’s resume method, which is the ResumeAfterProfileAsync method from the RootDialog in Listing 9-2:

        async Task ResumeAfterProfileAsync(IDialogContext context, IAwaitable<string> result)
        {
            try
            {
                string email = await result;

                await context.PostAsync($”Your profile email is now {email}”);
            }
            catch (ArgumentException ex)
            {
                await context.PostAsync($”Fail Message: {ex.Message}”);
            }

            context.Wait(MessageReceivedAsync);
        }
    }

In the resume method, wrap the await call to the result parameter in a try/catch block, where the catch block type is the same type as the Fail method parameter. In ResumeAfterProfileAsync, the catch type is ArgumentException, matching the ArgumentException argument to the Fail method.

The Reset Method

When you want to let a user cancel the current dialog stack and start over, you can use the Reset method. The Reset method unwinds the entire stack. The Reset case from the MessageReceivedAsync method in Listing 9-4 calls the Reset method, repeated here:

                case “Reset”:
                    context.Reset();
                    break;

Calling Reset, like the code above, pops all of the dialogs from the stack and throws an InvalidOperationException to the beginning of the call chain. The Post method, repeated below from Listing 9-1, shows how to handle a Reset:

        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                try
                {
                    await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
                }
                catch (InvalidOperationException ex)
                {
                    var client = new ConnectorClient(new Uri(activity.ServiceUrl));
                    var reply = activity.CreateReply($”Reset Message: {ex.Message}”);
                    client.Conversations.ReplyToActivity(reply);
                }
            }

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

This code wraps SendAsync in a try/catch block because that’s where the InvalidOperationException propagates. The code uses the ConnectorClient, which is discussed in Chapter 2, Setting Up a Project, to communicate with Bot Connector, sending an information message back to the user.

So far, you’ve seen navigation between dialogs based on C# conditional logic syntax. However, Bot Builder has a much more sophisticated set of tools to manage conversations, which you’ll learn about next.

Managing Conversations with Chaining

The ability to call and return values from dialogs, managing the dialog stack is very useful for managing conversations. You might like that and it could be all that’s necessary for a particular chatbot. However, there’s an additional set of chaining tools for even more diverse conversational patterns. This section discusses the Chain class shows how to use several of its methods to accomplish relatively sophisticated navigation tasks.

This section starts with the WineBotChain program and then breaks down several parts of that program into more manageable pieces. What you should see is how Chain allows creating very complex conversation patters. Yet, through the breakdown, you’ll see that it’s not hard to create conversations that might even be simpler.


Images Note

Whether you use Call/Forward/Done or Chain–it doesn’t matter because you can achieve the same goal with either technical approach. This choice is a matter of opinion and style for you and/or your team. Seeing how complex chains can be might lead some to decide they don’t want to use them at all. Yet others might find beauty in the ability to combine navigation logic in a smaller space, rather than having separate dialogs everywhere. My opinion is that neither extreme is optimal and somewhere in-between might lead to better designs.


The WineBotChain Program

WineBotChain demonstrates several features of using the Chain class and its members. It has several layers of dialogs, essentially pulling a lot of navigation code into one place. Because of the versatility of Chain, this isn’t an exhaustive set of examples, but a set of techniques you might use to think about how to use. Listing 9-5 shows the RootDialog for WineBotChain.

LISTING 9-5 Using the Chain Class – RootDialog.cs

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WineBotLib;
using static Microsoft.Bot.Builder.Dialogs.Chain;

namespace WineBotChain.Dialogs
{
    [Serializable]
    public class RootDialog : IDialog<object>
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            string prompt = “Which chain demo?”;
            var options = new[]
            {
                “From”,
                “LINQ”,
                “Loop”,
                “Switch”
            };

            PromptDialog.Choice(context, ResumeAfterChoiceAsync, options, prompt);

            return Task.CompletedTask;
        }

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

            switch (choice)
            {
                case “From”:
                    await DoChainFromAsync(context);
                    break;
                case “LINQ”:
                    await DoChainLinqAsync(context);
                    break;
                case “Loop”:
                    await DoChainLoopAsync(context);
                    break;
                case “Switch”:
                    DoChainSwitch(context);
                    break;
                default:
                    await context.PostAsync($”’{choice}’ isn’t implemented.”);
                    break;
            }
        }

        async Task<string> ProcessWineResultsAsync(WineForm wineResult)
        {
            List[] wines =
                await new WineApi().SearchAsync(
                    (int)wineResult.WineType,
                    (int)wineResult.Rating,
                    wineResult.InStock == StockingType.InStock,
                    “”);

            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.”;

            return message;
        }

        async Task ResumeAfterWineFormAsync(
            IDialogContext context, IAwaitable<WineForm> result)
        {
            WineForm wineResult = await result;

            string message = await ProcessWineResultsAsync(wineResult);

            await context.PostAsync(message);

            context.Wait(MessageReceivedAsync);
        }

        async Task DoChainFromAsync(IDialogContext context)
        {
            IDialog<WineForm> chain =
                Chain.From(() => FormDialog.FromForm<WineForm>(new WineForm().BuildForm));

            await context.Forward(
                chain, 
                ResumeAfterWineFormAsync,
                context.Activity.AsMessageActivity());
        }

        async Task DoChainLinqAsync(IDialogContext context)
        {
            var chain =
                from wineForm in FormDialog.FromForm(new WineForm().BuildForm)
                from searchTerm in new PromptDialog.PromptString(
                    “Search Terms?”, “Search Terms?”, 1)
                where wineForm.WineType.ToString().Contains(“Wine”)
                select Task.Run(() => ProcessWineResultsAsync(wineForm)).Result;

            await context.Forward(
                chain,
                ResumeAfterChainLinqAsync,
                context.Activity.AsMessageActivity());
        }

        async Task ResumeAfterChainLinqAsync(
            IDialogContext context, IAwaitable<string> result)
        {
            try
            {
                string response = await result;
                await context.PostAsync(response);
                context.Wait(MessageReceivedAsync);
            }
            catch (WhereCanceledException wce)
            {
                await context.PostAsync($”Where cancelled: {wce.Message}”);
            }
        }

        async Task DoChainLoopAsync(IDialogContext context)
        {
            IDialog<WineForm> chain =
                Chain.From(() => FormDialog.FromForm(
                     new WineForm().BuildForm, FormOptions.PromptInStart))
                     .Do(async (ctx, result) =>
                     {
                         try
                         {
                             WineForm wineResult = await result;
                             string message = await ProcessWineResultsAsync(wineResult);
                             await ctx.PostAsync(message);
                         }
                         catch (FormCanceledException fce)
                         {
                             await ctx.PostAsync($”Cancelled: {fce.Message}”);
                         }
                     })
                     .Loop();

            await context.Forward(
                chain,
                ResumeAfterWineFormAsync,
                context.Activity.AsMessageActivity());
        }

        void DoChainSwitch(IDialogContext context)
        {
            string prompt = “What would you like to do?”;
            var options = new[]
            {
                “Search Wine”,
                “Manage Profile”
            };

            PromptDialog.Choice(context, ResumeAfterMenuAsync, options, prompt);
        }

        async Task ResumeAfterMenuAsync(IDialogContext context, IAwaitable<string> result)
        {
            IDialog<string> chain =
                Chain
                    .PostToChain()
                    .Select(msg => msg.Text)
                    .Switch(
                        new RegexCase<IDialog<string>>(
                            new Regex(“^Search”, RegexOptions.IgnoreCase),
                            (reContext, choice) =>
                            {
                                return DoSearchCase();
                            }),
                        new Case<string, IDialog<string>>(choice => choice.Contains(“Manage”),
                            (manageContext, txt) =>
                            {
                                manageContext.PostAsync(“What is your name?”);
                                return DoManageCase();
                            }),
                        new DefaultCase<string, IDialog<string>>(
                            (defaultCtx, txt) =>
                            {
                                return Chain.Return(“Not Implemented.”);
                            })
            )
            .Unwrap()
            .PostToUser();

            await context.Forward(
                chain,
                ResumeAfterSwitchAsync,
                context.Activity.AsMessageActivity());
        }

        IDialog<string> DoSearchCase()
        {
            return
                Chain
                    .From(() => FormDialog.FromForm(
                        new WineForm().BuildForm, FormOptions.PromptInStart))
                    .ContinueWith(async (ctx, res) =>
                    {
                        WineForm wineResult = await res;
                        string message = await ProcessWineResultsAsync(wineResult);
                        return Chain.Return(message);
                    });
        }

        IDialog<string> DoManageCase()
        {
            return
                Chain
                    .PostToChain()
                    .Select(msg => $”Hi {msg.Text}’! What is your email?”)
                    .PostToUser()
                    .WaitToBot()
                    .Then(async (ctx, res) => (await res).Text)
                    .Select(msg => $”Thanks - your email, {msg}, is updated”);
        }

        async Task ResumeAfterSwitchAsync(IDialogContext context, IAwaitable<string> result)
        {
            string message = await result;
            context.Done(message);
        }
    }
}

As demonstrated plenty of times before, the chatbot’s Post method calls SendAsync on RootDialog, from Listing 9-5. RootDialog has the required StartAsync and typical MessageReceivedAsync for IDialog<T> types. MessageReceivedAsync uses a PromptDialog.Choice to ask the user what options they want: From, LINQ, Loop, or Switch. The continuation from Choice, ResumeAfterChoiceAsync runs the user’s response through a switch statement to launch the proper method to handle the user’s choice.

Each of the user’s choices represent approaches to take when designing navigation with Chain. The From option specifies a dialog to run and Loop runs a dialog multiple times. The LINQ option does exactly what it says, allowing LINQ statements that use dialogs. Switch is a way to choose which dialog to run, based on user input. From Switch, we’ll drill down some more to show new and different Chain methods. Let’s start with From.


Images Tip

The Chain class has several methods, each supporting a fluent interface. This allows you to create conversations that navigate from one dialog or response handler to another dialog or response handler in a seamless way. No doubt this chaining behavior inspired the name of the class.


Chain.From

The Chain.From method supports launching a dialog as part of a chain. In Listing 9-5, the DoChainFromAsync method, repeated below, handles the choice when the user selects From:

        async Task DoChainFromAsync(IDialogContext context)
        {
            IDialog<WineForm> chain =
                Chain.From(() => FormDialog.FromForm<WineForm>(new WineForm().BuildForm));

            await context.Forward(
                chain, 
                ResumeAfterWineFormAsync,
                context.Activity.AsMessageActivity());
        }

DoChainFromAsync uses Chain.From to launch the FormFlow form, WineForm. FormDialog.FromForm returns IFormDialog, an IDialog<T>, that From requires as part of its Func<IDialog<T>> parameter type.

Behind the scenes, From takes care of the Call and Done for dialog stack management. The subsequent call to Forward starts the Chain, handling the response in ResumeAfterWineFormAsync, repeated here:

        async Task ResumeAfterWineFormAsync(IDialogContext context, IAwaitable<WineForm> result)
        {
            WineForm wineResult = await result;

            string message = await ProcessWineResultsAsync(wineResult);

            await context.PostAsync(message);

            context.Wait(MessageReceivedAsync);
        }

Like a normal resume method, explained earlier in this chapter, this code processes the response from the dialog. This is the end of processing from the Chain, so the code calls context.Wait to set MessageReceivedAsync as the next method for RootDialog to send messages to.

Chain.Loop

When users select the Loop option, the DoChainLoopAsync method handles the request. The following code, repeated from Listing 9-5, shows how to loop, continually running the same dialog:

        async Task DoChainLoopAsync(IDialogContext context)
        {
            IDialog<WineForm> chain =
                Chain.From(() => FormDialog.FromForm(new WineForm().BuildForm, FormOptions.PromptInStart))
                     .Do(async (ctx, result) =>
                     {
                         try
                         {
                             WineForm wineResult = await result;
                             string message = await ProcessWineResultsAsync(wineResult);
                             await ctx.PostAsync(message);
                         }
                         catch (FormCanceledException fce)
                         {
                             await ctx.PostAsync($”Cancelled: {fce.Message}”);
                         }
                     })
                     .Loop();

            await context.Forward(
                chain,
                ResumeAfterWineFormAsync,
                context.Activity.AsMessageActivity());
        }

Similar to the previous section, this code calls Chain.From to launch the WineForm FormFlow dialog. The difference here is the call to Loop, causing the dialog to continually run. With a FormFlow dialog, a user can type Quit to exit the dialog, throwing a FormCanceledException. The Do method handles the response from the form, handling the FormCanceledException for when the user quits.

This example highlights the purpose of chaining, allowing navigation that launches a dialog, handles the results, and continues navigation operations until complete.

While the Forward starts the chain, the resume method, ResumeAfterWineFormAsync, won’t be called in this example because the only way out was to Quit, with the FormCanceledException. Fortunately, Chain has another method, While, that will support looping while a condition is true.

Chain.Switch

Whenever a chatbot needs to implement Chain logic based on user input, it can use the Switch method. Switch has three ways to handle user input: regex, custom logic, or default. The following DoChainSwitchAsync method, repeated from Listing 9-5, shows each of the cases:

        void DoChainSwitch(IDialogContext context)
        {
            string prompt = “What would you like to do?”;
            var options = new[]
            {
                “Search Wine”,
                “Manage Profile”
            };

            PromptDialog.Choice(context, ResumeAfterMenuAsync, options, prompt);
        }

DoChainSwitch uses PromptDialog.Choice, specifying ResumeAfterMenuAsync, repeated here:

        async Task ResumeAfterMenuAsync(IDialogContext context, IAwaitable<string> result)
        {
            IDialog<string> chain =
                Chain
                    .PostToChain()
                    .Select(msg => msg.Text)
                    .Switch(
                        new RegexCase<IDialog<string>>(new Regex(“^Search”, RegexOptions.IgnoreCase),
                            (reContext, choice) =>
                            {
                                return DoSearchCase();
                            }),
                        new Case<string, IDialog<string>>(choice => choice.Contains(“Manage”),
                            (manageContext, txt) =>
                            {
                                manageContext.PostAsync(“What is your name?”);
                                return DoManageCase();
                            }),
                        new DefaultCase<string, IDialog<string>>(
                            (defaultCtx, txt) =>
                            {
                                return Chain.Return(“Not Implemented.”);
                            })
            )
            .Unwrap()
            .PostToUser();

            await context.Forward(
                chain,
                ResumeAfterSwitchAsync,
                context.Activity.AsMessageActivity());
        }

This Chain has several methods: PostToChain, Select, Switch, Unwrap, and PostToUser. Each of these methods, except for the first, process the output dialog of the antecedent (previous) method. We need to go step-by-step, following the processing to see how this works.

Looking past the Chain, the context.Forward call starts the chain. As you know, Forward passes its third parameter, an IMessageActivity, to the dialog and Chain is an IDialog<T>, receiving that IMessageActivity instance. The PostToChain method sends the IMessageActivity, received from the caller, to the next method in the chain, Select.


Images Tip

While Chain, in the ResumeAfterMenuAsync method, is a couple levels into the hierarchy, you could also pass a Chain to SendAsync at the chatbot’s Post method, as the main dialog, which would pass the user’s IMessageActivity, requiring a call to PostToChain to pass that IMessageActivity to the next method in the chain.


The Select method takes the instance for whatever type the previous method passed and allows you to build a new projection to pass to the next method. In this example, Select takes the IMessageActivity, passed from PostToChain, and builds a string type projection, which is the Text property of the IMessageActivity. Select passes this string to the Switch method.

Switch takes antecedent input and runs it through each of its cases. Once Switch matches a case, it passes the results of that case to the next method in the chain. Logically, this works much the same as a C# switch statement, except for the addition of the input and output value flows. Switch supports three types of cases: RegexCase, Case, and DefaultCase.

The RegexCase has two parameters–a Regex instance and a case handler. The first parameter is an instance of the .NET Regex class, which allows specifying a regular expression to match the input on. RegexCase uses the Regex instance to determine if the input matches the regular expression. In this example, if the input starts with Search, the case matches and executes the handler. The handler type is ContextualSelector, accepting IBotContext and string parameters and returning an IDialog<T>, where T is string in this example.

The second parameter to Case is also a ContextualSelctor, but the first parameter, condition, is Func<T, bool>, where T is string in this case. The condition evaluates the input and returns a bool to indicate if the condition matches. This example matches if the user input contains Manage.

When none of the cases match, the Switch method executes the ContextSelector for DefaultCase. The DefaultCase implementation is another Chain method, Return, which returns an IDialog<T>, where T is a string in this example. Both the DoSearchCase and DoManageCase also return IDialog<string>. I’ll discuss these methods soon, but first explain why the next method, UnWrap, is required.

Switch passes IDialog<T> to the next method. What’s important about this scenario is that T is IDialog<string> because that’s what the ContextSelector of each Switch case returns. This means, Switch passes IDialog<IDialog<string>> to the next method. The code and dialogs called by a Chain method shouldn’t have to understand whether they’re being called by a Chain, Call, or Forward. Therefore, the Chain.Unwrap helps because it dereferences the inner IDialog<T> for you. Coming full circle, Unwrap accepts IDialog<IDialog<string>> from Swich, extracts the IDialog<string>, and passes the IDialog<string> to the next method, PostToUser.

The PostToUser method accepts an IDialog<string> from its antecedent and, as its name suggests, posts the string to the user. In this example, the strings are the responses from the Switch cases.

The nesting in this sample is extreme and might not be the way you would design your code, yet it demonstrates different ways to use chains. Let’s have fun and explore a bit more. The next sections drill-down on the DoSearchCase and DoManageCase logic, from the Switch cases.

Chain.ContinueWith

The previous example, for Chain.Loop, handles the response from the the dialog with a Do, which is for side-effect operations with the result of the antecedent dialog. There are also a couple other ways to handle dialog responses, including the ContinueWith, which is part of the DoSearchCase method, repeated below.

        IDialog<string> DoSearchCase()
        {
            return
                Chain
                    .From(() => FormDialog.FromForm(new WineForm().BuildForm, FormOptions.PromptInStart))
                    .ContinueWith(async (ctx, res) =>
                    {
                        WineForm wineResult = await res;
                        string message = await ProcessWineResultsAsync(wineResult);
                        return Chain.Return(message);
                    });
        }

ContinueWith takes a parameter of type Continuation that passes an IBotContext and IAwaitable<T> to the handler, where T is WineForm in this example. The Chain.Return returns IDialog<string>—the DoSearchCase method return type for the Switch method.

The next example shows even more chatbot conversation customization with Chain.

An Assortment of Posting and Waiting Chain methods

Rather than calling a separate dialog, Chain methods can interact with the user directly, using several posting and waiting methods. Here’s an example from DoManageCase:

        IDialog<string> DoManageCase()
        {
            return
                Chain
                    .PostToChain()
                    .Select(msg => $”Hi {msg.Text}’! What is your email?”)
                    .PostToUser()
                    .WaitToBot()
                    .Then(async (ctx, res) => (await res).Text)
                    .Select(msg => $”Thanks - your email, {msg}, is updated”);
        }

This example starts with PostToChain, taking the input from the Switch case and passing it to the next method in the Chain. If you recall, the case handler from the Switch method called manageContext.PostAsync(“What is your name?”) before calling DoManageCase, setting the Chain in DoManageCase to the next dialog to handle the user’s response.

Select receives the IMessageActivity, msg, from PostToChain and projects a new string for PostToUser.PostToUser sends the input string to the user.

At this point in time, we don’t have a message from the user, so we must wait for the user’s response, so the Chain calls WaitToBot, waiting for a user message to arrive for the chatbot.

You’ve seen Do and ContinueWith methods. Now the chatbot takes the user message that arrives after WaitToBot receives the user’s response and passes that response to Then. The Then method is another type of handler that accepts a Func<IBotContext, IAwaitable<T>, IDialog<T>> where T is IMessageActivity for this example. Then processes the result, and passes that result to Select.

Using the IMessageActivity from Then, Select projects a new string and returns a new IDialog<string> that is sent back to the Switch case.

This example demonstrates how you might use Chain for a quick interaction with the user. While there’s potential for this to be a complex set of interactions, you might consider it for simple interactions, rather than engage in the ceremony of a new IDialog<T>.


ContinueWith, Do, or Then – Which to Choose?

You’ve seen three Chain methods that implement lambdas: ContinueWith, Do, and Then. They’re syntax makes them look similar. Sometimes one might work in place of another, but they each have different features that can make them unique in specific circumstances. Listing 9-5 offers some indication of how these methods are used and the following discussion highlights aspects that illuminate differences.

ContinueWith lets you create a brand new IDialog<T> to return as the next dialog in the Chain. You can see in the DoSearchCase method, where ContinueWith uses Chain.Return to return a new IDialog<T>, where T is a new string.

Do doesn’t return any value. Its purpose is to perform some action, aka side-effect, and let the antecedent dialog continue to be the current dialog in the chain. In the DoChainLoopAsync method, Do just performs an action, sends a message to the user, and ends.

The purpose of Then is to extract a value from an IMessageActivity and return a value to the chain. In DoManageCase, the Then lambda returns the Text property because that’s all the next method needs. This is a useful way to project or transform any part of an IMessageActivity so the next method in the Chain doesn’t have to.

To summarize, here are a few quick rules to think about when choosing ContinueWith, Do, or Then:

Images ContinueWith: Return a new IDialog<T>.

Images Do: Perform a side-effect and stay on the antecedent (same) IDialog<T>.

Images Then: Project input into a new value for the next method in the Chain.


Because of how they’re named, it’s easy to accidentally use the wrong method, but we do have help from the compiler and IDE, giving us syntax errors because of type mismatches.

LINQ to Dialog

Previous sections show how you can use Chain in many ways. Another way to use Chain is via LINQ statements. In LINQ to Dialog, the data source is IDialog<T>. You can do from, let, select, and where clauses. While you can use the fluent syntax, such as earlier examples that used Select, you can also use LINQ query syntax, as demonstrated in the DoChainLinqAsync:

        async Task DoChainLinqAsync(IDialogContext context)
        {
            var chain =
                from wineForm in FormDialog.FromForm(new WineForm().BuildForm)
                from searchTerm in new PromptDialog.PromptString(“Search Terms?”, “Search Terms?”, 1)
                where wineForm.WineType.ToString().Contains(“Wine”)
                select Task.Run(() => ProcessWineResultsAsync(wineForm)).Result;

            await context.Forward(
                chain,
                ResumeAfterChainLinqAsync,
                context.Activity.AsMessageActivity());
        }

The two from clauses demonstrate how to perform a select-many. This example runs a sequence of dialogs, where WineForm runs first, returns a FormFlow dialog result in wineForm and then PromptDialog.PromptString runs, returning the dialog result value in searchTerm.

If all goes well, the select executes ProcessWineResultsAsync and returns the results of the chain as a string. However, notice that the where clause filters on whether wineForm.WineType has a value containing Wine. As long as the where clause is true all is good. If the user selects sake for WineType, the where clause is false, resulting in the Chain throwing a WhereCanceledException. This might be unexpected for developers looking at LINQ data implementations that filter on the where clause, but this is LINQ to Dialog and it indicates that the Chain is unable to complete its intended purpose, resulting in an exception.

Another gotcha that might not seem obvious is that you can’t wrap the code in DoChainLinqAsync in a try/catch to handle the WhereCanceledException. Remember, we’re working with async code, and the code awaits Forward, which starts the Chain and specifies the ResumeAfterChainLinqAsync method, repeated here:

        async Task ResumeAfterChainLinqAsync(IDialogContext context, IAwaitable<string> result)
        {
            try
            {
                string response = await result;
                await context.PostAsync(response);
                context.Wait(MessageReceivedAsync);
            }
            catch (WhereCanceledException wce)
            {
                await context.PostAsync($”Where cancelled: {wce.Message}”);
            }
        }

The ResumeAfterChainLinqAsync result parameter is IAwaitable<string>, like all callbacks for Call and Forward. Here, there is a try/catch block and the statement of interest is await result. Exceptions from the Chain can’t be handled until the code awaits the result, which is what ResumeAfterChainLinqAsync does.

Now you’ve seen several ways to use Chain. There are myriad options and the Bot Framework will no doubt be adding and enhancing features. You can take what is there, in the Chain feature set, and build on it to see which features work for you, your team, and project.

Handling Interruptions with IScorable

A lot of chatbot work goes into designing conversation flow. The easy part is what is often termed as the happy path, the conversation patterns that work perfectly as long as the user follows the script. The remaining work comes from handling the conversation paths that might not go as planned, such as that which happens in real life. An example might be a user booking an airline ticket and somewhere in the conversation asks how the weather is at the destination. Another example might be if a user is in the midst of answering questions for a registration form for a service and asks what the cancellation policy is.

Because users can say anything to a chatbot at any time, you need a tool to handle out-of-band communication, which is where IScorable comes in. An IScorable is a type that listens for incoming messages, evaluates whether it should handle the message, and votes to be the handler. Bot Builder picks the IScorable with the highest score and allows it to handle the message. Whenever an IScorable type handles a message, that overrides any handling on the dialog stack, allowing the winning IScorable to handle the user’s message and reply as appropriate.

The example in this section is for a situation where a user might ask for help. This is built into FormFlow, but other dialog types don’t have help unless they’ve been coded to handle it. In this scenario an IScorable could be useful, intercepting a help command to assist the user when their command would otherwise be interpreted as input to the current dialog at the top of the stack. Listing 9-6, from the ScorableHelp project in the accompanying source code, shows the HelpScorable that allows this.

LISTING 9-6 Creating an IScorableHelpScorable.cs

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Builder.Scorables.Internals;
using Microsoft.Bot.Connector;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace ScorableHelp.Dialogs
{
    public class HelpScorable : ScorableBase<IActivity, string, double>
    {
        readonly IBotToUser botToUser;

        public HelpScorable(IBotToUser botToUser)
        {
            this.botToUser = botToUser;
        }

        protected override async Task<string> PrepareAsync(
            IActivity activity, CancellationToken token)
        {
            var text = (activity as IMessageActivity)?.Text ?? “”;
            var regex = new Regex(“/help”, RegexOptions.IgnoreCase);
            var match = regex.Match(text);

            return match.Success ? match.Groups[0].Value : null;
        }

        protected override bool HasScore(IActivity item, string state)
        {
            return state != null;
        }

        protected override double GetScore(IActivity item, string state)
        {
            return 1.0;
        }

        protected override async Task PostAsync(
            IActivity item, string state, CancellationToken token)
        {
            await botToUser.PostAsync(“How may I help?”, cancellationToken: token);
        }

        protected override Task DoneAsync(IActivity item, string state, CancellationToken token)
        {
            return Task.CompletedTask;
        }
    }
}

HelpScorable derives from the abstract class ScorableBase, which derives from IScorable. Each of the methods override abstract methods from ScorableBase. The ScorableBase class helps by supporting strongly typed inheritance. Here’s a part of the ScorableBase class with all members, except the GetScore methods omitted for brevity:

    public abstract class ScorableBase<Item, State, Score> : IScorable<Item, Score>
    {
        protected abstract Score GetScore(Item item, State state);

        [DebuggerStepThrough]
        Score IScorable<Item, Score>.GetScore(Item item, object opaque)
        {
            var state = (State)opaque;
            if (!HasScore(item, state))
            {
                throw new InvalidOperationException();
            }

            return this.GetScore(item, state);
        }
    }

Notice that ScorableBase has three type parameters, but IScorable only has two. Item is the type of parameter that the class evaluates, State is the type for the value allowing the code to hold any information it needs to make a decision throughout the scorable process, and Score is the type of the result that it the output of the class. The goal of the IScorable is to evaluate Item and store shared information for all methods in State, allowing a final Score to be found. In Listing 9-5, Item is IActivity, State is string, and Score is double.

The two GetScore overloads in ScorableBase demonstrate how it enables a strongly typed child class. Bot Builder uses the interface type to call the explicitly implemented IScorable<Item, Score>.GetScore method, which has some checks on whether a score has been assigned. We’ll discuss the HelpScorable implementation of this in a following paragraph. What you should get out of this is that even though the opaque parameter is type object, IScorable<Item, Score>.GetScore delegates the call to the abstract GetScore, calling child class methods, like those in HelpScorable, with strongly typed parameters. Any class that implements IScorable directly would be weakly typed. ScorableBase is more convenient.

The HelpScorable constructor takes an IBotToUser parameter and saves the reference to use in the PostAsync method to respond to the user. Bot Builder calls each of the methods in the order shown in Listing 9-6, with a caveat that is explained shortly.

PrepareAsync is where the IScorable evaluates user input, via the IActivity parameter. This example uses a regular expression to see if the user typed /help. Notice the forward slash, this might be a useful convention to differentiate commands from normal conversation input. In fact, the Bot Framework has an IScorable, named /deleteprofile, for deleting user information that also uses the forward slash convention. Just type /deleteprofile and it will clear out all user data in Bot State. PrepareAsync returns a string that represents the result of the evaluation and is passed as the state parameter to HasScore.

Bot Builder could potentially be evaluating multiple IScorable instances. In this example, we know that it’s evaluating both /deleteprofile and /help. To minimize the list, each IScorable can indicate that it doesn’t want to vote to handle the user’s input, which is the purpose of the HasScore method. The HelpScorable example checks status for null as its test because PrepareAsync either returns a matched string or null if the user typed something other than /help. If an IScorable wants to vote, it returns true. When an IScorable returns false, it no longer participates in evaluation and Bot Builder doesn’t call any more of its methods.

GetScore returns its score to compete to handle the message. In this example, the score is 1.0, which is consistent with the implementation of /deleteprofile. The return value is a double and represents the level of certainty, confidence, or probability that this IScorable can properly handle the user’s input. You have the freedom to set the return values to accommodate which IScorable to win in your design.

It’s possible that multiple IScorables will return a score, resulting in competition to handle a single activity. When this happens, Bot Builder takes the IScorable with the highest score.

If an IScorable wins with the highest score, Bot Builder calls its PostAsync method. The purpose of the PostAsync method is to respond to user input. Doing so sooner might be a waste of processing if this IScorable didn’t win. This example posts a simple message to the user, but could also launch another dialog, which would become the current dialog on the dialog stack.

The DoneAsync method performs clean-up work, if any, and is the last method called.

You must write code to register an IScorable so that Bot Builder can find it. List 9-7 shows how to do this.

LISTING 9-7 Registering an IScorableGlobal.asax.cs

using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Scorables;
using Microsoft.Bot.Connector;
using ScorableHelp.Dialogs;
using System.Web.Http;

namespace ScorableHelp
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);

            Conversation.UpdateContainer(builder =>
            {
                builder.RegisterType<HelpScorable>()
                    .As<IScorable<IActivity, double>>()
                    .InstancePerLifetimeScope();
            });
        }
    }
}

Bot Framework uses an Inversion of Control (IoC) container named AutoFac. This is an open source tool, supporting unit testing and the loose coupling of code. It’s also a way to plug in types for various purposes with Bot Builder. In this example, the builder.RegisterType allows Bot Builder to know that it should allow HelpScorable to compete to handle user messages, along with other registered IScorable types.

The Conversation.UpdateContainer method is a Bot Builder type, exposing a ContainterBuilder instance, builder, that Bot Builder uses to register its types. You’ll see many examples that instantiate a ContainerBuilder and then call Update on the instance after registering types. However, AutoFac deprecated Update and that causes compiler warnings. Using Conversation.UpdateContainer fixes that problem.

That completes the discussion for IScorables, which is another way to respond to the user. On responses, some of the messages to the user have been quite plain and the next section shows how to improve the appearance of those messages.

Formatting Text Output

All of the output so far has been plain text. In the case of the response from the WineBot search, the response appears as a comma-separated list and that isn’t easy to read. In this section, you’ll learn how to format user output so it’s more readable. Listing 9-8, from the WineBotFormatted project in the accompanying source code, shows how to format text in Markdown format.

LISTING 9-8 Formatting Text – WineForm.cs

using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using WineBotLib;
using Microsoft.Bot.Builder.Dialogs;
using System.Threading.Tasks;
using System.Text;
using Microsoft.Bot.Connector;

namespace WineBotFormatted.Dialogs
{
    [Serializable]
    public class WineForm
    {
        public WineType WineType { get; set; }
        public RatingType Rating { get; set; }
        public StockingType InStock { get; set; }

        public IForm<WineForm> BuildForm()
        {
            return new FormBuilder<WineForm>()
                .Message(
                    “I have a few questions on your wine search. “ +
                    “You can type ”help” at any time for more info.”)
                .OnCompletion(WineFormCompletedAsync)
                .Build();
        }

        async Task WineFormCompletedAsync(IDialogContext context, WineForm wineResults)
        {
            List[] wines =
                await new WineApi().SearchAsync(
                    (int)wineResults.WineType,
                    (int)wineResults.Rating,
                    wineResults.InStock == StockingType.InStock,
                    “”);

            var message = new StringBuilder();

            if (wines.Any())
            {
                message.AppendLine(“# Top Matching Wines “);

                foreach (var wine in wines)
                    message.AppendLine($”* {wine.Name}”);
            }
            else
            {
                message.Append(“_Sorry, No wines found matching your criteria._”);
            }

            //var reply = (context.Activity as Activity).CreateReply(message.ToString());
            //reply.TextFormat = “plain”;
            //await context.PostAsync(reply);

            await context.PostAsync(message.ToString());
        }
    }
}

The WineFormCompletedAsync method shows how to format text. It’s using Markdown, which is a popular form of text formatting that you can learn more about with a Web search. This example uses the h1 symbol, which is a hash mark to create a heading - # Top Matching Wines. Each wine appears in a bulleted list, indicated by the star prefix - * {wine.Name}. Figure 9-1 shows what this looks like.

Images

FIGURE 9-1 Formatting message text with markdown.

As Figure 9-1 shows, the wine list is much more readable than the previous comma-separated version.

The default format of text is markdown, which might be surprising because characters like # and * can alter the appearance of the output if it wasn’t intended to be interpreted as markup. This assumes that a channel will try to translate the markdown to HTML, but on channels that are text only, such as SMS, you’ll see all the markdown characters as normal. This highlights the benefits of markdown in that it is very easy to read whether translated to HTML or not. If you want to have plain text output, you can set the TextFormat property of the reply message to plain. The following commented code, repeated from Listing 9-8 shows how to set the format to plain text:

        //var reply = (context.Activity as Activity).CreateReply(message.ToString());
        //reply.TextFormat = “plain”;
        //await context.PostAsync(reply);

The Activity type has a CreateReply method that takes an input Activity and converts it to a response Activity, essentially switching the Reciptient and From properties behind the scenes. Once you have a new activity, set the TextFormat property to plain.


Images Note

TextFormat also accepts a value of xml. However, that only works for Skype. To use it, you can decorate text with HTML markup. This might be useful for porting an older Skype chatbot that used HTML formatting, but isn’t practical for a chatbot that targets multiple platforms.


Summary

In this chapter, you learned various forms of advanced conversation. The chapter started with a discussion of the dialog stack, covered chainging, discussed IScorables, and showed how to format text.

You learned how the underlying navigation of dialogs are managed via stack, with the current dialog at the top of the stack. This continued to examples of using various methods to navigate from one dialog to another. You learned how dialogs can return results to a calling dialog that could process those results.

The Chain methods are very extensive and allow creating dialogs as well as other logic. You can manage navigation with chains via loops and case statements. There are ways to add continuations to handle the results of a dialog. You can even use LINQ to build dialog chains. There were several examples, showing how Chain could be used in a program and you’re encouraged to experiment more to get a feel for the possibilities.

You learned how to create an IScorable, which allows a chatbot to handle user interactions outside the normal flow of the current conversation. IScorables also need to be registered via the AutoFac IoC container that Bot Builder uses.

Finally, you learned how to use markdown to format text for output. This can offer a better user experience than plain text. In the next chapter, you’ll learn how to improve the user experience even more with cards.

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

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