Thursday, April 21, 2016

Bot Builder Dialogs


The Bot Framework includes a Bot Builder SDK that helps you manage conversations with users, including Dialogs, Form Flow, and other supporting types. The Bot Builder SDK is open source, supporting both C# and Node.js, and you can visit the source code on GitHub.

This post is an introduction to Dialogs and shows you how to use types that prompt a user, move between questions, and manage state. The example I’m using is a Bug Report Bot. It isn’t very sophisticated, but is designed to show you how to manage a simple conversation, using dialogs.

Project Set-Up

You can get started with the normal Bot Framework template, described on the Bot Framework Getting started page. The template already gives you a reference to the Microsoft.Bot.Connector library and now you’ll need a reference to the Microsoft.Bot.Builder library, which you can load via NuGet. Here’s the Package Manager console command if you prefer:
PM> Install-Package Microsoft.Bot.Builder
With your project set up and configured, you can start building dialogs.

Getting Started

A dialog is a class with state and methods that use Bot Builder types to manage interactions between bot and user. For the Bug Report Bot, I’ll create a class that contains fields and methods. State is important because it holds important information about an ongoing conversation. The following BugReportDialog class shows how to code for managing state.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace BugDialogBot
{
    [Serializable]
    class BugReportDialog : IDialog<object>
    {
        ProductOptions productOptions;
        PlatformOptions platformOptions;
        string description;
        
        // other members omitted until later
    }
}
The Bot Framework manages conversations by sending JSON messages through the Bot Connector and to/from user and bot. These messages hold the state of an ongoing conversation. To facilitate managing state with the Bot Framework, your dialog type must be serializable. To support this, the code decorates the BugReportDialog class with the Serializable attribute. Additionally, the class contains fields, representing the state of the conversation. Besides making your dialog class serializable and storing values in serializable fields and/or properties, you don’t have to do anything extra to manage state, because that’s one of the benefits of Bot Framework Dialogs. Both ProductOptions and PlatformOptions are enum types, shown below:
    public enum ProductOptions
    {
        Office,
        SQLServer,
        VisualStudio
    }

    public enum PlatformOptions
    {
        Linux,
        Mac,
        Windows
    }
These enum typess represent options that the user can select when prompted. The fields will hold the user’s response so that we have all of the user’s responses when reaching the end of the conversation/dialog.
Another interesting part of the BugReportDialog is the fact that it implements IDialog<object>, which I’ll discuss next.

The Dialog Entry Point

For IDialog<T>, you implement its single member, StartAsync, which is the entry point for the dialog. Within StartAsync, you’ll specify which dialog method the Bot Framework calls to initiate a conversation. The following code shows the StartAsync implementation for the Bug Report Bot:
    [Serializable]
    class BugReportDialog : IDialog<object>
    {
        ProductOptions productOptions;
        PlatformOptions platformOptions;
        string description;
        
        public async Task StartAsync(IDialogContext context)
        {
            context.Wait(ConversationStartedAsync);
        }
        
        // other members omitted until later

    }
The StartAsync method accepts an IDialogContext parameter, context, which has members to help you manage state and various aspects of a conversation. It’s essentially a convenient hook for various services in the Bot Framework. You’ll see IDialogContext again in later parts of this post because the Bot Framework passes it to every other dialog method. The StartAsync method for BugReportDialog uses the Wait method of context to specify the starting method, ConversationStartedAsync, to call when the user initiates a conversation with the bot. The next section shows how to write that starting method.

Interacting with the User

When the user initiates a conversation with the bot, the StartAsync method specifies which dialog method to start with. In the previous code, that was ConversationStartedAsync, which is shown below:
        public async Task ConversationStartedAsync(IDialogContext context, IAwaitable<Message> argument)
        {
            Message message = await argument;
            await context.PostAsync(message.Text);

            PromptDialog.Choice(
                context: context,
                resume: ResumeAndPromptPlatformAsync,
                options: Enum.GetValues(typeof(ProductOptions)).Cast<ProductOptions>().ToArray(),
                prompt: "Welcome to the Bug Report Bot! Please select the product you're having a problem with (SQLServer or VisualStudio):",
                retry: "I didn't understand. Please try again.");
        }
As mentioned earlier, every dialog method receives a IDialogContext parameter, context. They also receive an IAwaitable<T>, argument. The T is whatever type the method is expecting. Since StartAsync called context.Wait(ConversationStartedAsync), the T will be Message. The T will be specified by the previous method in the dialog chain and you’ll see how that works when covering subsequent methods in this post.
Since argument is awaitable, the code awaits to reference the Message passed by the Bot Framework. This lets you evaluate the value of the argument and build logic to handle the information that the user provided. In this example, we’re interested in the message.Text that the user typed. This is normally “Hi” or “Hello” and the code echoes that back to the user by calling the context.PostAsync method.
PromptDialog is a type in the Bot Builder SDK that manages an interaction with the user. There are different types of prompts, such as Yes/No, string, and number. The Choice prompt lets the user pick one of a number of options. Notice the Enum array passed as the options parameter. The user can choose one of the ProductOption types, mentioned earlier in this post. The prompt dialog uses a convention of breaking the words at capitalized letters in each Enum value – so ProductOptions.VisualStudio becomes “Visual Studio” to the user.
On the other PromptDialog parameters, context is the IDialogContext that I discussed earlier where the Bot Framework passes that to dialog methods. The prompt is the question the bot asks the user and retry is the message the bot uses if it doesn’t understand what the user typed.
What’s interesting here is the resume parameter. When you have a series of interactions, you can specify each of those interactions with a dialog method. The resume parameter specifies which dialog method to call after the user responds. In this case, the next dialog method is ResumeAndPromptPlatformAsync, which I’ll describe in the next section.

Managing Conversation Flow

As the user interacts with a bot, the Bot Framework routes those interactions to the dialog method you specified to handle the next user message. Since the ConversationStartedAsync method called PromptDialog the user’s response goes to the ResumeAndPromptPlatformAsync method as the argument parameter, shown below:
        public async Task ResumeAndPromptPlatformAsync(IDialogContext context, IAwaitable<ProductOptions> argument)
        {
            productOptions = await argument;

            PromptDialog.Choice(
                context: context,
                resume: ResumeAndPromptDescriptionAsync,
                options: Enum.GetValues(typeof(PlatformOptions)).Cast<PlatformOptions>().ToArray(),
                prompt: "Which platform did the problem occur on? (Linux, Mac, or Windows):",
                retry: "I didn't understand. Please try again.");
        }

        public async Task ResumeAndPromptDescriptionAsync(IDialogContext context, IAwaitable<PlatformOptions> argument)
        {
            platformOptions = await argument;

            PromptDialog.Text(
                context: context,
                resume: ResumeAndPromptSummaryAsync,
                prompt: "Please provide a detailed description of the problem:",
                retry: "I didn't understand. Please try again.");
        }
public async Task ResumeAndPromptSummaryAsync(IDialogContext context, IAwaitable<string> argument) { description = await argument; PromptDialog.Confirm( context: context, resume: ResumeAndHandleConfirmAsync, prompt: $"You entered '{productOptions}', '{platformOptions}', and '{description}'. Is that correct?", retry: "I didn't understand. Please try again."); }
There are three dialog methods above, each with their own argument handling and PromptDialog. All three methods store the argument in a class field, which is part of the state of the dialog class, so we can remember the user’s choices. A couple of the PromptDialogs are different from what you’re seen previously. PromptDialog.Text takes a freeform string from the user and PromptDialog.Confirm takes a Yes or No answer. The PromptDialog accepts a certain type and when the user responds, the argument parameter for the resumed dialog method is the type of the previous PromptDialog. Eventually, you’ll reach the end of the report.

Completing the Report

The PromptDialog.Confirm in the previous section summarized the options the user chose and asked them if that was correct. The next dialog method, ResumeAndHandleConfirmAsync, evaluates their response, replies accordingly, and re-starts the Bug Report Bot at the beginning so the user can add another Bug Report, if they like:
        public async Task ResumeAndHandleConfirmAsync(IDialogContext context, IAwaitable<bool> argument)
        {
            bool choicesAreCorrect = await argument;

            if (choicesAreCorrect)
                await context.PostAsync("Your bug report has been submitted. Thanks for the feedback!");
            else
                await context.PostAsync("I see. You're welcome to try again.");

            context.Wait(ConversationStartedAsync);
        }
Here, I’m doing more work with the argument, using an if statement to determine an appropriate response to the user. Since I don’t need a response from the user, I don’t use a PromptDialog. The IDialogContext has a helper method, PostAsync, that lets you send a message to the user. This would have been a good opportunity to put additional logic that submitted the bug report and cleaned up state. The IDialogContext has another method, Wait, which will set which dialog method the Bot Framework sends the next message to, which is the ConversationStartedAsync that you saw at the beginning of this post.

Summary

This post described how to use Bot Framework Dialogs. You saw how to configure dependencies and create a dialog class. The StartAsync method is the entry point to a dialog conversation and you build a chain of dialog methods from there. There are several types of PromptDialog methods, each allowing you to obtain different types of information from a user. You saw how to manage state, which allowed summarizing choices at the end of the dialog to complete the operation. In a later blog post, I’ll build on dialogs with an even more powerful Bot Builder tool named FormFlow.

@JoeMayo

15 comments:

  1. Hi Joe, I am trying some examples with choices, but I thought that the options are showed in the emulator, but I don't get to do this.
    Could you help me?

    ReplyDelete
    Replies
    1. Hi Eduardo,

      Does my demo code at https://github.com/JoeMayo/BotDemos help?

      Joe

      Delete
    2. Hi Joe,

      I want to show my options like these http://postimg.org/image/dyisxl1kx/.

      We can choose between:

      1. using Dialogs with Prompt.Choice that allows IEnumerable as options, but I think is is not possible now. The message only show the prompt field.

      2. using FormFlow only allows Enum types but the message shows the options.

      I want to use the first option because I fill a list with custom values.

      Do you have any idea about these?

      Thanks

      Eduardo






      Delete
    3. I just posted on how to do this with FormFlow: http://mayoster.blogspot.com/2016/05/dynamic-formflow-forms-in-bot-builder.html

      Joe

      Delete
    4. Thanks Joe.

      These is the answer that I want.

      Do you know if this option is possible with Dialog?

      Thanks again.

      Regards

      Delete
  2. It seems like Choice on a string array should work, but it doesn't, which is probably what you're seeing too. A couple work-arounds be to PromptDialog.Text and evaluate the string entered to see if it was valid. Another might be to Chain into a FormFlow dialog to handle dynamic content. Now I'm wondering if that's a bug in Choice.

    ReplyDelete
  3. This is a very helpful tutorial, but I am confused about something. I am building an app which requires me to keep a log of the conversation. But I am not able to understand, how to retrieve the messages sent to the user by the bot. Can you please help?

    ReplyDelete
    Replies
    1. Angsuman, I don't see an easy solution. The DialogContext is sealed. The closest way I can see is to capture text in prompts and PostAsync when it's created.

      Delete
    2. Can you suggest how I would be able to capture the text from prompt? And how to decide if its a prompt or a retry?

      Delete
    3. Maybe this will work:

      public async Task Post([FromBody]Message message)
      {
      if (message.Type == "Message")
      {
      string inMessageString = JsonConvert.SerializeObject(message);

      // do something with inMessage

      Message outMessage = await Conversation.SendAsync(message, MakeRoot);

      string outMessageString = JsonConvert.SerializeObject(outMessage);

      // do something with outMessageString

      return outMessage;
      }
      else
      {
      return HandleSystemMessage(message);
      }
      }

      The outMessage is what goes back to the user.

      Delete
  4. Hi Joe,

    Upon clicking the dialog button the post method is getting called instead of the Resume method that should have followed the prompt can you tell me what I'm doing wrong?

    ReplyDelete
  5. @Balbhim, The Post method will always be called as the entry point to the Bot. It hands off control to the dialog and the dialog should know which method will be called next, depending on how you have the resume parameter of the previous prompt set to.

    Note: Since I wrote this, the Bot Framework updated to v3.0 and I need to update my demos. I don't know when that will be, but it's on my todo list.

    ReplyDelete
  6. Hey Joe! Great article, very helpful on the choice dialog!

    One thing i can't figure out, how are you able to set the resume method to accept anything other than IAwaitable? I want always return an activity item from the choice instead of a string. Does this have to do with the type of coices that are accepted?

    ReplyDelete
    Replies
    1. It should say ..other than IAwaitable < string >, blogspot blocked it without the spaces

      Delete
  7. Hey Joe! I am new to bot framework.Is there any mechanism to put a small delay between multiple messages that are replied by bot consequently.So,that user think that there is someone who typing a message.

    ReplyDelete