mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Delete AgentResponse.{Try}Deserialize<T> methods (#3518)
* delete deserialize method of agent response * order usings * Update dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step05_StructuredOutput/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/Program.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/GettingStarted/AGUI/Step05_StateManagement/Server/SharedStateAgent.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
0a0de4ac21
commit
3df8fe3c3f
@@ -78,7 +78,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
@@ -103,4 +103,25 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? result = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (result is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = result;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -107,7 +107,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
// Try to deserialize the structured state response
|
||||
if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
// Serialize and emit as STATE_SNAPSHOT via DataContent
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
@@ -134,4 +134,25 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? deserialized = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (deserialized is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = deserialized;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ var updates = agentWithPersonInfo.RunStreamingAsync("Please provide information
|
||||
|
||||
// Assemble all the parts of the streamed output, since we can only deserialize once we have the full json,
|
||||
// then deserialize the response into the PersonInfo class.
|
||||
PersonInfo personInfo = (await updates.ToAgentResponseAsync()).Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>((await updates.ToAgentResponseAsync()).Text, JsonSerializerOptions.Web)!;
|
||||
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
|
||||
+2
-1
@@ -61,7 +61,8 @@ IAsyncEnumerable<AgentResponseUpdate> updates = agentWithPersonInfo.RunStreaming
|
||||
|
||||
// Assemble all the parts of the streamed output, since we can only deserialize once we have the full json,
|
||||
// then deserialize the response into the PersonInfo class.
|
||||
PersonInfo personInfo = (await updates.ToAgentResponseAsync()).Deserialize<PersonInfo>(JsonSerializerOptions.Web);
|
||||
PersonInfo personInfo = JsonSerializer.Deserialize<PersonInfo>((await updates.ToAgentResponseAsync()).Text, JsonSerializerOptions.Web)
|
||||
?? throw new InvalidOperationException("Failed to deserialize the streamed response into PersonInfo.");
|
||||
|
||||
Console.WriteLine("Assistant Output:");
|
||||
Console.WriteLine($"Name: {personInfo.Name}");
|
||||
|
||||
+2
-1
@@ -327,7 +327,8 @@ internal sealed class CriticExecutor : Executor<ChatMessage, CriticDecision>
|
||||
|
||||
// Convert the stream to a response and deserialize the structured output
|
||||
AgentResponse response = await updates.ToAgentResponseAsync(cancellationToken);
|
||||
CriticDecision decision = response.Deserialize<CriticDecision>(JsonSerializerOptions.Web);
|
||||
CriticDecision decision = JsonSerializer.Deserialize<CriticDecision>(response.Text, JsonSerializerOptions.Web)
|
||||
?? throw new JsonException("Failed to deserialize CriticDecision from response text.");
|
||||
|
||||
Console.WriteLine($"Decision: {(decision.Approved ? "✅ APPROVED" : "❌ NEEDS REVISION")}");
|
||||
if (!string.IsNullOrEmpty(decision.Feedback))
|
||||
|
||||
@@ -54,7 +54,7 @@ public class WeatherForecastAgent : DelegatingAIAgent
|
||||
|
||||
// If the agent returned a valid structured output response
|
||||
// we might be able to enhance the response with an adaptive card.
|
||||
if (response.TryDeserialize<WeatherForecastAgentResponse>(JsonSerializerOptions.Web, out var structuredOutput))
|
||||
if (TryDeserialize<WeatherForecastAgentResponse>(response.Text, JsonSerializerOptions.Web, out var structuredOutput))
|
||||
{
|
||||
var textContentMessage = response.Messages.FirstOrDefault(x => x.Contents.OfType<TextContent>().Any());
|
||||
if (textContentMessage is not null)
|
||||
@@ -112,4 +112,25 @@ public class WeatherForecastAgent : DelegatingAIAgent
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? result = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (result is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = result;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
#if NET
|
||||
using System.Buffers;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
#if NET
|
||||
using System.Text;
|
||||
#endif
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
@@ -290,117 +281,4 @@ public class AgentResponse
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the response text into the given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <returns>The result as the requested type.</returns>
|
||||
/// <exception cref="InvalidOperationException">The result is not parsable into the requested type.</exception>
|
||||
public T Deserialize<T>() =>
|
||||
this.Deserialize<T>(AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the response text into the given type using the specified serializer options.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <returns>The result as the requested type.</returns>
|
||||
/// <exception cref="InvalidOperationException">The result is not parsable into the requested type.</exception>
|
||||
public T Deserialize<T>(JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
_ = Throw.IfNull(serializerOptions);
|
||||
|
||||
var structuredOutput = this.GetResultCore<T>(serializerOptions, out var failureReason);
|
||||
return failureReason switch
|
||||
{
|
||||
FailureReason.ResultDidNotContainJson => throw new InvalidOperationException("The response did not contain JSON to be deserialized."),
|
||||
FailureReason.DeserializationProducedNull => throw new InvalidOperationException("The deserialized response is null."),
|
||||
_ => structuredOutput!,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to deserialize response text into the given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <param name="structuredOutput">The parsed structured output.</param>
|
||||
/// <returns><see langword="true" /> if parsing was successful; otherwise, <see langword="false" />.</returns>
|
||||
public bool TryDeserialize<T>([NotNullWhen(true)] out T? structuredOutput) =>
|
||||
this.TryDeserialize(AgentAbstractionsJsonUtilities.DefaultOptions, out structuredOutput);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to deserialize response text into the given type using the specified serializer options.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The output type to deserialize into.</typeparam>
|
||||
/// <param name="serializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="structuredOutput">The parsed structured output.</param>
|
||||
/// <returns><see langword="true" /> if parsing was successful; otherwise, <see langword="false" />.</returns>
|
||||
public bool TryDeserialize<T>(JsonSerializerOptions serializerOptions, [NotNullWhen(true)] out T? structuredOutput)
|
||||
{
|
||||
_ = Throw.IfNull(serializerOptions);
|
||||
|
||||
try
|
||||
{
|
||||
structuredOutput = this.GetResultCore<T>(serializerOptions, out var failureReason);
|
||||
return failureReason is null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static T? DeserializeFirstTopLevelObject<T>(string json, JsonTypeInfo<T> typeInfo)
|
||||
{
|
||||
#if NET
|
||||
// We need to deserialize only the first top-level object as a workaround for a common LLM backend
|
||||
// issue. GPT 3.5 Turbo commonly returns multiple top-level objects after doing a function call.
|
||||
// See https://community.openai.com/t/2-json-objects-returned-when-using-function-calling-and-json-mode/574348
|
||||
var utf8ByteLength = Encoding.UTF8.GetByteCount(json);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(utf8ByteLength);
|
||||
try
|
||||
{
|
||||
var utf8SpanLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
|
||||
var reader = new Utf8JsonReader(new ReadOnlySpan<byte>(buffer, 0, utf8SpanLength), new() { AllowMultipleValues = true });
|
||||
return JsonSerializer.Deserialize(ref reader, typeInfo);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
#else
|
||||
return JsonSerializer.Deserialize(json, typeInfo);
|
||||
#endif
|
||||
}
|
||||
|
||||
private T? GetResultCore<T>(JsonSerializerOptions serializerOptions, out FailureReason? failureReason)
|
||||
{
|
||||
var json = this.Text;
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
failureReason = FailureReason.ResultDidNotContainJson;
|
||||
return default;
|
||||
}
|
||||
|
||||
// If there's an exception here, we want it to propagate, since the Result property is meant to throw directly
|
||||
|
||||
T? deserialized = DeserializeFirstTopLevelObject(json!, (JsonTypeInfo<T>)serializerOptions.GetTypeInfo(typeof(T)));
|
||||
|
||||
if (deserialized is null)
|
||||
{
|
||||
failureReason = FailureReason.DeserializationProducedNull;
|
||||
return default;
|
||||
}
|
||||
|
||||
failureReason = default;
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
private enum FailureReason
|
||||
{
|
||||
ResultDidNotContainJson,
|
||||
DeserializationProducedNull
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,30 +214,6 @@ public class AgentResponseTests
|
||||
Assert.Equal(100, usageContent.Details.TotalTokenCount);
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
/// <summary>
|
||||
/// Since Json Serialization using reflection is disabled in .net core builds, and we are using a custom type here that wouldn't
|
||||
/// be registered with the default source generated serializer, this test will only pass in .net framework builds where reflection-based
|
||||
/// serialization is available.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputSuccess()
|
||||
{
|
||||
// Arrange.
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
var animal = response.Deserialize<Animal>();
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(animal);
|
||||
Assert.Equal(expectedResult.Id, animal.Id);
|
||||
Assert.Equal(expectedResult.FullName, animal.FullName);
|
||||
Assert.Equal(expectedResult.Species, animal.Species);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputWithJSOSuccess()
|
||||
{
|
||||
@@ -246,7 +222,7 @@ public class AgentResponseTests
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
var animal = response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options);
|
||||
var animal = JsonSerializer.Deserialize<Animal>(response.Text, TestJsonSerializerContext.Default.Options);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(animal);
|
||||
@@ -254,96 +230,4 @@ public class AgentResponseTests
|
||||
Assert.Equal(expectedResult.FullName, animal.FullName);
|
||||
Assert.Equal(expectedResult.Species, animal.Species);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputFailsWithEmptyString()
|
||||
{
|
||||
// Arrange.
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, string.Empty));
|
||||
|
||||
// Act & Assert.
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
|
||||
Assert.Equal("The response did not contain JSON to be deserialized.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputFailsWithInvalidJson()
|
||||
{
|
||||
// Arrange.
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "invalid json"));
|
||||
|
||||
// Act & Assert.
|
||||
Assert.Throws<JsonException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAsStructuredOutputFailsWithIncorrectTypedJson()
|
||||
{
|
||||
// Arrange.
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "[]"));
|
||||
|
||||
// Act & Assert.
|
||||
Assert.Throws<JsonException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
/// <summary>
|
||||
/// Since Json Serialization using reflection is disabled in .net core builds, and we are using a custom type here that wouldn't
|
||||
/// be registered with the default source generated serializer, this test will only pass in .net framework builds where reflection-based
|
||||
/// serialization is available.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryParseAsStructuredOutputSuccess()
|
||||
{
|
||||
// Arrange.
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
response.TryDeserialize(out Animal? animal);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(animal);
|
||||
Assert.Equal(expectedResult.Id, animal.Id);
|
||||
Assert.Equal(expectedResult.FullName, animal.FullName);
|
||||
Assert.Equal(expectedResult.Species, animal.Species);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void TryParseAsStructuredOutputWithJSOSuccess()
|
||||
{
|
||||
// Arrange.
|
||||
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
|
||||
|
||||
// Act.
|
||||
response.TryDeserialize(TestJsonSerializerContext.Default.Options, out Animal? animal);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(animal);
|
||||
Assert.Equal(expectedResult.Id, animal.Id);
|
||||
Assert.Equal(expectedResult.FullName, animal.FullName);
|
||||
Assert.Equal(expectedResult.Species, animal.Species);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseAsStructuredOutputFailsWithEmptyText()
|
||||
{
|
||||
// Arrange.
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, string.Empty));
|
||||
|
||||
// Act & Assert.
|
||||
Assert.False(response.TryDeserialize<Animal>(TestJsonSerializerContext.Default.Options, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseAsStructuredOutputFailsWithIncorrectTypedJson()
|
||||
{
|
||||
// Arrange.
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "[]"));
|
||||
|
||||
// Act & Assert.
|
||||
Assert.False(response.TryDeserialize<Animal>(TestJsonSerializerContext.Default.Options, out _));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user