Implement sequential conversation flow

APPLIES TO: yesSDK v4 no SDK v3

Gathering information by posing questions is one of the main ways a bot interacts with users. The dialogs library makes it easy to ask questions, as well as validate the response to make sure it matches a specific data type or meets custom validation rules.

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 general, a dialog is useful when the bot needs to gather information from the user. This topic details how to implement simple conversation flow by creating prompts and calling them from a waterfall dialog.

Prerequisites

About this sample

In the multi-turn prompt sample, we use a waterfall dialog, a few prompts, and a component dialog to create a simple interaction that asks the user a series of questions. The code uses a dialog to cycle through these steps:

Steps Prompt type
Ask the user for their mode of transportation Choice prompt
Ask the user for their name Text prompt
Ask the user if they want to provide their age Confirm prompt
If they answered yes, asks for their age Number prompt with validation to only accept ages greater than 0 and less than 150.
Asks if the collected information is "ok" Reuse Confirm prompt

Finally, if they answered yes, display the collected information; otherwise, tell the user that their information will not be kept.

Create the main dialog

To use dialogs, install the Microsoft.Bot.Builder.Dialogs NuGet package.

The bot interacts with the user via UserProfileDialog. When we create the bot's DialogBot class, we will set the UserProfileDialog as its main dialog. The bot then uses a Run helper method to access the dialog.

user profile dialog

Dialogs\UserProfileDialog.cs

We begin by creating the UserProfileDialog that derives from the ComponentDialog class, and has 6 steps.

In the UserProfileDialog constructor, create the waterfall steps, prompts and the waterfall dialog, and add them to the dialog set. The prompts need to be in the same dialog set in which they are used.

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

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

// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);

Next, we implement the steps that the dialog uses. To use a prompt, call it from a step in your dialog and retrieve the prompt result in the following step using stepContext.Result. Behind the scenes, prompts are a two-step dialog. First, the prompt asks for input; second, it returns the valid value, or starts over from the beginning with a reprompt until it receives a valid input.

You should always return a non-null DialogTurnResult from a waterfall step. If you do not, your dialog may not work as designed. Here we show the implementation for the NameStepAsync in the waterfall dialog.

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value;

    return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") }, cancellationToken);
}

In AgeStepAsync, we specify a retry prompt for when the user's input fails to validate, either because it is in a format that the prompt can not parse, or the input fails a validation criteria. In this case, if no retry prompt was provided, the prompt will use the initial prompt text to re-prompt the user for input.

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // User said "yes" so we will be prompting for the age.
        // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog.
        var promptOptions = new PromptOptions
        {
            Prompt = MessageFactory.Text("Please enter your age."),
            RetryPrompt = MessageFactory.Text("The value entered must be greater than 0 and less than 150."),
        };

        return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
    }
    else
    {
        // User said "no" so we will skip the next step. Give -1 as the age.
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}

UserProfile.cs

The user's mode of transportation, name, and age are saved in an instance of the UserProfile class.

public class UserProfile
{
    public string Transport { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }
}

Dialogs\UserProfileDialog.cs

In the last step, we check the stepContext.Result returned by the dialog called in the previous waterfall step. If the return value is true, we use the user profile accessor to get and update the user profile. To get the user profile, we call the GetAsync method, and then set the values of the userProfile.Transport, userProfile.Name, and userProfile.Age properties. Finally, we summarize the information for the user before calling EndDialogAsync which ends the dialog. Ending the dialog pops it off the dialog stack and returns an optional result to the dialog's parent. The parent is the dialog or method that started the dialog that just ended.

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

        userProfile.Transport = (string)stepContext.Values["transport"];
        userProfile.Name = (string)stepContext.Values["name"];
        userProfile.Age = (int)stepContext.Values["age"];

        var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}.";
        if (userProfile.Age != -1)
        {
            msg += $" And age as {userProfile.Age}.";
        }

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);
    }
    else
    {
        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);
}

Create the extension method to run the waterfall dialog

We've defined a Run extension method that we will use to create and access the dialog context. Here, accessor is the state property accessor for the dialog state property, and dialog is the user profile component dialog. Since component dialogs define an inner dialog set, we must create an outer dialog set that's visible to the message handler code and use that to create a dialog context.

The dialog context is created by calling the CreateContext method, and is used to interact with the dialog set from within the bot's turn handler. The dialog context includes the current turn context, the parent dialog, and the dialog state, which provides a method for preserving information within the dialog.

The dialog context allows you to start a dialog with the string ID, or continue the current dialog (such as a waterfall dialog that has multiple steps). The dialog context is passed through to all the bot's dialogs and waterfall steps.

DialogExtensions.cs

public static async Task Run(this Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken = default(CancellationToken))
{
    var dialogSet = new DialogSet(accessor);
    dialogSet.Add(dialog);

    var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
    var results = await dialogContext.ContinueDialogAsync(cancellationToken);
    if (results.Status == DialogTurnStatus.Empty)
    {
        await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken);
    }
}

Run the dialog

Bots\DialogBot.cs

The OnMessageActivityAsync handler uses the extension method to start or continue the dialog. In OnTurnAsync, we use the bot's state management objects to persist any state changes to storage. (The ActivityHandler.OnTurnAsync method calls the various activity handler methods, such as OnMessageActivityAsync. In this way, we are saving state after the message handler completes but before the turn itself completes.)

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);
}

Register services for the bot

This bot uses the following services.

  • Basic services for a bot: a credential provider, an adapter, and the bot implementation.
  • Services for managing state: storage, user state, and conversation state.
  • The dialog the bot will use.

Startup.cs

We register services for the bot in Startup. These services are available to other parts of the code through dependency injection.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
    // Create the credential provider to be used with the Bot Framework Adapter.
    services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();

    // Create the Bot Framework Adapter with error handling enabled. 
    services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

    // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) 
    services.AddSingleton<IStorage, MemoryStorage>();

    // Create the User state. (Used in this bot's Dialog implementation.)
    services.AddSingleton<UserState>();

    // Create the Conversation state. (Used by the Dialog system itself.)
    services.AddSingleton<ConversationState>();

    // The Dialog that will be run by the bot.
    services.AddSingleton<UserProfileDialog>();

    // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
    services.AddTransient<IBot, DialogBot<UserProfileDialog>>();
}

Note

Memory storage is used for testing purposes only and is not intended for production use. Be sure to use a persistent type of storage for a production bot.

To test the bot

  1. If you have not done so already, install the Bot Framework Emulator.
  2. Run the sample locally on your machine.
  3. Start the emulator, connect to your bot, and send messages as shown below.

Sample run of the multi-turn prompt dialog

Additional information

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 dialog 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.

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.

Next steps