Handling flow from Prompt Validators

Edit – warning this is about pre-release v4. Whilst it’s similar to the final release it’s not the same

Creating a basic flow with Botframework V4 is pretty straightforward. You define your steps and any other dialogs or prompts that you may wish to use. For example, consider this basic name capturing definition;

Add(Inputs.Text, new Microsoft.Bot.Builder.Dialogs.TextPrompt(TextValidator));
Add(Name, new WaterfallStep[]
{
    // Each step takes in a dialog context, arguments, and the next delegate.
    async (dc, args, next) =>
    {
        // Prompt for the user's first name.
        await dc.Prompt(Inputs.Text, "Hey, what's your first name?");
    },
    async (dc, args, next) =>
    {
        dc.ActiveDialog.State["1stName"] = args["Text"].ToString();

        // Prompt for the user's second name.
        await dc.Prompt(Inputs.Text, "what's your second name?");
    },
...
private Task TextValidator(ITurnContext context, TextResult toValidate)
{
    if (toValidate.Text == ".")
    {
        toValidate.Status = null;

    }

    return Task.CompletedTask;
}

The above code will reject the second name if the user types in a full-stop. But what will happen next needs to be considered. The current code will correctly reject the full-stop but it will restart the dialog and ask the user for their first name again. There are a couple of ways to handle this, and they can be implemented in unison.

Use a Retry Message

When a prompt has a Retry-Message associated with it then any failure from the Validator will keep the user on the same waterfall step.

...
 async (dc, args, next) =>
    {
        dc.ActiveDialog.State["1stName"] = args["Text"].ToString();
        // Prompt for the user's second name.
        await dc.Prompt(Inputs.Text, "what's your second name?",
            new PromptOptions
            {
                RetryPromptString = "what's your really your second name?"
            });
    },
...

The above code is probably all your need, but you may also want to equip your waterfall steps to be more…re-entrant.

Replay dialog

This is particularly useful if you provide a way to edit the user’s choices. The idea is that each step examines its arguments or state and if already fulfilled simply passes the flow onto the next step. In the final step you decide if everything has completed or if the flow should start again.
Example step;

async (dc, args, next) =>
    {
        if (args.ContainsKey("1stStep")
        {
          await next(args);
        }
        else
        {
          // Prompt for the user's first name.
          await dc.Prompt(Inputs.Text, "Hey, what's your first name?");
        }
    },
...

Final step;

async (dc, args, next) =>
    {
        if (AllConditionsMet(args))
        {
          // finish the dialog
          await dc.End(dc.ActiveDialog.State);
        }
        else
        {
          // replay the flow
          await dc.Replace("yourdialogName", dc.ActiveDialog.State);
        }
    },

Combining Retry and Replay

There is nothing stopping you using a combination of the methods. If you want to enable an editing scenario but want the framework to ensure a criteria is met for specific steps then just implement both.

ChoicePrompt, how to always call the validator in Botframework v4

BotFramework v4 has a number of helper prompts, TextPrompt, ChoicePrompt, etc. One common mechanism they share is a Validator delegate. When the user enters a value the Validator is invoked and you have an opportunity to check/change the outcome

var obj = new ValidatingTextPrompt(async (context, toValidate) =>
    {
        if (toValidate.Text.Length < minimumLength)
        {
            toValidate.Status = null;
            await context.SendActivity(minimumLengthMessage);
        }
    }
    );

The presence of the RetryPromptString means the ChoicePrompt will automatically retry of the user enters the incorrect value, such as 'frog'. However, what happens if the user enters the value '3'? Unfortunately this is considered as the 3rd choice and 'quit' will be selected. If your UI is really serving up numbers like this, that could be a real problem. Imagine if the list was 2,4,6 and you entered '3' or even worse '2'!? So I really want to add a Validator delegate that all prompts support;

this.Dialogs.Add("choicePrompt", new ChoicePrompt(Culture.English, ValidateChoice));

private async Task ValidateChoice(ITurnContext context, ChoiceResult toValidate)
{
    var userMessage = context.Activity.Text;
    if (userMessage == "3")
    {
        toValidate.Status = null;
        await Task.Delay(1);
    }
}

Sorted right? Wrong. Unfortunately there are two problems with this solutions; a) this is only called when a value from the choices list is selected (really??) b) the resulting selected value is passed in and not the original, i.e. ‘quit’ is passed in rather than ‘3’. My solution is to derive a new ChoicePrompt that will always call the available Validator with the original values;

public class ChoicePromptAlwaysVerify : Microsoft.Bot.Builder.Dialogs.ChoicePrompt
{
    private readonly PromptValidatorEx.PromptValidator validator;

    public ChoicePromptAlwaysVerify(string culture, PromptValidatorEx.PromptValidator validator = null) : base(culture, validator)
    {
        this.validator = validator;
    }

    protected override async Task OnRecognize(DialogContext dc, PromptOptions options)
    {
        var recognize = await base.OnRecognize(dc, options);
        if (this.validator != null)
        {
            await this.validator.Invoke(dc.Context, recognize);
        }

        return recognize;
    }
}

The code works by forcing the recognize override to call the validator. The downside is that this code will be called twice when the user makes a good choice (sigh), but it’s a small sacrifice to regain some consistent control over the valid values. It also allows for more specialized messages as the RetryMessage is fixed and has no chance to give a contextual response.