Implement sequential conversation flow

APPLIES TO: SDK v4

Gathering information by posing questions is one of the main ways a bot interacts with users. The dialogs library provides useful built-in features such as prompt classes that make it easy to ask questions and validate the response to make sure it matches a specific data type or meets custom validation rules.

You can manage linear and more complex conversation flows using the dialogs library. In a linear interaction, the bot runs through a fixed sequence of steps, and the conversation finishes. A dialog is useful when the bot needs to gather information from the user.

This article shows how to implement linear conversation flow by creating prompts and calling them from a waterfall dialog. For examples of how to write your own prompts without using the dialogs library, see the Create your own prompts to gather user input article.

Note

The Bot Framework JavaScript, C#, and Python SDKs will continue to be supported, however, the Java SDK is being retired with final long-term support ending in November 2023. Only critical security and bug fixes within this repository will be undertaken.

Existing bots built with the Java SDK will continue to function.

For new bot building, consider using Power Virtual Agents and read about choosing the right chatbot solution.

For more information, see The future of bot building.

Prerequisites

About this sample

The multi-turn prompts sample uses a waterfall dialog, a few prompts, and a component dialog to create a linear 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, ask for their age Number prompt, with validation to only accept ages greater than 0 and less than 150
If they're not using Microsoft Teams, ask them for a profile picture Attachment prompt, with validation to allow a missing attachment
Ask 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 won't 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 creating the bot's DialogBot class, the UserProfileDialog is set as its main dialog. The bot then uses a Run helper method to access the dialog.

Class diagram for the C# sample.

Dialogs\UserProfileDialog.cs

Begin by creating the UserProfileDialog that derives from the ComponentDialog class, and has seven 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're used.

public UserProfileDialog(UserState userState)
    : base(nameof(UserProfileDialog))
{
    _userProfileAccessor = userState.CreateProperty<UserProfile>("UserProfile");

    // This array defines how the Waterfall will execute.
    var waterfallSteps = new WaterfallStep[]
    {
        TransportStepAsync,
        NameStepAsync,
        NameConfirmStepAsync,
        AgeStepAsync,
        PictureStepAsync,
        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)));
    AddDialog(new AttachmentPrompt(nameof(AttachmentPrompt), PicturePromptValidatorAsync));

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

Next, add the steps that the dialog uses to prompt for input. 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. Then 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 don't, your dialog may not work as designed. Shown below is the implementation for 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, specify a retry prompt for when the user's input fails to validate, either because it's in a format that the prompt can't 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 reprompt 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; }

    public Attachment Picture { get; set; }
}

Dialogs\UserProfileDialog.cs

In the last step, check the stepContext.Result returned by the dialog called in the previous waterfall step. If the return value is true, the user profile accessor gets and updates the user profile. To get the user profile, call GetAsync and then set the values of the userProfile.Transport, userProfile.Name, userProfile.Age and userProfile.Picture properties. Finally, 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"];
        userProfile.Picture = (Attachment)stepContext.Values["picture"];

        var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}";

        if (userProfile.Age != -1)
        {
            msg += $" and your age as {userProfile.Age}";
        }

        msg += ".";

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

        if (userProfile.Picture != null)
        {
            try
            {
                await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(userProfile.Picture, "This is your profile picture."), cancellationToken);
            }
            catch
            {
                await stepContext.Context.SendActivityAsync(MessageFactory.Text("A profile picture was saved but could not be displayed here."), 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);
}

Run the dialog

Bots\DialogBot.cs

The OnMessageActivityAsync handler uses the RunAsync method to start or continue the dialog. OnTurnAsync uses 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, the state is saved after the message handler completes but before the turn itself completes.

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
    await base.OnTurnAsync(turnContext, cancellationToken);

    // Save any state changes that might have occurred 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.RunAsync(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

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

{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient().AddControllers().AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
        });

        // Create the Bot Framework Authentication to be used with the Bot Adapter.
        services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

        // Create the Bot 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>();

Note

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

Test your bot

  1. If you haven't 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.

An example transcript of a conversation with the multi-turn prompt bot.

Additional information

About dialog and bot state

In this bot, two state property accessors are defined:

  • 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's updated by the dialog context, such as when the begin dialog or continue dialog methods are called.
  • One created within user state for the user profile property. The bot uses this to track information it has about the user, and you must explicitly manage this state in the 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, a call to the save changes method, of the corresponding state management object, is performed.

This sample updates the user profile state from within the dialog. This practice can work for some bots, but it won't 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 and it's 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.

Definition of a prompt validator method

UserProfileDialog.cs

Below is a validator code example for the AgePromptValidatorAsync method definition. promptContext.Recognized.Value contains the parsed value, which is an integer here for the number prompt. promptContext.Recognized.Succeeded indicates whether the prompt was able to parse the user's input or not. The validator should return false to indicate that the value wasn't accepted and the prompt dialog should reprompt the user; otherwise, return true to accept the input and return from the prompt dialog. You can change the value in the validator per your scenario.

private static Task<bool> AgePromptValidatorAsync(PromptValidatorContext<int> promptContext, CancellationToken cancellationToken)
{
    // This condition is our validation rule. You can also change the value at this point.
    return Task.FromResult(promptContext.Recognized.Succeeded && promptContext.Recognized.Value > 0 && promptContext.Recognized.Value < 150);
}

Next steps