Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Microsoft.Agents.AI;
/// may involve multiple agents working together.
/// </remarks>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public abstract class AIAgent
public abstract partial class AIAgent
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,155 +11,126 @@
namespace Microsoft.Agents.AI;

/// <summary>
/// Provides an <see cref="AIAgent"/> that delegates to an <see cref="IChatClient"/> implementation.
/// Provides structured output methods for <see cref="AIAgent"/> that enable requesting responses in a specific type format.
/// </summary>
public sealed partial class ChatClientAgent
public abstract partial class AIAgent
{
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the session, and requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <remarks>
/// This overload is useful when the agent has sufficient context from previous messages in the session
/// or from its initial configuration to generate a meaningful response without additional input.
/// </remarks>
public Task<ChatClientAgentResponse<T>> RunAsync<T>(
public Task<AgentResponse<T>> RunAsync<T>(
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
this.RunAsync<T>([], session, serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken);
this.RunAsync<T>([], session, serializerOptions, options, cancellationToken);

/// <summary>
/// Runs the agent with a text message from the user, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <exception cref="ArgumentException"><paramref name="message"/> is <see langword="null"/>, empty, or contains only whitespace.</exception>
/// <remarks>
/// The provided text will be wrapped in a <see cref="ChatMessage"/> with the <see cref="ChatRole.User"/> role
/// before being sent to the agent. This is a convenience method for simple text-based interactions.
/// </remarks>
public Task<ChatClientAgentResponse<T>> RunAsync<T>(
public Task<AgentResponse<T>> RunAsync<T>(
string message,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(message);

return this.RunAsync<T>(new ChatMessage(ChatRole.User, message), session, serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken);
return this.RunAsync<T>(new ChatMessage(ChatRole.User, message), session, serializerOptions, options, cancellationToken);
}

/// <summary>
/// Runs the agent with a single chat message, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
public Task<ChatClientAgentResponse<T>> RunAsync<T>(
public Task<AgentResponse<T>> RunAsync<T>(
ChatMessage message,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(message);

return this.RunAsync<T>([message], session, serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken);
return this.RunAsync<T>([message], session, serializerOptions, options, cancellationToken);
}

/// <summary>
/// Runs the agent with a collection of chat messages, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="session">
/// The conversation session to use for this invocation. If <see langword="null"/>, a new session will be created.
/// The session will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="serializerOptions">Optional JSON serializer options to use for deserializing the response.</param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse"/> with the agent's output.</returns>
/// <typeparam name="T">The type of structured output to request.</typeparam>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentResponse{T}"/> with the agent's output.</returns>
/// <remarks>
/// <para>
/// This is the primary invocation method that implementations must override. It handles collections of messages,
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
/// context-rich conversations.
/// This method handles collections of messages, allowing for complex conversational scenarios including
/// multi-turn interactions, function calls, and context-rich conversations.
/// </para>
/// <para>
/// The messages are processed in the order provided and become part of the conversation history.
/// The agent's response will also be added to <paramref name="session"/> if one is provided.
/// </para>
/// </remarks>
public Task<ChatClientAgentResponse<T>> RunAsync<T>(
public async Task<AgentResponse<T>> RunAsync<T>(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
async Task<ChatResponse<T>> GetResponseAsync(IChatClient chatClient, List<ChatMessage> threadMessages, ChatOptions? chatOptions, CancellationToken ct)
{
return await chatClient.GetResponseAsync<T>(
threadMessages,
serializerOptions ?? AgentJsonUtilities.DefaultOptions,
chatOptions,
useJsonSchemaResponseFormat,
ct).ConfigureAwait(false);
}
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;

options = options?.Clone() ?? new AgentRunOptions();
options.ResponseFormat = ChatResponseFormat.ForJsonSchema<T>(serializerOptions);

static ChatClientAgentResponse<T> CreateResponse(ChatResponse<T> chatResponse)
{
return new ChatClientAgentResponse<T>(chatResponse)
{
ContinuationToken = WrapContinuationToken(chatResponse.ContinuationToken)
};
}
AgentResponse response = await this.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);

return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, session, options, cancellationToken);
return new AgentResponse<T>(response, serializerOptions);
}
}
23 changes: 23 additions & 0 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ public AgentResponse(ChatResponse response)
this.ContinuationToken = response.ContinuationToken;
}

/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse"/> class from an existing <see cref="AgentResponse"/>.
/// </summary>
/// <param name="response">The <see cref="AgentResponse"/> from which to copy properties.</param>
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This constructor creates a copy of an existing agent response, preserving all
/// metadata and storing the original response in <see cref="RawRepresentation"/> for access to
/// the underlying implementation details.
/// </remarks>
public AgentResponse(AgentResponse response)
{
_ = Throw.IfNull(response);

this.AdditionalProperties = response.AdditionalProperties;
this.CreatedAt = response.CreatedAt;
this.Messages = response.Messages;
this.RawRepresentation = response;
this.ResponseId = response.ResponseId;
this.Usage = response.Usage;
this.ContinuationToken = response.ContinuationToken;
}

/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse"/> class with the specified collection of messages.
/// </summary>
Expand Down
93 changes: 83 additions & 10 deletions dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse{T}.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.AI;
using System;
#if NET
using System.Buffers;
#endif

#if NET
using System.Text;
#endif
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Microsoft.Agents.AI;

/// <summary>
/// Represents the response of the specified type <typeparamref name="T"/> to an <see cref="AIAgent"/> run request.
/// </summary>
/// <typeparam name="T">The type of value expected from the agent.</typeparam>
public abstract class AgentResponse<T> : AgentResponse
public class AgentResponse<T> : AgentResponse
{
/// <summary>Initializes a new instance of the <see cref="AgentResponse{T}"/> class.</summary>
protected AgentResponse()
{
}
private readonly JsonSerializerOptions _serializerOptions;

/// <summary>
/// Initializes a new instance of the <see cref="AgentResponse{T}"/> class from an existing <see cref="ChatResponse"/>.
/// Initializes a new instance of the <see cref="AgentResponse{T}"/> class.
/// </summary>
/// <param name="response">The <see cref="ChatResponse"/> from which to populate this <see cref="AgentResponse{T}"/>.</param>
protected AgentResponse(ChatResponse response) : base(response)
/// <param name="response">The <see cref="AgentResponse"/> from which to populate this <see cref="AgentResponse{T}"/>.</param>
/// <param name="serializerOptions">The <see cref="JsonSerializerOptions"/> to use when deserializing the result.</param>
public AgentResponse(AgentResponse response, JsonSerializerOptions serializerOptions) : base(response)
{
this._serializerOptions = serializerOptions;
}

/// <summary>
/// Gets the result value of the agent response as an instance of <typeparamref name="T"/>.
/// </summary>
public abstract T Result { get; }
[JsonIgnore]
public virtual T Result
{
get
{
var structuredOutput = this.GetResultCore(this._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!,
};
}
}

private T? GetResultCore(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 static T? DeserializeFirstTopLevelObject(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 enum FailureReason
{
ResultDidNotContainJson,
DeserializationProducedNull
}
}
Loading
Loading