在 Bot Framework 中使用自适应对话框时,可以将 class-object 机器人状态保存在代码中吗?

Can class-object bot state be saved in code while using Adaptive Dialogs in Bot Framework?

我们遇到了 Bot Framework 的问题,当第一个步骤是 CodeAction 执行 accessor.GetAsync().SetAsync() 时,后续对话访问 属性 将因错误而崩溃:

[OnTurnError] unhandled error : Object of type 'Newtonsoft.Json.Linq.JValue' cannot be converted to type 'System.String'.

完整堆栈在这里:


System.ArgumentException: Object of type 'Newtonsoft.Json.Linq.JValue' cannot be converted to type 'System.String'.
   at System.RuntimeType.TryChangeType(Object value, Binder binder, CultureInfo culture, Boolean needsSpecialCast)
   at System.RuntimeType.CheckValue(Object value, Binder binder, CultureInfo culture, BindingFlags invokeAttr)
   at System.Reflection.MethodBase.CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig)
   at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index)
   at System.Reflection.PropertyInfo.SetValue(Object obj, Object value)
   at Microsoft.Bot.Builder.Dialogs.ObjectPath.SetObjectSegment(Object obj, Object segment, Object value, Boolean json)
   at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid5[T0,T1,T2,T3,T4](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
   at Microsoft.Bot.Builder.Dialogs.ObjectPath.SetPathValue(Object obj, String path, Object value, Boolean json)
   at Microsoft.Bot.Builder.Dialogs.Memory.DialogStateManager.SetValue(String path, Object value)
   at Microsoft.Bot.Builder.Dialogs.Adaptive.Input.InputDialog.ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.DialogContext.ContinueDialogAsync(CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.Adaptive.AdaptiveDialog.ContinueActionsAsync(DialogContext dc, Object options, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.Adaptive.AdaptiveDialog.ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.DialogContext.ContinueDialogAsync(CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.DialogManager.HandleBotOnTurnAsync(DialogContext dc, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.DialogManager.OnTurnAsync(ITurnContext context, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.Dialogs.DialogManager.OnTurnAsync(ITurnContext context, CancellationToken cancellationToken)
   at MyBot.Bot.MyBotBot`1.OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken) in H:\Work\MyBot\backendadaptivebot\api\Bots\MyBotBot.cs:line 50
   at Microsoft.Bot.Builder.RegisterClassMiddleware`1.OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.RegisterClassMiddleware`1.OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.RegisterClassMiddleware`1.OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.BotFrameworkAdapter.TenantIdWorkaroundForTeamsMiddleware.OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.MiddlewareSet.ReceiveActivityWithStatusAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken)
   at Microsoft.Bot.Builder.BotAdapter.RunPipelineAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken)

RootDialog class 构造函数中我们有:

            participantAccessor = _userState.CreateProperty<Profile>("profile");

            string[] paths = { ".", "Dialogs", $"RootDialog.lg" };
            string fullPath = Path.Combine(paths);

            // These steps are executed when this Adaptive Dialog begins
            Triggers = new List<OnCondition>()
                {
                    // Add a rule to welcome user
                    new OnConversationUpdateActivity()
                    {
                        Actions = WelcomeUserSteps()
                    },

                    // Respond to user on message activity
                    new OnUnknownIntent()
                    {
                        Actions = GetUserDetails()
                    },
                };

GetUserDetails()

        private static List<Dialog> GetUserDetails() 
        {
            return new List<Dialog>()
            {
     
                new CodeAction(PopulateProfile),
                new TextInput()
                {
                    Prompt = new ActivityTemplate("${RequestPhoneNumber()}"),
                    Property = "user.profile.MobileNumber",
                }

此TextInput 接收到用户输入后,发生崩溃。 但是,如果 CodeAction 被删除,机器人不会崩溃。

在这个 PopulateProfile 方法中,我们简单地访问我们在构造函数中创建的 属性,一个基本的 Profile class(仅包含简单的属性:字符串和整数)

        private static async Task<DialogTurnResult> PopulateProfile(DialogContext dc, System.Object options)
        {
            Profile profile = await participantAccessor.GetAsync(dc.Context, () => new Profile());
            profile.Complete = 0;             
            return await dc.EndDialogAsync(options);
        }

值得注意的是,这会按预期在 'Profile' 对象类型的 DialogContext State 中创建一个条目。但是,如果我们删除此 CodeAction 并在 GetUserDetails() 中替换为以下内容:

                new SetProperty() {
                    Property = "user.profile.Complete",
                    Value = 0,
                },    
                new TextInput()
                {
                    Prompt = new ActivityTemplate("${RequestPhoneNumber()}"),
                    Property = "user.profile.MobileNumber",
                }

然后 DialogContext.State 中对象的类型似乎是 JSON 对象,并且机器人按预期运行。

这是否意味着 Bot Framework 中的自适应对话框不支持通过代码将属性写入 POCO 类型?

所以必须通过简单的直接值(例如 user.profile.completed)或通过 SetProperty 操作来完成?

(我在任何地方都看不到这个记录)

编辑:这是简单的 Profile class:

    public class Profile
    {
        public string Id { get; set; }
        public string AssociatedAsset { get; set; }
        public string FullName { get; set; }
        public string PreferredName { get; set; }
        public string MobileNumber { get; set; }
        public string Email { get; set; }
        public int Complete { get; set; } = 0;
    }

您正在尝试结合两种 largely-incompatible 做事方式。状态 属性 访问器是 Bot Builder v4 中访问状态的原始方式,但自适应对话是一个全新的系统,它们有自己的方式来访问状态。

这是出了什么问题。尽管 TextInput tries to assign a string to this.value, DialogStateManager.SetValue immediately converts that string to a JToken. So even if that JToken got retrieved as a string it would still be converted back to a JToken when assigning to user.profile.MobileNumber. And if user.profile has been serialized with type information then it will be deserialized as that type, which means it will get converted to a Profile object before the JToken is assigned to its MobileNumber property. You could raise this as a bug in the Bot Builder .NET GitHub repo,但请注意,您正在尝试做一些违背自适应对话框设计的事情。

如果您想使用自适应对话框,您应该对此保持一致,并以自适应对话框的方式进行所有操作。代码操作很容易出错,因此应谨慎使用。如果您使用代码操作所做的事情可以使用不同的自适应对话操作来完成,那么您应该使用不同的操作,因为它将包含内置的自适应对话功能。例如,如果你想设置一个 属性 那么你应该像你看到的那样使用 SetProperty

如果您真的想在代码操作中设置 属性,那么您应该使用自适应对话方式而不是使用状态 属性 访问器。自适应对话框使用 SetValue 来设置它们的属性,因此您也应该这样做。这将确保以自适应对话框可以轻松使用的方式格式化数据。

private static async Task<DialogTurnResult> PopulateProfile(DialogContext dc, object options)
{
    //var profile = await UserState.CreateProperty<Profile>("profile").GetAsync(dc.Context, () => new Profile());
    //profile.Complete = 0;

    dc.State.SetValue("user.profile.Complete", 0);

    return await dc.EndDialogAsync(options);
}