繁体   English   中英

在 Bot Framework V4 中保留自定义提示验证

[英]Leaving custom prompt validation in Bot Framework V4

我开始在 Microsoft 的 Bot Framework V4 中构建一个对话框,为此我想使用提示的自定义验证。 几个月前,当 4.4 版发布时,一个新属性“AttemptCount”被添加到PromptValidatorContext 中 此属性提供有关用户回答多少次的信息。 显然,如果用户被多次重新提示,最好结束当前对话。 但是,我没有找到摆脱这种状态的方法,因为与 DialogContext(或 WaterfallStepContext)不同,给定的 PromptValidatorContext 不提供替换对话框的方法。 我在github上问了这个问题,但没有得到答案。

public class MyComponentDialog : ComponentDialog
{
    readonly WaterfallDialog waterfallDialog;

    public MyComponentDialog(string dialogId) : (dialogId)
    {
        // Waterfall dialog will be started when MyComponentDialog is called.
        this.InitialDialogId = DialogId.MainDialog;

        this.waterfallDialog = new WaterfallDialog(DialogId.MainDialog, new WaterfallStep[] { this.StepOneAsync, this.StepTwoAsync});
        this.AddDialog(this.waterfallDialog);

        this.AddDialog(new TextPrompt(DialogId.TextPrompt, CustomTextValidatorAsync));
    }

    public async Task<DialogTurnResult> StepOneAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        var promptOptions = new PromptOptions
                            {
                                Prompt = MessageFactory.Text("Hello from text prompt"),
                                RetryPrompt = MessageFactory.Text("Hello from retry prompt")
                            };

        return await stepContext.PromptAsync(DialogId.TextPrompt, promptOptions, cancellationToken).ConfigureAwait(false);
    }

    public async Task<DialogTurnResult> StepTwoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // Handle validated result...
    }

    // Critical part:
    public async Task<bool> CustomTextValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
    {
        if (promptContext.AttemptCount > 3)
        {
            // How do I get out of here? :-/
        }

        if (promptContext.Context.Activity.Text.Equals("password")
        {
            // valid user input
            return true;    
        }

        // invalid user input
        return false;
    }
}

如果此功能实际上缺失,我可能可以通过将信息保存在 TurnState 中并在我的StepTwo检查它来做一个解决方法。 像这样的东西:

promptContext.Context.TurnState["validation"] = ValidationEnum.TooManyAttempts;

但这真的感觉不对;-) 有没有人有想法?

干杯,安德烈亚斯

根据您要在验证器函数中执行的操作以及要将管理对话框堆栈的代码放置在何处,您有几个选项。

选项 1:返回false

就像我在评论中提到的那样,您第一次有机会从堆栈中弹出对话框将在验证器函数本身中。

if (promptContext.AttemptCount > 3)
{
    var dc = await BotUtil.Dialogs.CreateContextAsync(promptContext.Context, cancellationToken);
    await dc.CancelAllDialogsAsync(cancellationToken);
    return false;
}

您对此感到担忧是正确的,因为如果您做得不正确,这实际上会导致问题。 SDK 不希望您在验证器函数中操作对话框堆栈,因此您需要了解验证器函数返回时会发生什么并采取相应的行动。

选项 1.1:发送活动

您可以在源代码中看到提示将尝试重新提示而不检查提示是否仍在对话框堆栈中:

 if (!dc.Context.Responded) { await OnPromptAsync(dc.Context, state, options, true, cancellationToken).ConfigureAwait(false); }

这意味着即使您清除验证器函数内的对话框堆栈,当您返回false时,提示仍会尝试重新提示。 我们不希望这种情况发生,因为对话已经被取消,如果机器人提出一个它不会接受答案的问题,那将看起来很糟糕并让用户感到困惑。 但是,此源代码确实提供了有关如何避免重新提示的提示。 如果TurnContext.Respondedfalse它只会重新TurnContext.Responded 您可以通过发送活动将其设置为true

选项 1.1.1:发送消息活动

让用户知道他们已经用完了所有的尝试是有意义的,如果你在你的验证器函数中向用户发送这样的消息,那么你就不必担心任何不需要的自动重新提示:

await promptContext.Context.SendActivityAsync("Cancelling all dialogs...");

选项 1.1.2:发送事件活动

如果您不想向用户显示实际消息,您可以发送一个不会在对话中呈现的不可见事件活动。 这仍然TurnContext.Responded设置为true

await promptContext.Context.SendActivityAsync(new Activity(ActivityTypes.Event));

选项 1.2:取消提示

我们可能不需要以避免提示呼叫其OnPromptAsync如果特定提示类型允许的方式,以避免内部时再次提示OnPromptAsync 再次查看源代码,但这次在TextPrompt.cs 中,我们可以看到OnPromptAsync在哪里进行OnPromptAsync提示:

 if (isRetry && options.RetryPrompt != null) { await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false); } else if (options.Prompt != null) { await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false); }

因此,如果我们不想向用户发送任何活动(可见或其他方式),我们可以通过将其PromptRetryPrompt属性设置为 null 来停止重新提示文本提示:

promptContext.Options.Prompt = null;
promptContext.Options.RetryPrompt = null;

选项 2:返回true

当我们从验证器函数向上移动调用堆栈时,取消对话的第二个机会是在下一个瀑布步骤中,就像您在问题中提到的那样。 这可能是您最好的选择,因为它最不老套:它不依赖于对可能会更改的内部 SDK 代码的任何特殊理解。 在这种情况下,您的整个验证器功能可能如此简单:

private Task<bool> ValidateAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
    if (promptContext.AttemptCount > 3 || IsCorrectPassword(promptContext.Context.Activity.Text))
    {
        // valid user input
        // or continue to next step anyway because of too many attempts
        return Task.FromResult(true);
    }

    // invalid user input
    // when there haven't been too many attempts
    return Task.FromResult(false);
}

请注意,我们使用称为IsCorrectPassword的方法来确定密码是否正确。 这很重要,因为此选项取决于在下一个瀑布步骤中重用该功能。 您曾提到需要在TurnState保存信息,但这是不必要的,因为我们需要知道的一切都已经在转弯上下文中了。 验证基于活动的文本,因此我们可以在下一步中再次验证相同的文本。

选项 2.1:使用WaterfallStepContext.Context.Activity.Text

用户输入的文本仍然可以在WaterfallStepContext.Context.Activity.Text因此您的下一个瀑布步骤可能如下所示:

async (stepContext, cancellationToken) =>
{
    if (IsCorrectPassword(stepContext.Context.Activity.Text))
    {
        return await stepContext.NextAsync(null, cancellationToken);
    }
    else
    {
        await stepContext.Context.SendActivityAsync("Cancelling all dialogs...");
        return await stepContext.CancelAllDialogsAsync(cancellationToken);
    }
},

选项 2.2:使用WaterfallStepContext.Result

瀑布步骤上下文有一个内置的Result属性,它引用上一步的结果。 在文本提示的情况下,它将是该提示返回的字符串。 你可以这样使用它:

if (IsCorrectPassword((string)stepContext.Result))

选项 3:抛出异常

在调用堆栈的更DialogContext.ContinueDialogAsync ,您可以通过在验证器函数中抛出异常来处理最初调用DialogContext.ContinueDialogAsync的消息处理程序中的事情,例如在其答案的删除部分中提到的 CameronL。 虽然使用异常来触发有意的代码路径通常被认为是不好的做法,但这与您提到要复制的 Bot Builder v3 中重试限制的工作方式非常相似。

选项 3.1:使用基本Exception类型

你可以只抛出一个普通的异常。 为了在捕获时更容易地将这个异常与其他异常区分开来,您可以选择在异常的Source属性中包含一些元数据:

if (promptContext.AttemptCount > 3)
{
    throw new Exception(BotUtil.TooManyAttemptsMessage);
}

然后你可以像这样抓住它:

try
{
    await dc.ContinueDialogAsync(cancellationToken);
}
catch (Exception ex)
{
    if (ex.Message == BotUtil.TooManyAttemptsMessage)
    {
        await turnContext.SendActivityAsync("Cancelling all dialogs...");
        await dc.CancelAllDialogsAsync(cancellationToken);
    }
    else
    {
        throw ex;
    }
}

选项 3.2:使用派生的异常类型

如果您定义自己的异常类型,则可以使用它来仅捕获此特定异常。

public class TooManyAttemptsException : Exception

你可以这样扔:

throw new TooManyAttemptsException();

然后你可以像这样抓住它:

try
{
    await dc.ContinueDialogAsync(cancellationToken);
}
catch (TooManyAttemptsException)
{
    await turnContext.SendActivityAsync("Cancelling all dialogs...");
    await dc.CancelAllDialogsAsync(cancellationToken);
}

在用户状态类中声明一个标志变量并更新if块中的标志:

if (promptContext.AttemptCount > 3)
{
   \\fetch user state object
    \\update flag here
    return true;
}

返回true您将进入瀑布步骤中的下一个对话框,您可以在其中检查标志值、显示适当的消息并终止对话流程。 您可以参考微软文档了解如何使用用户状态数据

提示验证器上下文对象是一个更具体的对象,只与验证器通过或失败有关。

** 删除了不正确的答案 **

您可以使用WaterfallStepPromptValidator创建一个类。 该类将 (i) 处理退出 PromptValidator 的逻辑,以及 (ii) 处理之后取消/结束/继续对话的逻辑。 此解决方案是Kyle Delaney 答案的类别,它在PromptValidator 中返回 true

我称该类为WaterfallStepValidation

private readonly Func<string, Task<bool>> _validator;
private readonly int _retryCount;
private bool _isInputValid = false;

public WaterfallStepValidation(Func<string, Task<bool>> validator, int retryCount)
{
    _validator = validator;
    _retryCount = retryCount;
}

public async Task<DialogTurnResult> CheckValidInputStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if (!_isInputValid)
    {
        await stepContext.Context.SendActivityAsync("Could not proceed...");
        // Here you could also end all dialogs or just proceed to the next step
        return await stepContext.EndDialogAsync(false);
    }
    return await stepContext.NextAsync(stepContext.Result, cancellationToken);
}

public async Task<bool> PromptValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
    _isInputValid = await _validator(promptContext.Recognized.Value);

    if (!_isInputValid && promptContext.AttemptCount >= _retryCount)
    {
        _isInputValid = false;
        return true;
    }

    return _isInputValid;
}

然后你这样称呼它:

var ageStepValidation = new WaterfallStepValidation(AgeValidator, retryCount: 3);
AddDialog(new TextPrompt("AgeTextPromptId", ageStepValidation.PromptValidatorAsync));

var waterfallSteps = new List<WaterfallStep>()
{
    PromptNameStepAsync,
    PromptAgeStepAsync,
    ageStepValidation.CheckValidInputStepAsync,
    PromptChoicesStepAsync
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));

我认为这是解决此问题的优雅解决方法。 这种方法的弱点是:

  • 您必须在验证步骤之后立即放置CheckValidInputStepAsync
  • 仍然是一种hack,因为如果验证失败, PromptValidator返回true

但是,优点是:

  • 它不会做大黑客,只是返回 true
  • 大部分逻辑都封装在WaterfallStepValidation类中

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM