Botbuilder-dotnet: Confirming during WaterfallStep flow

Created on 7 Aug 2018  路  11Comments  路  Source: microsoft/botbuilder-dotnet

Hi.

I have a bot with a WaterfallStep dialog. This drops through the steps, prompting the user for various answers.

Is there an easy way for the dialog to confirm that the previous value entered is correct before moving on to the next step in the waterfall? Or perhaps move back to the previous step if the user doesn't confirm that their entered value was correct?

Thanks

Gary

Most helpful comment

Here's the raw dialog set we'll have in the topic (current live, as opposed to daily build)...

    using Microsoft.Bot.Builder.Core.Extensions;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Prompts.Choices;
    using Microsoft.Bot.Schema;
    using Microsoft.Recognizers.Text;
    using System;
    using System.Collections.Generic;
    using System.Linq;

    /// <summary>Contains the set of dialogs and prompts for the hotel bot.</summary>
    public class HotelDialog : DialogSet
    {
        /// <summary>The ID of the top-level dialog.</summary>
        public const string MainMenu = "mainMenu";

        /// <summary>Contains the IDs for the other dialogs in the set.</summary>
        private static class Dialogs
        {
            public const string OrderDinner = "orderDinner";
            public const string OrderPrompt = "orderPrompt";
            public const string ReserveTable = "reserveTable";
        }

        /// <summary>Contains the IDs for the prompts used by the dialogs.</summary>
        private static class Inputs
        {
            public const string Choice = "choicePrompt";
            public const string Number = "numberPrompt";
        }

        /// <summary>Contains the keys used to manage dialog state</summary>
        private static class Outputs
        {
            public const string OrderCart = "orderCart";
            public const string OrderTotal = "orderTotal";
            public const string RoomNumber = "roomNumber";
        }

        /// <summary>Describes an option for the top-level dialog.</summary>
        private class WelcomeChoice
        {
            /// <summary>The text to show the guest for this option.</summary>
            public string Description { get; set; }

            /// <summary>The ID of the associated dialog for this option.</summary>
            public string DialogName { get; set; }
        }

        /// <summary>Describes an option for the food-selection dialog.</summary>
        /// <remarks>We have two types of options. One represents meal items that the guest
        /// can add to their order. The other represents a request to process or cancel the
        /// order.</remarks>
        private class MenuChoice
        {
            /// <summary>The request text for cancelling the meal order.</summary>
            public const string Cancel = "Cancel order";

            /// <summary>The request text for processing the meal order.</summary>
            public const string Process = "Process order";

            /// <summary>The name of the meal item or the request.</summary>
            public string Name { get; set; }

            /// <summary>The price of the meal item; or NaN for a request.</summary>
            public double Price { get; set; }

            /// <summary>The text to show the guest for this option.</summary>
            public string Description => (double.IsNaN(Price)) ? Name : $"{Name} - ${Price:0.00}";
        }

        /// <summary>Contains the lists used to present options to the guest.</summary>
        private static class Lists
        {
            /// <summary>The options for the top-level dialog.</summary>
            public static List<WelcomeChoice> WelcomeOptions { get; } = new List<WelcomeChoice>
            {
                new WelcomeChoice { Description = "Order dinner", DialogName = Dialogs.OrderDinner },
                new WelcomeChoice { Description = "Reserve a table", DialogName = Dialogs.ReserveTable },
            };

            private static List<string> WelcomeList { get; } = WelcomeOptions.Select(x => x.Description).ToList();

            /// <summary>The choices to present in the choice prompt for the top-level dialog.</summary>
            public static List<Choice> WelcomeChoices { get; } = ChoiceFactory.ToChoices(WelcomeList);

            /// <summary>The reprompt action for the top-level dialog.</summary>
            public static Activity WelcomeReprompt
            {
                get
                {
                    var reprompt = MessageFactory.SuggestedActions(WelcomeList, "Please choose an option");
                    reprompt.AttachmentLayout = AttachmentLayoutTypes.List;
                    return reprompt as Activity;
                }
            }

            /// <summary>The options for the food-selection dialog.</summary>
            public static List<MenuChoice> MenuOptions { get; } = new List<MenuChoice>
            {
                new MenuChoice { Name = "Potato Salad", Price = 5.99 },
                new MenuChoice { Name = "Tuna Sandwich", Price = 6.89 },
                new MenuChoice { Name = "Clam Chowder", Price = 4.50 },
                new MenuChoice { Name = MenuChoice.Process, Price = double.NaN },
                new MenuChoice { Name = MenuChoice.Cancel, Price = double.NaN },
            };

            private static List<string> MenuList { get; } = MenuOptions.Select(x => x.Description).ToList();

            /// <summary>The choices to present in the choice prompt for the food-selection dialog.</summary>
            public static List<Choice> MenuChoices { get; } = ChoiceFactory.ToChoices(MenuList);

            /// <summary>The reprompt action for the food-selection dialog.</summary>
            public static Activity MenuReprompt
            {
                get
                {
                    var reprompt = MessageFactory.SuggestedActions(MenuList, "Please choose an option");
                    reprompt.AttachmentLayout = AttachmentLayoutTypes.List;
                    return reprompt as Activity;
                }
            }
        }

        /// <summary>Contains the guest's dinner order.</summary>
        private class OrderCart : List<MenuChoice> { }

        /// <summary>Creates a new instance of the hotel dialog set.</summary>
        public HotelDialog()
        {
            // Add the prompts.
            this.Add(Inputs.Choice, new ChoicePrompt(Culture.English));
            this.Add(Inputs.Number, new NumberPrompt<int>(Culture.English));

            // Add the main welcome dialog.
            this.Add(MainMenu, new WaterfallStep[]
                {
                    async (dc, args, next) =>
                    {
                        //// Initialize the dialog state.
                        //if (dc.ActiveDialog.State != null)
                        //{
                        //    dc.ActiveDialog.State.Clear();
                        //}
                        //else
                        //{
                        //    dc.ActiveDialog.State = new Dictionary<string, object>();
                        //}

                        // Greet the guest and ask them to choose an option.
                        await dc.Context.SendActivity("Welcome to Contoso Hotel and Resort.");
                        await dc.Prompt(Inputs.Choice, "How may we serve you today?", new ChoicePromptOptions
                        {
                            Choices = Lists.WelcomeChoices,
                            RetryPromptActivity = Lists.WelcomeReprompt,
                        });
                    },
                    async (dc, args, next) =>
                    {
                        // Begin a child dialog associated with the chosen option.
                        var choice = (FoundChoice)args["Value"];
                        var dialogId = Lists.WelcomeOptions[choice.Index].DialogName;

                       // await dc.Begin(dialogId, dc.ActiveDialog.State);
                        await dc.Begin(dialogId);
                    },
                    async (dc, args, next) =>
                    {
                        // Start this dialog over again.
                        await dc.Replace(MainMenu);
                    },
                });

            // Add the order-dinner dialog.
            this.Add(Dialogs.OrderDinner, new WaterfallStep[]
                {
                    async (dc, args, next) =>
                    {
                        await dc.Context.SendActivity("Welcome to our Dinner order service.");

                        // Start the food selection dialog.
                        await dc.Begin(Dialogs.OrderPrompt);
                    },
                    async (dc, args, next) =>
                    {
                        if (args.TryGetValue(Outputs.OrderCart, out object arg) && arg is OrderCart cart)
                        {
                            // If there are items in the order, record the order and ask for a room number.
                            dc.ActiveDialog.State[Outputs.OrderCart] = cart;
                            await dc.Prompt(Inputs.Number, "What is your room number?", new PromptOptions
                            {
                                RetryPromptString = "Please enter your room number."
                            });
                        }
                        else
                        {
                            // Otherwise, assume the order was cancelled by the guest and exit.
                            await dc.End();
                        }
                    },
                    async (dc, args, next) =>
                    {
                        // Get and save the guest's answer.
                        var roomNumber = args["Text"] as string;
                        dc.ActiveDialog.State[Outputs.RoomNumber] = roomNumber;

                        // Process the dinner order using the collected order cart and room number.

                        await dc.Context.SendActivity($"Thank you. Your order will be delivered to room {roomNumber} within 45 minutes.");
                        await dc.End();
                    },
                });

            // Add the food-selection dialog.
            this.Add(Dialogs.OrderPrompt, new WaterfallStep[]
                {
                    async (dc, args, next) =>
                    {
                        if (args is null || !args.ContainsKey(Outputs.OrderCart))
                        {
                            // First time through, initialize the order state.
                            dc.ActiveDialog.State[Outputs.OrderCart] = new OrderCart();
                            dc.ActiveDialog.State[Outputs.OrderTotal] = 0.0;
                        }
                        else
                        {
                            // Otherwise, set the order state to that of the arguments.
                            dc.ActiveDialog.State = new Dictionary<string, object>(args);
                        }

                        await dc.Prompt(Inputs.Choice, "What would you like?", new ChoicePromptOptions
                        {
                            Choices = Lists.MenuChoices,
                            RetryPromptActivity = Lists.MenuReprompt,
                        });
                    },
                    async (dc, args, next) =>
                    {
                        // Get the guest's choice.
                        var choice = (FoundChoice)args["Value"];
                        var option = Lists.MenuOptions[choice.Index];

                        // Get the current order from dialog state.
                        var cart = (OrderCart)dc.ActiveDialog.State[Outputs.OrderCart];

                        if (option.Name is MenuChoice.Process)
                        {
                            if (cart.Count > 0)
                            {
                                // If there are any items in the order, then exit this dialog,
                                // and return the list of selected food items.
                                await dc.End(new Dictionary<string, object>
                                {
                                    [Outputs.OrderCart] = cart
                                });
                            }
                            else
                            {
                                // Otherwise, send an error message and restart from
                                // the beginning of this dialog.
                                await dc.Context.SendActivity(
                                    "Your cart is empty. Please add at least one item to the cart.");
                                await dc.Replace(Dialogs.OrderPrompt);
                            }
                        }
                        else if (option.Name is MenuChoice.Cancel)
                        {
                            await dc.Context.SendActivity("Your order has been cancelled.");

                            // Exit this dialog, returning an empty property bag.
                            dc.ActiveDialog.State.Clear();
                            await dc.End(new Dictionary<string, object>());
                        }
                        else
                        {
                            // Add the selected food item to the order and update the order total.
                            cart.Add(option);
                            var total = (double)dc.ActiveDialog.State[Outputs.OrderTotal] + option.Price;
                            dc.ActiveDialog.State[Outputs.OrderTotal] = total;

                            await dc.Context.SendActivity($"Added {option.Name} (${option.Price:0.00}) to your order." +
                                Environment.NewLine + Environment.NewLine +
                                $"Your current total is ${total:0.00}.");

                            // Present the order options again, passing in the current order state.
                            await dc.Replace(Dialogs.OrderPrompt, dc.ActiveDialog.State);
                        }
                    },
                });

            // Add the table-reservation dialog.
            this.Add(Dialogs.ReserveTable, new WaterfallStep[]
                {
                    // Replace this waterfall with your reservation steps.
                    async (dc, args, next) =>
                    {
                        await dc.Context.SendActivity("Your table has been reserved.");
                        await dc.End();
                    }
                });
        }
    }

All 11 comments

@garypretty , This is covered a bit in the using prompts topic, see the Validate a prompt response section. Short form, if you include a validator, the step will repeat until the input validates. You would also want to Specify prompt options and provide a retry prompt.

Thanks. I thought about using a validator, but I'm not clear that during this step I can present the user with a confirmation prompt. E.g. Is this the right email XXXX? Yes / No and then fail the validation if they answer no. I guess the question is can I have a multi step validation process?

Oh, yes, that is a less straight-forward problem. We are in the process of writing a topic that will help. I'll post a link when it is live (today or tomorrow). You could break up your waterfall dialog, and then loop though a specific section as necessary (using a combination of the dialog context's begin, end, and replace methods).

That sounds exactly like what I need. Please do share once you have it. Thanks 馃憤

Also if you have a small code snippet you can share for this that I can figure out for myself that would help as I could do with solving this tonight if possible. Thanks for your help.

Here's the raw dialog set we'll have in the topic (current live, as opposed to daily build)...

    using Microsoft.Bot.Builder.Core.Extensions;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Prompts.Choices;
    using Microsoft.Bot.Schema;
    using Microsoft.Recognizers.Text;
    using System;
    using System.Collections.Generic;
    using System.Linq;

    /// <summary>Contains the set of dialogs and prompts for the hotel bot.</summary>
    public class HotelDialog : DialogSet
    {
        /// <summary>The ID of the top-level dialog.</summary>
        public const string MainMenu = "mainMenu";

        /// <summary>Contains the IDs for the other dialogs in the set.</summary>
        private static class Dialogs
        {
            public const string OrderDinner = "orderDinner";
            public const string OrderPrompt = "orderPrompt";
            public const string ReserveTable = "reserveTable";
        }

        /// <summary>Contains the IDs for the prompts used by the dialogs.</summary>
        private static class Inputs
        {
            public const string Choice = "choicePrompt";
            public const string Number = "numberPrompt";
        }

        /// <summary>Contains the keys used to manage dialog state</summary>
        private static class Outputs
        {
            public const string OrderCart = "orderCart";
            public const string OrderTotal = "orderTotal";
            public const string RoomNumber = "roomNumber";
        }

        /// <summary>Describes an option for the top-level dialog.</summary>
        private class WelcomeChoice
        {
            /// <summary>The text to show the guest for this option.</summary>
            public string Description { get; set; }

            /// <summary>The ID of the associated dialog for this option.</summary>
            public string DialogName { get; set; }
        }

        /// <summary>Describes an option for the food-selection dialog.</summary>
        /// <remarks>We have two types of options. One represents meal items that the guest
        /// can add to their order. The other represents a request to process or cancel the
        /// order.</remarks>
        private class MenuChoice
        {
            /// <summary>The request text for cancelling the meal order.</summary>
            public const string Cancel = "Cancel order";

            /// <summary>The request text for processing the meal order.</summary>
            public const string Process = "Process order";

            /// <summary>The name of the meal item or the request.</summary>
            public string Name { get; set; }

            /// <summary>The price of the meal item; or NaN for a request.</summary>
            public double Price { get; set; }

            /// <summary>The text to show the guest for this option.</summary>
            public string Description => (double.IsNaN(Price)) ? Name : $"{Name} - ${Price:0.00}";
        }

        /// <summary>Contains the lists used to present options to the guest.</summary>
        private static class Lists
        {
            /// <summary>The options for the top-level dialog.</summary>
            public static List<WelcomeChoice> WelcomeOptions { get; } = new List<WelcomeChoice>
            {
                new WelcomeChoice { Description = "Order dinner", DialogName = Dialogs.OrderDinner },
                new WelcomeChoice { Description = "Reserve a table", DialogName = Dialogs.ReserveTable },
            };

            private static List<string> WelcomeList { get; } = WelcomeOptions.Select(x => x.Description).ToList();

            /// <summary>The choices to present in the choice prompt for the top-level dialog.</summary>
            public static List<Choice> WelcomeChoices { get; } = ChoiceFactory.ToChoices(WelcomeList);

            /// <summary>The reprompt action for the top-level dialog.</summary>
            public static Activity WelcomeReprompt
            {
                get
                {
                    var reprompt = MessageFactory.SuggestedActions(WelcomeList, "Please choose an option");
                    reprompt.AttachmentLayout = AttachmentLayoutTypes.List;
                    return reprompt as Activity;
                }
            }

            /// <summary>The options for the food-selection dialog.</summary>
            public static List<MenuChoice> MenuOptions { get; } = new List<MenuChoice>
            {
                new MenuChoice { Name = "Potato Salad", Price = 5.99 },
                new MenuChoice { Name = "Tuna Sandwich", Price = 6.89 },
                new MenuChoice { Name = "Clam Chowder", Price = 4.50 },
                new MenuChoice { Name = MenuChoice.Process, Price = double.NaN },
                new MenuChoice { Name = MenuChoice.Cancel, Price = double.NaN },
            };

            private static List<string> MenuList { get; } = MenuOptions.Select(x => x.Description).ToList();

            /// <summary>The choices to present in the choice prompt for the food-selection dialog.</summary>
            public static List<Choice> MenuChoices { get; } = ChoiceFactory.ToChoices(MenuList);

            /// <summary>The reprompt action for the food-selection dialog.</summary>
            public static Activity MenuReprompt
            {
                get
                {
                    var reprompt = MessageFactory.SuggestedActions(MenuList, "Please choose an option");
                    reprompt.AttachmentLayout = AttachmentLayoutTypes.List;
                    return reprompt as Activity;
                }
            }
        }

        /// <summary>Contains the guest's dinner order.</summary>
        private class OrderCart : List<MenuChoice> { }

        /// <summary>Creates a new instance of the hotel dialog set.</summary>
        public HotelDialog()
        {
            // Add the prompts.
            this.Add(Inputs.Choice, new ChoicePrompt(Culture.English));
            this.Add(Inputs.Number, new NumberPrompt<int>(Culture.English));

            // Add the main welcome dialog.
            this.Add(MainMenu, new WaterfallStep[]
                {
                    async (dc, args, next) =>
                    {
                        //// Initialize the dialog state.
                        //if (dc.ActiveDialog.State != null)
                        //{
                        //    dc.ActiveDialog.State.Clear();
                        //}
                        //else
                        //{
                        //    dc.ActiveDialog.State = new Dictionary<string, object>();
                        //}

                        // Greet the guest and ask them to choose an option.
                        await dc.Context.SendActivity("Welcome to Contoso Hotel and Resort.");
                        await dc.Prompt(Inputs.Choice, "How may we serve you today?", new ChoicePromptOptions
                        {
                            Choices = Lists.WelcomeChoices,
                            RetryPromptActivity = Lists.WelcomeReprompt,
                        });
                    },
                    async (dc, args, next) =>
                    {
                        // Begin a child dialog associated with the chosen option.
                        var choice = (FoundChoice)args["Value"];
                        var dialogId = Lists.WelcomeOptions[choice.Index].DialogName;

                       // await dc.Begin(dialogId, dc.ActiveDialog.State);
                        await dc.Begin(dialogId);
                    },
                    async (dc, args, next) =>
                    {
                        // Start this dialog over again.
                        await dc.Replace(MainMenu);
                    },
                });

            // Add the order-dinner dialog.
            this.Add(Dialogs.OrderDinner, new WaterfallStep[]
                {
                    async (dc, args, next) =>
                    {
                        await dc.Context.SendActivity("Welcome to our Dinner order service.");

                        // Start the food selection dialog.
                        await dc.Begin(Dialogs.OrderPrompt);
                    },
                    async (dc, args, next) =>
                    {
                        if (args.TryGetValue(Outputs.OrderCart, out object arg) && arg is OrderCart cart)
                        {
                            // If there are items in the order, record the order and ask for a room number.
                            dc.ActiveDialog.State[Outputs.OrderCart] = cart;
                            await dc.Prompt(Inputs.Number, "What is your room number?", new PromptOptions
                            {
                                RetryPromptString = "Please enter your room number."
                            });
                        }
                        else
                        {
                            // Otherwise, assume the order was cancelled by the guest and exit.
                            await dc.End();
                        }
                    },
                    async (dc, args, next) =>
                    {
                        // Get and save the guest's answer.
                        var roomNumber = args["Text"] as string;
                        dc.ActiveDialog.State[Outputs.RoomNumber] = roomNumber;

                        // Process the dinner order using the collected order cart and room number.

                        await dc.Context.SendActivity($"Thank you. Your order will be delivered to room {roomNumber} within 45 minutes.");
                        await dc.End();
                    },
                });

            // Add the food-selection dialog.
            this.Add(Dialogs.OrderPrompt, new WaterfallStep[]
                {
                    async (dc, args, next) =>
                    {
                        if (args is null || !args.ContainsKey(Outputs.OrderCart))
                        {
                            // First time through, initialize the order state.
                            dc.ActiveDialog.State[Outputs.OrderCart] = new OrderCart();
                            dc.ActiveDialog.State[Outputs.OrderTotal] = 0.0;
                        }
                        else
                        {
                            // Otherwise, set the order state to that of the arguments.
                            dc.ActiveDialog.State = new Dictionary<string, object>(args);
                        }

                        await dc.Prompt(Inputs.Choice, "What would you like?", new ChoicePromptOptions
                        {
                            Choices = Lists.MenuChoices,
                            RetryPromptActivity = Lists.MenuReprompt,
                        });
                    },
                    async (dc, args, next) =>
                    {
                        // Get the guest's choice.
                        var choice = (FoundChoice)args["Value"];
                        var option = Lists.MenuOptions[choice.Index];

                        // Get the current order from dialog state.
                        var cart = (OrderCart)dc.ActiveDialog.State[Outputs.OrderCart];

                        if (option.Name is MenuChoice.Process)
                        {
                            if (cart.Count > 0)
                            {
                                // If there are any items in the order, then exit this dialog,
                                // and return the list of selected food items.
                                await dc.End(new Dictionary<string, object>
                                {
                                    [Outputs.OrderCart] = cart
                                });
                            }
                            else
                            {
                                // Otherwise, send an error message and restart from
                                // the beginning of this dialog.
                                await dc.Context.SendActivity(
                                    "Your cart is empty. Please add at least one item to the cart.");
                                await dc.Replace(Dialogs.OrderPrompt);
                            }
                        }
                        else if (option.Name is MenuChoice.Cancel)
                        {
                            await dc.Context.SendActivity("Your order has been cancelled.");

                            // Exit this dialog, returning an empty property bag.
                            dc.ActiveDialog.State.Clear();
                            await dc.End(new Dictionary<string, object>());
                        }
                        else
                        {
                            // Add the selected food item to the order and update the order total.
                            cart.Add(option);
                            var total = (double)dc.ActiveDialog.State[Outputs.OrderTotal] + option.Price;
                            dc.ActiveDialog.State[Outputs.OrderTotal] = total;

                            await dc.Context.SendActivity($"Added {option.Name} (${option.Price:0.00}) to your order." +
                                Environment.NewLine + Environment.NewLine +
                                $"Your current total is ${total:0.00}.");

                            // Present the order options again, passing in the current order state.
                            await dc.Replace(Dialogs.OrderPrompt, dc.ActiveDialog.State);
                        }
                    },
                });

            // Add the table-reservation dialog.
            this.Add(Dialogs.ReserveTable, new WaterfallStep[]
                {
                    // Replace this waterfall with your reservation steps.
                    async (dc, args, next) =>
                    {
                        await dc.Context.SendActivity("Your table has been reserved.");
                        await dc.End();
                    }
                });
        }
    }

Thanks. Just seen this. Will check this out as soon as I can. Thanks very much for sharing!

Please reactivate if you need additional clarification.

@JonathanFingold Hi. Just wanted to say a massive thanks for sharing that code with me. Answered my scenario perfectly. Really appreciate the assistance.

@JonathanFingold did you ever post this as documentation. Can't find it anywhere

@xtianus79 , unfortunately, the dev team decided not to take it as a sample. However, sample 43.complex-dialog demonstrates similar functionality.

Was this page helpful?
0 / 5 - 0 ratings