Develop Chat Bot Using Microsoft Bot Builder SDK V4 - Part Two - Manage Conversation Using Multi-Step Dialog

In my previous article, we have discussed how to kick-start a chatbot development using Microsoft Bot Builder SDK version 4. Now we are going to implement guided chatbot conversation flow using Dialogs library.

Highlights of the article

  • Build a multistep dialog for reserving a table for New Year ’s Eve party.
  • Manage user preferences using conversation state.
  • Leveraging inbuilt prompts - TextPrompt, NumberPrompt, ChoicePrompt

Prerequisite

We can manage guided conversations by chaining the dialogs. We are going to use DialogSet to combine prompts and waterfall steps. Let’s take an example of bot using which, the user will register for New Year’s Eve in a local resto-bar. I am going to use the same solution that we created in article Quick Start.

If you are ready with prerequisites for development with bot framework, then let's code...

*** You can pull code from GitHub or attachment in the article. 

PartyRegistrationState.cs (add new class file)

I have added a new class PartyRegistrationState to maintain conversation state. PartyRegistrationState is inherited from the Dictionary so that it can be used while creating DialogContext. Our conversation state will have to capture the following information from the user for registration to complete - Name, Gender, ArrivalTime, TotalAttendees, CuisinePreferences, ComplementoryDrink.

  1. public class PartyRegistrationState : Dictionary<stringobject>    
  2.     {    
  3.         private const string NameKey = "name";    
  4.         private const string GenderKey = "gender";    
  5.         private const string ArrivalTimeKey = "arrivaltime";    
  6.         private const string TotalAttendeesKey = "totalattendees";    
  7.         private const string CuisinesPreferencesKey = "cuisinespreferences";    
  8.         private const string ComplementoryDrinkKey = "complementorydrink";    
  9.     
  10.         public PartyRegistrationState()    
  11.         {    
  12.             this[NameKey] = null;    
  13.             this[GenderKey] = 0;    
  14.             this[ArrivalTimeKey] = null;    
  15.             this[TotalAttendeesKey] = null;    
  16.             this[CuisinesPreferencesKey] = null;    
  17.             this[ComplementoryDrinkKey] = null;    
  18.         }    
  19.     
  20.         public string Name    
  21.         {    
  22.             get => (string)this[NameKey];    
  23.             set => this[NameKey] = value;    
  24.         }    
  25.     
  26.         public string Gender    
  27.         {    
  28.             get => (string)this[GenderKey];    
  29.             set => this[GenderKey] = value;    
  30.         }    
  31.     
  32.         public DateTime ArrivalTime    
  33.         {    
  34.             get => (DateTime)this[ArrivalTimeKey];    
  35.             set => this[ArrivalTimeKey] = value;    
  36.         }    
  37.     
  38.         public int TotalAttendees    
  39.         {    
  40.             get => (int)this[TotalAttendeesKey];    
  41.             set => this[TotalAttendeesKey] = value;    
  42.         }    
  43.     
  44.         public List<string> CuisinesPreferences    
  45.         {    
  46.             get => (List<string>)this[CuisinesPreferencesKey];    
  47.             set => this[CuisinesPreferencesKey] = value;    
  48.         }    
  49.     
  50.         public string ComplementoryDrink    
  51.         {    
  52.             get => (string)this[ComplementoryDrinkKey];    
  53.             set => this[ComplementoryDrinkKey] = value;    
  54.         }    
  55.     }  

Startup.cs (.Net Core App default file)

We need to change Configure Services method to use our new state class. While adding ConversationState middleware we will mention PartyRegistrationState type and datastore will be memory storage. I have highlighted updates in file.

  1. public void ConfigureServices(IServiceCollection services)    
  2.    {    
  3.        services.AddBot<PartyRegistrationBot>(options =>    
  4.        {    
  5.            options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);    
  6.     
  7.            options.Middleware.Add(new CatchExceptionMiddleware<Exception>(async (context, exception) =>    
  8.            {    
  9.                await context.TraceActivity("partyRegistrationBot Exception", exception);    
  10.                await context.SendActivity("Sorry, it looks like something went wrong!");    
  11.            }));    
  12.     
  13.            IStorage dataStore = new MemoryStorage();    
  14.            options.Middleware.Add(new ConversationState<PartyRegistrationState>(dataStore));    
  15.        });  
  16.    }  

PartyRegistrationForm.cs (add new class file)

I have added const string properties for prompt names and dialog name. StartDialog attribute is declared as public because we are going to access it in another class (PartyRegistrationBot) to begin a conversation. I have added Build method to create and return DialogSet. It will add each prompt and start dialog to dialogset object. I have added

  • AskNamePrompt as TextPrompt that means, prompt is expecting text from user and user input will be validated by NameValidator function.
  • AskTotalAttendeesPrompt as NumberPrompt that means, the prompt is expecting number from the user and the user input will be validated by TotalAttendeesValidator function.
  • AskGenderPrompt, AskCuisinePreferencePrompt, AskComplementoryPrompt as ChoicePrompt that means, prompt will provide choice options for the user to select, with English language culture for recognizing the user inputs.
  • AskArrivalTimePrompt, as DateTimePrompt that means, the prompt is expecting date from user and input will be validated by ArrivalTimeValidator function.
  • StartDialog as WaterfallStep array. It is the collection of steps for each prompt. This step is function implemented for a specific task.
  1. public static class PartyRegistrationForm    
  2.   {    
  3.       public const string StartDialog = "StartDialog";    
  4.     
  5.       private const string AskNamePrompt = "AskName";    
  6.       private const string AskGenderPrompt = "AskGender";    
  7.       private const string AskArrivalTimePrompt = "AskArrivalTime";    
  8.       private const string AskTotalAttendeesPrompt = "AskTotalAttendees";    
  9.       private const string AskCuisinesPreferencesPrompt = "AskCuisinesPreferences";    
  10.       private const string AskComplementoryDrinkPrompt = "AskComplementoryDrink";      
  11.         
  12.       public static DialogSet Build()    
  13.       {    
  14.           var dialogs = new DialogSet();    
  15.     
  16.           dialogs.Add(AskNamePrompt, new TextPrompt(NameValidator));   
  17.    
  18.           dialogs.Add(AskGenderPrompt, new ChoicePrompt(Culture.English)  { Style = BotPrompts.ListStyle.Auto  });     
  19.    
  20.           dialogs.Add(AskArrivalTimePrompt, new DateTimePrompt(Culture.English, ArrivalTimeValidator));    
  21.   
  22.           dialogs.Add(AskTotalAttendeesPrompt, new NumberPrompt<int>(Culture.English, TotalAttendeesValidator));    
  23.   
  24.           dialogs.Add(AskCuisinesPreferencesPrompt, new ChoicePrompt(Culture.English) { Style = BotPrompts.ListStyle.Auto });    
  25.   
  26.           dialogs.Add(AskComplementoryDrinkPrompt, new ChoicePrompt(Culture.English) { Style = BotPrompts.ListStyle.Auto });  
  27.     
  28.           dialogs.Add(StartDialog, new WaterfallStep[]    
  29.                                    {    
  30.                                      AskNameStep, AskGenderStep, AskArrivalTimeStep, AskTotalAttendeesStep, AskCuisinesPreferencesStep,  AskComplementoryDrinkStep, FinalStep    
  31.                                    });    
  32.     
  33.           return dialogs;    
  34.       }    
  35.   }  

Step 1 - AskNameStep

Waterfall contains the first step as AskNameStep. It prompts for AsknamePrompts with mentioned text. When the user enters text, the control will go to NameValidator function where length validation is done on Name entered. If it's not between 6 and 25, then result status will be set and the message will be sent to the user for reentering the value. If the input is valid, then the control will move to the next step in waterfall; i.e., AskGenderStep.

  1. private static async Task AskNameStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2.       {    
  3.           await dialogContext.Prompt(AskNamePrompt, "May I know your good name?");    
  4.       }    
  5.     
  6.       private static async Task NameValidator(ITurnContext context, BotPrompts.TextResult result)    
  7.       {    
  8.           if (result.Value.Length <= 6 || (result.Value.Length > 25))    
  9.           {    
  10.               result.Status = BotPrompts.PromptStatus.OutOfRange;    
  11.               await context.SendActivity("Your name should be at least 6 and at most 25 characters long.");    
  12.           }    
  13.       }  

Step 2 - AskGenderStep

When control comes to AskGenderStep, text entered by the user for AskNameStep will be available in result parameter. I have typecasted the result to TextResult and saved its value to state against Name attribute. Next, it prompts for AskGenderPrompts with gender choice options. To get choice options, GetGenderOptions method is called from DataService class. When the user enters text, control will go to the next step in waterfall i.e. AskArrivalTimeStep.

  1. private static async Task AskGenderStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2.      {    
  3.          var state = dialogContext.Context.GetConversationState<PartyRegistrationState>();    
  4.          state.Name = (result as BotPrompts.TextResult).Value;    
  5.     
  6.          await dialogContext.Prompt(AskGenderPrompt, "Please select gender, if you want else you can skip.", DataService.GetGenderOptions());    
  7.      }  

Step 3 - AskArrivalTimeStep

When control comes to AskArrivalTimeStep, the option chosen by the user will be available in the result parameter. I have typecasted the result in ChoiceResult and saved its value in conversation state against Gender attribute. Next, It prompts for AskArrivalTimePrompt. When the user enters the date, the control will go to ArrivalTimeValidator function where date validation is done on the value entered. If user text is not resolved to proper date or past date is entered, then the result status will be set to OutOfRange and message will be sent to the user for reentering the value. If the input is valid, then the control will move to the next step in waterfall i.e. AskTotalAttendeesStep.

  1. private static async Task AskArrivalTimeStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2.        {    
  3.            var state = dialogContext.Context.GetConversationState<PartyRegistrationState>();    
  4.            state.Gender = (result as BotPrompts.ChoiceResult).Value.Value;    
  5.     
  6.            await dialogContext.Prompt(AskArrivalTimePrompt, "What will be your arrival time?");    
  7.        }    
  8.     
  9.        private static async Task ArrivalTimeValidator(ITurnContext context, BotPrompts.DateTimeResult result)    
  10.        {    
  11.            if (result.Resolution.Count == 0)    
  12.            {    
  13.                await context.SendActivity("Sorry, I could not understand your preffered time.");    
  14.                result.Status = BotPrompts.PromptStatus.NotRecognized;    
  15.            }    
  16.     
  17.            var now = DateTime.Now;    
  18.            DateTime time = default(DateTime);    
  19.            var resolution = result.Resolution.FirstOrDefault(res => DateTime.TryParse(res.Value, out time) && time > now);    
  20.     
  21.            if (resolution != null)    
  22.            {    
  23.                result.Resolution.Clear();    
  24.                result.Resolution.Add(resolution);    
  25.            }    
  26.            else    
  27.            {    
  28.                await context.SendActivity("Please enter future time");    
  29.                result.Status = BotPrompts.PromptStatus.OutOfRange;    
  30.            }    
  31.        }   

Step 4 - AskTotalAttendeesStep

When control comes to AskTotalAttendeesStep, date value resolved from user input will be available in the result parameter. I have typecasted result in DateTimeResult and saved its value in conversation state against ArrivalTime attribute. Next, it prompts for AskTotalAttendeesPrompt. When the user enters a value, the control will go to TotalAttendeesValidator function where number validation is done on the value entered. If user value is not between 2 and 9 then result status will be set to OutOfRange and message will be sent to the user for reentering the value. If the input is valid then control will move to the next step in waterfall i.e. AskCuisinesPreferencesStep.

  1. private static async Task AskTotalAttendeesStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2.       {    
  3.           var state = dialogContext.Context.GetConversationState<PartyRegistrationState>();    
  4.           state.ArrivalTime = DateTime.Parse((result as BotPrompts.DateTimeResult).Resolution.FirstOrDefault().Value);    
  5.     
  6.           await dialogContext.Prompt(AskTotalAttendeesPrompt, $"How many guests are accompanying you?{Environment.NewLine}If more than 3, you will get complementory drink ! :)");    
  7.       }    
  8.     
  9.       private static async Task TotalAttendeesValidator(ITurnContext context, BotPrompts.NumberResult<int> result)    
  10.       {    
  11.           if (result.Value < 2 || result.Value > 9)    
  12.           {    
  13.               result.Status = BotPrompts.PromptStatus.OutOfRange;    
  14.               await context.SendActivity("You can book entries for minimum 2 and maximum 9 people");    
  15.           }    
  16.       }   

Step 5 - AskCuisinesPreferencesStep

When control comes to AskCuisinesPreferencesStep, attendees value entered by user will be available in result parameter. I have typecasted result to integer and saved its value in conversation state against TotalAttendees attibute. Next, It prompts for AskCuisinesPreferencesPrompt with cuisine type choice options. To get options, GetCuisinOptions method is called from DataService class. When user selects cuisin, then control will move to next step in waterfall i.e. AskComplementoryDrinkStep.

  1. private static async Task AskCuisinesPreferencesStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2. {    
  3.     var state = dialogContext.Context.GetConversationState<PartyRegistrationState>();    
  4.     state.TotalAttendees = (result as BotPrompts.NumberResult<int>).Value;    
  5.     
  6.     await dialogContext.Prompt(AskCuisinesPreferencesPrompt, "Which type of cuisines you would like to have?", DataService.GetCuisinsOptions());    
  7. }   

Step 6 - AskComplementoryDrinkStep

When control comes to AskComplementoryDrinkStep, the option chosen by the user will be available in the result parameter. I have typecasted result in ChoiceResult and saved its value in conversation state against CuisinePreference attribute. Next, if the number of attendees is more than 3, then it prompts for AskComplementoryDringPrompt. When user chooses complementory drink or number of attendees are less than 3, then control will move to the next step in waterfall; i.e. FinalStep.

  1. private static async Task AskComplementoryDrinkStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2.       {    
  3.           var state = dialogContext.Context.GetConversationState<PartyRegistrationState>();    
  4.           if (null == state.CuisinesPreferences)    
  5.           {    
  6.               state.CuisinesPreferences = new List<string>();    
  7.           }    
  8.           state.CuisinesPreferences.Add((result as BotPrompts.ChoiceResult).Value.Value);    
  9.     
  10.           if (3 < state.TotalAttendees)    
  11.           {    
  12.               await dialogContext.Prompt(AskComplementoryDrinkPrompt, "Which complementory drink you would like to have?", DataService.GetComplementoryDrinkChoices());    
  13.           }    
  14.           else    
  15.           {    
  16.               await next();    
  17.           }    
  18.       }  

Step 7 - FinalStep

When control comes to FinalStep, the option chosen by the user will be available in result parameter. I have typecasted result in ChoiceResult and saved its value in conversation state against complementary drink attribute. Next, the bot will show user-entered information and end the current dialog and control will go to OnTurn method of the bot.

  1. private static async Task FinalStep(DialogContext dialogContext, object result, SkipStepFunction next)    
  2.        {    
  3.            var state = dialogContext.Context.GetConversationState<PartyRegistrationState>();    
  4.     
  5.            if (null != result)    
  6.            {    
  7.                state.ComplementoryDrink = (result as BotPrompts.TextResult).Value;    
  8.            }    
  9.     
  10.            await dialogContext.Context.SendActivity($"Okay {state.Name}, I will book a table for {state.TotalAttendees} people with preferred food as {string.Join(",", state.CuisinesPreferences)}.");    
  11.     
  12.            if (3 < state.TotalAttendees)    
  13.            {    
  14.                await dialogContext.Context.SendActivity($"And you will also get {state.ComplementoryDrink} complementory. Njoy!");    
  15.            }    
  16.     
  17.            await dialogContext.Context.SendActivity($"Thank you for your interest.");    
  18.     
  19.            await dialogContext.End();    
  20.        }  

Test

Hit F5 to test the bot application. It will host the application with IIS Express and open browser. To understand the control flow, insert breakpoints in step methods and build method in PartyRegistrationForm.cs file. 

Now launch bot emulator app. Put URL as 'http://localhost:{{port no. from browser url}}/api/messages' to connect emulator with bot application. Keep App ID and App Password blank, click on connect. Start chatting with the bot and book a table for New Year's Eve. 

In the next article, we will discuss how to integrate LUIS app in chatbot using Bot Builder SDK version 4. Till then Happy Chatting! :)

Read more on Microsoft Bot Framework


Similar Articles