Manage simple conversation flow with dialogs

Note

This topic is for the latest release of the SDK (v4). You can find content for the older version of the SDK (v3) here.

You can manage simple and complex conversation flows using the dialogs library. In a simple conversation flow, the user starts from the first step of a waterfall, continues through to the last step, and the conversation finishes. Complex conversation flows include branches and loop.

Dialogs are structures in your bot that act like a functions in your bot's program. Dialogs build the messages your bot sends, and carry out the computational tasks required. They are designed to perform a specific operations, in a specific order. They can be invoked in different ways - sometimes in response to a user, sometimes in response to some outside stimuli, or by other dialogs.

Using dialogs enables the bot developer to guide the conversational flow. You can create multiple dialogs and link them together to create 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, waterfall dialogs and component dialogs to help you manage conversation flow. You can use prompts to ask users for different types of information. You can use a waterfall to combine multiple steps together in a sequence. And you can use component dialogs to create modular dialog systems containing multiple sub-dialogs.

In this article, we use dialog sets to create a conversation flow that contains both prompts and waterfalls. We have two example dialogs. The first is a one-step dialog that performs an operation that requires no user input. The second is a multi-step dialog that prompts the user for some information.

Install the dialogs library

We'll start from a basic EchoBot template. For instructions, see the quickstart for .NET.

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 as necessary.

using Microsoft.Bot.Builder.Dialogs;

Create a dialog set

In this first example, we'll create a one-step dialog that can add two numbers together and display the result.

To use dialogs, you must first create a dialog set. Dialog sets are objects that group together one or more dialogs for use together. Your bot might have multiple dialog sets to group related dialogs together - but we'll start with just one.

The Microsoft.Bot.Builder.Dialogs library provides a DialogSet class. Create an AdditionDialog class, and add the using statements we'll need. You can add named dialogs and sets of dialogs to a dialog set, and then access them by name later.

using Microsoft.Bot.Builder.Dialogs;

Derive the class from DialogSet, and define the IDs and keys we'll use to identify the dialogs and input information for this dialog set.

/// <summary> Defines a simple dialog for adding two numbers together. </summary>
public class AdditionDialogSet : DialogSet
{
    /// <summary>The name of the main dialog in the set.</summary>
    public const string Main = "additionDialog";

    /// <summary>Contains the IDs for the prompts used in the dialog set.</summary>
    private struct Inputs
    {
        public const string First = "first";
        public const string Second = "second";
    }
}

After creating a dialog set, add dialogs to the set to make them available to the bot to use. The dialog does not run immediately - adding it to a dialog set only prepares it to run later.

The ID of each dialog (for example, addTwoNumbers) must be unique within each dialog set. You can define as many dialogs as necessary within each set. If you want to create multiple dialog sets and have them work seemlessly together, see Create modular bot logic.

The dialog library defines the following types of dialogs:

  • A prompt dialog is a 2-turn dialog that requires at least two step 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 single-turn dialog.

Create a single-turn dialog

Single-turn dialog are useful for implementing interactions where the bot only sends a single message. 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 objects 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.

Add the following constructor to the AdditionDialog class.

/// <summary>Defines the steps of the dialog.</summary>
public AdditionDialogSet(IStatePropertyAccessor<DialogState> dialogState)
    : base(dialogState)
{
    Add(new WaterfallDialog(Main, new WaterfallStep[]
    {
        async (step, cancellationToken) =>
        {
            // Get the input from the options to the dialog and add them.
            var options = step.Options as TwoNumbersClass;
            var sum = options.First + options.Second;

            // Display the result to the user.
            await step.Context.SendActivityAsync($"{options.First} + {options.Second} = {sum}");

            // End the dialog.
            return await step.EndDialogAsync();
        },
    }));
}

Next add the class that we need to hold our input.

public class TwoNumbersClass
{
    public double First { get; set; }
    public double Second { get; set; }
}

Pass arguments to the dialog

In your bot code, update your using statements.

using Microsoft.Bot;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Core.Extensions;
using Microsoft.Bot.Schema;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

Add a dialog accessors to the class for the addition dialog.

private readonly BotAccessors _accessors;
private readonly AdditionDialogSet _dialogs;
private readonly ILogger _logger;

public AdditionBot(BotAccessors accessors, ILoggerFactory loggerFactory)
{
    if (loggerFactory == null)
    {
        throw new System.ArgumentNullException(nameof(loggerFactory));
    }

    _logger = loggerFactory.CreateLogger<AdditionBot>();
    _logger.LogTrace("EchoBot turn start");
    _accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
    _dialogs = new AdditionDialogSet(_accessors.DialogStateAccessor);
}

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

public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    // Handle any message activity from the user.
    if (turnContext.Activity.Type == ActivityTypes.Message)
    {
        // Generate a dialog context for the addition dialog.
        var dc = await _dialogs.CreateContextAsync(turnContext, cancellationToken);

        // 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(turnContext.Activity.Text, out double first, out double second))
        {
            await dc.BeginDialogAsync(AdditionDialogSet.Main, new AdditionDialogSet.TwoNumbersClass { First = first, Second = second }, cancellationToken);
        }
        else
        {
            // Echo back to the user whatever they typed.
            await turnContext.SendActivityAsync($"You said '{turnContext.Activity.Text}'");
        }
    }
}

Add a TryParseAddingTwoNumbers 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 static 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);

    first = 0;
    second = 0;
    if (matches.Count > 0)
    {
        var matched = matches[0];
        if (double.TryParse(matched.Groups[1].Value, out first)
            && double.TryParse(matched.Groups[2].Value, out second))
        {
            return true;
        }
    }

    return false;
}

If you're using the EchoBot template, change the name of the EchoBotAccessors class to BotAccessors and modify it to contain the following.

public class BotAccessors
{
    /// <summary> Gets the ConversationState object for the conversation. </summary>
    public ConversationState ConversationState { get; }

    /// <summary> Gets the IStatePropertyAccessor{T} name used for the DialogState accessor. </summary>
    public static string DialogStateName { get; } = $"{nameof(BotAccessors)}.DialogState";

    /// <summary> Gets or sets the IStatePropertyAccessor{T} for DialogState. </summary>
    public IStatePropertyAccessor<DialogState> DialogStateAccessor { get; set; }

    public BotAccessors(ConversationState conversationState)
    {
        this.ConversationState = conversationState
            ?? throw new ArgumentNullException(nameof(conversationState));
    }
}

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

In this example, we create a multi-step dialog to prompt the user for information utilizing the dialogSet we created above.

Create a dialog with waterfall steps

A WaterfallDialog is a specific implementation of a dialog that is commonly used to collect information from the user or guide the user through a series of tasks. Each step of the conversation is implemented as a function. At each step, the bot prompts the user for input, waits for a response, and then passes the result to the next step. The result of the first function is passed as an argument into the next function, and so on.

For example, the following code sample defines three functions in an array that represents the three steps of a waterfall. After each prompt, the bot acknowledges the user's input but doesn't store it in any way. If you want to persist user input, see Persist user data for more details.

This shows a constructor for a greeting dialog, where GreetingDialog derives from DialogSet, Inputs.Text contains the ID we're using for the TextPrompt object, and Main contains the ID for the greeting dialog itself.

public GreetingDialog()
{
    // Include a text prompt.
    Add(new TextPrompt(Inputs.Text));

    // Define the dialog logic for greeting the user.
    Add(new WaterfallDialog(Main, WaterfallStep[]
    {
        async (step, cancellationToken) =>
        {
            // Ask for their name.
            return await step.PromptAsync(Inputs.Text, new PromptOptions
            {
                Prompt = MessageFactory.Text("What is your name?"),
            });
        },

        async (step, cancellationToken) =>
        {
            // Save the prompt result in dialog state.
            step.Values[Values.Name] = step.Result;

            // Acknowledge their input.
            await step.Context.SendActivityAsync($"Hi, {step.Result}!");

            // Ask where they work.
            return await step.PromptAsync(Inputs.Text, new PromptOptions
            {
                Prompt = MessageFactory.Text("Where do you work?"),
            });
        },

        async (step, cancellationToken) =>
        {
            // Save the prompt result in dialog state.
            step.Values[Values.WorkPlace] = step.Result;

            // Acknowledge their input.
            await step.Context.SendActivityAsync($"{step.Result} is a fun place.");

            // End the dialog and return the collected information.
            return await step.EndDialogAsync(new Output
            {
                Name = step.Values[Values.Name] as string,
                WorkPlace = step.Values[Values.WorkPlace] as string,
            });
        },
    }));
}

The signature for a waterfall step is as follows:

Parameter Description
step The step context.
options Optional, contains the arguments passed into the step.
next Optional, a method that allows you to proceed to the next step of the waterfall without prompting. You can provide an option argument when you call this method, This allows you to pass arguments to the next step in the waterfall.

Each step must call the next() delegate, or one of other the dialog context methods: beginDialog, endDialog, prompt, or replaceDialog. Otherwise, the bot will get stuck: the waterfall will not move forward to the next step, and the same step will be re-executed each time the user sends the bot a message.

When you reached the end of the waterfall, it is best practice to return with the endDialog method. See End a dialog section below for more information.

Start a dialog

To start a dialog, pass the dialogId you want to start into the dialog context's beginDialog, prompt, or replaceDialog method. The beginDialog method will push the dialog onto the top of the stack, while the replaceDialog method will pop the current dialog off the stack and push the replacing dialog onto the stack.

To start a dialog without arguments:

// Start the greetings dialog.
await step.BeginDialogAsync(GreetingDialogSet.Main);

To start a dialog with arguments:

// Start the greetings dialog with the 'userName' passed in.
await step.BeginDialogAsync(GreetingDialogSet.Main, userName);

To start a prompt dialog:

Here, Inputs.Text contains the ID of a TextPrompt that is in the same dialog set.

// Ask a user for their name.
return await step.PromptAsync(Inputs.Text, new PromptOptions
{
    Prompt = MessageFactory.Text("What is your name?"),
});

Depending on the type of prompt you are starting, the prompt's argument signature may be different. The DialogSet.prompt method is a helper method. This method takes in arguments and constructs the appropriate options for the prompt; then, it calls the begin method to start the prompt dialog. For more information on prompts, see Prompt user for input.

End a dialog

The endDialog method ends a dialog by popping it off the stack and returns an optional result to the parent dialog.

It is best practice to explicitly call the endDialog method at the end of the dialog; however, it is not required because the dialog will automatically be popped off the stack for you when you reach the end of the waterfall.

To end a dialog:

// End the current dialog by popping it off the stack.
await step.EndDialogAsync();

To end a dialog and return information to the parent dialog, include a property bag argument.

// End the current dialog and return information to the parent dialog.
return await step.EndDialogAsync(new Output
{
    Name = step.Values[Values.Name] as string,
    WorkPlace = step.Values[Values.WorkPlace] as string,
});

Clear the dialog stack

If you want to pop all dialogs off the stack, you can clear the dialog stack by calling the cancel all dialogs method.

// Pop all dialogs from the current stack.
await step.CancelAllDialogsAsync();

Repeat a dialog

To repeat a dialog, use the replaceDialog method. The dialog context's replaceDialog method will pop the current dialog off the stack and push the replacing dialog onto the top of the stack and begin that dialog. This is a great way to handle complex conversation flows and a good technique to manage menus.

// End the current dialog and start the main menu dialog.
await step.ReplaceDialogAsync(MainMenu.Main);

Next steps

Now that you've learned how to manage simple conversation flows, let's take a look at how you can leverage the replaceDialog method to handle complex conversation flows.