Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA

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,
Expand All @@ -103,4 +103,25 @@ protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingA
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ stateObj is not JsonElement state ||
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(
Expand All @@ -134,4 +134,25 @@ stateObj is not JsonElement state ||
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

// 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}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@

// 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}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@ public override async ValueTask<CriticDecision> HandleAsync(

// 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))
Expand Down
23 changes: 22 additions & 1 deletion dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessag

// 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)
Expand Down Expand Up @@ -112,4 +112,25 @@ private static AdaptiveCard CreateWeatherCard(string? location, string? conditio
});
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;
}
}
}
124 changes: 1 addition & 123 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -290,117 +281,4 @@ public AgentResponseUpdate[] ToAgentResponseUpdates()

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
}
}
Loading
Loading