Telerik blogs
How ToT2 Dark_1200x303

We are building a PizzaBot using Microsoft’s Bot Framework v4 to help prove that chatbots have gone from a nifty side project to a new marketing and sales channel.

Chatbots have evolved from a simple input/output model into an automated tool for marketing, sales, and operations (ChatOps). Part of this growth is out of a desire to do more with fewer people by automating, and I think the other part is fueled by popular movies that show superheroes assisted by AIs. Whatever the reason, bot frameworks have evolved to match the increasing uses for complex bots.

We are going to design a chatbot for ordering a pizza using Microsoft’s Bot Framework SDK v4. Microsoft’s Bot Framework provides lots of features to allow for building a modular chatbot along with some utilities allowing it to be deployed easily in Azure. We are going to be using some of the modular patterns from the framework to design our chatbot along with implementing storage mechanisms for conversation-level storage and user-level storage.

Bot Framework Background

Microsoft released v4 of their Bot Framework in September of 2018. The release of V4 came with an emphasis on using ASP.NET Core, along with adding some better ways of handling state and sequential dialogs compared to v3. Microsoft has done a great job of building out an extensible framework for building, testing, and deploying your chatbot, as well as providing the right hooks for integration with other providers. One of the biggest benefits to Microsoft’s Bot Framework is that they host their bot through a REST API, which allows you to host your bot within an application or website.

Setting Up Your Environment

Before we jump into designing the chatbot or coding anything up, we should set up our development environment. Visual Studio does not come with the Bot Framework installed or as an option during installation, so you’ll need to install it via the Extensions menu. After you have Visual Studio installed, you’ll need to download the Bot Framework V4 Templates for Visual Studio Extension from the Extensions -> Manage Extensions menu.

After you get the Bot Framework Extension downloaded, you’ll be able to create Bot Projects from the New Project menu. After you create your new Bot Project, you’ll want to add the package Microsoft.Bot.Builder.Dialogs via NuGet. This will allow you to create reusable dialogs for your chatbot.

At this point, you’ll be able to create Bot projects within Visual Studio. Let’s jump into designing our chatbot.

Steps to Get the Environment Set Up

  • Step 1: Download Bot Framework V4 Templates for Visual Studio extension in Visual Studio
  • Step 2: Create Bot Project
  • Step 3: Add Microsoft.Bot.Builder.Dialogs from NuGet

Designing the Interface for the User

One of the most important steps when building out a chatbot is designing the conversation flow. The conversation flow and questions will determine the code structure, prompts, and prompt validation. A poorly designed chatbot can be redesigned, but the code will definitely be harder to refactor and it may turn into a complete rewrite.

For our chatbot, we are going to start with a sequential chatbot that walks a user through the pizza-ordering process along with some validation. Once we have our basic chatbot in place, we can start replacing steps or entire dialogs with LUIS (Language Understanding) for better conversation flow. Based on the last time I called in a pizza, these were the basic steps:

Step 1 - Welcome User [Message]
Step 2 - Prompt for Delivery or Takeout [Choice Prompt]
Step 3 - Prompt for Pizza Type [Choice Prompt]
Step 4 - Prompt for Pizza Size [Choice Prompt]
Step 5 - Prompt for Number of Pizzas [Number Prompt with validation]
Step 6 - Confirm Order (Go to Step 7) or Edit Order (Go to Step 2) [Confirm Prompt]
Step 7 - Place Order [Message]

Prompts

The Bot Framework gives us quite a few prompts that we can use out of the box without a lot of setup. We are only going to go over the few that we need for our chatbot—Choice Prompt, Number Prompt, and Confirm Prompt. Check out Microsoft’s Docs for their full list of prompts.

For choice prompt, we can restrict the user to a known set of good responses. In a web environment, where the client can present the user with the options, the user will get the choice of buttons to select. In a chat client, they will just have their options texted out to them.

The number prompt will parse the user’s input into a number. We can check for validity of the number or verify the number is within a specific range before proceeding. This keeps the user from ordering -100 pizzas.

Finally, the confirm prompt allows us to prompt the user with a yes/no question, with the result converted to a boolean.

There are few other prompts types, but these are the main prompts that we are going to focus on for our bot.

Sequential Flow

We have designed the initial flow of the conversation that we want to have with our users as they interact with our pizza bot along with the type of information that we want from them. It is time for us to start translating some of this into psuedo code.

Structurally, we are going to design this chatbot with two levels. The first level will be responsible for starting the Order Pizza Waterfall and placing the order once the user is done. The reason behind the two levels is the Waterfall pattern in the framework allows for sequential flow, so we want to start over the Order Pizza Waterfall if the user wants to make a change to their pizza.

Top Waterfall

  • Welcome
  • Order Pizza Waterfall
  • Place Order

Order Pizza Waterfall

  • Delivery Or Takeout
  • Pizza Type
  • Pizza Size
  • Number of Pizzas
  • Confirmation

Coding

Now that we have defined our conversation flow, prompt types, and conversation structure, it’s time to start writing some code. First, we are going to start with our dialog code. We are going to build out the prompts, then we will add storage and API hooks to complete the chatbot

Bot Flow

Since we have designed the conversation flow that we want our users to go through, we can start with the PizzaDialog. If you notice, the PizzaDialog class is inheriting from the ComponentDialog. If you were curious and clicked on the full list of PromptTypes earlier, you would have seen ComponentDialog listed as part of the class hierarchy for Prompts. The short description is that ComponentDialog is a more complex prompt that can be reused. For a longer explanation, check out this link.

public class PizzaDialog : ComponentDialog
{
    private readonly IStatePropertyAccessor<UserData> _userDataAccessor;
    private readonly IStatePropertyAccessor<ConversationData> _conversationDataAccessor;

    private static string TOP_LEVEL_WATERFALL_NAME = "INITIAL";
    private static String NUM_PIZZA_DIALOG_PROMPT_NAME = "NUM_PIZZA_PROMPT";

    public PizzaDialog(UserState userState, ConversationState conversationState)
        : base(nameof(PizzaDialog))
    {
        _userDataAccessor = userState.CreateProperty<UserData>("UserData");
        _conversationDataAccessor = conversationState.CreateProperty<ConversationData>("ConversationData");

        var topLevelWaterfallSteps = new WaterfallStep[]
        {
            StartAsync
        };

        // This array defines how the Waterfall will execute.
        var waterfallSteps = new WaterfallStep[]
        {
            TakeoutOrDeliveryStepAsync,
            PizzaTypeStepAsync,
            PizzaSizeStepAsync,
            NumberOfPizzasStepAsync,
            ConfirmOrderStepAsync,
            PlaceOrderStepAsync
        };

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

        // The initial child Dialog to run.
        InitialDialogId = TOP_LEVEL_WATERFALL_NAME;
    }} //end of PizzaDialog class

After we have created our conversation in PizzaDialog, we need to hook it up to our Bot that will be handling the events as messages come and go from our bot.

I have broken up our Bot into two separate classes. One of them is a base runner to allow us to build other dialogs, and the other is specifically for handling the Ordering Pizza aspect of our chatbot. By breaking it up, we can easily build additional bots to handle distinct conversations that are not related to Order Pizza without building the same boilerplate.

BasePizzaBot.cs

namespace PizzaBot.Bots
{
// This IBot implementation can run any type of Dialog. The use of type parameterization is to allows multiple different bots
// to be run at different endpoints within the same project. This can be achieved by defining distinct Controller types
// each with dependency on distinct IBot types, this way ASP Dependency Injection can glue everything together without ambiguity.
// The ConversationState is used by the Dialog system. The UserState isn't, however, it might have been used in a Dialog implementation,
// and the requirement is that all BotState objects are saved at the end of a turn.
public class BasePizzaBot<T> : ActivityHandler where T : Dialog
{
    protected readonly Dialog Dialog;
    protected readonly BotState ConversationState;
    protected readonly BotState UserState;
    protected readonly ILogger Logger;

    public BasePizzaBot(ConversationState conversationState, UserState userState, T dialog, ILogger<BasePizzaBot<T>> logger)
    {
        ConversationState = conversationState;
        UserState = userState;
        Dialog = dialog;
        Logger = logger;
    }

    //Event that occurs on the end of a chat message processing, we want to persist conversation and user state at this time.
    public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        await base.OnTurnAsync(turnContext, cancellationToken);

        // Save any state changes that might have occured 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.Run(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
    }
}
}

PizzaBot.cs

namespace PizzaBot.Bots
{
    public class PizzaBot<T> : BasePizzaBot<T> where T : Dialog
    {
        public PizzaBot(ConversationState conversationState, UserState userState, T dialog, ILogger<BasePizzaBot<T>> logger) 
            : base(conversationState, userState, dialog, logger)
        {
        }

        protected override async Task OnMembersAddedAsync(
            IList<ChannelAccount> membersAdded,
            ITurnContext<IConversationUpdateActivity> turnContext,
            CancellationToken cancellationToken)
        {
            foreach (var member in membersAdded)
            {
                // Greet anyone that was not the target (recipient) of this message.
                // To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details.
                if (member.Id != turnContext.Activity.Recipient.Id)
                {
                    var reply = MessageFactory.Text($"Hello {member.Name} and Welcome to Pizza Bot. " +
                        "Type anything to get started.");
                    await turnContext.SendActivityAsync(reply, cancellationToken);
                }
            }
        }
    }
}

Bot State Storage

Now that we have our conversation written out, we need to add in some way to store the data. If you take a look at the code we wrote for the BasePizzaBot.cs, you’ll see two lines related to BotState:

    protected readonly BotState ConversationState;
    protected readonly BotState UserState;

With these two lines, we are storing User State and Conversation State, so now is a good time to explain these concepts. There are two types of storage when it comes to your chatbot, Conversation State and User State. Conversation State lives for the life of the current conversation—it is great for maintaining short-term memory and remembering what your user is saying with the ability to easily forget if nothing happens to convert it to long-term storage. User State is long-lived information about the user like their username, signup date, and any other domain-specific information that you would like to persist longer than the conversation. Conversation State is stored within memory, while User State is usually stored in a database.

For our bot, we are using local memory for our User State. You can easily swap this out via the IStorage interface for a database or longer living data store.

Startup.cs

// Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) 
    // Added Local Memory Storage for the user to simulate a DB or longer running store
    services.AddSingleton<IStorage, MemoryStorage>();

    // Create the User state. 
    // Added to store UserState which is about the user and longer lived
    services.AddSingleton<UserState>();

    // Create the Conversation state.
    // Added ConversationState to store local data for immediate conversation
    services.AddSingleton<ConversationState>();

...

Testing Your Chatbot

At this point, we have a fully functional chatbot, but we have not tested it yet. Luckily, the Bot Framework comes with an emulator that hooks up nicely with our chatbot when running locally to allow us to test our bot. You can download the emulator from Microsoft’s Github page.

Once you download the appropriate binary for your OS, you should be able to run the emulator. After the emulator is up and running, you can go into Visual Studio and hit Run (Start with Debugging). Your default browser should pop up with a link to your chatbot’s API. The default URL and port for the PizzaBot looks like this: http://localhost:3978/api/messages. You can create a new Bot Configuration using the URL and interact with your bot.

While the emulator is just a single environment and doesn’t represent any other chat clients or apps, it shows you how the bot is processing the information from the client’s perspective which is great for rapid iterations.

Next Steps

We have built our chatbot, but there are a few enhancements to take this chatbot to the next level. For starters, we have built our chatbot with a sequential flow for ordering. We can extend this by using allow the user to free form their order and parse out the text and only prompt follow-up questions for missing pieces of data. We can achieve this using LUIS (Language Understand) service from Azure. Also, we can build on additional bots that can allow the user to ask about their order status without rewriting the current ordering bot.

While ordering a pizza is a very specific and targeted use of a chatbot, I hope you can see how easy it is to build and test a chatbot to help your company in their march towards automation. Whether you are on the development team trying to build a FAQ bot for your app, or marketing team trying to qualify leads, chatbots are coming to help you out, and they are easy to get started with. Happy Coding!

For the full source code, check out the repo.


Richard Reedy
About the Author

Richard Reedy

Richard Reedy has been working in the software field for over 12 years. He has worked on everything from operations to backend server development to really awesome frontend UI. He enjoys building great products and the teams around them. His latest venture is enabling technology to better serve food trucks around the United States.

Comments

Comments are disabled in preview mode.