在Bot Framework V4中保留自定义提示验证 [英] Leaving custom prompt validation in Bot Framework V4

查看:74
本文介绍了在Bot Framework V4中保留自定义提示验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我开始在Microsoft的Bot Framework V4中构建一个对话框,为此,我想使用提示的自定义验证.几个月前,当4.4版发布时,新属性"AttemptCount"被添加到 github 上问了这个问题,但没有得到答案./p>

I started building a dialog in Microsoft's Bot Framework V4 and for that I want to use the custom validation of prompts. A couple of month ago, when version 4.4 was released, a new property "AttemptCount" was added to the PromptValidatorContext. This property gives information on how many times a user gave an answer. Obviously, it would be nice to end the current dialog if a user was reprompted several times. However, I did not find a way to get out of this state, because the given PromptValidatorContext does not offer a way to replace the dialog, unlike a DialogContext (or WaterfallStepContext). I asked that question on github, but didn't get an answer.

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中进行检查来解决.像这样:

If this feature is actually missing, I could probably do a workaround by saving the information in the TurnState and checking it in my StepTwo. Something like this:

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

但是,这感觉并不正确;-) 有人有主意吗?

But this doesn't really feel right ;-) Does anyone has an idea?

干杯, 安德烈亚斯(Andreas)

Cheers, Andreas

推荐答案

您有一些选择,具体取决于您要在验证器函数中执行的操作以及要放置用于管理对话框堆栈的代码的位置.

You have a few options depending on what you want to do in the validator function and where you want to put the code that manages the dialog stack.

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

Your first opportunity to pop dialogs off the stack will be in the validator function itself, like I mentioned in the comments.

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

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

You were right to be apprehensive about this, because this actually can cause problems if you don't do it correctly. The SDK does not expect you to manipulate the dialog stack within a validator function, and so you need to be aware of what happens when the validator function returns and act accordingly.

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

You can see in the source code that a prompt will try to reprompt without checking to see if the prompt is still on the dialog stack:

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

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

This means that even if you clear the dialog stack inside your validator function, the prompt will still try to reprompt after that when you return false. We don't want that to happen because the dialog has already been cancelled, and if the bot asks a question that it won't be accepting answers to then that will look bad and confuse the user. However, this source code does provide a hint about how to avoid reprompting. It will only reprompt if TurnContext.Responded is false. You can set it to true by sending an activity.

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

It makes sense to let the user know that they've used up all their attempts, and if you send the user such a message in your validator function then you won't have to worry about any unwanted automatic reprompts:

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.再次查看源代码,但是这次在

Option 1.2: nullify the prompt

We may not need to avoid having the prompt call its OnPromptAsync if the specific prompt type allows a way to avoid reprompting inside OnPromptAsync. Again having a look at the source code but this time in TextPrompt.cs, we can see where OnPromptAsync does its reprompting:

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,就可以阻止文本提示重新提示:

So if we don't want to send any activities to the user (visible or otherwise), we can stop a text prompt from reprompting simply by setting both its Prompt and RetryPrompt properties to null:

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

选项2:返回true

第二个取消对话框的方法是在下一步从瀑布进入下一步,就像您在问题中提到的那样,这是我们从验证器函数上移到调用堆栈的第一步.这可能是您最好的选择,因为它的安全性最低:它不依赖于对内部SDK代码的任何特殊理解,并且可能会有所更改.在这种情况下,您整个验证程序的功能就可以像这样简单:

Option 2: return true

The second opportunity to cancel dialogs as we move up the call stack from the validator function is in the next waterfall step, like you mentioned in your question. This may be your best option because it's the least hacky: it doesn't depend on any special understanding of the internal SDK code that could be subject to change. In this case your whole validator function could be as simple as this:

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中,但这是不必要的,因为我们需要知道的一切都已经在转弯上下文中了.验证基于活动的文本,因此我们可以在下一步中再次验证相同的文本.

Note that we're using a method called IsCorrectPassword to determine if the password is correct. This is important because this option depends on reusing that functionality in the next waterfall step. You had mentioned needing to save information in TurnState but this is unnecessary since everything we need to know is already in the turn context. The validation is based on the activity's text, so we can just validate that same text again in the next step.

用户输入的文本仍将在WaterfallStepContext.Context.Activity.Text中提供给您,因此您的下一个瀑布步骤应如下所示:

The text that the user entered will still be available to you in WaterfallStepContext.Context.Activity.Text so your next waterfall step could look like this:

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属性,该属性引用上一步的结果.对于文本提示,它将是该提示返回的字符串.您可以像这样使用它:

Option 2.2: use WaterfallStepContext.Result

Waterfall step contexts have a builtin Result property that refers to the result of the previous step. In the case of a text prompt, it will be the string returned by that prompt. You can use it like this:

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

选项3:引发异常

再往上走,您可以通过在验证器函数中引发异常来处理最初调用DialogContext.ContinueDialogAsync的消息处理程序中的内容,例如在其答案的删除部分中提到的CameronL.虽然通常认为使用异常来触发有意的代码路径是一种不好的做法,但这确实类似于您提到要复制的Bot Builder v3中的重试限制如何工作.

Option 3: throw an exception

Going further up the call stack, you can handle things in the message handler that originally called DialogContext.ContinueDialogAsync by throwing an exception in your validator function, like CameronL mentioned in the deleted portion of their answer. While it's generally considered bad practice to use exceptions to trigger intentional code paths, this does closely resemble how retry limits worked in Bot Builder v3, which you mentioned wanting to replicate.

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

You can throw just an ordinary exception. To make it easier to tell this exception apart from other exceptions when you catch it, you can optionally include some metadata in the exception's Source property:

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

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

Then you can catch it like this:

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:使用派生的异常类型

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

Option 3.2: use a derived exception type

If you define your own exception type, you can use that to only catch this specific exception.

public class TooManyAttemptsException : Exception

您可以这样扔它:

throw new TooManyAttemptsException();

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

Then you can catch it like this:

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

这篇关于在Bot Framework V4中保留自定义提示验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆