Create advanced conversation flow using branches and loops

APPLIES TO: yesSDK v4 no SDK v3

You can manage simple and complex conversation flows using the dialogs library. In this article, we will show you how to manage complex conversations that branch and loop. We'll also show you how to pass arguments between different parts of the dialog.

Prerequisites

About this sample

This sample represents a bot that can sign users up to review up to two companies from a list.

DialogAndWelcomeBot extends DialogBot, which defines the handlers for different activities and the bot's turn handler. DialogBot runs the dialogs:

  • The run method is used by DialogBot to kick off the dialog.
  • MainDialog is the parent of the other two dialogs, which are called at certain times in the dialogs. Details on those dialogs are provided throughout this article.

The dialogs are split into MainDialog, TopLevelDialog, and ReviewSelectionDialog component dialogs, which together do the following:

  • They ask for the user's name and age, and then branch based on the user's age.
    • If the user is too young, they do not ask the user to review any companies.
    • If the user is old enough, they start to collect the user's review preferences.
      • They allow the user to select a company to review.
      • If the user chooses a company, they loop to allow a second company to be selected.
  • Finally, they thank the user for participating.

They use waterfall dialogs and a few prompts to manage a complex conversation.

Complex bot flow

To use dialogs, your project needs to install the Microsoft.Bot.Builder.Dialogs NuGet package.

Startup.cs

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

  • 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.
// 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>();

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

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.

Define a class in which to store the collected information

UserProfile.cs

/// <summary>Contains information about a user.</summary>
public class UserProfile
{
    public string Name { get; set; }
    public int Age { get; set; }

    // The list of companies the user wants to review.
    public List<string> CompaniesToReview { get; set; } = new List<string>();
}

Create the dialogs to use

Dialogs\MainDialog.cs

We've defined a component dialog, MainDialog, which contains a couple main steps and directs the dialogs and prompts. The initial step calls TopLevelDialog which is explained below.

private async Task<DialogTurnResult> InitialStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    return await stepContext.BeginDialogAsync(nameof(TopLevelDialog), null, cancellationToken);
}

private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var userInfo = (UserProfile)stepContext.Result;

    string status = "You are signed up to review "
        + (userInfo.CompaniesToReview.Count is 0 ? "no companies" : string.Join(" and ", userInfo.CompaniesToReview))
        + ".";

    await stepContext.Context.SendActivityAsync(status);

    var accessor = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
    await accessor.SetAsync(stepContext.Context, userInfo, cancellationToken);

    return await stepContext.EndDialogAsync(null, cancellationToken);
}

Dialogs\TopLevelDialog.cs

The initial, top-level dialog has four steps:

  1. Ask for the user's name.
  2. Ask for the user's age.
  3. Branch based on the user's age.
  4. Finally, thank the user for participating and return the collected information.

In the first step we're clearing the user's profile, so that the dialog will start with an empty profile each time. Since the last step will return information when it ends, the AcknowledgementStepAsync concludes with saving it to the user state, then returning that info to the main dialog for use in the final step.

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Create an object in which to collect the user's information within the dialog.
    stepContext.Values[UserInfo] = new UserProfile();

    var promptOptions = new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") };

    // Ask the user to enter their name.
    return await stepContext.PromptAsync(nameof(TextPrompt), promptOptions, cancellationToken);
}

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Set the user's name to what they entered in response to the name prompt.
    var userProfile = (UserProfile)stepContext.Values[UserInfo];
    userProfile.Name = (string)stepContext.Result;

    var promptOptions = new PromptOptions { Prompt = MessageFactory.Text("Please enter your age.") };

    // Ask the user to enter their age.
    return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
}

private async Task<DialogTurnResult> StartSelectionStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Set the user's age to what they entered in response to the age prompt.
    var userProfile = (UserProfile)stepContext.Values[UserInfo];
    userProfile.Age = (int)stepContext.Result;

    if (userProfile.Age < 25)
    {
        // If they are too young, skip the review selection dialog, and pass an empty list to the next step.
        await stepContext.Context.SendActivityAsync(
            MessageFactory.Text("You must be 25 or older to participate."),
            cancellationToken);
        return await stepContext.NextAsync(new List<string>(), cancellationToken);
    }
    else
    {
        // Otherwise, start the review selection dialog.
        return await stepContext.BeginDialogAsync(nameof(ReviewSelectionDialog), null, cancellationToken);
    }
}

private async Task<DialogTurnResult> AcknowledgementStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // Set the user's company selection to what they entered in the review-selection dialog.
    var userProfile = (UserProfile)stepContext.Values[UserInfo];
    userProfile.CompaniesToReview = stepContext.Result as List<string> ?? new List<string>();

    // Thank them for participating.
    await stepContext.Context.SendActivityAsync(
        MessageFactory.Text($"Thanks for participating, {((UserProfile)stepContext.Values[UserInfo]).Name}."),
        cancellationToken);

    // Exit the dialog, returning the collected user information.
    return await stepContext.EndDialogAsync(stepContext.Values[UserInfo], cancellationToken);
}

Dialogs\ReviewSelectionDialog.cs

The review-selection dialog is started from the top-level dialog's StartSelectionStepAsync, and has two steps:

  1. Ask the user to choose a company to review or choose done to finish.
  2. Repeat this dialog or exit, as appropriate.

In this design, the top-level dialog will always precede the review-selection dialog on the stack, and the review-selection dialog can be thought of as a child of the top-level dialog.

private async Task<DialogTurnResult> SelectionStepAsync(
    WaterfallStepContext stepContext,
    CancellationToken cancellationToken)
{
    // Continue using the same selection list, if any, from the previous iteration of this dialog.
    var list = stepContext.Options as List<string> ?? new List<string>();
    stepContext.Values[CompaniesSelected] = list;

    // Create a prompt message.
    string message;
    if (list.Count is 0)
    {
        message = $"Please choose a company to review, or `{DoneOption}` to finish.";
    }
    else
    {
        message = $"You have selected **{list[0]}**. You can review an additional company, " +
            $"or choose `{DoneOption}` to finish.";
    }

    // Create the list of options to choose from.
    var options = _companyOptions.ToList();
    options.Add(DoneOption);
    if (list.Count > 0)
    {
        options.Remove(list[0]);
    }

    var promptOptions = new PromptOptions
    {
        Prompt = MessageFactory.Text(message),
        RetryPrompt = MessageFactory.Text("Please choose an option from the list."),
        Choices = ChoiceFactory.ToChoices(options),
    };

    // Prompt the user for a choice.
    return await stepContext.PromptAsync(nameof(ChoicePrompt), promptOptions, cancellationToken);
}

private async Task<DialogTurnResult> LoopStepAsync(
    WaterfallStepContext stepContext,
    CancellationToken cancellationToken)
{
    // Retrieve their selection list, the choice they made, and whether they chose to finish.
    var list = stepContext.Values[CompaniesSelected] as List<string>;
    var choice = (FoundChoice)stepContext.Result;
    var done = choice.Value == DoneOption;

    if (!done)
    {
        // If they chose a company, add it to the list.
        list.Add(choice.Value);
    }

    if (done || list.Count >= 2)
    {
        // If they're done, exit and return their list.
        return await stepContext.EndDialogAsync(list, cancellationToken);
    }
    else
    {
        // Otherwise, repeat this dialog, passing in the list from this iteration.
        return await stepContext.ReplaceDialogAsync(nameof(ReviewSelectionDialog), list, cancellationToken);
    }
}

Implement the code to manage the dialog

The bot's turn handler repeats the one conversation flow defined by these dialogs. When we receive a message from the user:

  1. Continue the active dialog, if there is one.
    • If no dialog was active, we clear the user profile and start the top-level dialog.
    • If the active dialog completed, we collect and save the returned information and display a status message.
    • Otherwise, the active dialog is still mid-process, and we don't need to do anything else at the moment.
  2. Save the conversation state, so that any updates to the dialog state are persisted.

Bots\DialogBot.cs

The message handler calls the RunAsync method to manage the dialog, and we've overridden the turn handler to save any changes to the conversation and user state that may have happened during the turn. The base OnTurnAsync will call the OnMessageActivityAsync method, ensuring the save calls happen at the end of that turn.

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.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

Bots\DialogAndWelcome.cs

DialogAndWelcomeBot extends DialogBot above to provide a welcome message when the user joins the conversation, and is what is created in Startup.cs.

protected override async Task OnMembersAddedAsync(
    IList<ChannelAccount> membersAdded,
    ITurnContext<IConversationUpdateActivity> turnContext,
    CancellationToken cancellationToken)
{
    foreach (var member in membersAdded)
    {
        // Greet anyone that was not the target (recipient) of this message.
        // To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details.
        if (member.Id != turnContext.Activity.Recipient.Id)
        {
            var reply = MessageFactory.Text($"Welcome to Complex Dialog Bot {member.Name}. " +
                "This bot provides a complex conversation, with multiple dialogs. " +
                "Type anything to get started.");
            await turnContext.SendActivityAsync(reply, cancellationToken);
        }
    }
}

Branch and loop

Dialogs\TopLevelDialog.cs

Here is sample branch logic from a step in the top level dialog:

if (userProfile.Age < 25)
{
    // If they are too young, skip the review selection dialog, and pass an empty list to the next step.
    await stepContext.Context.SendActivityAsync(
        MessageFactory.Text("You must be 25 or older to participate."),
        cancellationToken);
    return await stepContext.NextAsync(new List<string>(), cancellationToken);
}
else
{
    // Otherwise, start the review selection dialog.
    return await stepContext.BeginDialogAsync(nameof(ReviewSelectionDialog), null, cancellationToken);
}

Dialogs\ReviewSelectionDialog.cs

Here is sample looping logic from a step in the review selection dialog:

if (done || list.Count >= 2)
{
    // If they're done, exit and return their list.
    return await stepContext.EndDialogAsync(list, cancellationToken);
}
else
{
    // Otherwise, repeat this dialog, passing in the list from this iteration.
    return await stepContext.ReplaceDialogAsync(nameof(ReviewSelectionDialog), list, cancellationToken);
}

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.

test complex dialog sample

Additional resources

For an introduction on how to implement a dialog, see implement sequential conversation flow, which uses a single waterfall dialog and a few prompts to create a simple interaction that asks the user a series of questions.

The Dialogs library includes basic validation for prompts. You can also add custom validation. For more information, see gather user input using a dialog prompt.

To simplify your dialog code and reuse it multiple bots, you can define portions of a dialog set as a separate class. For more information, see reuse dialogs.

Next steps