Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialization Failure with WaterfallStepContext.Values #4

Open
karamem0 opened this issue Nov 21, 2024 · 11 comments
Open

Deserialization Failure with WaterfallStepContext.Values #4

karamem0 opened this issue Nov 21, 2024 · 11 comments
Assignees

Comments

@karamem0
Copy link

Version

What package version of the SDK are you using.

  • Nuget package version: 0.1.26.29702
  • dll product version: 0.1.26+7406fcd041

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):

  • How are you Hosting this: Desktop
  • Are you deploying: Local
  • Are you using Azure Bot Services: Yes
  • What Client are you using: WebChat
  • What .net version is your build in: .NET 8

Additional context
Add any other context about the problem here.

@karamem0 karamem0 added the triage Initial state for our team to determine nessessary action label Nov 21, 2024
@tracyboehrer tracyboehrer self-assigned this Nov 21, 2024
@tracyboehrer
Copy link
Member

@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
Copy link
Member

@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
Copy link
Author

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

@karamem0
Copy link
Author

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
Copy link
Member

tracyboehrer commented Nov 21, 2024

@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
Copy link
Member

@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.

@tracyboehrer tracyboehrer removed the triage Initial state for our team to determine nessessary action label Nov 21, 2024
@karamem0
Copy link
Author

karamem0 commented Nov 22, 2024

@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

@tracyboehrer
Copy link
Member

@karamem0 Thanks! I'll dig into this.

@tracyboehrer
Copy link
Member

tracyboehrer commented Dec 10, 2024

@karamem0 This is a different from the original exception, right? I can reproduce the The JSON value could not be converted to System.Int32. Path: $.$type | LineNumber: 0 | BytePositionInLine: 22. though.

@tracyboehrer
Copy link
Member

@karamem0 The issue in the more recent repro is with the DictionaryOfObjectConverter converter. Just doesn't like the Dictionary<int,object>. I'll let you know. System.Text.Json just doesn't handle this the same way.

@tracyboehrer
Copy link
Member

@karamem0 I have a resolution for The JSON value could not be converted to System.Int32. Path: $.$type | LineNumber: 0 | BytePositionInLine: 22.. Basically it was choking because of the type info properties and a Dictionary<int, object>. However I am going to leave this issue open since it's not clear to me what the originally reported exception is about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants