microsoft / Agents-for-net

This repository is for active development of the Agent Framework and SDK components for .NET
MIT License
11 stars 2 forks source link

Deserialization Failure with WaterfallStepContext.Values #4

Open karamem0 opened 1 day ago

karamem0 commented 1 day ago

Version

What package version of the SDK are you using.

Describe the bug When inserting objects of different types into WaterfallStepContext.Values, the deserialization process fails.

   at System.Text.Json.ThrowHelper.ThrowNotSupportedException(WriteStack& state, NotSupportedException ex)
   at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Serialize(Utf8JsonWriter writer, T& rootValue, Object rootValueBoxed)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsObject(Utf8JsonWriter writer, Object rootValue)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Serialize(Utf8JsonWriter writer, T& rootValue, Object rootValueBoxed)
   at System.Text.Json.JsonSerializer.WriteString[TValue](TValue& value, JsonTypeInfo`1 jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
   at Microsoft.Agents.BotBuilder.Dialogs.ObjectPath.GetNormalizedValue(Object value, Boolean json)
   at Microsoft.Agents.BotBuilder.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.Agents.BotBuilder.Dialogs.ObjectPath.SetPathValue(Object obj, String path, Object value, Boolean json)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogContext.EndActiveDialogAsync(DialogReason reason, Object result, CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogContext.EndDialogAsync(Object result, CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.ComponentDialog.ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogContext.ContinueDialogAsync(CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogExtensions.InnerRunAsync(ITurnContext turnContext, String dialogId, DialogContext dialogContext, CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogExtensions.InternalRunAsync(ITurnContext turnContext, String dialogId, DialogContext dialogContext, DialogStateManagerConfiguration stateConfiguration, CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogExtensions.InternalRunAsync(ITurnContext turnContext, String dialogId, DialogContext dialogContext, DialogStateManagerConfiguration stateConfiguration, CancellationToken cancellationToken)
   at Microsoft.Agents.BotBuilder.Dialogs.DialogExtensions.RunAsync(Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor`1 accessor, CancellationToken cancellationToken)
   at Karamem0.BookingsBot.Bots.DialogBot`1.OnMessageActivityAsync(ITurnContext`1 turnContext, CancellationToken cancellationToken)
   at Microsoft.Agents.Protocols.Adapter.ActivityHandler.OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
   at Karamem0.BookingsBot.Bots.DialogBot`1.OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
   at Microsoft.Agents.Protocols.Adapter.MiddlewareSet.ReceiveActivityWithStatusAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken)
   at Microsoft.Agents.Protocols.Adapter.BotAdapter.RunPipelineAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken)

To Reproduce

  1. Use BlobsStorage as IStorage.
  2. Define 3 steps in the WaterfallDialog, .
  3. At the first step:

    1. Add Dictionary<string, object> object to the StepContext.Value.

          ```
         stepContext.Values["Value1"] = new Dictionary<string, object?>() { { "Key1", "Value1" } }; 
          ```
    2. Show a prompt and the user responds. (The Value1 is saved to Blob storage with '$type' and '$typeAssembly')
  4. At the second step:

    1. Add string object to the StepContext.Value.

          ```csharp
          stepContext.Values["Value2"] = "This is a bot"; 
          ```
    2. Show a prompt and the user responds. (The Value2 is saved to Blob storage)
  5. The error raised before continue to the third step.

The JSON is:

{
  "id": "WaterfallDialog",
  "state": {
    "options": null,
    "values": {
      "Value1": [
        {
          "key": "Key1",
          "value": "Value1"
        }
      ],
      "$type": "System.Collections.Generic.Dictionary\u00602[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]",
      "$typeAssembly": "System.Private.CoreLib",
      "Value2": "This is a bot"
    }
  }
}

Expected behavior The values should be deserialized with its type or JsonElement.

Screenshots N/A

Hosting Information (please complete the following information):

Additional context Add any other context about the problem here.

tracyboehrer commented 1 day ago

@karamem0 Thanks! Would it be possible to share the code for a repro? This was one of the trickier areas to switch from NewtonSoft to System.Text.Json.

tracyboehrer commented 1 day ago

@karamem0 I have a dialog like this. Same as yours?

    public class TestDialog : ComponentDialog
    {
        public TestDialog(UserState userState)
            : base(nameof(TestDialog))
        {
            // This array defines how the Waterfall will execute.
            var waterfallSteps = new WaterfallStep[]
            {
                StepOne,
                StepTwo,
                StepThree
            };

            // Add named dialogs to the DialogSet. These names are saved in the dialog state.
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new TextPrompt(nameof(TextPrompt)));

            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }

        private static async Task<DialogTurnResult> StepOne(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["Value1"] = new Dictionary<string, object?>() { { "Key1", "Value1" } };

            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("One") }, cancellationToken);
        }

        private static async Task<DialogTurnResult> StepTwo(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["Value2"] = "This is a bot";

            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Two") }, cancellationToken);
        }
        private static async Task<DialogTurnResult> StepThree(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text($"Three: {stepContext.Result}") }, cancellationToken);
        }
    }
karamem0 commented 1 day ago

Hi @tracyboehrer, Yes, that's right. The actual code is more complex, but I'm trying to do something similar.

karamem0 commented 1 day ago

As a workaround, I created the extension method.


public static class WaterfallStepContextExtensions
{

    public static void SetValue<T>(this WaterfallStepContext target, string key, T? value)
    {
        target.Values[key] = JsonSerializer.Serialize(value);
    }

    public static T? GetValue<T>(this WaterfallStepContext target, string key)
    {
        if (target.Values.TryGetValue(key, out var value))
        {
            if (value is string jsonStr)
            {
                return JsonSerializer.Deserialize<T>(jsonStr);
            }
            else
            if (value is JsonElement element)
            {
                var jsonObj = element.GetString();
                if (jsonObj is not null)
                {
                    return JsonSerializer.Deserialize<T>(jsonObj);
                }
            }
        }
        return default;
    }

}
tracyboehrer commented 1 day ago

@karamem0 My version works with MemoryStorage, which if we're doing the same thing the problem isn't where I thought it would be. Possibly CosmosDbPartitionedStorage? I will check that next. My initial guess was something up the chain... ObjectPath. Because whatever it's doing, the serializer doesn't like it. In all likelihood, this is just a difference between System.Text.Json and NewtonSoft, and we didn't account for it.

tracyboehrer commented 1 day ago

@karamem0 So not storage related which makes sense because that isn't in the stack. Though, I can't reproduce. Cleary it's happening for you.

karamem0 commented 17 hours ago

@tracyboehrer

I changed your code a little then I could reproduce the issue. I tested with BlobsStorage and Azurite.

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.BotBuilder;
using Microsoft.Agents.BotBuilder.Dialogs;
using Microsoft.Agents.Protocols.Primitives;

namespace EchoBot.Dialogs
{
    public class TestDialog : ComponentDialog
    {
        public TestDialog(UserState userState)
            : base(nameof(TestDialog))
        {
            // This array defines how the Waterfall will execute.
            var waterfallSteps = new WaterfallStep[]
            {
                StepOne,
                StepTwo,
                StepThree
            };

            // Add named dialogs to the DialogSet. These names are saved in the dialog state.
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new TextPrompt(nameof(TextPrompt)));

            // The initial child Dialog to run.
            InitialDialogId = nameof(WaterfallDialog);
        }

        private static async Task<DialogTurnResult> StepOne(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            stepContext.Values["Value1"] = new Dictionary<string, object?>() { { "Key1", "Value1" } };

            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("One") }, cancellationToken);
        }

        private static async Task<DialogTurnResult> StepTwo(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
-            stepContext.Values["Value2"] = "This is a bot";
+            stepContext.Values["Value2"] = new Dictionary<int, object?>() { { 2, "Value2" } };

            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Two") }, cancellationToken);
        }

        private static async Task<DialogTurnResult> StepThree(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text($"Three: {stepContext.Result}") }, cancellationToken);
        }
    }
}

Image