Create a Multi turn Prompt Bot with Azure Bot Framework SDK C# & .NET {Part 3}

Create Multi Turn Prompt Bots with Azure Bot Framework
Bhargil Joshi
15-Aug-2020
Reading Time: 7 minutes

The bot framework enables you to build bots that support different types of interaction with users. Your bot can also have more guided interactions where it provides the users with choices or actions.

The conversation can use simple text strings or more complex rich cards that contain text, images, and action buttons. And, you can add natural language interactions, which let your users interact with your bots naturally and expressively.

In the previous article, Part 2 we did learn how to create a bot using Bot Framework using the C# template, and then testing it with the Bot Framework emulator and how to manage input from the user and show a welcome message and show card based on user’s input like ‘help’, ‘intro’ etc.

In this article, Part 3 we will be going to learn how to create a bot to use waterfall dialog, prompt, and component dialog to create a simple interaction that asks a user for general info like name, age, etc.

Here is the Glimpse of Full Series of Creating Basic to Fully Functional Customize Chat Bot using Azure Bot Framework SDK.

  1. Part 1: Basic introduction about Bot Framework and create + deploy simple echo bot.
  2. Part 2: Welcome user and Prompt simple user interaction bot.
  3. Part 3: Multi-turn bot using card and action suggestion (take input and return with suggestion).
  4. Part 4: NLP and Language Understanding Bot.

Prerequisites:

  • Visual Studio 2019 or later
  • Bot Framework SDK v4 Template for C#
  • .NET Core 3.1
  • Bot Framework Emulator
  • Basic knowledge ASP.NET Core and Async programming language C#

How to Create a Multi Turn Prompt Bot? Step by Step Tutorial

Step 1: Download and Install Visual Studio Template using this link .

Step 2: Create a New Project using Echo Bot (.net Core 3.1 Bot Framework v4) Template.

create new project using echo bot

These templates contain all the code that’s necessary to create a bot for the quick start.

Step 3: Start your project now, it will run the default port locally on 3978 looks like this.

EchoBotDemo Bot

Step 4: Download and Install Bot Emulator using this link.

Step 5: A conversation between users and bot involves asking dialog (Prompting) the user for information, parsing the user’s response, and then acting on the information.

For that, we have to create prompt users for information using the different prompt types including in the botbuilder-dialogs library and supported by the SDK. (Learn more about botbuilder-dialogs here ).

This library includes a verity of pre-built prompt classes, including text, numbers, and determines types.

Step 6: Install 2 nugets package for bot builder

  1. Install-Package Microsoft.Bot.Builder.Dialogs -Version 4.9.4
  2. Install-Package Microsoft.Bot.Builder.Integration.AspNet.Core -Version 4.9.4

Step 7: Create a folder in solutions name “Bot”, now add a new class “DialogBot.cs” and write below code for declaring class called dialog.

public class DialogBot<T> : ActivityHandler where T : Dialog 
    {
        protected readonly Dialog Dialog;
        protected readonly BotState ConversationState;
        protected readonly BotState UserState;
        protected readonly ILogger Logger;

Step 8: Create 3 new functions in dialog bot class which useful for async call and message activity.

public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger)
        {
            ConversationState = conversationState;
            UserState = userState;
            Dialog = dialog;
            Logger = logger;
        }
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
        {
            await base.OnTurnAsync(turnContext, cancellationToken);

            // Save any state changes that might have occurred during the turn.
            await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
            await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
        }
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            Logger.LogInformation("Running dialog with Message Activity.");

            // Run the Dialog with the new message Activity.
            await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
        }

Step 9: Create a new class UserProfile.cs and add methods for each dialog interaction.

Transport Steps Code

private static async Task<DialogTurnResult> TransportStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
            // Running a prompt here means the next WaterfallStep will be run when the user's response is received.
            return await stepContext.PromptAsync(nameof(ChoicePrompt),
                new PromptOptions
                {
                    Prompt = MessageFactory.Text("Please enter your mode of transport."),
                    Choices = ChoiceFactory.ToChoices(new List<string> { "Car", "Bus", "Bicycle" }),
                }, cancellationToken);
        }

Name Steps Code

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value;

            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") },
                cancellationToken);
        }

Name Steps Confirm Code

private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["name"] = (string)stepContext.Result;

            // We can send messages to the user at any point in the WaterfallStep.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Thanks {stepContext.Result}."), cancellationToken);

            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
            return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Would you like to give your age?") }, cancellationToken);
        }

Age Steps Code

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            if ((bool)stepContext.Result)
            {
                // User said "yes" so we will be prompting for the age.
                // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
                var promptOptions = new PromptOptions
                {
                    Prompt = MessageFactory.Text("Please enter your age."),
                    RetryPrompt = MessageFactory.Text("The value entered must be greater than 0 and less than 150."),
                };

                return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
            }
            else
            {
                // User said "no" so we will skip the next step. Give -1 as the age.
                return await stepContext.NextAsync(-1, cancellationToken);
            }
        }

Get Picture Step Code

private static async Task<DialogTurnResult> PictureStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["age"] = (int)stepContext.Result;

            var msg = (int)stepContext.Values["age"] == -1 ? "No age given." : $"I have your age as {stepContext.Values["age"]}.";

            // We can send messages to the user at any point in the WaterfallStep.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

            if (stepContext.Context.Activity.ChannelId == Channels.Msteams)
            {
                // This attachment prompt example is not designed to work for Teams attachments, so skip it in this case
                await stepContext.Context.SendActivityAsync(MessageFactory.Text("Skipping attachment prompt in Teams channel..."), cancellationToken);
                return await stepContext.NextAsync(null, cancellationToken);
            }
            else
            {
                // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
                var promptOptions = new PromptOptions
                {
                    Prompt = MessageFactory.Text("Please attach a profile picture (or type any message to skip)."),
                    RetryPrompt = MessageFactory.Text("The attachment must be a jpeg/png image file."),
                };

                return await stepContext.PromptAsync(nameof(AttachmentPrompt), promptOptions, cancellationToken);
            }
        }

Confirm step code

private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["picture"] = ((IList<Attachment>)stepContext.Result)?.FirstOrDefault();

            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
            return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Is this ok?") }, cancellationToken);
        }

After the input with user final summary will show code

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            if ((bool)stepContext.Result)
            {
                var userProfile = await _userProfileAccessor.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
                userProfile.Transport = (string)stepContext.Values["transport"];
                userProfile.Name = (string)stepContext.Values["name"];
                userProfile.Age = (int)stepContext.Values["age"];
                userProfile.Picture = (Attachment)stepContext.Values["picture"];

                var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}";

                if (userProfile.Age != -1)
                {
                    msg += $" and your age as {userProfile.Age}";
                }
                msg += ".";
                await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);
                if (userProfile.Picture != null)
                {
                    try
                    {
                        await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(userProfile.Picture, "This is your profile picture."), cancellationToken);
                    }
                    catch
                    {
                        await stepContext.Context.SendActivityAsync(MessageFactory.Text("A profile picture was saved but could not be displayed here."), cancellationToken);
                    }
                }
            }
            else
            {
                await stepContext.Context.SendActivityAsync(MessageFactory.Text("Thanks. Your profile will not be kept."), cancellationToken);
            }
            return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
        }

We can check validation for age and picture format like this.

private static Task<bool> AgePromptValidatorAsync(PromptValidatorContext<int> promptContext, CancellationToken cancellationToken)
        {
            // This condition is our validation rule. You can also change the value at this point.
            return Task.FromResult(promptContext.Recognized.Succeeded && promptContext.Recognized.Value > 0 && promptContext.Recognized.Value < 150);
        }
private static async Task<bool> PicturePromptValidatorAsync(PromptValidatorContext<IList<Attachment>> promptContext, CancellationToken cancellationToken)
        {
            if (promptContext.Recognized.Succeeded)
            {
                var attachments = promptContext.Recognized.Value;
                var validImages = new List<Attachment>();

                foreach (var attachment in attachments)
                {
                    if (attachment.ContentType == "image/jpeg" || attachment.ContentType == "image/png")
                    {
                        validImages.Add(attachment);
                    }
                }

                promptContext.Recognized.Value = validImages;

                // If none of the attachments are valid images, the retry prompt should be sent.
                return validImages.Any();
            }
            else
            {
                await promptContext.Context.SendActivityAsync("No attachments received. Proceeding without a profile picture...");

                // We can return true from a validator function even if Recognized.Succeeded is false.
                return true;
            }
        }

Now add all the methods in declaration part.

public class UserProfileDialog : ComponentDialog
    {
        private readonly IStatePropertyAccessor<UserProfile> _userProfileAccessor;
        public UserProfileDialog(UserState userState)
            : base(nameof(UserProfileDialog))
        {
            _userProfileAccessor = userState.CreateProperty<UserProfile>("UserProfile");

            // This array defines how the Waterfall will execute.
            var waterfallSteps = new WaterfallStep[]
            {
                TransportStepAsync,
                NameStepAsync,
                NameConfirmStepAsync,
                AgeStepAsync,
                PictureStepAsync,
                ConfirmStepAsync,
                SummaryStepAsync,
            };

            // Add named dialogs to the DialogSet. These names are saved in the dialog state.
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new NumberPrompt<int>(nameof(NumberPrompt<int>), AgePromptValidatorAsync));
            AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
            AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
            AddDialog(new AttachmentPrompt(nameof(AttachmentPrompt), PicturePromptValidatorAsync));

            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }

Step 10: Now add few steps in startup file.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers().AddNewtonsoftJson();

            // Create the Bot Framework Adapter with error handling enabled.
            services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

            // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
            services.AddSingleton<IStorage, MemoryStorage>();

            // Create the User state. (Used in this bot's Dialog implementation.)
            services.AddSingleton<UserState>();

            // Create the Conversation state. (Used by the Dialog system itself.)
            services.AddSingleton<ConversationState>();

            // The Dialog that will be run by the bot.
            services.AddSingleton<UserProfileDialog>();

            // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
            services.AddTransient<IBot, DialogBot<UserProfileDialog>>();
        }

Step 11: Now, your Multi Turn Prompt Bot is ready just run the project and emulator.

Azure Bot Framework Emulator

Click on the open Bot button & enter URL http://localhost:3978/api/messages then press on the connect button.

Open Bot Button Azure Bot Framework Emulator
Multi Turn Prompt Bot Emulator Output Screen 1
Multi Turn Prompt Bot Emulator Output Screen 2
Multi Turn Prompt Bot Emulator Output Screen 3
Multi Turn Prompt Bot Emulator Output Screen 4

It will show you a final Summary.

Multi Turn Prompt Bot Emulator Final Summary Output Screen

Conclusion is, This bot has been created using Bot Framework, it shows how to use the prompts classes included in botbuilder-dialogs. This bot will ask for the user’s name and age, then store the responses. It demonstrates a multi-turn dialog flow using a text prompt, a numbered prompt, and state accessors to store and retrieve values.

If you’re looking for experienced Azure Multi Turn Prompt Bot Developers or NET Core Developers, Contact us to discuss more.