Bot Framework:如何停止等待中断后重新提示的提示响应的瀑布对话框?
Bot Framework: How do I stop a waterfall dialog which is waiting for a prompt response from re-prompting after an interruption?
预期功能
我创建了一项技能,旨在让用户与真人开始对话。为了完成此操作,系统会向他们显示带有 1 个选择的提示:“连接”。当他们单击“连接”时,技能将移动到下一个瀑布步骤,该步骤执行代码以启动真人对话。我 运行 遇到的问题是,如果他们不点击“连接”而是输入其他内容,那么无论他们得到什么响应,都会跟随相同的“连接”提示。这是可能发生的正常情况。他们可能不想与真人交谈,他们可能想继续与机器人交谈。
“连接”按钮实际上只是为了执行代码以开始对话。除了将其放入瀑布对话框并让下一步成为我要执行的代码之外,我不知道还有什么其他方法可以做到这一点。
正在复制
我在这个 repo 中创建了一个类似的例子:LoopingPrompt Repo
在我的仓库中有一个名为“ComposerMultiSkillDialog”的机器人
它包含一些技能和一些意图。
运行 项目
为了 运行 这个项目你需要 Visual Studio & Bot Composer
打开解决方案文件:MultiSkillComposerBot.sln
确保Bot.Skills是启动项目。
按 F5 到 运行 项目。它应该从端口 36352 开始。
然后打开Bot Composer到文件夹:ComposerMultiSkillDialog
启动机器人然后使用“打开网络聊天”
键入“循环”以查看带有选项“处理提示”的提示
然后输入“呼叫技能1”打断
注意“呼叫技能1”完成后再次出现“处理提示”的提示。
随着用户说出更多内容,直到他们单击“处理提示”按钮,这种情况将继续发生。
目标
我们的目标是防止这种行为,并且只在第一次出现“处理提示”。如果有中断就不会再出现了。
尝试解决
目前我尝试过的是:
找到一种方法来添加“最大回合数”,这是 Bot Composer 中的一个可用选项。据我所知,这不是 stepContext.PromptAsync 中的可用选项
当第二个“Handle Prompt”出现时,我调试了代码。代码通过控制器进入 LoopingPromptDialog 的构造函数并进入 AddAdditionalDialogs 方法。我希望它会进入 PromptStepAsync,我可以在其中放置某种计数器来检测是否已经达到并停止它,但从未调用 PromptStepAsync。我不确定“处理提示”实际上是如何再次发送回聊天的。
我无法让在“处理提示”之后调用的代码成为它自己的意图。如果用户键入了触发该意图的内容,我不想立即开始与真人聊天。所以不能有一个“连接”意图来开始与真人聊天。
我试图检查一个动作是否可以以某种方式链接到英雄卡片响应,这将执行代码但未能找到类似的东西。
感谢任何帮助!谢谢你的时间。
我找到了解决这个问题的可行方法。更改的详细信息可以在 Commit
中找到
我不得不覆盖声明类型“Microsoft.BeginSkill”的默认功能。具体来说,我覆盖了“RepromptDialogAsync”方法。
我采取的步骤:
添加 BeginSkillNonRePrompting
此 class 覆盖声明类型“BeginSkill”的默认行为,即 .dialog 文件中的 $kind = Microsoft.BeginSkill。它检查技能的配置以确定是否应该允许重新提示,如果不允许则阻止它。
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Extensions.Configuration;
namespace Microsoft.BotFramework.Composer.WebApp.Dialogs
{
public class BeginSkillNonRePrompting : BeginSkill
{
private readonly string _dialogOptionsStateKey = $"{typeof(BeginSkill).FullName}.DialogOptionsData";
public BeginSkillNonRePrompting([CallerFilePath] string callerPath = "", [CallerLineNumber] int callerLine = 0)
: base(callerPath, callerLine)
{
}
public override Task RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken = default)
{
//get the skill endpoint - this contains the skill name from configuration - we need this to get the related skill:{SkillName}:disableReprompt value
var skillEndpoint = ((SkillDialogOptions)instance.State["Microsoft.Bot.Builder.Dialogs.Adaptive.Actions.BeginSkill.DialogOptionsData"]).Skill.SkillEndpoint;
//get IConfiguration so that we can use it to determine if this skill should be reprompting or not
var config = turnContext.TurnState.Get<IConfiguration>() ?? throw new NullReferenceException("Unable to locate IConfiguration in HostContext");
//the id looks like this:
//BeginSkillNonRePrompting['=settings.skill['testLoopingPrompt'].msAppId','']
//parse out the skill name
var startingSearchValue = "=settings.skill['";
var startOfSkillName = instance.Id.IndexOf(startingSearchValue) + startingSearchValue.Length;
var endingOfSkillName = instance.Id.Substring(startOfSkillName).IndexOf("']");
var skillName = instance.Id.Substring(startOfSkillName, endingOfSkillName);
//if we do not want to reprompt call EndDialogAsync instead of RepromptDialogAsync
if (Convert.ToBoolean(config[$"skill:{skillName}:disableReprompt"]))
{
//this does not actually appear to remove the dialog from the stack
//if I call "Call Skill 1" again then this line is still hit
//so it seems like the dialog hangs around but shouldn't actually show to the user
//not sure how to resolve this but it's not really an issue as far as I can tell
return EndDialogAsync(turnContext, instance, DialogReason.EndCalled, cancellationToken);
}
else
{
LoadDialogOptions(turnContext, instance);
return base.RepromptDialogAsync(turnContext, instance, cancellationToken);
}
}
private void LoadDialogOptions(ITurnContext context, DialogInstance instance)
{
var dialogOptions = (SkillDialogOptions)instance.State[_dialogOptionsStateKey];
DialogOptions.BotId = dialogOptions.BotId;
DialogOptions.SkillHostEndpoint = dialogOptions.SkillHostEndpoint;
DialogOptions.ConversationIdFactory = context.TurnState.Get<SkillConversationIdFactoryBase>() ?? throw new NullReferenceException("Unable to locate SkillConversationIdFactoryBase in HostContext");
DialogOptions.SkillClient = context.TurnState.Get<BotFrameworkClient>() ?? throw new NullReferenceException("Unable to locate BotFrameworkClient in HostContext");
DialogOptions.ConversationState = context.TurnState.Get<ConversationState>() ?? throw new NullReferenceException($"Unable to get an instance of {nameof(ConversationState)} from TurnState.");
DialogOptions.ConnectionName = dialogOptions.ConnectionName;
// Set the skill to call
DialogOptions.Skill = dialogOptions.Skill;
}
}
}
添加 AdaptiveComponentRegistrationCustom
此 class 允许将 AdaptiveBotComponentCustom 添加到 Startup.cs
中的 ComponentRegistration
using System;
using System.Linq;
using Microsoft.Bot.Builder.Dialogs.Adaptive;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Dialogs.Declarative;
using Microsoft.Bot.Builder.Dialogs.Declarative.Obsolete;
using Microsoft.BotFramework.Composer.WebApp.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.BotFramework.Composer.WebApp.Components
{
/// <summary>
/// <see cref="ComponentRegistration"/> implementation for adaptive components.
/// </summary>
[Obsolete("Use `AdaptiveBotComponent` instead.")]
public class AdaptiveComponentRegistrationCustom : DeclarativeComponentRegistrationBridge<AdaptiveBotComponentCustom>
{
}
}
添加 AdaptiveBotComponentCustom
这 class 覆盖了默认的 AdaptiveBotComponent 行为并用 DeclarativeType 替换了 DeclarativeType 中的依赖注入
using System;
using System.Linq;
using Microsoft.Bot.Builder.Dialogs.Adaptive;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Dialogs.Declarative;
using Microsoft.Bot.Builder.Dialogs.Declarative.Obsolete;
using Microsoft.BotFramework.Composer.WebApp.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.BotFramework.Composer.WebApp.Components
{
public class AdaptiveBotComponentCustom : AdaptiveBotComponent
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
base.ConfigureServices(services, configuration);
//this is the replace the DeclarativeType BeginSkill so that we can handle the reprompting situation differently
//in some cases we don't want to reprompt when there has been an interruption
//this will be configured in appSettings.json under skill:{SkillName}:disableReprompt
var beginSkillDI = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(DeclarativeType<BeginSkill>));
if (beginSkillDI != null)
{
services.Remove(beginSkillDI);
}
//adding the cust class which will deal with use the configuration setting skill:{SkillName}:disableReprompt
services.AddSingleton<DeclarativeType>(sp => new DeclarativeType<BeginSkillNonRePrompting>(BeginSkill.Kind));
}
}
}
添加appSettings.json
添加将触发功能以防止重新提示的设置
{
"skill": {
"testLoopingPrompt": {
"disableReprompt": "true"
}
}
}
修改Program.cs
添加了一行以加载 appSettings.json 文件,其中包含防止重新提示特定技能的设置。
builder.AddJsonFile("appSettings.json");
修改Startup.cs
更改配置 ComponentRegistration 的部分。这将使用新的 BeginSkillNonRePrompting class 而不是正常的 BeginSkill class.
ComponentRegistration.Add(new AdaptiveComponentRegistration());
到
ComponentRegistration.Add(new AdaptiveComponentRegistrationCustom());
剩余问题
虽然这解决了用户方面的问题,但“RepromptDialogAsync”中的“EndDialogAsync”实际上并没有从堆栈中获取对话框。它仍然继续进入“RepromptDialogAsync”方法,尽管从用户的角度来看它总是什么都不做。我现在只有简短的对话。我将很快进入更长的对话,如果/切换/多个技能/多个用户提示,因此需要注意与此更改相关的任何问题。
预期功能
我创建了一项技能,旨在让用户与真人开始对话。为了完成此操作,系统会向他们显示带有 1 个选择的提示:“连接”。当他们单击“连接”时,技能将移动到下一个瀑布步骤,该步骤执行代码以启动真人对话。我 运行 遇到的问题是,如果他们不点击“连接”而是输入其他内容,那么无论他们得到什么响应,都会跟随相同的“连接”提示。这是可能发生的正常情况。他们可能不想与真人交谈,他们可能想继续与机器人交谈。
“连接”按钮实际上只是为了执行代码以开始对话。除了将其放入瀑布对话框并让下一步成为我要执行的代码之外,我不知道还有什么其他方法可以做到这一点。
正在复制
我在这个 repo 中创建了一个类似的例子:LoopingPrompt Repo
在我的仓库中有一个名为“ComposerMultiSkillDialog”的机器人
它包含一些技能和一些意图。
运行 项目
为了 运行 这个项目你需要 Visual Studio & Bot Composer
打开解决方案文件:MultiSkillComposerBot.sln
确保Bot.Skills是启动项目。
按 F5 到 运行 项目。它应该从端口 36352 开始。
然后打开Bot Composer到文件夹:ComposerMultiSkillDialog
启动机器人然后使用“打开网络聊天”
键入“循环”以查看带有选项“处理提示”的提示
然后输入“呼叫技能1”打断
注意“呼叫技能1”完成后再次出现“处理提示”的提示。
随着用户说出更多内容,直到他们单击“处理提示”按钮,这种情况将继续发生。
目标
我们的目标是防止这种行为,并且只在第一次出现“处理提示”。如果有中断就不会再出现了。
尝试解决
目前我尝试过的是:
找到一种方法来添加“最大回合数”,这是 Bot Composer 中的一个可用选项。据我所知,这不是 stepContext.PromptAsync 中的可用选项
当第二个“Handle Prompt”出现时,我调试了代码。代码通过控制器进入 LoopingPromptDialog 的构造函数并进入 AddAdditionalDialogs 方法。我希望它会进入 PromptStepAsync,我可以在其中放置某种计数器来检测是否已经达到并停止它,但从未调用 PromptStepAsync。我不确定“处理提示”实际上是如何再次发送回聊天的。
我无法让在“处理提示”之后调用的代码成为它自己的意图。如果用户键入了触发该意图的内容,我不想立即开始与真人聊天。所以不能有一个“连接”意图来开始与真人聊天。
我试图检查一个动作是否可以以某种方式链接到英雄卡片响应,这将执行代码但未能找到类似的东西。
感谢任何帮助!谢谢你的时间。
我找到了解决这个问题的可行方法。更改的详细信息可以在 Commit
中找到我不得不覆盖声明类型“Microsoft.BeginSkill”的默认功能。具体来说,我覆盖了“RepromptDialogAsync”方法。
我采取的步骤:
添加 BeginSkillNonRePrompting
此 class 覆盖声明类型“BeginSkill”的默认行为,即 .dialog 文件中的 $kind = Microsoft.BeginSkill。它检查技能的配置以确定是否应该允许重新提示,如果不允许则阻止它。
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Extensions.Configuration;
namespace Microsoft.BotFramework.Composer.WebApp.Dialogs
{
public class BeginSkillNonRePrompting : BeginSkill
{
private readonly string _dialogOptionsStateKey = $"{typeof(BeginSkill).FullName}.DialogOptionsData";
public BeginSkillNonRePrompting([CallerFilePath] string callerPath = "", [CallerLineNumber] int callerLine = 0)
: base(callerPath, callerLine)
{
}
public override Task RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken = default)
{
//get the skill endpoint - this contains the skill name from configuration - we need this to get the related skill:{SkillName}:disableReprompt value
var skillEndpoint = ((SkillDialogOptions)instance.State["Microsoft.Bot.Builder.Dialogs.Adaptive.Actions.BeginSkill.DialogOptionsData"]).Skill.SkillEndpoint;
//get IConfiguration so that we can use it to determine if this skill should be reprompting or not
var config = turnContext.TurnState.Get<IConfiguration>() ?? throw new NullReferenceException("Unable to locate IConfiguration in HostContext");
//the id looks like this:
//BeginSkillNonRePrompting['=settings.skill['testLoopingPrompt'].msAppId','']
//parse out the skill name
var startingSearchValue = "=settings.skill['";
var startOfSkillName = instance.Id.IndexOf(startingSearchValue) + startingSearchValue.Length;
var endingOfSkillName = instance.Id.Substring(startOfSkillName).IndexOf("']");
var skillName = instance.Id.Substring(startOfSkillName, endingOfSkillName);
//if we do not want to reprompt call EndDialogAsync instead of RepromptDialogAsync
if (Convert.ToBoolean(config[$"skill:{skillName}:disableReprompt"]))
{
//this does not actually appear to remove the dialog from the stack
//if I call "Call Skill 1" again then this line is still hit
//so it seems like the dialog hangs around but shouldn't actually show to the user
//not sure how to resolve this but it's not really an issue as far as I can tell
return EndDialogAsync(turnContext, instance, DialogReason.EndCalled, cancellationToken);
}
else
{
LoadDialogOptions(turnContext, instance);
return base.RepromptDialogAsync(turnContext, instance, cancellationToken);
}
}
private void LoadDialogOptions(ITurnContext context, DialogInstance instance)
{
var dialogOptions = (SkillDialogOptions)instance.State[_dialogOptionsStateKey];
DialogOptions.BotId = dialogOptions.BotId;
DialogOptions.SkillHostEndpoint = dialogOptions.SkillHostEndpoint;
DialogOptions.ConversationIdFactory = context.TurnState.Get<SkillConversationIdFactoryBase>() ?? throw new NullReferenceException("Unable to locate SkillConversationIdFactoryBase in HostContext");
DialogOptions.SkillClient = context.TurnState.Get<BotFrameworkClient>() ?? throw new NullReferenceException("Unable to locate BotFrameworkClient in HostContext");
DialogOptions.ConversationState = context.TurnState.Get<ConversationState>() ?? throw new NullReferenceException($"Unable to get an instance of {nameof(ConversationState)} from TurnState.");
DialogOptions.ConnectionName = dialogOptions.ConnectionName;
// Set the skill to call
DialogOptions.Skill = dialogOptions.Skill;
}
}
}
添加 AdaptiveComponentRegistrationCustom
此 class 允许将 AdaptiveBotComponentCustom 添加到 Startup.cs
中的 ComponentRegistrationusing System;
using System.Linq;
using Microsoft.Bot.Builder.Dialogs.Adaptive;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Dialogs.Declarative;
using Microsoft.Bot.Builder.Dialogs.Declarative.Obsolete;
using Microsoft.BotFramework.Composer.WebApp.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.BotFramework.Composer.WebApp.Components
{
/// <summary>
/// <see cref="ComponentRegistration"/> implementation for adaptive components.
/// </summary>
[Obsolete("Use `AdaptiveBotComponent` instead.")]
public class AdaptiveComponentRegistrationCustom : DeclarativeComponentRegistrationBridge<AdaptiveBotComponentCustom>
{
}
}
添加 AdaptiveBotComponentCustom
这 class 覆盖了默认的 AdaptiveBotComponent 行为并用 DeclarativeType 替换了 DeclarativeType 中的依赖注入
using System;
using System.Linq;
using Microsoft.Bot.Builder.Dialogs.Adaptive;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Dialogs.Declarative;
using Microsoft.Bot.Builder.Dialogs.Declarative.Obsolete;
using Microsoft.BotFramework.Composer.WebApp.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.BotFramework.Composer.WebApp.Components
{
public class AdaptiveBotComponentCustom : AdaptiveBotComponent
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
base.ConfigureServices(services, configuration);
//this is the replace the DeclarativeType BeginSkill so that we can handle the reprompting situation differently
//in some cases we don't want to reprompt when there has been an interruption
//this will be configured in appSettings.json under skill:{SkillName}:disableReprompt
var beginSkillDI = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(DeclarativeType<BeginSkill>));
if (beginSkillDI != null)
{
services.Remove(beginSkillDI);
}
//adding the cust class which will deal with use the configuration setting skill:{SkillName}:disableReprompt
services.AddSingleton<DeclarativeType>(sp => new DeclarativeType<BeginSkillNonRePrompting>(BeginSkill.Kind));
}
}
}
添加appSettings.json
添加将触发功能以防止重新提示的设置
{
"skill": {
"testLoopingPrompt": {
"disableReprompt": "true"
}
}
}
修改Program.cs
添加了一行以加载 appSettings.json 文件,其中包含防止重新提示特定技能的设置。
builder.AddJsonFile("appSettings.json");
修改Startup.cs
更改配置 ComponentRegistration 的部分。这将使用新的 BeginSkillNonRePrompting class 而不是正常的 BeginSkill class.
ComponentRegistration.Add(new AdaptiveComponentRegistration());
到
ComponentRegistration.Add(new AdaptiveComponentRegistrationCustom());
剩余问题
虽然这解决了用户方面的问题,但“RepromptDialogAsync”中的“EndDialogAsync”实际上并没有从堆栈中获取对话框。它仍然继续进入“RepromptDialogAsync”方法,尽管从用户的角度来看它总是什么都不做。我现在只有简短的对话。我将很快进入更长的对话,如果/切换/多个技能/多个用户提示,因此需要注意与此更改相关的任何问题。