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

时间:2019-06-27 11:54:20

标签: c# botframework

我开始在Microsoft的Bot Framework V4中构建一个对话框,为此,我想使用提示的自定义验证。两个月前,当版本4.4发行时,新属性“ AttemptCount”已添加到PromptValidatorContext中。此属性提供有关用户给出答案次数的信息。显然,如果多次提示用户,最好结束当前对话框。但是,我没有找到摆脱这种状态的方法,因为给定的PromptValidatorContext与DialogContext(或WaterfallStepContext)不同,它没有提供替换对话框的方法。我在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;

但这并不真的很正确;-) 有人有主意吗?

干杯, 安德烈亚斯(Andreas)

3 个答案:

答案 0 :(得分:1)

根据验证器功能的要执行的操作以及将管理对话框堆栈的代码放置在何处,您可以有几种选择。

选项1:返回false

像我在评论中提到的那样,您第一个从对话框中弹出对话框的机会是在验证器函数本身中。

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

您应该对此有所担心,因为如果您未正确执行此操作,实际上可能会导致问题。 SDK并不希望您在验证器函数中操作对话框堆栈,因此您需要知道当验证器函数返回并采取相应措施时会发生什么。

选项1.1:发送活动

您可以在source code中看到提示将尝试重新提示,而无需检查提示是否仍在对话框堆栈中:

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

这意味着,即使您清除了验证器函数中的对话框堆栈,返回false后,提示符仍将尝试重新提示。我们不希望这种情况发生,因为对话框已被取消,并且如果漫游器问一个问题,即它不会接受答案,那么它将看起来很糟糕并使用户感到困惑。但是,此源代码确实提供了有关如何避免再次提示的提示。仅当TurnContext.Respondedfalse时才会提示。您可以通过发送活动将其设置为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在哪里重新提示:

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的消息处理程序中的事情,例如在其答案的删除部分中提到的CameronL。虽然通常认为使用异常来触发有意的代码路径是一种不好的做法,但这确实类似于您提到要复制的Bot Builder v3中的重试限制如何工作。

选项3.1:使用基本的Exception类型

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

if (promptContext.AttemptCount > 3)
{
    throw new Exception("Too many attempts") { Source = ID };
}

然后您可以像这样捕获它:

try
{
    await dc.ContinueDialogAsync(cancellationToken);
}
catch (Exception ex)
{
    if (ex.Source == TestDialog.ID)
    {
        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);
}

答案 1 :(得分:0)

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

**删除了错误的答案**

答案 2 :(得分:0)

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

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

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