Implement sequential conversation flow

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 interaction, the bot runs through a fixed sequence of steps, and the conversation finishes. In this article, we use a waterfall dialog, a few prompts, and a dialog set to create a simple interaction that asks the user a series of questions. We draw on code from the multi-turn prompt [C# | JS] sample.

To use dialogs in general, you need the Microsoft.Bot.Builder.Dialogs NuGet package for your project or solution.

The following sections reflect the steps you would take to implement simple dialogs for most bots:

Configure your bot

We will need a state property accessor assigned to the dialog set that the bot can use to manage dialog state.

We will initialize the state property accessor for the bot's dialog state in the configuration code in the Startup.cs file.

We define a MultiTurnPromptsBotAccessors class to hold the state management objects and state property accessors for the bot. Here, we're calling out only portions of the code.

public class MultiTurnPromptsBotAccessors
{
    // Initializes a new instance of the class.
    public MultiTurnPromptsBotAccessors(ConversationState conversationState, UserState userState)
    {
        ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
        UserState = userState ?? throw new ArgumentNullException(nameof(userState));
    }

    public IStatePropertyAccessor<DialogState> ConversationDialogState { get; set; }
    public IStatePropertyAccessor<UserProfile> UserProfile { get; set; }

    public ConversationState ConversationState { get; }
    public UserState UserState { get; }
}

We register the accessors class in the ConfigureServices method of the Statup class. Again, we're calling out only portions of the code.

public void ConfigureServices(IServiceCollection services)
{
    // ...

    // Create and register state accessors.
    // Accessors created here are passed into the IBot-derived class on every turn.
    services.AddSingleton<MultiTurnPromptsBotAccessors>(sp =>
    {
        // We need to grab the conversationState we added on the options in the previous step
        var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
        var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
        var userState = options.State.OfType<UserState>().FirstOrDefault();

        // Create the custom state accessor.
        // State accessors enable other components to read and write individual properties of state.
        var accessors = new MultiTurnPromptsBotAccessors(conversationState, userState)
        {
            ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
            UserProfile = userState.CreateProperty<UserProfile>("UserProfile"),
        };

        return accessors;
    });
}

Through dependency injection, the accessors will be available to the bot's constructor code.

Update the bot turn handler to call the dialog

To run the dialog, the bot's turn handler needs to create a dialog context for the dialog set that contains the dialogs for the bot. (A bot could define multiple dialog sets, but as a general rule, you should just define one for your bot. Dialogs library describes key aspects of dialogs.)

The dialog is run from the bot's turn handler. The handler first creates a DialogContext and either continues the active dialog or begins a new dialog as appropriate. The handler then saves conversation and user state at the end of the turn.

In the MultiTurnPromptsBot class, we've defined a _dialogs property that contains the dialog set, from which we generate a dialog context. Again, we're showing only part of the turn handler code here.

public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    // ...
    if (turnContext.Activity.Type == ActivityTypes.Message)
    {
        // Run the DialogSet - let the framework identify the current state of the dialog from
        // the dialog stack and figure out what (if any) is the active dialog.
        var dialogContext = await _dialogs.CreateContextAsync(turnContext, cancellationToken);
        var results = await dialogContext.ContinueDialogAsync(cancellationToken);

        // If the DialogTurnStatus is Empty we should start a new dialog.
        if (results.Status == DialogTurnStatus.Empty)
        {
            await dialogContext.BeginDialogAsync("details", null, cancellationToken);
        }
    }

    // ...
    // Save the dialog state into the conversation state.
    await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);

    // Save the user profile updates into the user state.
    await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}

In the bot's turn handler, we create a dialog context for the dialog set. The dialog context accesses the state cache for the bot, effectively remembering where in the conversation the last turn left off.

If there is an active dialog, dialog context's continue dialog method progresses it, using the user's input that triggered this turn; otherwise, the bot calls the dialog context's begin dialog method to start a dialog.

Finally, we call the save changes method on the state management objects to persist any changes that have happened this turn.

About dialog and bot state

In this bot, we've defined two state property accessors:

  • One created within conversation state for the dialog state property. The dialog state tracks where the user is within the dialogs of a dialog set, and it is updated by the dialog context, such as when we call the begin dialog or continue dialog methods.
  • One created within user state for the user profile property. The bot uses this to track information it has about the user, and we explicitly manage this state in our bot code.

The get and set methods of a state property accessor get and set the value of the property in the state management object's cache. The cache is populated the first time the value of a state property is requested in a turn, but it must be persisted explicitly. In order to persist changes to both of these state properties, we call the save changes method of the corresponding state management object.

For more information, see dialog state.

Initialize your bot and define your dialog

Our simple conversation is modeled as a series of questions posed to the user. The C# and JavaScript versions have slightly different steps:

  1. Ask them for their name.
  2. Ask whether they are willing to provide their age.
  3. If so, ask for their age; otherwise, skip this step.
  4. Ask whether the information gathered is correct.
  5. Send a status message and end.

Here are a couple things things to remember when defining your own waterfall steps.

  • Each bot turn reflects input from the user, followed by a response from the bot. Thus, you are asking the user for input at the end of a waterfall step, and receiving their answer in the next waterfall step.
  • Each prompt is effectively a two-step dialog that presents its prompt and loops until it receives "valid" input. (You can rely on the built-in validation for each type of prompt, or you can add your own custom validation to the prompt. For more information, see get user input.)

In this sample, the dialog is defined within the bot file and initialized in the bot's constructor.

Define an instance property for the dialog set.

// The DialogSet that contains all the Dialogs that can be used at runtime.
private DialogSet _dialogs;

Create the dialog set within the bot's constructor, adding the prompts and the waterfall dialog to the set.

public MultiTurnPromptsBot(MultiTurnPromptsBotAccessors accessors)
{
    _accessors = accessors ?? throw new ArgumentNullException(nameof(accessors));

    // The DialogSet needs a DialogState accessor, it will call it when it has a turn context.
    _dialogs = new DialogSet(accessors.ConversationDialogState);

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

    // Add named dialogs to the DialogSet. These names are saved in the dialog state.
    _dialogs.Add(new WaterfallDialog("details", waterfallSteps));
    _dialogs.Add(new TextPrompt("name"));
    _dialogs.Add(new NumberPrompt<int>("age"));
    _dialogs.Add(new ConfirmPrompt("confirm"));
}

In this sample, we define each step as a separate method. You can also define the steps in-line in the constructor using lambda expressions.

private static async Task<DialogTurnResult> NameStepAsync(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 users response is received.
    return await stepContext.PromptAsync("name", new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") }, cancellationToken);
}

private async Task<DialogTurnResult> NameConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Get the current profile object from user state.
    var userProfile = await _accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

    // Update the profile.
    userProfile.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("confirm", new PromptOptions { Prompt = MessageFactory.Text("Would you like to give your age?") }, cancellationToken);
}

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // User said "yes" so we will be prompting for the age.

        // Get the current profile object from user state.
        var userProfile = await _accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog.
        return await stepContext.PromptAsync("age", new PromptOptions { Prompt = MessageFactory.Text("Please enter your age.") }, cancellationToken);
    }
    else
    {
        // User said "no" so we will skip the next step. Give -1 as the age.
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}


private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Get the current profile object from user state.
    var userProfile = await _accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

    // Update the profile.
    userProfile.Age = (int)stepContext.Result;

    // We can send messages to the user at any point in the WaterfallStep.
    if (userProfile.Age == -1)
    {
        await stepContext.Context.SendActivityAsync(MessageFactory.Text($"No age given."), cancellationToken);
    }
    else
    {
        // We can send messages to the user at any point in the WaterfallStep.
        await stepContext.Context.SendActivityAsync(MessageFactory.Text($"I have your age as {userProfile.Age}."), cancellationToken);
    }

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

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // Get the current profile object from user state.
        var userProfile = await _accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

        // We can send messages to the user at any point in the WaterfallStep.
        if (userProfile.Age == -1)
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"I have your name as {userProfile.Name}."), cancellationToken);
        }
        else
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"I have your name as {userProfile.Name} and age as {userProfile.Age}."), cancellationToken);
        }
    }
    else
    {
        // We can send messages to the user at any point in the WaterfallStep.
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("Thanks. Your profile will not be kept."), cancellationToken);
    }

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is the end.
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}

This sample updates the user profile state from within the dialog. This practice can work for a simple bot, but will not work if you want to reuse a dialog across bots.

There are various options for keeping dialog steps and bot state separate. For example, once your dialog gathers complete information, you can:

  • Use the end dialog method to provide the collected data as return value back to the parent context. This can be the bot's turn handler or an earlier active dialog on the dialog stack. This is how the prompt classes are designed.
  • Generate a request to an appropriate service. This might work well if your bot acts as a front end to a larger service.

Test your dialog

Build and run your bot locally, then interact with your bot using the Emulator.

  1. The bot sends an initial greeting message in response to the conversation update activity in which the user is added to the conversation.
  2. Enter hi or other input. Since there is not yet an active dialog this turn, the bot starts the details dialog.
    • The bot sends the first prompt of the dialog and waits for more input.
  3. Answer questions as the bot asks them, progressing through the dialog.
  4. The last step of the dialog sends a Thanks message, based on your inputs.
    • When the dialog ends, it's removed from the dialog stack, and the bot no longer has an active dialog.
  5. Enter hi or other input to start the dialog again.

Next steps