Manage conversation flow with dialogs

[This topic is pre-release documentation for V4 SDK content and is subject to change. You can find V3 SDK content here]

Managing conversation flow is an essential task in building bots. With the Bot Builder SDK, you can manage conversation flow using dialogs.

A dialog is like a function in a program. It is generally designed to perform a specific operation and it can be invoked as often as it is needed. You can chain multiple dialogs together to handle just about any conversation flow that you want your bot to handle. The dialogs library in the Bot Builder SDK includes built-in features such as prompts and waterfalls to help you manage conversation flow through dialogs. The prompts library provides various prompts you can use to ask users for different types of information. The waterfalls provide a way for you to combine multiple steps together in a sequence.

This article will show you how to create a dialogs object and add prompts and waterfall steps into a dialog set to manage both simple conversation flows and complex conversation flows.

Install the dialogs library

To use dialogs, install the Microsoft.Bot.Builder.Dialogs NuGet package for your project or solution. Then reference the dialogs library in using statements in your code files. For example:

using Microsoft.Bot.Builder.Dialogs;

Create a dialog stack

To use dialogs, you must first create a dialog set.

The Microsoft.Bot.Builder.Dialogs library provides a DialogSet class. To a dialog set you can add named dialogs and sets of dialogs and then access them by name later.

IDialog dialog = null;
// Initialize dialog.

DialogSet dialogs = new DialogSet();
dialogs.Add("dialog name", dialog);

Creating a dialog only adds the dialog definition to the set. The dialog is not run until it is pushed onto the dialog stack by calling a begin or replace method.

The dialog name (for example, addTwoNumbers) must be unique within each dialog set. You can define as many dialogs as necessary within each set.

The dialog library defines the following dialogs:

  • A prompt dialog where the dialog uses at least two functions, one to prompt the user for input and the other to process the input. You can string these together using the waterfall model.
  • A waterfall dialog defines a sequence of waterfall steps, which run in order. A waterfall dialog can have a single step, in which case it can be thought of as a simple, one-step dialog.

Create a single-step dialog

Single-step dialogs can be useful for capturing single-turn conversational flows. This example creates a bot that can detect if the user says something like "1 + 2", and starts an addTwoNumbers dialog to reply with "1 + 2 = 3".

Values are passed into and returned from dialogs as IDictionary<string,object> property bags.

To create a simple dialog within a dialog set, use the Add method. The following adds a one-step waterfall named addTwoNumbers.

This step assumes that the dialog arguments getting passed in contain first and second properties that represent the numbers to be added.

Start with the EchoBot template. Then add code in your bot class to add the dialog in the constructor.

public class EchoBot : IBot
{
    private DialogSet _dialogs;

    public EchoBot()
    {
        _dialogs = new DialogSet();
        _dialogs.Add("addTwoNumbers", new WaterfallStep[]
        {              
            async (dc, args, next) =>
            {
                double sum = (double)args["first"] + (double)args["second"];
                await dc.Context.SendActivity($"{args["first"]} + {args["second"]} = {sum}");
                await dc.End();
            }
        });
    }

    // The rest of the class definition is omitted here but would include OnTurn()
}

Pass arguments to the dialog

To call the dialog from within your bot's OnTurn method, modify OnTurn to contain the following:

public async Task OnTurn(ITurnContext context)
{
    // This bot is only handling Messages
    if (context.Activity.Type == ActivityTypes.Message)
    {
        // Get the conversation state from the turn context
        var state = context.GetConversationState<EchoState>();

        // create a dialog context
        var dialogCtx = _dialogs.CreateContext(context, state);

        // Bump the turn count. 
        state.TurnCount++;

        await dialogCtx.Continue();
        if (!context.Responded)
        {
            // Call a helper function that identifies if the user says something 
            // like "2 + 3" or "1.25 + 3.28" and extract the numbers to add            
            if (TryParseAddingTwoNumbers(context.Activity.Text, out double first, out double second))
            { 
                var dialogArgs = new Dictionary<string, object>
                {
                    ["first"] = first,
                    ["second"] = second
                };                        
                await dialogCtx.Begin("addTwoNumbers", dialogArgs);
            }
            else
            {
                // Echo back to the user whatever they typed.
                await context.SendActivity($"Turn: {state.TurnCount}. You said '{context.Activity.Text}'");
            }
        }
    }
}

Add the helper function to the bot class. The helper function just uses a simple regex to detect if the user's message is a request to add 2 numbers.

// Recognizes if the message is a request to add 2 numbers, in the form: number + number, 
// where number may have optionally have a decimal point.: 1 + 1, 123.99 + 45, 0.4+7. 
// For the sake of simplicity it doesn't handle negative numbers or numbers like 1,000 that contain a comma.
// If you need more robust number recognition, try System.Recognizers.Text
public bool TryParseAddingTwoNumbers(string message, out double first, out double second)
{
    // captures a number with optional -/+ and optional decimal portion
    const string NUMBER_REGEXP = "([-+]?(?:[0-9]+(?:\\.[0-9]+)?|\\.[0-9]+))";
    // matches the plus sign with optional spaces before and after it
    const string PLUSSIGN_REGEXP = "(?:\\s*)\\+(?:\\s*)";
    const string ADD_TWO_NUMBERS_REGEXP = NUMBER_REGEXP + PLUSSIGN_REGEXP + NUMBER_REGEXP;
    var regex = new Regex(ADD_TWO_NUMBERS_REGEXP);
    var matches = regex.Matches(message);
    var succeeded = false;
    first = 0;
    second = 0;
    if (matches.Count == 0)
    {
        succeeded = false;
    }
    else
    {
        var matched = matches[0];
        if ( System.Double.TryParse(matched.Groups[1].Value, out first) 
            && System.Double.TryParse(matched.Groups[2].Value, out second))
        {
            succeeded = true;
        } 
    }
    return succeeded;
}

If you're using the EchoBot template, modify the EchoState class in EchoState.cs as follows:

/// <summary>
/// Class for storing conversation state.
/// This bot only stores the turn count in order to echo it to the user
/// </summary>
public class EchoState: Dictionary<string, object>
{
    private const string TurnCountKey = "TurnCount";
    public EchoState()
    {
        this[TurnCountKey] = 0;            
    }

    public int TurnCount
    {
        get { return (int)this[TurnCountKey]; }
        set { this[TurnCountKey] = value; }
    }
}

Run the bot

Try running the bot in the Bot Framework Emulator, and say things like "what's 1+1?" to it.

run the bot

Using dialogs to guide the user through steps

Create a composite dialog

The following snippets are taken from the Microsoft.Bot.Samples.Dialog.Prompts sample code in the botbuilder-dotnet repo.

In Startup.cs:

  1. Rename your bot to DialogContainerBot.
  2. Use a simple dictionary as a property bag for the conversation state for the bot.
public void ConfigureServices(IServiceCollection services)
{
    services.AddBot<DialogContainerBot>(options =>
    {
        options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);
        options.Middleware.Add(new ConversationState<Dictionary<string, object>>(new MemoryStorage()));
    });
}

Rename your EchoBot to DialogContainerBot.

In DialogContainerBot.cs, define a class for a profile dialog.

public class ProfileControl : DialogContainer
{
    public ProfileControl()
        : base("fillProfile")
    {
        Dialogs.Add("fillProfile", 
            new WaterfallStep[]
            {
                async (dc, args, next) =>
                {
                    dc.ActiveDialog.State = new Dictionary<string, object>();
                    await dc.Prompt("textPrompt", "What's your name?");
                },
                async (dc, args, next) =>
                {
                    dc.ActiveDialog.State["name"] = args["Value"];
                    await dc.Prompt("textPrompt", "What's your phone number?");
                },
                async (dc, args, next) =>
                {
                    dc.ActiveDialog.State["phone"] = args["Value"];
                    await dc.End(dc.ActiveDialog.State);
                }
            }
        );
        Dialogs.Add("textPrompt", new Builder.Dialogs.TextPrompt());
    }
}

Then, within the bot definition, declare a field for the bot's main dialog and initialize it in the bot's constructor. The bot's main dialog includes the profile dialog.

private DialogSet _dialogs;

public DialogContainerBot()
{
    _dialogs = new DialogSet();

    _dialogs.Add("getProfile", new ProfileControl());
    _dialogs.Add("firstRun",
        new WaterfallStep[]
        {
            async (dc, args, next) =>
            {
                    await dc.Context.SendActivity("Welcome! We need to ask a few questions to get started.");
                    await dc.Begin("getProfile");
            },
            async (dc, args, next) =>
            {
                await dc.Context.SendActivity($"Thanks {args["name"]} I have your phone number as {args["phone"]}!");
                await dc.End();
            }
        }
    );
}

In the bot's OnTurn method:

  • Greet the user when the conversation starts.

  • Initialize and continue the main dialog whenever we get a message from the user.

    If the dialog hasn't generated a response, assume that it completed earlier or hasn't started yet and begin it, specifying the name of the dialog in the set to start with.

public async Task OnTurn(ITurnContext turnContext)
{
    try
    {
        switch (turnContext.Activity.Type)
        {
            case ActivityTypes.ConversationUpdate:
                foreach (var newMember in turnContext.Activity.MembersAdded)
                {
                    if (newMember.Id != turnContext.Activity.Recipient.Id)
                    {
                        await turnContext.SendActivity("Hello and welcome to the Composite Control bot.");
                    }
                }
                break;

            case ActivityTypes.Message:
                var state = ConversationState<Dictionary<string, object>>.Get(turnContext);
                var dc = _dialogs.CreateContext(turnContext, state);

                await dc.Continue();

                if (!turnContext.Responded)
                {
                    await dc.Begin("firstRun");
                }

                break;
        }
    }
    catch (Exception e)
    {
        await turnContext.SendActivity($"Exception: {e.Message}");
    }
}

Next steps

Now that you learn how to use dialogs, prompts, and waterfalls to manage conversation flow, let's take a look at how we can break our dialogs into modular tasks instead of lumping them all together in the main bot logic's dialogs object.