Creating Complex Conversations with Bot Framework v4


To have a natural conversation with a chatbot is not easy. After all, you need to make the chatbot talk like a human being. However, with the advancement in AI technologies such as language understanding, you can create more natural conversations.

 

 

In this post, I am going to step through an example to have a more complex conversation using language understand and the bot framework v4.

 

Note: For this post, I am assuming you already have a good understanding of creating basic conversation and have some experience with LUIS. If that is not the case, I recommend you to check out my post about Dialog and integrating LUIS into your chatbot. In addition, I am adding on top of the generated source code when you create a web app bot on Azure. If you’re not sure how to obtain the generated source code, refer to my post on getting started with Bot Framework v4 development.


 

Example

 

The example will be a table reservation dialog that gathers the user’s name and contact information, the size of the party, and the date. It will use language understanding to process what the user says and skip asking certain questions if the information is already provided. For example, if the user provides their name and party size, the bot will ask them for their contact info and the date.

 

Training LUIS to Understand Key Information

 

For the bot to understand utterances that relate to reserving a table, the language understanding portion of the bot (LUIS) must be trained to understand what the user might say. Luckily, there is a prebuilt domain for ResturantReservation that you can add into your LUIS app so it can start to understand utterances that might mean reserving a table. To add the prebuilt domain, go to your LUIS app > Prebuilt domain at the bottom left corner > Find RestaurantReservation > Click add domain > Train LUIS.

 

 

After training LUIS, it should be able to start understanding statements that have an intention of reserving a table. To test it out, in the LUIS app select the Test tab and type in some statements you would say to reserve a table.

 

 

In addition, you will need to add in some entities so LUIS can recognize a person’s name, a phone number, a number, and a date. Each of them is a prebuilt entity, so you can add the entities and LUIS will start being able to recognize them. To add the entities, in the LUIS app, go to Entities on the left blade > Add prebuilt entity > Add number, personName, datetimeV2, and phonenumber. Test out LUIS and it should be able to recognize them now.

 

 

Making a Reservation Dialog

 

The dialog for making a reservation dialog will act as a high-level dialog. It contains all the steps necessary to make a reservation. The steps will be the following: gathering the user’s information, asking for the party size, and the date of the reservation.

 

Since gathering the user’s information contains multiple information I’ll break it into its own ComponentDialog.

 

public class MakeReservationDialog : WaterfallDialog
{
	public MakeReservationDialog(
            string dialogId,
            IEnumerable<WaterfallStep> steps = null)
            : base(dialogId, steps)
    {
        AddStep(AskForContactInformation);
        AddStep(ProcessContactInformation);
        AddStep(AskForPartySizeAndDate);
        AddStep(ProcessPartySizeAndDate);
        AddStep(AskForPartySize);
        AddStep(ProcessPartySize);
        AddStep(AskForDateOfReservation);
        AddStep(ProcessDateOfReservation);
        AddStep(ShowConfirmation);
    }

    // ...
}

 

Contact Information Dialog

 

This dialog is a ComponentDialog, which means it is reusable. Since asking for the user’s information is pretty common, it makes sense to isolate the dialog into its own that way other dialogs that need to gather the user’s information can use it too. For this example, it doesn’t matter, but this is still important to know how to do when you’re building a more complex chatbot.

 

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BasicBot.Dialogs.ExamplePrompts;
using Luis;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.BotBuilderSamples;

namespace BasicBot.Dialogs.ComplexExample
{
    public class ContactInfoDialog : ComponentDialog
    {
        private const string PersonNameKey = "PersonName";
        private const string ContactNumberKey = "ContactNumber";
        private readonly string NameAndContact = "What is your name and contact number?";
        private readonly string WhatIsName = "What is your name?";
        private readonly string WhatIsContact = "What is a good number to contact you?";
        private readonly string TextPromptId = "textPromptId";

        public ContactInfoDialog(string dialogId) : base(dialogId)
        {
            InitialDialogId = dialogId;

            WaterfallStep[] steps = {
                AskForNameAndContactNumberAsync,
                ProcessNameAndContactResponseAsync,
                AskForNameAsync,
                ProcessNameAsync,
                AskForContactNumberAsync,
                ProcessContactNumberAsync,
                ReturnGatheredInformationAsync
            };

            AddDialog(new WaterfallDialog(dialogId, steps));
            AddDialog(new TextPrompt(TextPromptId));
        }

        private async Task<DialogTurnResult> ProcessContactNumberAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            LuisModel luisResults = await BotServices.Instance
                .LuisServices[Microsoft.BotBuilderSamples.BasicBot.LuisConfiguration]
                .RecognizeAsync<LuisModel>(stepContext.Context, cancellationToken)
                .ConfigureAwait(false);

            if (luisResults?.Entities?.personName?.Any() is true)
            {
                stepContext.Values[PersonNameKey] = luisResults.Entities.personName.FirstOrDefault();
            }

            if (luisResults?.Entities?.phonenumber?.Any() is true)
            {
                stepContext.Values[ContactNumberKey] = luisResults.Entities.phonenumber.FirstOrDefault();
            }

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> ProcessNameAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            LuisModel luisResults = await BotServices.Instance
                .LuisServices[Microsoft.BotBuilderSamples.BasicBot.LuisConfiguration]
                .RecognizeAsync<LuisModel>(stepContext.Context, cancellationToken)
                .ConfigureAwait(false);

            if (luisResults?.Entities?.personName?.Any() is true)
            {
                stepContext.Values[PersonNameKey] = luisResults.Entities.personName.FirstOrDefault();
            }

            if (luisResults?.Entities?.phonenumber?.Any() is true)
            {
                stepContext.Values[ContactNumberKey] = luisResults.Entities.phonenumber.FirstOrDefault();
            }

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> AskForNameAndContactNumberAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            return await stepContext.PromptAsync(
                TextPromptId,
                new PromptOptions
                {
                    Prompt = MessageFactory.Text(NameAndContact)
                },
                cancellationToken
            );
        }

        private async Task<DialogTurnResult> ProcessNameAndContactResponseAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            LuisModel luisResults = await BotServices.Instance
                .LuisServices[Microsoft.BotBuilderSamples.BasicBot.LuisConfiguration]
                .RecognizeAsync<LuisModel>(stepContext.Context, cancellationToken)
                .ConfigureAwait(false);

            if (luisResults?.Entities?.personName?.Any() is true)
            {
                stepContext.Values[PersonNameKey] = luisResults.Entities.personName.FirstOrDefault();
            }

            if (luisResults?.Entities?.phonenumber?.Any() is true)
            {
                stepContext.Values[ContactNumberKey] = luisResults.Entities.phonenumber.FirstOrDefault();
            }

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> AskForNameAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            if (stepContext.Values.ContainsKey(PersonNameKey))
            {
                return await stepContext.NextAsync();
            }

            return await stepContext.PromptAsync(
                TextPromptId,
                new PromptOptions
                {
                    Prompt = MessageFactory.Text(WhatIsName)
                },
                cancellationToken
            );
        }

        private async Task<DialogTurnResult> AskForContactNumberAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            if (stepContext.Values.ContainsKey(ContactNumberKey))
            {
                return await stepContext.NextAsync();
            }

            return await stepContext.PromptAsync(
                TextPromptId,
                new PromptOptions
                {
                    Prompt = MessageFactory.Text(WhatIsContact)
                },
                cancellationToken
            );
        }

        private async Task<DialogTurnResult> ReturnGatheredInformationAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            ContactInformation contactInformation = new ContactInformation
            {
                Name = (string)stepContext.Values[PersonNameKey],
                PhoneNumber = (string)stepContext.Values[ContactNumberKey]
            };

            return await stepContext.EndDialogAsync(contactInformation);
        }

        public static string Id = "contactInfoDialogId";
    }
}

 

Asking for Party Size

 

This is a text prompt that asks the user to input how many people there will be. Notice that instead of using a number prompt, it is a text prompt. This is because we’re using LUIS to process the user’s responses. This way instead of limiting the user to say “8”, we can let them say it in the way they want. For example, they can say “There will be 8 people”.

 

private async Task<DialogTurnResult> AskForPartySize(
           WaterfallStepContext stepContext,
           CancellationToken cancellationToken
)
{
    if (stepContext.Values.ContainsKey(PartySizeKey))
    {
        return await stepContext.NextAsync();
    }

    return await stepContext.PromptAsync(
        ExamplePromptsDialog.TextPromptId,
        new PromptOptions
        {
            Prompt = MessageFactory.Text(HowManyPeople),
            RetryPrompt = MessageFactory.Text("Sorry I don't quite understand what you said. Can you tell me how many people is coming?")
        },
        cancellationToken
    );
}

 

Date of Reservation

 

This is a text prompt that asks the user for the date they want for the reservation. This is a text prompt opposed to a date-time prompt because we’re using LUIS. By using a text prompt, we’re giving the user the freedom to provide the answer in any way they would like.

 

private async Task<DialogTurnResult> AskForDateOfReservation(
       WaterfallStepContext stepContext,
       CancellationToken cancellationToken
)
{
    if (stepContext.Values.ContainsKey(DateKey))
    {
        return await stepContext.NextAsync();
    }

    return await stepContext.PromptAsync(
        ExamplePromptsDialog.TextPromptId,
        new PromptOptions
        {
            Prompt = MessageFactory.Text(WhatDate),
            RetryPrompt = MessageFactory.Text("Sorry I don't quite understand what you said. Can you tell me the month and day you want to reserve?")
        },
        cancellationToken
    );
}

 

Tying It All Together

 

For the dialog to work, you will need to add them into the DialogSet.

 

// ...
Dialogs = new DialogSet(_dialogStateAccessor);
Dialogs.Add(MakeReservationDialog.Instance);
Dialogs.Add(new ContactInfoDialog(ContactInfoDialog.Id));
Dialogs.Add(new TextPrompt(ExamplePromptsDialog.TextPromptId));
// ...

 

Once you have added all the dialogs, you can start the Making Reservation Dialog with the BeginDialogAsync method.

 

Here are two reservations one providing partial information and is guided step-by-step, while the other takes all the information provided at once.

 

 

 

Here is the full code of the Making Reservation Dialog.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BasicBot.Dialogs.ExamplePrompts;
using Luis;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.BotBuilderSamples;

namespace BasicBot.Dialogs.ComplexExample
{
    public class MakeReservationDialog : WaterfallDialog
    {
        private const string PartySizeKey = "PartySize";
        public static string Id = "makeReservationDialogId";
        public static MakeReservationDialog Instance { get; } = new MakeReservationDialog(Id);
        private const string DateKey = "Date";
        private const string ContactInfoKey = "ContactInformationKey";
        private const string HowManyPeople = "How many people will there be?";
        private const string WhatDate = "What month and day do you want to reserve?";
        private const string HowBigIsPartyAndWhatDate = "How big is your party and when do want your table?";

        public MakeReservationDialog(
            string dialogId,
            IEnumerable<WaterfallStep> steps = null)
            : base(dialogId, steps)
        {
            AddStep(AskForContactInformation);
            AddStep(ProcessContactInformation);
            AddStep(AskForPartySizeAndDate);
            AddStep(ProcessPartySizeAndDate);
            AddStep(AskForPartySize);
            AddStep(ProcessPartySize);
            AddStep(AskForDateOfReservation);
            AddStep(ProcessDateOfReservation);
            AddStep(ShowConfirmation);
        }

        private async Task<DialogTurnResult> ProcessPartySizeAndDate(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            LuisModel luisResults = await BotServices.Instance
                .LuisServices[Microsoft.BotBuilderSamples.BasicBot.LuisConfiguration]
                .RecognizeAsync<LuisModel>(stepContext.Context, cancellationToken)
                .ConfigureAwait(false);

            if (luisResults?.Entities?.number?.Any() is true)
            {
                stepContext.Values[PartySizeKey] = luisResults.Entities.number.FirstOrDefault();
            }

            if (luisResults?.Entities?.datetime?.Any() is true)
            {
                stepContext.Values[DateKey] = luisResults.Entities.datetime.FirstOrDefault();
            }

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> AskForPartySizeAndDate(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            return await stepContext.PromptAsync(
                ExamplePromptsDialog.TextPromptId,
                new PromptOptions
                {
                    Prompt = MessageFactory.Text(HowBigIsPartyAndWhatDate)
                },
                cancellationToken
            );
        }

        private async Task<DialogTurnResult> ProcessPartySize(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            LuisModel luisResults = await BotServices.Instance
                .LuisServices[Microsoft.BotBuilderSamples.BasicBot.LuisConfiguration]
                .RecognizeAsync<LuisModel>(stepContext.Context, cancellationToken)
                .ConfigureAwait(false);

            if (luisResults?.Entities?.number?.Any() is true)
            {
                double partySize = luisResults.Entities.number.FirstOrDefault();
                stepContext.Values[PartySizeKey] = partySize;
            }
            else
            {
                stepContext.Values[PartySizeKey] = 0;
            }
            

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> ProcessDateOfReservation(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            LuisModel luisResults = await BotServices.Instance
                .LuisServices[Microsoft.BotBuilderSamples.BasicBot.LuisConfiguration]
                .RecognizeAsync<LuisModel>(stepContext.Context, cancellationToken)
                .ConfigureAwait(false);

            if (luisResults?.Entities?.datetime?.Any() is true)
            {
                stepContext.Values[DateKey] = luisResults?.Entities?.datetime?.FirstOrDefault()?.ToString();
            }
            

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> AskForContactInformation(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            return await stepContext.BeginDialogAsync(ContactInfoDialog.Id);
        }

        private async Task<DialogTurnResult> ProcessContactInformation(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken
        )
        {
            switch (stepContext.Result)
            {
                case ContactInformation contactInformation:
                    stepContext.Values[ContactInfoKey] = contactInformation;
                    break;
            }

            return await stepContext.NextAsync();
        }

        private async Task<DialogTurnResult> AskForPartySize(
           WaterfallStepContext stepContext,
           CancellationToken cancellationToken
        )
        {
            if (stepContext.Values.ContainsKey(PartySizeKey))
            {
                return await stepContext.NextAsync();
            }

            return await stepContext.PromptAsync(
                ExamplePromptsDialog.TextPromptId,
                new PromptOptions
                {
                    Prompt = MessageFactory.Text(HowManyPeople),
                    RetryPrompt = MessageFactory.Text("Sorry I don't quite understand what you said. Can you tell me how many people is coming?")
                },
                cancellationToken
            );
        }

        private async Task<DialogTurnResult> AskForDateOfReservation(
           WaterfallStepContext stepContext,
           CancellationToken cancellationToken
        )
        {
            if (stepContext.Values.ContainsKey(DateKey))
            {
                return await stepContext.NextAsync();
            }

            return await stepContext.PromptAsync(
                ExamplePromptsDialog.TextPromptId,
                new PromptOptions
                {
                    Prompt = MessageFactory.Text(WhatDate),
                    RetryPrompt = MessageFactory.Text("Sorry I don't quite understand what you said. Can you tell me the month and day you want to reserve?")
                },
                cancellationToken
            );
        }

        private async Task<DialogTurnResult> ShowConfirmation(
           WaterfallStepContext stepContext,
           CancellationToken cancellationToken
        )
        {
            ReservationInfo reservationInfo = new ReservationInfo();
            reservationInfo.ContactInformation = stepContext.Values[ContactInfoKey] as ContactInformation;
            reservationInfo.PartySize = (double)stepContext.Values[PartySizeKey];
            reservationInfo.Date = (string)stepContext.Values[DateKey];

            string confirmationMessage = $"Thank you {reservationInfo?.ContactInformation?.Name}."
                    + $"{Environment.NewLine}Here is a summary of your reservation:{Environment.NewLine}"
                    + $"Contact Number: {reservationInfo?.ContactInformation?.PhoneNumber}{Environment.NewLine}"
                    + $"Party Size: {reservationInfo?.PartySize}{Environment.NewLine}"
                    + $"Date: {reservationInfo?.Date}";

            await stepContext.Context.SendActivityAsync(MessageFactory.Text(confirmationMessage));

            return await stepContext.EndDialogAsync(reservationInfo);
        }
    }
}

 

I hope this post was helpful to you. If you found this post helpful, share it with others so they can benefit too.

 

What are your experiences with creating complex conversations with Bot Framework?

 

To get in touch, you can follow me on Twitter, leave a comment, or send me an email at steven@brightdevelopers.com.


About Steven To

Steven To is a software developer that specializes in mobile development with a background in computer engineering. Beyond his passion for software development, he also has an interest in Virtual Reality, Augmented Reality, Artificial Intelligence, Personal Development, and Personal Finance. If he is not writing software, then he is out learning something new.