chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the base abstraction for all AI agents, defining the core interface for agent interactions and conversation management.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AIAgent"/> serves as the foundational class for implementing AI agents that can participate in conversations
|
||||
/// and process user requests. An agent instance may participate in multiple concurrent conversations, and each conversation
|
||||
/// may involve multiple agents working together.
|
||||
/// <para>
|
||||
/// <strong>Security considerations:</strong> An <see cref="AIAgent"/> orchestrates data flow across trust boundaries —
|
||||
/// messages are sent to external AI services, context providers, chat history stores, and function tools. Agent Framework
|
||||
/// passes messages through as-is without validation or sanitization. Developers must be aware that:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>User-supplied messages may contain prompt injection attempts designed to manipulate LLM behavior.</description></item>
|
||||
/// <item><description>LLM responses should be treated as untrusted output — they may contain hallucinations, malicious payloads (e.g., scripts, SQL),
|
||||
/// or content influenced by indirect prompt injection. Always validate and sanitize LLM output before rendering in HTML, executing as code,
|
||||
/// or using in database queries.</description></item>
|
||||
/// <item><description>Messages with different roles carry different trust levels: <c>system</c> messages have the highest trust and must be developer-controlled;
|
||||
/// <c>user</c>, <c>assistant</c>, and <c>tool</c> messages should be treated as untrusted.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public abstract partial class AIAgent
|
||||
{
|
||||
private static readonly AsyncLocal<AgentRunContext?> s_currentContext = new();
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string DebuggerDisplay =>
|
||||
this.Name is { } name ? $"Id = {this.Id}, Name = {name}" : $"Id = {this.Id}";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this agent instance.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A unique string identifier for the agent. For in-memory agents, this defaults to a randomly-generated ID,
|
||||
/// while service-backed agents typically use the identifier assigned by the backing service.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Agent identifiers are used for tracking, telemetry, and distinguishing between different
|
||||
/// agent instances in multi-agent scenarios. They should remain stable for the lifetime
|
||||
/// of the agent instance.
|
||||
/// </remarks>
|
||||
public string Id { get => this.IdCore ?? field; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a custom identifier for the agent, which can be overridden by derived classes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A string representing the agent's identifier, or <see langword="null"/> if the default ID should be used.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Derived classes can override this property to provide a custom identifier.
|
||||
/// When <see langword="null"/> is returned, the <see cref="Id"/> property will use the default randomly-generated identifier.
|
||||
/// </remarks>
|
||||
protected virtual string? IdCore => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the human-readable name of the agent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The agent's name, or <see langword="null"/> if no name has been assigned.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The agent name is typically used for display purposes and to help users identify
|
||||
/// the agent's purpose or capabilities in user interfaces.
|
||||
/// </remarks>
|
||||
public virtual string? Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a description of the agent's purpose, capabilities, or behavior.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A descriptive text explaining what the agent does, or <see langword="null"/> if no description is available.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The description helps models and users understand the agent's intended purpose and capabilities,
|
||||
/// which is particularly useful in multi-agent systems.
|
||||
/// </remarks>
|
||||
public virtual string? Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="AgentRunContext"/> for the current agent run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value flows across async calls.
|
||||
/// </remarks>
|
||||
public static AgentRunContext? CurrentRunContext
|
||||
{
|
||||
get => s_currentContext.Value;
|
||||
protected set => s_currentContext.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>Asks the <see cref="AIAgent"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
|
||||
/// <param name="serviceType">The type of object being requested.</param>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AIAgent"/>,
|
||||
/// including itself or any services it might be wrapping. For example, to access the <see cref="AIAgentMetadata"/> for the instance,
|
||||
/// <see cref="GetService"/> may be used to request it.
|
||||
/// </remarks>
|
||||
public virtual object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
_ = Throw.IfNull(serviceType);
|
||||
|
||||
return serviceKey is null && serviceType.IsInstanceOfType(this)
|
||||
? this
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Asks the <see cref="AIAgent"/> for an object of type <typeparamref name="TService"/>.</summary>
|
||||
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AIAgent"/>,
|
||||
/// including itself or any services it might be wrapping.
|
||||
/// </remarks>
|
||||
public TService? GetService<TService>(object? serviceKey = null)
|
||||
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new conversation session that is compatible with this agent.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance ready for use with this agent.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method creates a fresh conversation session that can be used to maintain state
|
||||
/// and context for interactions with this agent. Each session represents an independent
|
||||
/// conversation session.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the agent supports multiple session types, this method returns the default or
|
||||
/// configured session type. For service-backed agents, the actual session creation
|
||||
/// may be deferred until first use to optimize performance.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<AgentSession> CreateSessionAsync(CancellationToken cancellationToken = default)
|
||||
=> this.CreateSessionCoreAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of session creation logic.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance ready for use with this agent.</returns>
|
||||
/// <remarks>
|
||||
/// This is the primary session creation method that implementations must override.
|
||||
/// </remarks>
|
||||
protected abstract ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an agent session to its JSON representation.
|
||||
/// </summary>
|
||||
/// <param name="session">The <see cref="AgentSession"/> to serialize.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings to customize the serialization process.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation. The task result contains a <see cref="JsonElement"/> with the serialized session state.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="session"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The type of <paramref name="session"/> is not supported by this agent.</exception>
|
||||
/// <remarks>
|
||||
/// This method enables saving conversation sessions to persistent storage,
|
||||
/// allowing conversations to resume across application restarts or be migrated between
|
||||
/// different agent instances. Use <see cref="DeserializeSessionAsync"/> to restore the session.
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Serialized sessions may contain conversation content, session identifiers,
|
||||
/// and other potentially sensitive data including PII. Ensure that serialized session data is stored securely with
|
||||
/// appropriate access controls and encryption at rest.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<JsonElement> SerializeSessionAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> this.SerializeSessionCoreAsync(session, jsonSerializerOptions, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of session serialization logic.
|
||||
/// </summary>
|
||||
/// <param name="session">The <see cref="AgentSession"/> to serialize.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings to customize the serialization process.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation. The task result contains a <see cref="JsonElement"/> with the serialized session state.</returns>
|
||||
/// <remarks>
|
||||
/// This is the primary session serialization method that implementations must override.
|
||||
/// </remarks>
|
||||
protected abstract ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an agent session from its JSON serialized representation.
|
||||
/// </summary>
|
||||
/// <param name="serializedState">A <see cref="JsonElement"/> containing the serialized session state.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedState"/>.</returns>
|
||||
/// <exception cref="ArgumentException">The <paramref name="serializedState"/> is not in the expected format.</exception>
|
||||
/// <exception cref="JsonException">The serialized data is invalid or cannot be deserialized.</exception>
|
||||
/// <remarks>
|
||||
/// This method enables restoration of conversation sessions from previously saved state,
|
||||
/// allowing conversations to resume across application restarts or be migrated between
|
||||
/// different agent instances.
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Restoring a session from an untrusted source is equivalent to accepting untrusted input.
|
||||
/// Serialized sessions may contain conversation content, session identifiers, and potentially sensitive data. A compromised
|
||||
/// storage backend could alter message roles to escalate trust, or inject adversarial content that influences LLM behavior.
|
||||
/// Treat serialized session data as sensitive and ensure it is stored and transmitted securely.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<AgentSession> DeserializeSessionAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> this.DeserializeSessionCoreAsync(serializedState, jsonSerializerOptions, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of session deserialization logic.
|
||||
/// </summary>
|
||||
/// <param name="serializedState">A <see cref="JsonElement"/> containing the serialized session state.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentSession"/> instance with the state from <paramref name="serializedState"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This is the primary session deserialization method that implementations must override.
|
||||
/// </remarks>
|
||||
protected abstract ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the session.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</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>
|
||||
/// <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<AgentResponse> RunAsync(
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunAsync([], session, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a text message from the user.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</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>
|
||||
/// <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<AgentResponse> RunAsync(
|
||||
string message,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(message);
|
||||
|
||||
return this.RunAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a single chat message.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</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>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
|
||||
public Task<AgentResponse> RunAsync(
|
||||
ChatMessage message,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(message);
|
||||
|
||||
return this.RunAsync([message], session, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with a collection of chat messages, providing the core invocation logic that all other overloads delegate to.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</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>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method delegates to <see cref="RunCoreAsync"/> to perform the actual agent invocation. It 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>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Agent Framework does not validate or sanitize message content — it is passed through
|
||||
/// to the underlying AI service as-is. If input messages include untrusted user content, developers should be aware of prompt injection risks.
|
||||
/// System-role messages must be developer-controlled and should never contain end-user input.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public Task<AgentResponse> RunAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
CurrentRunContext = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
|
||||
return this.RunCoreAsync(messages, session, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of the agent invocation logic with a collection of chat messages.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</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>
|
||||
/// <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.
|
||||
/// </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>
|
||||
protected abstract Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode without providing new input messages, relying on existing context and instructions.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunStreamingAsync([], session, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode with a text message from the user.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</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.
|
||||
/// Streaming invocation provides real-time updates as the agent generates its response.
|
||||
/// </remarks>
|
||||
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
string message,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(message);
|
||||
|
||||
return this.RunStreamingAsync(new ChatMessage(ChatRole.User, message), session, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode with a single chat message.
|
||||
/// </summary>
|
||||
/// <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="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
|
||||
public IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
ChatMessage message,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(message);
|
||||
|
||||
return this.RunStreamingAsync([message], session, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent in streaming mode with a collection of chat messages, providing the core streaming invocation logic.
|
||||
/// </summary>
|
||||
/// <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 updates generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method delegates to <see cref="RunCoreStreamingAsync"/> to perform the actual streaming invocation. It provides real-time
|
||||
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each <see cref="AgentResponseUpdate"/> represents a portion of the complete response, allowing consumers
|
||||
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Agent Framework does not validate or sanitize message content — it is passed through
|
||||
/// to the underlying AI service as-is. If input messages include untrusted user content, developers should be aware of prompt injection risks.
|
||||
/// System-role messages must be developer-controlled and should never contain end-user input.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentRunContext context = new(this, session, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), options);
|
||||
CurrentRunContext = context;
|
||||
await foreach (var update in this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
|
||||
// Restore context again when resuming after the caller code executes.
|
||||
CurrentRunContext = context;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
|
||||
/// </summary>
|
||||
/// <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 updates generated during invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="AgentResponseUpdate"/> instances representing the streaming response.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the primary streaming invocation method that implementations must override. It provides real-time
|
||||
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each <see cref="AgentResponseUpdate"/> represents a portion of the complete response, allowing consumers
|
||||
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected abstract IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides metadata information about an <see cref="AIAgent"/> instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class contains descriptive information about an agent that can be used for identification,
|
||||
/// telemetry, and logging purposes.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("ProviderName = {ProviderName}")]
|
||||
public sealed class AIAgentMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AIAgentMetadata"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerName">
|
||||
/// The name of the agent provider, if applicable. Where possible, this should map to the
|
||||
/// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
|
||||
/// </param>
|
||||
public AIAgentMetadata(string? providerName = null)
|
||||
{
|
||||
this.ProviderName = providerName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the agent provider.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The provider name that identifies the underlying service or implementation powering the agent.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Where possible, this maps to the appropriate name defined in the
|
||||
/// OpenTelemetry Semantic Conventions for Generative AI systems.
|
||||
/// </remarks>
|
||||
public string? ProviderName { get; }
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides structured output methods for <see cref="AIAgent"/> that enable requesting responses in a specific type format.
|
||||
/// </summary>
|
||||
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">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="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{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<AgentResponse<T>> RunAsync<T>(
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
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">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="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{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<AgentResponse<T>> RunAsync<T>(
|
||||
string message,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(message);
|
||||
|
||||
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">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="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{T}"/> with the agent's output.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
|
||||
public Task<AgentResponse<T>> RunAsync<T>(
|
||||
ChatMessage message,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(message);
|
||||
|
||||
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">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="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{T}"/> with the agent's output.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 async Task<AgentResponse<T>> RunAsync<T>(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
var responseFormat = ChatResponseFormat.ForJsonSchema<T>(serializerOptions);
|
||||
|
||||
(responseFormat, bool isWrappedInObject) = StructuredOutputSchemaUtilities.WrapNonObjectSchema(responseFormat);
|
||||
|
||||
options = options?.Clone() ?? new AgentRunOptions();
|
||||
options.ResponseFormat = responseFormat;
|
||||
|
||||
AgentResponse response = await this.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new AgentResponse<T>(response, serializerOptions) { IsWrappedInObject = isWrappedInObject };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#if NET
|
||||
using System;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
#if NET
|
||||
using System.Runtime.CompilerServices;
|
||||
#else
|
||||
using System.Text;
|
||||
#endif
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>Internal extensions for working with <see cref="AIContent"/>.</summary>
|
||||
internal static class AIContentExtensions
|
||||
{
|
||||
/// <summary>Concatenates the text of all <see cref="TextContent"/> instances in the list.</summary>
|
||||
public static string ConcatText(this IEnumerable<AIContent> contents)
|
||||
{
|
||||
if (contents is IList<AIContent> list)
|
||||
{
|
||||
int count = list.Count;
|
||||
switch (count)
|
||||
{
|
||||
case 0:
|
||||
return string.Empty;
|
||||
|
||||
case 1:
|
||||
return (list[0] as TextContent)?.Text ?? string.Empty;
|
||||
|
||||
default:
|
||||
#if NET
|
||||
DefaultInterpolatedStringHandler builder = new(count, 0, null, stackalloc char[512]);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (list[i] is TextContent text)
|
||||
{
|
||||
builder.AppendLiteral(text.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToStringAndClear();
|
||||
#else
|
||||
StringBuilder builder = new();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (list[i] is TextContent text)
|
||||
{
|
||||
builder.Append(text.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return string.Concat(contents.OfType<TextContent>());
|
||||
}
|
||||
|
||||
/// <summary>Concatenates the <see cref="ChatMessage.Text"/> of all <see cref="ChatMessage"/> instances in the list.</summary>
|
||||
/// <remarks>A newline separator is added between each non-empty piece of text.</remarks>
|
||||
public static string ConcatText(this IList<ChatMessage> messages)
|
||||
{
|
||||
int count = messages.Count;
|
||||
switch (count)
|
||||
{
|
||||
case 0:
|
||||
return string.Empty;
|
||||
|
||||
case 1:
|
||||
return messages[0].Text;
|
||||
|
||||
default:
|
||||
#if NET
|
||||
DefaultInterpolatedStringHandler builder = new(count, 0, null, stackalloc char[512]);
|
||||
bool needsSeparator = false;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string text = messages[i].Text;
|
||||
if (text.Length > 0)
|
||||
{
|
||||
if (needsSeparator)
|
||||
{
|
||||
builder.AppendLiteral(Environment.NewLine);
|
||||
}
|
||||
|
||||
builder.AppendLiteral(text);
|
||||
|
||||
needsSeparator = true;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToStringAndClear();
|
||||
#else
|
||||
StringBuilder builder = new();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string text = messages[i].Text;
|
||||
if (text.Length > 0)
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.Append(text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents additional context information that can be dynamically provided to AI models during agent invocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="AIContext"/> serves as a container for contextual information that <see cref="AIContextProvider"/> instances
|
||||
/// can supply to enhance AI model interactions. This context is merged with
|
||||
/// the agent's base configuration before being passed to the underlying AI model.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The context system enables dynamic, runtime-specific enhancements to agent capabilities including:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Adding relevant background information from knowledge bases</description></item>
|
||||
/// <item><description>Injecting task-specific instructions or guidelines</description></item>
|
||||
/// <item><description>Providing specialized tools or functions for the current interaction</description></item>
|
||||
/// <item><description>Including contextual messages that inform the AI about the current situation</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Context information is transient by default and applies only to the current invocation, however messages
|
||||
/// added through the <see cref="Messages"/> property will be permanently incorporated into the conversation history.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class AIContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets additional instructions to provide to the AI model for the current invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Instructions text that will be combined with any existing agent instructions or system prompts,
|
||||
/// or <see langword="null"/> if no additional instructions should be provided.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These instructions are transient and apply only to the current AI model invocation. They are combined
|
||||
/// with any existing agent instructions, system prompts, and conversation history to provide comprehensive
|
||||
/// context to the AI model.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Instructions can be used to:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Provide context-specific behavioral guidance</description></item>
|
||||
/// <item><description>Add domain-specific knowledge or constraints</description></item>
|
||||
/// <item><description>Modify the agent's persona or response style for the current interaction</description></item>
|
||||
/// <item><description>Include situational awareness information</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public string? Instructions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sequence of messages to use for the current invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A sequence of <see cref="ChatMessage"/> instances to be used for the current invocation,
|
||||
/// or <see langword="null"/> if no messages should be used.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Unlike <see cref="Instructions"/> and <see cref="Tools"/>, messages added through this property may become
|
||||
/// permanent additions to the conversation history.
|
||||
/// If chat history is managed by the underlying AI service, these messages will become part of chat history.
|
||||
/// If chat history is managed using a <see cref="ChatHistoryProvider"/>, these messages will be passed to the
|
||||
/// <see cref="ChatHistoryProvider.InvokedCoreAsync(ChatHistoryProvider.InvokedContext, System.Threading.CancellationToken)"/> method,
|
||||
/// and the provider can choose which of these messages to permanently add to the conversation history.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property is useful for:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Injecting relevant historical context e.g. memories</description></item>
|
||||
/// <item><description>Injecting relevant background information e.g. via Retrieval Augmented Generation</description></item>
|
||||
/// <item><description>Adding system messages that provide ongoing context</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IEnumerable<ChatMessage>? Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a sequence of tools or functions to make available to the AI model for the current invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A sequence of <see cref="AITool"/> instances that will be available to the AI model during the current invocation,
|
||||
/// or <see langword="null"/> if no additional tools should be provided.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These tools are transient and apply only to the current AI model invocation. Any existing tools
|
||||
/// are provided as input to the <see cref="AIContextProvider"/> instances, so context providers can choose to modify or replace the existing tools
|
||||
/// as needed based on the current context. The resulting set of tools is then passed to the underlying AI model, which may choose to utilize them when generating responses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Context-specific tools enable:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Providing specialized functions based on user intent or conversation context</description></item>
|
||||
/// <item><description>Adding domain-specific capabilities for particular types of queries</description></item>
|
||||
/// <item><description>Enabling access to external services or data sources relevant to the current task</description></item>
|
||||
/// <item><description>Offering interactive capabilities tailored to the current conversation state</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IEnumerable<AITool>? Tools { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for components that enhance AI context during agent invocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// An AI context provider is a component that participates in the agent invocation lifecycle by:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Listening to changes in conversations</description></item>
|
||||
/// <item><description>Providing additional context to agents during invocation</description></item>
|
||||
/// <item><description>Supplying additional function tools for enhanced capabilities</description></item>
|
||||
/// <item><description>Processing invocation results for state management or learning</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Context providers operate through a two-phase lifecycle: they are called at the start of invocation via
|
||||
/// <see cref="InvokingAsync"/> to provide context, and optionally called at the end of invocation via
|
||||
/// <see cref="InvokedAsync"/> to process results.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security considerations:</strong> Context providers may inject messages with any role, including <c>system</c>, which
|
||||
/// has the highest trust level and directly shapes LLM behavior. Developers must ensure that all providers attached to an agent
|
||||
/// are trusted. Agent Framework does not validate or filter the data returned by providers — it is accepted as-is and merged into
|
||||
/// the request context. If a provider retrieves data from an external source (e.g., a vector database or memory service), be aware
|
||||
/// that a compromised data source could introduce adversarial content designed to manipulate LLM behavior via indirect prompt injection.
|
||||
/// Implementers should validate and sanitize data retrieved from external sources before returning it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class AIContextProvider
|
||||
{
|
||||
private static IEnumerable<ChatMessage> DefaultExternalOnlyFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);
|
||||
private static IEnumerable<ChatMessage> DefaultNoopFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages;
|
||||
|
||||
private IReadOnlyList<string>? _stateKeys;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AIContextProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="provideInputMessageFilter">An optional filter function to apply to input messages before providing context via <see cref="ProvideAIContextAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
|
||||
/// <param name="storeInputRequestMessageFilter">An optional filter function to apply to request messages before storing context via <see cref="StoreAIContextAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
|
||||
/// <param name="storeInputResponseMessageFilter">An optional filter function to apply to response messages before storing context via <see cref="StoreAIContextAsync"/>. If not set, defaults to a no-op filter that includes all response messages.</param>
|
||||
protected AIContextProvider(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
|
||||
{
|
||||
this.ProvideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter;
|
||||
this.StoreInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExternalOnlyFilter;
|
||||
this.StoreInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter function to apply to input messages before providing context via <see cref="ProvideAIContextAsync"/>.
|
||||
/// </summary>
|
||||
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> ProvideInputMessageFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter function to apply to request messages before storing context via <see cref="StoreAIContextAsync"/>.
|
||||
/// </summary>
|
||||
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> StoreInputRequestMessageFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter function to apply to response messages before storing context via <see cref="StoreAIContextAsync"/>.
|
||||
/// </summary>
|
||||
protected Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> StoreInputResponseMessageFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of keys used to store the provider state in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value is a single-element set containing the name of the concrete type (e.g. <c>"TextSearchProvider"</c>).
|
||||
/// Implementations may override this to provide custom keys, for example when multiple
|
||||
/// instances of the same provider type are used in the same session, or when a provider
|
||||
/// stores state under more than one key.
|
||||
/// </remarks>
|
||||
public virtual IReadOnlyList<string> StateKeys => this._stateKeys ??= [this.GetType().Name];
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide additional context.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 the <see cref="AIContext"/> with additional context to be used by the agent during this invocation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementers can load any additional context required at this time, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
|
||||
/// <item><description>Adding system instructions or prompts</description></item>
|
||||
/// <item><description>Providing function tools for the current invocation</description></item>
|
||||
/// <item><description>Injecting contextual messages from conversation history</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Data retrieved from external sources (e.g., vector databases, memory services, or
|
||||
/// knowledge bases) may contain adversarial content designed to influence LLM behavior via indirect prompt injection.
|
||||
/// Implementers should validate data integrity and consider the trustworthiness of the data source.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide additional context.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 the <see cref="AIContext"/> with additional context to be used by the agent during this invocation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementers can load any additional context required at this time, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
|
||||
/// <item><description>Adding system instructions or prompts</description></item>
|
||||
/// <item><description>Providing function tools for the current invocation</description></item>
|
||||
/// <item><description>Injecting contextual messages from conversation history</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of this method filters the input messages using the configured provide-input message filter
|
||||
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
|
||||
/// then calls <see cref="ProvideAIContextAsync"/> to get additional context,
|
||||
/// stamps any messages from the returned context with <see cref="AgentRequestMessageSourceType.AIContextProvider"/> source attribution,
|
||||
/// and merges the returned context with the original (unfiltered) input context (concatenating instructions, messages, and tools).
|
||||
/// For most scenarios, overriding <see cref="ProvideAIContextAsync"/> is sufficient to provide additional context,
|
||||
/// while still benefiting from the default filtering, merging and source stamping behavior.
|
||||
/// However, for scenarios that require more control over context filtering, merging or source stamping, overriding this method
|
||||
/// allows you to directly control the full <see cref="AIContext"/> returned for the invocation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputContext = context.AIContext;
|
||||
|
||||
// Create a filtered context for ProvideAIContextAsync, filtering input messages
|
||||
// to exclude non-external messages (e.g. chat history, other AI context provider messages).
|
||||
#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
var filteredContext = new InvokingContext(
|
||||
context.Agent,
|
||||
context.Session,
|
||||
new AIContext
|
||||
{
|
||||
Instructions = inputContext.Instructions,
|
||||
Messages = inputContext.Messages is not null ? this.ProvideInputMessageFilter(inputContext.Messages) : null,
|
||||
Tools = inputContext.Tools
|
||||
});
|
||||
#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
var provided = await this.ProvideAIContextAsync(filteredContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mergedInstructions = (inputContext.Instructions, provided.Instructions) switch
|
||||
{
|
||||
(null, null) => null,
|
||||
(string a, null) => a,
|
||||
(null, string b) => b,
|
||||
(string a, string b) => a + "\n" + b
|
||||
};
|
||||
|
||||
var providedMessages = provided.Messages is not null
|
||||
? provided.Messages.Select(m => m.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!))
|
||||
: null;
|
||||
|
||||
var mergedMessages = (inputContext.Messages, providedMessages) switch
|
||||
{
|
||||
(null, null) => null,
|
||||
(var a, null) => a,
|
||||
(null, var b) => b,
|
||||
(var a, var b) => a.Concat(b)
|
||||
};
|
||||
|
||||
var mergedTools = (inputContext.Tools, provided.Tools) switch
|
||||
{
|
||||
(null, null) => null,
|
||||
(var a, null) => a,
|
||||
(null, var b) => b,
|
||||
(var a, var b) => a.Concat(b)
|
||||
};
|
||||
|
||||
return new AIContext
|
||||
{
|
||||
Instructions = mergedInstructions,
|
||||
Messages = mergedMessages,
|
||||
Tools = mergedTools
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, provides additional AI context to be merged with the input context for the current invocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is called from <see cref="InvokingCoreAsync"/>.
|
||||
/// Note that <see cref="InvokingCoreAsync"/> can be overridden to directly control context merging and source stamping, in which case
|
||||
/// it is up to the implementer to call this method as needed to retrieve the additional context.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In contrast with <see cref="InvokingCoreAsync"/>, this method only returns additional context to be merged with the input,
|
||||
/// while <see cref="InvokingCoreAsync"/> is responsible for returning the full merged <see cref="AIContext"/> for the invocation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Any messages, tools, or instructions returned by this method will be merged into the
|
||||
/// AI request context. If data is retrieved from external or untrusted sources, implementers should validate and sanitize it
|
||||
/// to prevent indirect prompt injection attacks.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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="AIContext"/>
|
||||
/// with additional context to be merged with the input context.
|
||||
/// </returns>
|
||||
protected virtual ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<AIContext>(new AIContext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of the agent invocation to process the invocation results.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</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.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementers can use the request and response messages in the provided <paramref name="context"/> to:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Update state based on conversation outcomes</description></item>
|
||||
/// <item><description>Extract and store memories or preferences from user messages</description></item>
|
||||
/// <item><description>Log or audit conversation details</description></item>
|
||||
/// <item><description>Perform cleanup or finalization tasks</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="AIContextProvider"/> is passed a reference to the <see cref="AgentSession"/> via <see cref="InvokingContext"/> and <see cref="InvokedContext"/>
|
||||
/// allowing it to store state in the <see cref="AgentSession.StateBag"/>. Since an <see cref="AIContextProvider"/> is used with many different sessions, it should
|
||||
/// not store any session-specific information within its own instance fields. Instead, any session-specific state should be stored in the associated <see cref="AgentSession.StateBag"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is called regardless of whether the invocation succeeded or failed.
|
||||
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of the agent invocation to process the invocation results.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</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.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementers can use the request and response messages in the provided <paramref name="context"/> to:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Update internal state based on conversation outcomes</description></item>
|
||||
/// <item><description>Extract and store memories or preferences from user messages</description></item>
|
||||
/// <item><description>Log or audit conversation details</description></item>
|
||||
/// <item><description>Perform cleanup or finalization tasks</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is called regardless of whether the invocation succeeded or failed.
|
||||
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of this method skips execution for any invocation failures,
|
||||
/// filters the request messages using the configured store-input request message filter
|
||||
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
|
||||
/// filters the response messages using the configured store-input response message filter
|
||||
/// (which defaults to a no-op, so all response messages are processed),
|
||||
/// and calls <see cref="StoreAIContextAsync"/> to process the invocation results.
|
||||
/// For most scenarios, overriding <see cref="StoreAIContextAsync"/> is sufficient to process invocation results,
|
||||
/// while still benefiting from the default error handling and filtering behavior.
|
||||
/// However, for scenarios that require more control over error handling or message filtering, overriding this method
|
||||
/// allows you to directly control the processing of invocation results.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputRequestMessageFilter(context.RequestMessages), this.StoreInputResponseMessageFilter(context.ResponseMessages!));
|
||||
#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
return this.StoreAIContextAsync(subContext, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, processes invocation results at the end of the agent invocation.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</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.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is called from <see cref="InvokedCoreAsync"/>.
|
||||
/// Note that <see cref="InvokedCoreAsync"/> can be overridden to directly control error handling, in which case
|
||||
/// it is up to the implementer to call this method as needed to process the invocation results.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In contrast with <see cref="InvokedCoreAsync"/>, this method only processes the invocation results,
|
||||
/// while <see cref="InvokedCoreAsync"/> is also responsible for error handling.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of <see cref="InvokedCoreAsync"/> only calls this method if the invocation succeeded.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Messages being processed/stored may contain PII and sensitive conversation content.
|
||||
/// Implementers should ensure appropriate encryption at rest and access controls for the storage backend.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
|
||||
default;
|
||||
|
||||
/// <summary>Asks the <see cref="AIContextProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
|
||||
/// <param name="serviceType">The type of object being requested.</param>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AIContextProvider"/>,
|
||||
/// including itself or any services it might be wrapping. This enables advanced scenarios where consumers need access to
|
||||
/// specific provider implementations or their internal services.
|
||||
/// </remarks>
|
||||
public virtual object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
_ = Throw.IfNull(serviceType);
|
||||
|
||||
return serviceKey is null && serviceType.IsInstanceOfType(this)
|
||||
? this
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Asks the <see cref="AIContextProvider"/> for an object of type <typeparamref name="TService"/>.</summary>
|
||||
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AIContextProvider"/>,
|
||||
/// including itself or any services it might be wrapping. This is a convenience overload of <see cref="GetService(Type, object?)"/>.
|
||||
/// </remarks>
|
||||
public TService? GetService<TService>(object? serviceKey = null)
|
||||
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about the invocation before the underlying AI model is invoked, including the messages
|
||||
/// that will be used. Context providers can use this information to determine what additional context
|
||||
/// should be provided for the invocation.
|
||||
/// </remarks>
|
||||
public sealed class InvokingContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokingContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent being invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="aiContext">The AI context to be used by the agent for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agent"/> or <paramref name="aiContext"/> is <see langword="null"/>.</exception>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public InvokingContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
AIContext aiContext)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.AIContext = Throw.IfNull(aiContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent that is being invoked.
|
||||
/// </summary>
|
||||
public AIAgent Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent session associated with the agent invocation.
|
||||
/// </summary>
|
||||
public AgentSession? Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="AIContext"/> being built for the current invocation. Context providers can modify
|
||||
/// and return or return a new <see cref="AIContext"/> instance to provide additional context for the invocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If multiple <see cref="AIContextProvider"/> instances are used in the same invocation, each <see cref="AIContextProvider"/>
|
||||
/// will receive the context returned by the previous <see cref="AIContextProvider"/> allowing them to build on top of each other's context.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first <see cref="AIContextProvider"/> in the invocation pipeline will receive an <see cref="AIContext"/> instance
|
||||
/// that already contains the caller provided messages that will be used by the agent for this invocation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It may also contain messages from chat history, if a <see cref="ChatHistoryProvider"/> is being used.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public AIContext AIContext { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about a completed agent invocation, including the accumulated
|
||||
/// request messages (user input, chat history and any others provided by AI context providers) that were used
|
||||
/// and the response messages that were generated. It also indicates whether the invocation succeeded or failed.
|
||||
/// </remarks>
|
||||
public sealed class InvokedContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a successful invocation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that was invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="requestMessages">The accumulated request messages (user input, chat history and any others provided by AI context providers)
|
||||
/// that were used by the agent for this invocation.</param>
|
||||
/// <param name="responseMessages">The response messages generated during this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="responseMessages"/> is <see langword="null"/>.</exception>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public InvokedContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IEnumerable<ChatMessage> requestMessages,
|
||||
IEnumerable<ChatMessage> responseMessages)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
this.ResponseMessages = Throw.IfNull(responseMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a failed invocation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that was invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="requestMessages">The accumulated request messages (user input, chat history and any others provided by AI context providers)
|
||||
/// that were used by the agent for this invocation.</param>
|
||||
/// <param name="invokeException">The exception that caused the invocation to fail.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="invokeException"/> is <see langword="null"/>.</exception>
|
||||
public InvokedContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IEnumerable<ChatMessage> requestMessages,
|
||||
Exception invokeException)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
this.InvokeException = Throw.IfNull(invokeException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent that is being invoked.
|
||||
/// </summary>
|
||||
public AIAgent Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent session associated with the agent invocation.
|
||||
/// </summary>
|
||||
public AgentSession? Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the accumulated request messages (user input, chat history and any others provided by AI context providers)
|
||||
/// that were used by the agent for this invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing all messages that were used by the agent for this invocation.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing the response,
|
||||
/// or <see langword="null"/> if the invocation failed.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage>? ResponseMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The exception that caused the invocation to fail, or <see langword="null"/> if the invocation succeeded.
|
||||
/// </value>
|
||||
public Exception? InvokeException { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods to allow storing and retrieving properties using the type name of the property as the key.
|
||||
/// </summary>
|
||||
public static class AdditionalPropertiesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an additional property using the type name of the property as the key.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property to add.</typeparam>
|
||||
/// <param name="additionalProperties">The dictionary of additional properties.</param>
|
||||
/// <param name="value">The value to add.</param>
|
||||
public static void Add<T>(this AdditionalPropertiesDictionary additionalProperties, T value)
|
||||
{
|
||||
_ = Throw.IfNull(additionalProperties);
|
||||
|
||||
additionalProperties.Add(typeof(T).FullName!, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to add a property using the type name of the property as the key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method uses the full name of the type parameter as the key. If the key already exists,
|
||||
/// the value is not updated and the method returns <see langword="false"/>.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of the property to add.</typeparam>
|
||||
/// <param name="additionalProperties">The dictionary of additional properties.</param>
|
||||
/// <param name="value">The value to add.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the value was added successfully; <see langword="false"/> if the key already exists.
|
||||
/// </returns>
|
||||
public static bool TryAdd<T>(this AdditionalPropertiesDictionary additionalProperties, T value)
|
||||
{
|
||||
_ = Throw.IfNull(additionalProperties);
|
||||
|
||||
return additionalProperties.TryAdd(typeof(T).FullName!, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a value from the additional properties dictionary using the type name of the property as the key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method uses the full name of the type parameter as the key when searching the dictionary.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of the property to be retrieved.</typeparam>
|
||||
/// <param name="additionalProperties">The dictionary containing additional properties.</param>
|
||||
/// <param name="value">
|
||||
/// When this method returns, contains the value retrieved from the dictionary, if found and successfully converted to the requested type;
|
||||
/// otherwise, the default value of <typeparamref name="T"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if a non-<see langword="null"/> value was found
|
||||
/// in the dictionary and converted to the requested type; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool TryGetValue<T>(this AdditionalPropertiesDictionary additionalProperties, [NotNullWhen(true)] out T? value)
|
||||
{
|
||||
_ = Throw.IfNull(additionalProperties);
|
||||
|
||||
return additionalProperties.TryGetValue(typeof(T).FullName!, out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the additional properties dictionary contains a property with the name of the provided type as the key.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property to check for.</typeparam>
|
||||
/// <param name="additionalProperties">The dictionary of additional properties.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the dictionary contains a property with the name of the provided type as the key; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool Contains<T>(this AdditionalPropertiesDictionary additionalProperties)
|
||||
{
|
||||
_ = Throw.IfNull(additionalProperties);
|
||||
|
||||
return additionalProperties.ContainsKey(typeof(T).FullName!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a property from the additional properties dictionary using the name of the provided type as the key.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property to remove.</typeparam>
|
||||
/// <param name="additionalProperties">The dictionary of additional properties.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the property was successfully removed; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
public static bool Remove<T>(this AdditionalPropertiesDictionary additionalProperties)
|
||||
{
|
||||
_ = Throw.IfNull(additionalProperties);
|
||||
|
||||
return additionalProperties.Remove(typeof(T).FullName!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods and configurations for JSON serialization operations within the Microsoft Agent Framework.
|
||||
/// </summary>
|
||||
public static partial class AgentAbstractionsJsonUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for JSON serialization operations of agent abstraction types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
|
||||
/// includes source generated contracts for all common exchange types contained in this library.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It additionally turns on the following settings:
|
||||
/// <list type="number">
|
||||
/// <item><description>Enables <see cref="JsonSerializerDefaults.Web"/> defaults.</description></item>
|
||||
/// <item><description>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</description></item>
|
||||
/// <item><description>Enables <see cref="JsonNumberHandling.AllowReadingFromString"/> as the default number handling for number types.</description></item>
|
||||
/// <item><description>
|
||||
/// Enables <see cref="JavaScriptEncoder.UnsafeRelaxedJsonEscaping"/> when escaping JSON strings.
|
||||
/// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, such as HTML and XML.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures the default JSON serialization options for agent abstraction types.
|
||||
/// </summary>
|
||||
/// <returns>The configured options.</returns>
|
||||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
// Copy the configuration from the source generated context.
|
||||
JsonSerializerOptions options = new(JsonContext.Default.Options)
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as AIJsonUtilities
|
||||
};
|
||||
|
||||
// Chain in the resolvers from both AIJsonUtilities and our source generated context.
|
||||
// We want AIJsonUtilities first to ensure any M.E.AI types are handled via its resolver.
|
||||
options.TypeInfoResolverChain.Clear();
|
||||
options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
|
||||
options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!);
|
||||
|
||||
// If reflection-based serialization is enabled by default, this includes
|
||||
// the default type info resolver that utilizes reflection, but we need to manually
|
||||
// apply the same converter AIJsonUtilities adds for string-based enum serialization,
|
||||
// as that's not propagated as part of the resolver.
|
||||
if (JsonSerializer.IsReflectionEnabledByDefault)
|
||||
{
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
|
||||
|
||||
// Agent abstraction types
|
||||
[JsonSerializable(typeof(AgentRunOptions))]
|
||||
[JsonSerializable(typeof(AgentResponse))]
|
||||
[JsonSerializable(typeof(AgentResponse[]))]
|
||||
[JsonSerializable(typeof(AgentResponseUpdate))]
|
||||
[JsonSerializable(typeof(AgentResponseUpdate[]))]
|
||||
[JsonSerializable(typeof(InMemoryChatHistoryProvider.State))]
|
||||
[JsonSerializable(typeof(AgentSessionStateBag))]
|
||||
[JsonSerializable(typeof(ConcurrentDictionary<string, AgentSessionStateBagValue>))]
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed partial class JsonContext : JsonSerializerContext;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents attribution information for the source of an agent request message for a specific run, including the component type and
|
||||
/// identifier.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this struct to identify which component provided a message during an agent run.
|
||||
/// This is useful to allow filtering of messages based on their source, such as distinguishing between user input, middleware-generated messages, and chat history.
|
||||
/// </remarks>
|
||||
public readonly struct AgentRequestMessageSourceAttribution : IEquatable<AgentRequestMessageSourceAttribution>
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the key used in <see cref="ChatMessage.AdditionalProperties"/> to store the <see cref="AgentRequestMessageSourceAttribution"/>
|
||||
/// associated with the agent request message.
|
||||
/// </summary>
|
||||
public static readonly string AdditionalPropertiesKey = "_attribution";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceAttribution"/> struct with the specified source type and identifier.
|
||||
/// </summary>
|
||||
/// <param name="sourceType">The <see cref="AgentRequestMessageSourceType"/> of the component that provided the message.</param>
|
||||
/// <param name="sourceId">The unique identifier of the component that provided the message.</param>
|
||||
public AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType sourceType, string? sourceId)
|
||||
{
|
||||
this.SourceType = sourceType;
|
||||
this.SourceId = sourceId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of component that provided the message for the current agent run.
|
||||
/// </summary>
|
||||
public AgentRequestMessageSourceType SourceType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the component that provided the message for the current agent run.
|
||||
/// </summary>
|
||||
public string? SourceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="AgentRequestMessageSourceAttribution"/> is equal to the current instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="AgentRequestMessageSourceAttribution"/> to compare with the current instance.</param>
|
||||
/// <returns><see langword="true"/> if the specified instance is equal to the current instance; otherwise, <see langword="false"/>.</returns>
|
||||
public bool Equals(AgentRequestMessageSourceAttribution other)
|
||||
{
|
||||
return this.SourceType == other.SourceType &&
|
||||
string.Equals(this.SourceId, other.SourceId, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><see langword="true"/> if the specified object is equal to the current instance; otherwise, <see langword="false"/>.</returns>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is AgentRequestMessageSourceAttribution other && this.Equals(other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the current instance.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the source type and source identifier.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return this.SourceId is null
|
||||
? $"{this.SourceType}"
|
||||
: $"{this.SourceType}:{this.SourceId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for the current instance.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the current instance.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = (hash * 31) + this.SourceType.GetHashCode();
|
||||
hash = (hash * 31) + (this.SourceId?.GetHashCode() ?? 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two <see cref="AgentRequestMessageSourceAttribution"/> instances are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance to compare.</param>
|
||||
/// <param name="right">The second instance to compare.</param>
|
||||
/// <returns><see langword="true"/> if the instances are equal; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool operator ==(AgentRequestMessageSourceAttribution left, AgentRequestMessageSourceAttribution right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two <see cref="AgentRequestMessageSourceAttribution"/> instances are not equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance to compare.</param>
|
||||
/// <param name="right">The second instance to compare.</param>
|
||||
/// <returns><see langword="true"/> if the instances are not equal; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool operator !=(AgentRequestMessageSourceAttribution left, AgentRequestMessageSourceAttribution right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the source of an agent request message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Input messages for a specific agent run can originate from various sources.
|
||||
/// This type helps to identify whether a message came from outside the agent pipeline,
|
||||
/// whether it was produced by middleware, or came from chat history.
|
||||
/// </remarks>
|
||||
public readonly struct AgentRequestMessageSourceType : IEquatable<AgentRequestMessageSourceType>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentRequestMessageSourceType"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value representing the source of the agent request message.</param>
|
||||
public AgentRequestMessageSourceType(string value) => this.Value = Throw.IfNullOrWhitespace(value);
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value representing the source of the agent request message.
|
||||
/// </summary>
|
||||
public string Value { get { return field ?? External.Value; } }
|
||||
|
||||
/// <summary>
|
||||
/// The message came from outside the agent pipeline (e.g., user input).
|
||||
/// </summary>
|
||||
public static AgentRequestMessageSourceType External { get; } = new AgentRequestMessageSourceType(nameof(External));
|
||||
|
||||
/// <summary>
|
||||
/// The message was produced by middleware.
|
||||
/// </summary>
|
||||
public static AgentRequestMessageSourceType AIContextProvider { get; } = new AgentRequestMessageSourceType(nameof(AIContextProvider));
|
||||
|
||||
/// <summary>
|
||||
/// The message came from chat history.
|
||||
/// </summary>
|
||||
public static AgentRequestMessageSourceType ChatHistory { get; } = new AgentRequestMessageSourceType(nameof(ChatHistory));
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance and another specified <see cref="AgentRequestMessageSourceType"/> object have the same value.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="AgentRequestMessageSourceType"/> to compare to this instance.</param>
|
||||
/// <returns><see langword="true"/> if the value of the <paramref name="other"/> parameter is the same as the value of this instance; otherwise, <see langword="false"/>.</returns>
|
||||
public bool Equals(AgentRequestMessageSourceType other)
|
||||
{
|
||||
return string.Equals(this.Value, other.Value, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance and a specified object have the same value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare to this instance.</param>
|
||||
/// <returns><see langword="true"/> if <paramref name="obj"/> is a <see cref="AgentRequestMessageSourceType"/> and its value is the same as this instance; otherwise, <see langword="false"/>.</returns>
|
||||
public override bool Equals(object? obj) => obj is AgentRequestMessageSourceType other && this.Equals(other);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of this instance.
|
||||
/// </summary>
|
||||
/// <returns>The string value representing the source of the agent request message.</returns>
|
||||
public override string ToString() => this.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A 32-bit signed integer hash code.</returns>
|
||||
public override int GetHashCode() => this.Value?.GetHashCode() ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two specified <see cref="AgentRequestMessageSourceType"/> objects have the same value.
|
||||
/// </summary>
|
||||
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
|
||||
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
|
||||
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is the same as the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool operator ==(AgentRequestMessageSourceType left, AgentRequestMessageSourceType right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two specified <see cref="AgentRequestMessageSourceType"/> objects have different values.
|
||||
/// </summary>
|
||||
/// <param name="left">The first <see cref="AgentRequestMessageSourceType"/> to compare.</param>
|
||||
/// <param name="right">The second <see cref="AgentRequestMessageSourceType"/> to compare.</param>
|
||||
/// <returns><see langword="true"/> if the value of <paramref name="left"/> is different from the value of <paramref name="right"/>; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool operator !=(AgentRequestMessageSourceType left, AgentRequestMessageSourceType right) => !(left == right);
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response to an <see cref="AIAgent"/> run request, containing messages and metadata about the interaction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="AgentResponse"/> provides one or more response messages and metadata about the response.
|
||||
/// A typical response will contain a single message, however a response may contain multiple messages
|
||||
/// in a variety of scenarios. For example, if the agent internally invokes functions or tools, performs
|
||||
/// RAG retrievals or has other complex logic, a single run by the agent may produce many messages showing
|
||||
/// the intermediate progress that the agent made towards producing the agent result.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To get the text result of the response, use the <see cref="Text"/> property or simply call <see cref="ToString()"/> on the <see cref="AgentResponse"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class AgentResponse
|
||||
{
|
||||
/// <summary>The response messages.</summary>
|
||||
private IList<ChatMessage>? _messages;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentResponse"/> class.</summary>
|
||||
public AgentResponse()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentResponse"/> class.</summary>
|
||||
/// <param name="message">The response message to include in this response.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
|
||||
public AgentResponse(ChatMessage message)
|
||||
{
|
||||
_ = Throw.IfNull(message);
|
||||
|
||||
this.Messages.Add(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentResponse"/> class from an existing <see cref="ChatResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see cref="ChatResponse"/> from which to populate this <see cref="AgentResponse"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This constructor creates an agent response that wraps an existing <see cref="ChatResponse"/>, preserving all
|
||||
/// metadata and storing the original response in <see cref="RawRepresentation"/> for access to
|
||||
/// the underlying implementation details.
|
||||
/// </remarks>
|
||||
public AgentResponse(ChatResponse response)
|
||||
{
|
||||
_ = Throw.IfNull(response);
|
||||
|
||||
this.AdditionalProperties = response.AdditionalProperties;
|
||||
this.CreatedAt = response.CreatedAt;
|
||||
this.FinishReason = response.FinishReason;
|
||||
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 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>
|
||||
protected AgentResponse(AgentResponse response)
|
||||
{
|
||||
_ = Throw.IfNull(response);
|
||||
|
||||
this.AdditionalProperties = response.AdditionalProperties;
|
||||
this.CreatedAt = response.CreatedAt;
|
||||
this.FinishReason = response.FinishReason;
|
||||
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>
|
||||
/// <param name="messages">The collection of response messages, or <see langword="null"/> to create an empty response.</param>
|
||||
public AgentResponse(IList<ChatMessage>? messages)
|
||||
{
|
||||
this._messages = messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of messages to be represented by this response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing the agent's response.
|
||||
/// If the backing collection is <see langword="null"/>, accessing this property will create an empty list.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property provides access to all messages generated during the agent's execution. While most
|
||||
/// responses contain a single assistant message, complex agent behaviors may produce multiple messages
|
||||
/// showing intermediate steps, function calls, or different types of content.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The collection is mutable and can be modified after creation. Setting this property to <see langword="null"/>
|
||||
/// will cause subsequent access to return an empty list.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[AllowNull]
|
||||
public IList<ChatMessage> Messages
|
||||
{
|
||||
get => this._messages ??= new List<ChatMessage>(1);
|
||||
set => this._messages = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the concatenated text content of all messages in this response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A string containing the combined text from all <see cref="TextContent"/> instances
|
||||
/// across all messages in <see cref="Messages"/>, or an empty string if no text content is present.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// This property provides a convenient way to access the textual response without needing to
|
||||
/// iterate through individual messages and content items. Non-text content is ignored.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string Text => this._messages?.ConcatText() ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the agent that generated this response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A unique string identifier for the agent, or <see langword="null"/> if not specified.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// This identifier helps track which agent generated the response in multi-agent scenarios
|
||||
/// or for debugging and telemetry purposes.
|
||||
/// </remarks>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this specific response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A unique string identifier for this response instance, or <see langword="null"/> if not assigned.
|
||||
/// </value>
|
||||
public string? ResponseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the continuation token for getting the result of a background agent response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AIAgent"/> implementations that support background responses will return
|
||||
/// a continuation token if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
|
||||
/// and the result of the response has not been obtained yet. If the response has completed and the result has been obtained,
|
||||
/// the token will be <see langword="null"/>.
|
||||
/// <para>
|
||||
/// This property should be used in conjunction with <see cref="AgentRunOptions.ContinuationToken"/> to
|
||||
/// continue to poll for the completion of the response. Pass this token to
|
||||
/// <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>
|
||||
/// to poll for completion.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public ResponseContinuationToken? ContinuationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp indicating when this response was created.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="DateTimeOffset"/> representing when the response was generated,
|
||||
/// or <see langword="null"/> if not specified.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The creation timestamp is useful for auditing, logging, and understanding
|
||||
/// the chronology of agentic interactions.
|
||||
/// </remarks>
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason for the agent response finishing.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="ChatFinishReason"/> value indicating why the response finished (e.g., stop, length, content filter, tool calls),
|
||||
/// or <see langword="null"/> if the finish reason is not available.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property is particularly useful for detecting non-normal completions, such as content filtering
|
||||
/// or token limit truncation, which may require special handling by the caller.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ChatFinishReason? FinishReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resource usage information for generating this response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="UsageDetails"/> instance containing token counts and other usage metrics,
|
||||
/// or <see langword="null"/> if usage information is not available.
|
||||
/// </value>
|
||||
public UsageDetails? Usage { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the raw representation of the run response from an underlying implementation.</summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="AgentResponse"/> is created to represent some underlying object from another object
|
||||
/// model, this property can be used to store that original object. This can be useful for debugging or
|
||||
/// for enabling a consumer to access the underlying object model if needed.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public object? RawRepresentation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional properties associated with this response.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An <see cref="AdditionalPropertiesDictionary"/> containing custom properties,
|
||||
/// or <see langword="null"/> if no additional properties are present.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Additional properties provide a way to include custom metadata or provider-specific
|
||||
/// information that doesn't fit into the standard response schema. This is useful for
|
||||
/// preserving implementation-specific details or extending the response with custom data.
|
||||
/// </remarks>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => this.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Converts this <see cref="AgentResponse"/> into a collection of <see cref="AgentResponseUpdate"/> instances
|
||||
/// suitable for streaming scenarios.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An array of <see cref="AgentResponseUpdate"/> instances that collectively represent
|
||||
/// the same information as this response.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is useful for converting complete responses back into streaming format,
|
||||
/// which may be needed for scenarios that require uniform handling of both streaming
|
||||
/// and non-streaming agent responses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each message in <see cref="Messages"/> becomes a separate update, and usage information
|
||||
/// is included as an additional update if present. The order of updates preserves the
|
||||
/// original message sequence.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public AgentResponseUpdate[] ToAgentResponseUpdates()
|
||||
{
|
||||
AgentResponseUpdate? extra = null;
|
||||
if (this.AdditionalProperties is not null || this.Usage is not null)
|
||||
{
|
||||
extra = new AgentResponseUpdate
|
||||
{
|
||||
AdditionalProperties = this.AdditionalProperties,
|
||||
};
|
||||
|
||||
if (this.Usage is { } usage)
|
||||
{
|
||||
extra.Contents.Add(new UsageContent(usage));
|
||||
}
|
||||
}
|
||||
|
||||
int messageCount = this._messages?.Count ?? 0;
|
||||
var updates = new AgentResponseUpdate[messageCount + (extra is not null ? 1 : 0)];
|
||||
|
||||
int i;
|
||||
for (i = 0; i < messageCount; i++)
|
||||
{
|
||||
ChatMessage message = this._messages![i];
|
||||
updates[i] = new AgentResponseUpdate
|
||||
{
|
||||
AdditionalProperties = message.AdditionalProperties,
|
||||
AuthorName = message.AuthorName,
|
||||
Contents = message.Contents,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
Role = message.Role,
|
||||
|
||||
FinishReason = this.FinishReason,
|
||||
AgentId = this.AgentId,
|
||||
ResponseId = this.ResponseId,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt ?? this.CreatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
if (extra is not null)
|
||||
{
|
||||
updates[i] = extra;
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for working with <see cref="AgentResponse"/> and <see cref="AgentResponseUpdate"/> instances.
|
||||
/// </summary>
|
||||
public static class AgentResponseExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ChatResponse"/> from an <see cref="AgentResponse"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="response">The <see cref="AgentResponse"/> to convert.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> built from the specified <paramref name="response"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="response"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// If the <paramref name="response"/>'s <see cref="AgentResponse.RawRepresentation"/> is already a
|
||||
/// <see cref="ChatResponse"/> instance, that instance is returned directly.
|
||||
/// Otherwise, a new <see cref="ChatResponse"/> is created and populated with the data from the <paramref name="response"/>.
|
||||
/// The resulting instance is a shallow copy; any reference-type members (e.g. <see cref="AgentResponse.Messages"/>)
|
||||
/// will be shared between the two instances.
|
||||
/// </remarks>
|
||||
public static ChatResponse AsChatResponse(this AgentResponse response)
|
||||
{
|
||||
Throw.IfNull(response);
|
||||
|
||||
return
|
||||
response.RawRepresentation as ChatResponse ??
|
||||
new()
|
||||
{
|
||||
AdditionalProperties = response.AdditionalProperties,
|
||||
CreatedAt = response.CreatedAt,
|
||||
FinishReason = response.FinishReason,
|
||||
Messages = response.Messages,
|
||||
RawRepresentation = response,
|
||||
ResponseId = response.ResponseId,
|
||||
Usage = response.Usage,
|
||||
ContinuationToken = response.ContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ChatResponseUpdate"/> from an <see cref="AgentResponseUpdate"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="responseUpdate">The <see cref="AgentResponseUpdate"/> to convert.</param>
|
||||
/// <returns>A <see cref="ChatResponseUpdate"/> built from the specified <paramref name="responseUpdate"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="responseUpdate"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// If the <paramref name="responseUpdate"/>'s <see cref="AgentResponseUpdate.RawRepresentation"/> is already a
|
||||
/// <see cref="ChatResponseUpdate"/> instance, that instance is returned directly.
|
||||
/// Otherwise, a new <see cref="ChatResponseUpdate"/> is created and populated with the data from the <paramref name="responseUpdate"/>.
|
||||
/// The resulting instance is a shallow copy; any reference-type members (e.g. <see cref="AgentResponseUpdate.Contents"/>)
|
||||
/// will be shared between the two instances.
|
||||
/// </remarks>
|
||||
public static ChatResponseUpdate AsChatResponseUpdate(this AgentResponseUpdate responseUpdate)
|
||||
{
|
||||
Throw.IfNull(responseUpdate);
|
||||
|
||||
return
|
||||
responseUpdate.RawRepresentation as ChatResponseUpdate ??
|
||||
new()
|
||||
{
|
||||
AdditionalProperties = responseUpdate.AdditionalProperties,
|
||||
AuthorName = responseUpdate.AuthorName,
|
||||
Contents = responseUpdate.Contents,
|
||||
CreatedAt = responseUpdate.CreatedAt,
|
||||
FinishReason = responseUpdate.FinishReason,
|
||||
MessageId = responseUpdate.MessageId,
|
||||
RawRepresentation = responseUpdate,
|
||||
ResponseId = responseUpdate.ResponseId,
|
||||
Role = responseUpdate.Role,
|
||||
ContinuationToken = responseUpdate.ContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an asynchronous enumerable of <see cref="ChatResponseUpdate"/> instances from an asynchronous
|
||||
/// enumerable of <see cref="AgentResponseUpdate"/> instances.
|
||||
/// </summary>
|
||||
/// <param name="responseUpdates">The sequence of <see cref="AgentResponseUpdate"/> instances to convert.</param>
|
||||
/// <returns>An asynchronous enumerable of <see cref="ChatResponseUpdate"/> instances built from <paramref name="responseUpdates"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="responseUpdates"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// Each <see cref="AgentResponseUpdate"/> is converted to a <see cref="ChatResponseUpdate"/> using
|
||||
/// <see cref="AsChatResponseUpdate"/>.
|
||||
/// </remarks>
|
||||
public static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAsync(
|
||||
this IAsyncEnumerable<AgentResponseUpdate> responseUpdates)
|
||||
{
|
||||
Throw.IfNull(responseUpdates);
|
||||
|
||||
await foreach (var responseUpdate in responseUpdates.ConfigureAwait(false))
|
||||
{
|
||||
yield return responseUpdate.AsChatResponseUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines a sequence of <see cref="AgentResponseUpdate"/> instances into a single <see cref="AgentResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="updates">The sequence of updates to be combined into a single response.</param>
|
||||
/// <returns>A single <see cref="AgentResponse"/> that represents the combined state of all the updates.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="updates"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// As part of combining <paramref name="updates"/> into a single <see cref="AgentResponse"/>, the method will attempt to reconstruct
|
||||
/// <see cref="ChatMessage"/> instances. This includes using <see cref="AgentResponseUpdate.MessageId"/> to determine
|
||||
/// message boundaries, as well as coalescing contiguous <see cref="AIContent"/> items where applicable, e.g. multiple
|
||||
/// <see cref="TextContent"/> instances in a row may be combined into a single <see cref="TextContent"/>.
|
||||
/// </remarks>
|
||||
public static AgentResponse ToAgentResponse(
|
||||
this IEnumerable<AgentResponseUpdate> updates)
|
||||
{
|
||||
_ = Throw.IfNull(updates);
|
||||
|
||||
AgentResponseDetails additionalDetails = new();
|
||||
ChatResponse chatResponse =
|
||||
AsChatResponseUpdatesWithAdditionalDetails(updates, additionalDetails)
|
||||
.ToChatResponse();
|
||||
|
||||
return new AgentResponse(chatResponse)
|
||||
{
|
||||
AgentId = additionalDetails.AgentId,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously combines a sequence of <see cref="AgentResponseUpdate"/> instances into a single <see cref="AgentResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="updates">The asynchronous sequence of updates to be combined into a single response.</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 a single <see cref="AgentResponse"/> that represents the combined state of all the updates.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="updates"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the asynchronous version of <see cref="ToAgentResponse(IEnumerable{AgentResponseUpdate})"/>.
|
||||
/// It performs the same combining logic but operates on an asynchronous enumerable of updates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// As part of combining <paramref name="updates"/> into a single <see cref="AgentResponse"/>, the method will attempt to reconstruct
|
||||
/// <see cref="ChatMessage"/> instances. This includes using <see cref="AgentResponseUpdate.MessageId"/> to determine
|
||||
/// message boundaries, as well as coalescing contiguous <see cref="AIContent"/> items where applicable, e.g. multiple
|
||||
/// <see cref="TextContent"/> instances in a row may be combined into a single <see cref="TextContent"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static Task<AgentResponse> ToAgentResponseAsync(
|
||||
this IAsyncEnumerable<AgentResponseUpdate> updates,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(updates);
|
||||
|
||||
return ToAgentResponseAsync(updates, cancellationToken);
|
||||
|
||||
static async Task<AgentResponse> ToAgentResponseAsync(
|
||||
IAsyncEnumerable<AgentResponseUpdate> updates,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
AgentResponseDetails additionalDetails = new();
|
||||
ChatResponse chatResponse = await
|
||||
AsChatResponseUpdatesWithAdditionalDetailsAsync(updates, additionalDetails, cancellationToken)
|
||||
.ToChatResponseAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new AgentResponse(chatResponse)
|
||||
{
|
||||
AgentId = additionalDetails.AgentId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ChatResponseUpdate> AsChatResponseUpdatesWithAdditionalDetails(
|
||||
IEnumerable<AgentResponseUpdate> updates,
|
||||
AgentResponseDetails additionalDetails)
|
||||
{
|
||||
foreach (var update in updates)
|
||||
{
|
||||
UpdateAdditionalDetails(update, additionalDetails);
|
||||
yield return update.AsChatResponseUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesWithAdditionalDetailsAsync(
|
||||
IAsyncEnumerable<AgentResponseUpdate> updates,
|
||||
AgentResponseDetails additionalDetails,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
UpdateAdditionalDetails(update, additionalDetails);
|
||||
yield return update.AsChatResponseUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateAdditionalDetails(AgentResponseUpdate update, AgentResponseDetails details)
|
||||
{
|
||||
if (update.AgentId is { Length: > 0 })
|
||||
{
|
||||
details.AgentId = update.AgentId;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AgentResponseDetails
|
||||
{
|
||||
public string? AgentId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single streaming response chunk from an <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="AgentResponseUpdate"/> is so named because it represents updates
|
||||
/// that layer on each other to form a single agent response. Conceptually, this combines the roles of
|
||||
/// <see cref="AgentResponse"/> and <see cref="ChatMessage"/> in streaming output.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To get the text result of this response chunk, use the <see cref="Text"/> property or simply call <see cref="ToString()"/> on the <see cref="AgentResponseUpdate"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The relationship between <see cref="AgentResponse"/> and <see cref="AgentResponseUpdate"/> is
|
||||
/// codified in the <see cref="AgentResponseExtensions.ToAgentResponseAsync"/> and
|
||||
/// <see cref="AgentResponse.ToAgentResponseUpdates"/>, which enable bidirectional conversions
|
||||
/// between the two. Note, however, that the provided conversions may be lossy, for example if multiple
|
||||
/// updates all have different <see cref="RawRepresentation"/> objects whereas there's only one slot for
|
||||
/// such an object available in <see cref="AgentResponse.RawRepresentation"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("[{Role}] {ContentForDebuggerDisplay}{EllipsesForDebuggerDisplay,nq}")]
|
||||
public class AgentResponseUpdate
|
||||
{
|
||||
/// <summary>The response update content items.</summary>
|
||||
private IList<AIContent>? _contents;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
|
||||
[JsonConstructor]
|
||||
public AgentResponseUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
|
||||
/// <param name="role">The role of the author of the update.</param>
|
||||
/// <param name="content">The text content of the update.</param>
|
||||
public AgentResponseUpdate(ChatRole? role, string? content)
|
||||
: this(role, content is null ? null : [new TextContent(content)])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
|
||||
/// <param name="role">The role of the author of the update.</param>
|
||||
/// <param name="contents">The contents of the update.</param>
|
||||
public AgentResponseUpdate(ChatRole? role, IList<AIContent>? contents)
|
||||
{
|
||||
this.Role = role;
|
||||
this._contents = contents;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="AgentResponseUpdate"/> class.</summary>
|
||||
/// <param name="chatResponseUpdate">The <see cref="ChatResponseUpdate"/> from which to seed this <see cref="AgentResponseUpdate"/>.</param>
|
||||
public AgentResponseUpdate(ChatResponseUpdate chatResponseUpdate)
|
||||
{
|
||||
_ = Throw.IfNull(chatResponseUpdate);
|
||||
|
||||
this.AdditionalProperties = chatResponseUpdate.AdditionalProperties;
|
||||
this.AuthorName = chatResponseUpdate.AuthorName;
|
||||
this.Contents = chatResponseUpdate.Contents;
|
||||
this.CreatedAt = chatResponseUpdate.CreatedAt;
|
||||
this.FinishReason = chatResponseUpdate.FinishReason;
|
||||
this.MessageId = chatResponseUpdate.MessageId;
|
||||
this.RawRepresentation = chatResponseUpdate;
|
||||
this.ResponseId = chatResponseUpdate.ResponseId;
|
||||
this.Role = chatResponseUpdate.Role;
|
||||
this.ContinuationToken = chatResponseUpdate.ContinuationToken;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the name of the author of the response update.</summary>
|
||||
public string? AuthorName
|
||||
{
|
||||
get => field;
|
||||
set => field = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the role of the author of the response update.</summary>
|
||||
public ChatRole? Role { get; set; }
|
||||
|
||||
/// <summary>Gets the text of this update.</summary>
|
||||
/// <remarks>
|
||||
/// This property concatenates the text of all <see cref="TextContent"/> objects in <see cref="Contents"/>.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string Text => this._contents is not null ? this._contents.ConcatText() : string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the agent run response update content items.</summary>
|
||||
[AllowNull]
|
||||
public IList<AIContent> Contents
|
||||
{
|
||||
get => this._contents ??= [];
|
||||
set => this._contents = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the raw representation of the response update from an underlying implementation.</summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="AgentResponseUpdate"/> is created to represent some underlying object from another object
|
||||
/// model, this property can be used to store that original object. This can be useful for debugging or
|
||||
/// for enabling a consumer to access the underlying object model if needed.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public object? RawRepresentation { get; set; }
|
||||
|
||||
/// <summary>Gets or sets additional properties for the update.</summary>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the agent that produced the response.</summary>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the response of which this update is a part.</summary>
|
||||
public string? ResponseId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the ID of the message of which this update is a part.</summary>
|
||||
/// <remarks>
|
||||
/// A single streaming response may be composed of multiple messages, each of which may be represented
|
||||
/// by multiple updates. This property is used to group those updates together into messages.
|
||||
///
|
||||
/// Some providers may consider streaming responses to be a single message, and in that case
|
||||
/// the value of this property may be the same as the response ID.
|
||||
///
|
||||
/// This value is used when <see cref="AgentResponseExtensions.ToAgentResponseAsync(IAsyncEnumerable{AgentResponseUpdate}, System.Threading.CancellationToken)"/>
|
||||
/// groups <see cref="AgentResponseUpdate"/> instances into <see cref="AgentResponse"/> instances.
|
||||
/// The value must be unique to each call to the underlying provider, and must be shared by
|
||||
/// all updates that are part of the same logical message within a streaming response.
|
||||
/// </remarks>
|
||||
public string? MessageId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets a timestamp for the response update.</summary>
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the continuation token for resuming the streamed agent response of which this update is a part.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AIAgent"/> implementations that support background responses will return
|
||||
/// a continuation token on each update if background responses are allowed in <see cref="AgentRunOptions.AllowBackgroundResponses"/>
|
||||
/// except for the last update, for which the token will be <see langword="null"/>.
|
||||
/// <para>
|
||||
/// This property should be used for stream resumption, where the continuation token of the latest received update should be
|
||||
/// passed to <see cref="AgentRunOptions.ContinuationToken"/> on subsequent calls to <see cref="AIAgent.RunStreamingAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>
|
||||
/// to resume streaming from the point of interruption.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ResponseContinuationToken? ContinuationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason for the agent response finishing.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="ChatFinishReason"/> value indicating why the response finished (e.g., stop, length, content filter, tool calls),
|
||||
/// or <see langword="null"/> if the finish reason is not available or not yet determined (mid-stream).
|
||||
/// </value>
|
||||
public ChatFinishReason? FinishReason { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.Text;
|
||||
|
||||
/// <summary>Gets a <see cref="AIContent"/> object to display in the debugger display.</summary>
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
private AIContent? ContentForDebuggerDisplay => this._contents is { Count: > 0 } ? this._contents[0] : null;
|
||||
|
||||
/// <summary>Gets an indication for the debugger display of whether there's more content.</summary>
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
private string EllipsesForDebuggerDisplay => this._contents is { Count: > 1 } ? ", ..." : string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
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;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
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 class AgentResponse<T> : AgentResponse
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentResponse{T}"/> class.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serializerOptions"/> is <see langword="null"/>.</exception>
|
||||
public AgentResponse(AgentResponse response, JsonSerializerOptions serializerOptions) : base(response)
|
||||
{
|
||||
_ = Throw.IfNull(serializerOptions);
|
||||
|
||||
this._serializerOptions = serializerOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the JSON schema has an extra object wrapper.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The wrapper is required for any non-JSON-object-typed values such as numbers, enum values, and arrays.
|
||||
/// </remarks>
|
||||
public bool IsWrappedInObject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result value of the agent response as an instance of <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public virtual T Result
|
||||
{
|
||||
get
|
||||
{
|
||||
var json = this.Text;
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
throw new InvalidOperationException("The response did not contain JSON to be deserialized.");
|
||||
}
|
||||
|
||||
if (this.IsWrappedInObject)
|
||||
{
|
||||
json = StructuredOutputSchemaUtilities.UnwrapResponseData(json!);
|
||||
}
|
||||
|
||||
T? deserialized = DeserializeFirstTopLevelObject(json!, (JsonTypeInfo<T>)this._serializerOptions.GetTypeInfo(typeof(T)));
|
||||
if (deserialized is null)
|
||||
{
|
||||
throw new InvalidOperationException("The deserialized response is null.");
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>Provides context for an in-flight agent run.</summary>
|
||||
public sealed class AgentRunContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentRunContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agent">The <see cref="AIAgent"/> that is executing the current run.</param>
|
||||
/// <param name="session">The <see cref="AgentSession"/> that is associated with the current run if any.</param>
|
||||
/// <param name="requestMessages">The request messages passed into the current run.</param>
|
||||
/// <param name="agentRunOptions">The <see cref="AgentRunOptions"/> that was passed to the current run.</param>
|
||||
public AgentRunContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IReadOnlyCollection<ChatMessage> requestMessages,
|
||||
AgentRunOptions? agentRunOptions)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
this.RunOptions = agentRunOptions;
|
||||
}
|
||||
|
||||
/// <summary>Gets the <see cref="AIAgent"/> that is executing the current run.</summary>
|
||||
public AIAgent Agent { get; }
|
||||
|
||||
/// <summary>Gets the <see cref="AgentSession"/> that is associated with the current run.</summary>
|
||||
public AgentSession? Session { get; }
|
||||
|
||||
/// <summary>Gets the request messages passed into the current run.</summary>
|
||||
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
|
||||
|
||||
/// <summary>Gets the <see cref="AgentRunOptions"/> that was passed to the current run.</summary>
|
||||
public AgentRunOptions? RunOptions { get; }
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides optional parameters and configuration settings for controlling agent run behavior.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementations of <see cref="AIAgent"/> may provide subclasses of <see cref="AgentRunOptions"/> with additional options specific to that agent type.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class AgentRunOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentRunOptions"/> class.
|
||||
/// </summary>
|
||||
public AgentRunOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentRunOptions"/> class by copying values from the specified options.
|
||||
/// </summary>
|
||||
/// <param name="options">The options instance from which to copy values.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
|
||||
protected AgentRunOptions(AgentRunOptions options)
|
||||
{
|
||||
_ = Throw.IfNull(options);
|
||||
this.ContinuationToken = options.ContinuationToken;
|
||||
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
|
||||
this.AdditionalProperties = options.AdditionalProperties?.Clone();
|
||||
this.ResponseFormat = options.ResponseFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the continuation token for resuming and getting the result of the agent response identified by this token.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is used for background responses that can be activated via the <see cref="AllowBackgroundResponses"/>
|
||||
/// property if the <see cref="AIAgent"/> implementation supports them.
|
||||
/// Streamed background responses, such as those returned by default by <see cref="AIAgent.RunStreamingAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>
|
||||
/// can be resumed if interrupted. This means that a continuation token obtained from the <see cref="AgentResponseUpdate.ContinuationToken"/>
|
||||
/// of an update just before the interruption occurred can be passed to this property to resume the stream from the point of interruption.
|
||||
/// Non-streamed background responses, such as those returned by <see cref="AIAgent.RunAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>,
|
||||
/// can be polled for completion by obtaining the token from the <see cref="AgentResponse.ContinuationToken"/> property
|
||||
/// and passing it via this property on subsequent calls to <see cref="AIAgent.RunAsync(AgentSession?, AgentRunOptions?, System.Threading.CancellationToken)"/>.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public ResponseContinuationToken? ContinuationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the background responses are allowed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Background responses allow running long-running operations or tasks asynchronously in the background that can be resumed by streaming APIs
|
||||
/// and polled for completion by non-streaming APIs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When this property is set to true, non-streaming APIs may start a background operation and return an initial
|
||||
/// response with a continuation token. Subsequent calls to the same API should be made in a polling manner with
|
||||
/// the continuation token to get the final result of the operation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When this property is set to true, streaming APIs may also start a background operation and begin streaming
|
||||
/// response updates until the operation is completed. If the streaming connection is interrupted, the
|
||||
/// continuation token obtained from the last update that has one should be supplied to a subsequent call to the same streaming API
|
||||
/// to resume the stream from the point of interruption and continue receiving updates until the operation is completed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property only takes effect if the implementation it's used with supports background responses.
|
||||
/// If the implementation does not support background responses, this property will be ignored.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool? AllowBackgroundResponses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional properties associated with these options.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An <see cref="AdditionalPropertiesDictionary"/> containing custom properties,
|
||||
/// or <see langword="null"/> if no additional properties are present.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Additional properties provide a way to include custom metadata or provider-specific
|
||||
/// information that doesn't fit into the standard options schema. This is useful for
|
||||
/// preserving implementation-specific details or extending the options with custom data.
|
||||
/// </remarks>
|
||||
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see langword="null"/>, no response format is specified and the agent will use its default.
|
||||
/// This property can be set to <see cref="ChatResponseFormat.Text"/> to specify that the response should be unstructured text,
|
||||
/// to <see cref="ChatResponseFormat.Json"/> to specify that the response should be structured JSON data, or
|
||||
/// an instance of <see cref="ChatResponseFormatJson"/> constructed with a specific JSON schema to request that the
|
||||
/// response be structured JSON data according to that schema. It is up to the agent implementation if or how
|
||||
/// to honor the request. If the agent implementation doesn't recognize the specific kind of <see cref="ChatResponseFormat"/>,
|
||||
/// it can be ignored.
|
||||
/// </remarks>
|
||||
public ChatResponseFormat? ResponseFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Produces a clone of the current <see cref="AgentRunOptions"/> instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A clone of the current <see cref="AgentRunOptions"/> instance.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The clone will have the same values for all properties as the original instance. Any collections, like <see cref="AdditionalProperties"/>,
|
||||
/// are shallow-cloned, meaning a new collection instance is created, but any references contained by the collections are shared with the original.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Derived types should override <see cref="Clone"/> to return an instance of the derived type.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public virtual AgentRunOptions Clone() => new(this);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Base abstraction for all agent threads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// An <see cref="AgentSession"/> contains the state of a specific conversation with an agent which may include:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Conversation history or a reference to externally stored conversation history.</description></item>
|
||||
/// <item><description>Memories or a reference to externally stored memories.</description></item>
|
||||
/// <item><description>Any other state that the agent needs to persist across runs for a conversation.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// An <see cref="AgentSession"/> may also have behaviors attached to it that may include:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Customized storage of state.</description></item>
|
||||
/// <item><description>Data extraction from and injection into a conversation.</description></item>
|
||||
/// <item><description>Chat history reduction, e.g. where messages needs to be summarized or truncated to reduce the size.</description></item>
|
||||
/// </list>
|
||||
/// An <see cref="AgentSession"/> is always constructed by an <see cref="AIAgent"/> so that the <see cref="AIAgent"/>
|
||||
/// can attach any necessary behaviors to the <see cref="AgentSession"/>. See the <see cref="AIAgent.CreateSessionAsync(System.Threading.CancellationToken)"/>
|
||||
/// and <see cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> methods for more information.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because of these behaviors, an <see cref="AgentSession"/> may not be reusable across different agents, since each agent
|
||||
/// may add different behaviors to the <see cref="AgentSession"/> it creates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To support conversations that may need to survive application restarts or separate service requests, an <see cref="AgentSession"/> can be serialized
|
||||
/// and deserialized, so that it can be saved in a persistent store.
|
||||
/// The <see cref="AIAgent"/> provides the <see cref="AIAgent.SerializeSessionAsync(AgentSession, JsonSerializerOptions?, System.Threading.CancellationToken)"/> method to serialize the session to a
|
||||
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> method
|
||||
/// can be used to deserialize the session.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security considerations:</strong> Serialized sessions may contain conversation content, session identifiers,
|
||||
/// and other potentially sensitive data including PII. Developers should:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Treat serialized session data as sensitive and store it securely with appropriate access controls and encryption at rest.</description></item>
|
||||
/// <item><description>Treat restoring a session from an untrusted source as equivalent to accepting untrusted input. A compromised storage backend
|
||||
/// could alter message roles to escalate trust, or inject adversarial content that influences LLM behavior.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="AIAgent"/>
|
||||
/// <seealso cref="AIAgent.CreateSessionAsync(System.Threading.CancellationToken)"/>
|
||||
/// <seealso cref="AIAgent.DeserializeSessionAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/>
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public abstract class AgentSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSession"/> class.
|
||||
/// </summary>
|
||||
protected AgentSession()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSession"/> class.
|
||||
/// </summary>
|
||||
protected AgentSession(AgentSessionStateBag stateBag)
|
||||
{
|
||||
this.StateBag = Throw.IfNull(stateBag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets any arbitrary state associated with this session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Data stored in the <see cref="StateBag"/> will be included when the session is serialized.
|
||||
/// Avoid storing secrets, credentials, or highly sensitive data in the state bag without appropriate encryption,
|
||||
/// as this data may be persisted to external storage.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("stateBag")]
|
||||
public AgentSessionStateBag StateBag { get; protected set; } = new();
|
||||
|
||||
/// <summary>Asks the <see cref="AgentSession"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
|
||||
/// <param name="serviceType">The type of object being requested.</param>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AgentSession"/>,
|
||||
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatHistoryProvider"/> if available for the instance,
|
||||
/// <see cref="GetService"/> may be used to request it.
|
||||
/// </remarks>
|
||||
public virtual object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
_ = Throw.IfNull(serviceType);
|
||||
|
||||
return serviceKey is null && serviceType.IsInstanceOfType(this)
|
||||
? this
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Asks the <see cref="AgentSession"/> for an object of type <typeparamref name="TService"/>.</summary>
|
||||
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AgentSession"/>,
|
||||
/// including itself or any services it might be wrapping.
|
||||
/// </remarks>
|
||||
public TService? GetService<TService>(object? serviceKey = null)
|
||||
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string DebuggerDisplay => $"StateBag Count = {this.StateBag.Count}";
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="AgentSession"/>.
|
||||
/// </summary>
|
||||
public static class AgentSessionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the in-memory chat history messages associated with the specified agent session, if the agent is storing memories in the session using the <see cref="InMemoryChatHistoryProvider"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is only applicable when using <see cref="InMemoryChatHistoryProvider"/> and if the service does not require in-service chat history storage.
|
||||
/// </remarks>
|
||||
/// <param name="session">The agent session from which to retrieve in-memory chat history.</param>
|
||||
/// <param name="messages">When this method returns, contains the list of chat history messages if available; otherwise, null.</param>
|
||||
/// <param name="stateKey">An optional key used to identify the chat history state in the session's state bag. If null, the default key for
|
||||
/// in-memory chat history is used.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional JSON serializer options to use when accessing the session state. If null, default options are used.</param>
|
||||
/// <returns><see langword="true"/> if the in-memory chat history messages were found and retrieved; <see langword="false"/> otherwise.</returns>
|
||||
public static bool TryGetInMemoryChatHistory(this AgentSession session, [MaybeNullWhen(false)] out List<ChatMessage> messages, string? stateKey = null, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
_ = Throw.IfNull(session);
|
||||
|
||||
if (session.StateBag.TryGetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), out InMemoryChatHistoryProvider.State? state, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions) && state?.Messages is not null)
|
||||
{
|
||||
messages = state.Messages;
|
||||
return true;
|
||||
}
|
||||
|
||||
messages = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the in-memory chat message history for the specified agent session, replacing any existing messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is only applicable when using <see cref="InMemoryChatHistoryProvider"/> and if the service does not require in-service chat history storage.
|
||||
/// If messages are set, but a different <see cref="ChatHistoryProvider"/> is used, or if chat history is stored in the underlying AI service, the messages will be ignored.
|
||||
/// </remarks>
|
||||
/// <param name="session">The agent session whose in-memory chat history will be updated.</param>
|
||||
/// <param name="messages">The list of chat messages to store in memory for the session. Replaces any existing messages for the specified
|
||||
/// state key.</param>
|
||||
/// <param name="stateKey">The key used to identify the in-memory chat history within the session's state bag. If null, a default key is
|
||||
/// used.</param>
|
||||
/// <param name="jsonSerializerOptions">The serializer options used when accessing or storing the state. If null, default options are applied.</param>
|
||||
public static void SetInMemoryChatHistory(this AgentSession session, List<ChatMessage> messages, string? stateKey = null, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
_ = Throw.IfNull(session);
|
||||
|
||||
if (session.StateBag.TryGetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), out InMemoryChatHistoryProvider.State? state, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions) && state is not null)
|
||||
{
|
||||
state.Messages = messages;
|
||||
return;
|
||||
}
|
||||
|
||||
session.StateBag.SetValue(stateKey ?? nameof(InMemoryChatHistoryProvider), new InMemoryChatHistoryProvider.State() { Messages = messages }, jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a thread-safe key-value store for managing session-scoped state with support for type-safe access and JSON
|
||||
/// serialization options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SessionState enables storing and retrieving objects associated with a session using string keys.
|
||||
/// Values can be accessed in a type-safe manner and are serialized or deserialized using configurable JSON serializer
|
||||
/// options. This class is designed for concurrent access and is safe to use across multiple threads.
|
||||
/// </remarks>
|
||||
[JsonConverter(typeof(AgentSessionStateBagJsonConverter))]
|
||||
public class AgentSessionStateBag
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, AgentSessionStateBagValue> _state;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSessionStateBag"/> class.
|
||||
/// </summary>
|
||||
public AgentSessionStateBag()
|
||||
{
|
||||
this._state = new ConcurrentDictionary<string, AgentSessionStateBagValue>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentSessionStateBag"/> class.
|
||||
/// </summary>
|
||||
/// <param name="state">The initial state dictionary.</param>
|
||||
internal AgentSessionStateBag(ConcurrentDictionary<string, AgentSessionStateBagValue>? state)
|
||||
{
|
||||
this._state = state ?? new ConcurrentDictionary<string, AgentSessionStateBagValue>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of key-value pairs contained in the session state.
|
||||
/// </summary>
|
||||
public int Count => this._state.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a value from the session state.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value to retrieve.</typeparam>
|
||||
/// <param name="key">The key from which to retrieve the value.</param>
|
||||
/// <param name="value">The value if found and convertible to the required type; otherwise, null.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing/deserializing the value.</param>
|
||||
/// <returns><see langword="true"/> if the value was successfully retrieved, <see langword="false"/> otherwise.</returns>
|
||||
public bool TryGetValue<T>(string key, out T? value, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
where T : class
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(key);
|
||||
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
if (this._state.TryGetValue(key, out var stateValue))
|
||||
{
|
||||
return stateValue.TryReadDeserializedValue(out value, jso);
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value from the session state.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value to get.</typeparam>
|
||||
/// <param name="key">The key from which to retrieve the value.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing/deserialing the value.</param>
|
||||
/// <returns>The retrieved value or null if not found.</returns>
|
||||
/// <exception cref="InvalidOperationException">The value could not be deserialized into the required type.</exception>
|
||||
public T? GetValue<T>(string key, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
where T : class
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(key);
|
||||
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
if (this._state.TryGetValue(key, out var stateValue))
|
||||
{
|
||||
return stateValue.ReadDeserializedValue<T>(jso);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a value in the session state.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value to set.</typeparam>
|
||||
/// <param name="key">The key to store the value under.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing the value.</param>
|
||||
public void SetValue<T>(string key, T? value, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
where T : class
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(key);
|
||||
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
var stateValue = this._state.GetOrAdd(key, _ =>
|
||||
new AgentSessionStateBagValue(value, typeof(T), jso));
|
||||
|
||||
stateValue.SetDeserialized(value, typeof(T), jso);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to remove a value from the session state.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the value to remove.</param>
|
||||
/// <returns><see langword="true"/> if the value was successfully removed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool TryRemoveValue(string key)
|
||||
=> this._state.TryRemove(Throw.IfNullOrWhitespace(key), out _);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes all session state values to a JSON object.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="JsonElement"/> representing the serialized session state.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a session state value is not properly initialized.</exception>
|
||||
public JsonElement Serialize()
|
||||
{
|
||||
return JsonSerializer.SerializeToElement(this._state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ConcurrentDictionary<string, AgentSessionStateBagValue>)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a JSON object into an <see cref="AgentSessionStateBag"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="jsonElement">The element to deserialize.</param>
|
||||
/// <returns>The deserialized <see cref="AgentSessionStateBag"/>.</returns>
|
||||
public static AgentSessionStateBag Deserialize(JsonElement jsonElement)
|
||||
{
|
||||
if (jsonElement.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
|
||||
{
|
||||
return new AgentSessionStateBag();
|
||||
}
|
||||
|
||||
return new AgentSessionStateBag(
|
||||
jsonElement.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ConcurrentDictionary<string, AgentSessionStateBagValue>))) as ConcurrentDictionary<string, AgentSessionStateBagValue>
|
||||
?? new ConcurrentDictionary<string, AgentSessionStateBagValue>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Custom JSON converter for <see cref="AgentSessionStateBag"/> that serializes and deserializes
|
||||
/// the internal dictionary contents rather than the container object's public properties.
|
||||
/// </summary>
|
||||
public sealed class AgentSessionStateBagJsonConverter : JsonConverter<AgentSessionStateBag>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override AgentSessionStateBag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var element = JsonElement.ParseValue(ref reader);
|
||||
return AgentSessionStateBag.Deserialize(element);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, AgentSessionStateBag value, JsonSerializerOptions options)
|
||||
{
|
||||
var element = value.Serialize();
|
||||
element.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Used to store a value in session state.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(AgentSessionStateBagValueJsonConverter))]
|
||||
internal class AgentSessionStateBagValue
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private DeserializedCache? _cache;
|
||||
private JsonElement _jsonValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SessionStateValue class with the specified value.
|
||||
/// </summary>
|
||||
/// <param name="jsonValue">The serialized value to associate with the session state.</param>
|
||||
public AgentSessionStateBagValue(JsonElement jsonValue)
|
||||
{
|
||||
this.JsonValue = jsonValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SessionStateValue class with the specified value.
|
||||
/// </summary>
|
||||
/// <param name="deserializedValue">The value to associate with the session state. Can be any object, including null.</param>
|
||||
/// <param name="valueType">The type of the value.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options to use for serializing the value.</param>
|
||||
public AgentSessionStateBagValue(object? deserializedValue, Type valueType, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
this._cache = new DeserializedCache(deserializedValue, valueType, jsonSerializerOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value associated with this instance.
|
||||
/// </summary>
|
||||
public JsonElement JsonValue
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
// We are assuming here that JsonValue will only be read when the object is being serialized,
|
||||
// which means that we will only call SerializeToElement when serializing and therefore it's
|
||||
// OK to serialize on each read if the cache is set.
|
||||
if (this._cache is { } cache)
|
||||
{
|
||||
this._jsonValue = JsonSerializer.SerializeToElement(cache.Value, cache.Options.GetTypeInfo(cache.ValueType));
|
||||
}
|
||||
|
||||
return this._jsonValue;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
this._jsonValue = value;
|
||||
this._cache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the deserialized value of this session state value.
|
||||
/// Returns false if the value could not be deserialized into the required type, or if the value is undefined.
|
||||
/// Returns true and sets the out parameter to null if the value is null.
|
||||
/// </summary>
|
||||
public bool TryReadDeserializedValue<T>(out T? value, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
where T : class
|
||||
{
|
||||
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
lock (this._lock)
|
||||
{
|
||||
switch (this._cache)
|
||||
{
|
||||
case DeserializedCache { Value: null, ValueType: Type cacheValueType } when cacheValueType == typeof(T):
|
||||
value = null;
|
||||
return true;
|
||||
case DeserializedCache { Value: T cacheValue, ValueType: Type cacheValueType } when cacheValueType == typeof(T):
|
||||
value = cacheValue;
|
||||
return true;
|
||||
case DeserializedCache { ValueType: Type cacheValueType } when cacheValueType != typeof(T):
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this._jsonValue)
|
||||
{
|
||||
case JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Undefined:
|
||||
value = null;
|
||||
return false;
|
||||
case JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Null:
|
||||
value = null;
|
||||
return true;
|
||||
default:
|
||||
T? result = this._jsonValue.Deserialize(jso.GetTypeInfo(typeof(T))) as T;
|
||||
if (result is null)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
this._cache = new DeserializedCache(result, typeof(T), jso);
|
||||
|
||||
value = result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the deserialized value of this session state value, throwing an exception if the value could not be deserialized into the required type or is undefined.
|
||||
/// </summary>
|
||||
public T? ReadDeserializedValue<T>(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
where T : class
|
||||
{
|
||||
var jso = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
lock (this._lock)
|
||||
{
|
||||
switch (this._cache)
|
||||
{
|
||||
case DeserializedCache { Value: null, ValueType: Type cacheValueType } when cacheValueType == typeof(T):
|
||||
return null;
|
||||
case DeserializedCache { Value: T cacheValue, ValueType: Type cacheValueType } when cacheValueType == typeof(T):
|
||||
return cacheValue;
|
||||
case DeserializedCache { ValueType: Type cacheValueType } when cacheValueType != typeof(T):
|
||||
throw new InvalidOperationException($"The type of the cached value is {cacheValueType.FullName}, but the requested type is {typeof(T).FullName}.");
|
||||
}
|
||||
|
||||
switch (this._jsonValue)
|
||||
{
|
||||
case JsonElement jsonElement when jsonElement.ValueKind == JsonValueKind.Null || jsonElement.ValueKind == JsonValueKind.Undefined:
|
||||
return null;
|
||||
default:
|
||||
T? result = this._jsonValue.Deserialize(jso.GetTypeInfo(typeof(T))) as T;
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to deserialize session state value to type {typeof(T).FullName}.");
|
||||
}
|
||||
|
||||
this._cache = new DeserializedCache(result, typeof(T), jso);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the deserialized value of this session state value, updating the cache accordingly.
|
||||
/// This does not update the JsonValue directly; the JsonValue will be updated on the next read or when the object is serialized.
|
||||
/// </summary>
|
||||
public void SetDeserialized<T>(T? deserializedValue, Type valueType, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
this._cache = new DeserializedCache(deserializedValue, valueType, jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct DeserializedCache
|
||||
{
|
||||
public DeserializedCache(object? value, Type valueType, JsonSerializerOptions options)
|
||||
{
|
||||
this.Value = value;
|
||||
this.ValueType = valueType;
|
||||
this.Options = options;
|
||||
}
|
||||
|
||||
public object? Value { get; }
|
||||
|
||||
public Type ValueType { get; }
|
||||
|
||||
public JsonSerializerOptions Options { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Custom JSON converter for <see cref="AgentSessionStateBagValue"/> that serializes and deserializes
|
||||
/// the <see cref="AgentSessionStateBagValue.JsonValue"/> directly rather than wrapping it in a container object.
|
||||
/// </summary>
|
||||
internal sealed class AgentSessionStateBagValueJsonConverter : JsonConverter<AgentSessionStateBagValue>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override AgentSessionStateBagValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var element = JsonElement.ParseValue(ref reader);
|
||||
return new AgentSessionStateBagValue(element);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, AgentSessionStateBagValue value, JsonSerializerOptions options)
|
||||
{
|
||||
value.JsonValue.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for fetching chat messages from, and adding chat messages to, chat history for the purposes of agent execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="ChatHistoryProvider"/> defines the contract that an <see cref="AIAgent"/> can use to retrieve messsages from chat history
|
||||
/// and provide notification of newly produced messages.
|
||||
/// Implementations are responsible for managing message persistence, retrieval, and any necessary optimization
|
||||
/// strategies such as truncation, summarization, or archival.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Key responsibilities include:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Storing chat messages with proper ordering and metadata preservation</description></item>
|
||||
/// <item><description>Retrieving messages in chronological order for agent context</description></item>
|
||||
/// <item><description>Managing storage limits through truncation, summarization, or other strategies</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="ChatHistoryProvider"/> is passed a reference to the <see cref="AgentSession"/> via <see cref="InvokingContext"/> and <see cref="InvokedContext"/>
|
||||
/// allowing it to store state in the <see cref="AgentSession.StateBag"/>. Since a <see cref="ChatHistoryProvider"/> is used with many different sessions, it should
|
||||
/// not store any session-specific information within its own instance fields. Instead, any session-specific state should be stored in the associated <see cref="AgentSession.StateBag"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A <see cref="ChatHistoryProvider"/> is only relevant for scenarios where the underlying AI service that the agent is using
|
||||
/// does not use in-service chat history storage.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security considerations:</strong> Agent Framework does not validate or filter the messages returned by the provider
|
||||
/// during load — they are accepted as-is and treated identically to user-supplied messages. Implementers must ensure that only
|
||||
/// trusted data is returned. If the underlying storage is compromised, adversarial content could influence LLM behavior via
|
||||
/// indirect prompt injection — for example, injected messages could alter the conversation context or impersonate different roles.
|
||||
/// Messages stored in chat history may contain PII and sensitive conversation content; implementers should consider encryption
|
||||
/// at rest and appropriate access controls for the storage backend.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class ChatHistoryProvider
|
||||
{
|
||||
private static IEnumerable<ChatMessage> DefaultExcludeChatHistoryFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
|
||||
private static IEnumerable<ChatMessage> DefaultNoopFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages;
|
||||
|
||||
private IReadOnlyList<string>? _stateKeys;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _provideOutputMessageFilter;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storeInputRequestMessageFilter;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storeInputResponseMessageFilter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatHistoryProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="provideOutputMessageFilter">An optional filter function to apply to messages when retrieving them from the chat history.</param>
|
||||
/// <param name="storeInputRequestMessageFilter">An optional filter function to apply to request messages before storing them in the chat history. If not set, defaults to excluding messages with source type <see cref="AgentRequestMessageSourceType.ChatHistory"/>.</param>
|
||||
/// <param name="storeInputResponseMessageFilter">An optional filter function to apply to response messages before storing them in the chat history. If not set, defaults to a no-op filter that includes all response messages.</param>
|
||||
protected ChatHistoryProvider(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
|
||||
{
|
||||
this._provideOutputMessageFilter = provideOutputMessageFilter;
|
||||
this._storeInputRequestMessageFilter = storeInputRequestMessageFilter ?? DefaultExcludeChatHistoryFilter;
|
||||
this._storeInputResponseMessageFilter = storeInputResponseMessageFilter ?? DefaultNoopFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of keys used to store the provider state in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value is a single-element set containing the name of the concrete type (e.g. <c>"InMemoryChatHistoryProvider"</c>).
|
||||
/// Implementations may override this to provide custom keys, for example when multiple
|
||||
/// instances of the same provider type are used in the same session, or when a provider
|
||||
/// stores state under more than one key.
|
||||
/// </remarks>
|
||||
public virtual IReadOnlyList<string> StateKeys => this._stateKeys ??= [this.GetType().Name];
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide messages for the next agent invocation.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 a collection of <see cref="ChatMessage"/>
|
||||
/// instances that will be used for the agent invocation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
|
||||
/// storage constraints, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Truncating older messages while preserving recent context</description></item>
|
||||
/// <item><description>Summarizing message groups to maintain essential context</description></item>
|
||||
/// <item><description>Implementing sliding window approaches for message retention</description></item>
|
||||
/// <item><description>Archiving old messages while keeping active conversation context</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide messages for the next agent invocation.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 a collection of <see cref="ChatMessage"/>
|
||||
/// instances that will be used for the agent invocation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
|
||||
/// storage constraints, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Truncating older messages while preserving recent context</description></item>
|
||||
/// <item><description>Summarizing message groups to maintain essential context</description></item>
|
||||
/// <item><description>Implementing sliding window approaches for message retention</description></item>
|
||||
/// <item><description>Archiving old messages while keeping active conversation context</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of this method, calls <see cref="ProvideChatHistoryAsync"/> to get the chat history messages, applies the optional retrieval output filter,
|
||||
/// and merges the returned messages with the caller provided messages (with chat history messages appearing first) before returning the full message list to be used for the invocation.
|
||||
/// For most scenarios, overriding <see cref="ProvideChatHistoryAsync"/> is sufficient to return the desired chat history messages, while still benefiting from the default merging and filtering behavior.
|
||||
/// However, for scenarios that require more control over message filtering, merging or source stamping, overriding this method allows you to directly control the full set of messages returned for the invocation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var output = await this.ProvideChatHistoryAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this._provideOutputMessageFilter is not null)
|
||||
{
|
||||
output = this._provideOutputMessageFilter(output);
|
||||
}
|
||||
|
||||
return output
|
||||
.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!))
|
||||
.Concat(context.RequestMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, provides the chat history messages to be used for the current invocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is called from <see cref="InvokingCoreAsync"/>.
|
||||
/// Note that <see cref="InvokingCoreAsync"/> can be overridden to directly control message filtering, merging and source stamping, in which case
|
||||
/// it is up to the implementer to call this method as needed to retrieve the unfiltered/unmerged chat history messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In contrast with <see cref="InvokingCoreAsync"/>, this method only returns additional messages to be added to the request,
|
||||
/// while <see cref="InvokingCoreAsync"/> is responsible for returning the full set of messages to be used for the invocation (including caller provided messages).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Messages are returned in chronological order to maintain proper conversation flow and context for the agent.
|
||||
/// The oldest messages appear first in the collection, followed by more recent messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Messages loaded from storage should be treated with the same caution as user-supplied
|
||||
/// messages. A compromised storage backend could alter message roles to escalate trust (e.g., changing <c>user</c> messages to
|
||||
/// <c>system</c> messages) or inject adversarial content that influences LLM behavior.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 a collection of <see cref="ChatMessage"/>
|
||||
/// instances in ascending chronological order (oldest first).
|
||||
/// </returns>
|
||||
protected virtual ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<IEnumerable<ChatMessage>>([]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of the agent invocation to add new messages to the chat history.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</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 add operation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
|
||||
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
|
||||
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations may perform additional processing during message addition, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Validating message content and metadata</description></item>
|
||||
/// <item><description>Applying storage optimizations or compression</description></item>
|
||||
/// <item><description>Triggering background maintenance operations</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is called regardless of whether the invocation succeeded or failed.
|
||||
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
|
||||
this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of the agent invocation to add new messages to the chat history.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</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 add operation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
|
||||
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
|
||||
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations may perform additional processing during message addition, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Validating message content and metadata</description></item>
|
||||
/// <item><description>Applying storage optimizations or compression</description></item>
|
||||
/// <item><description>Triggering background maintenance operations</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is called regardless of whether the invocation succeeded or failed.
|
||||
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of this method, skips execution for any invocation failures, filters messages using the optional storage input request and response message filters
|
||||
/// and calls <see cref="StoreChatHistoryAsync"/> to store new chat history messages.
|
||||
/// For most scenarios, overriding <see cref="StoreChatHistoryAsync"/> is sufficient to store chat history messages, while still benefiting from the default error handling and filtering behavior.
|
||||
/// However, for scenarios that require more control over error handling or message filtering, overriding this method allows you to directly control the messages that are stored for the invocation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputRequestMessageFilter(context.RequestMessages), this._storeInputResponseMessageFilter(context.ResponseMessages!));
|
||||
#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
return this.StoreChatHistoryAsync(subContext, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, adds new messages to the chat history at the end of the agent invocation.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</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 add operation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
|
||||
/// The <see cref="ChatHistoryProvider"/> is responsible for preserving message ordering and ensuring that subsequent calls to
|
||||
/// <see cref="InvokingCoreAsync"/> return messages in the correct chronological order.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Implementations may perform additional processing during message addition, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Validating message content and metadata</description></item>
|
||||
/// <item><description>Applying storage optimizations or compression</description></item>
|
||||
/// <item><description>Triggering background maintenance operations</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is called from <see cref="InvokedCoreAsync"/>.
|
||||
/// Note that <see cref="InvokedCoreAsync"/> can be overridden to directly control message filtering and error handling, in which case
|
||||
/// it is up to the implementer to call this method as needed to store messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In contrast with <see cref="InvokedCoreAsync"/>, this method only stores messages,
|
||||
/// while <see cref="InvokedCoreAsync"/> is also responsible for messages filtering and error handling.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of <see cref="InvokedCoreAsync"/> only calls this method if the invocation succeeded.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Security consideration:</strong> Messages being stored may contain PII and sensitive conversation content.
|
||||
/// Implementers should ensure appropriate encryption at rest and access controls for the storage backend.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
|
||||
default;
|
||||
|
||||
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
|
||||
/// <param name="serviceType">The type of object being requested.</param>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="ChatHistoryProvider"/>,
|
||||
/// including itself or any services it might be wrapping.
|
||||
/// </remarks>
|
||||
public virtual object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
_ = Throw.IfNull(serviceType);
|
||||
|
||||
return serviceKey is null && serviceType.IsInstanceOfType(this)
|
||||
? this
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of type <typeparamref name="TService"/>.</summary>
|
||||
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
|
||||
/// <remarks>
|
||||
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="ChatHistoryProvider"/>,
|
||||
/// including itself or any services it might be wrapping.
|
||||
/// </remarks>
|
||||
public TService? GetService<TService>(object? serviceKey = null)
|
||||
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about the invocation including the new messages that will be used.
|
||||
/// A <see cref="ChatHistoryProvider"/> can use this information to determine what messages should be provided
|
||||
/// for the invocation.
|
||||
/// </remarks>
|
||||
public sealed class InvokingContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent being invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="requestMessages">The messages to be used by the agent for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public InvokingContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IEnumerable<ChatMessage> requestMessages)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent that is being invoked.
|
||||
/// </summary>
|
||||
public AIAgent Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent session associated with the agent invocation.
|
||||
/// </summary>
|
||||
public AgentSession? Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages that will be used by the agent for this invocation. <see cref="ChatHistoryProvider"/> instances can modify
|
||||
/// and return or return a new message list to add additional messages for the invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing the messages that will be used by the agent for this invocation.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If multiple <see cref="ChatHistoryProvider"/> instances are used in the same invocation, each <see cref="ChatHistoryProvider"/>
|
||||
/// will receive the messages returned by the previous <see cref="ChatHistoryProvider"/> allowing them to build on top of each other's context.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first <see cref="ChatHistoryProvider"/> in the invocation pipeline will receive the
|
||||
/// caller provided messages.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokedCoreAsync(InvokedContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about a completed agent invocation, including the accumulated
|
||||
/// request messages (user input, chat history and any others provided by AI context providers) that were used
|
||||
/// and the response messages that were generated. It also indicates whether the invocation succeeded or failed.
|
||||
/// </remarks>
|
||||
public sealed class InvokedContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a successful invocation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that was invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="requestMessages">The accumulated request messages (user input, chat history and any others provided by AI context providers)
|
||||
/// that were used by the agent for this invocation.</param>
|
||||
/// <param name="responseMessages">The response messages generated during this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="responseMessages"/> is <see langword="null"/>.</exception>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public InvokedContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IEnumerable<ChatMessage> requestMessages,
|
||||
IEnumerable<ChatMessage> responseMessages)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
this.ResponseMessages = Throw.IfNull(responseMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokedContext"/> class for a failed invocation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that was invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="requestMessages">The accumulated request messages (user input, chat history and any others provided by AI context providers)
|
||||
/// that were used by the agent for this invocation.</param>
|
||||
/// <param name="invokeException">The exception that caused the invocation to fail.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agent"/>, <paramref name="requestMessages"/>, or <paramref name="invokeException"/> is <see langword="null"/>.</exception>
|
||||
public InvokedContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IEnumerable<ChatMessage> requestMessages,
|
||||
Exception invokeException)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
this.InvokeException = Throw.IfNull(invokeException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent that is being invoked.
|
||||
/// </summary>
|
||||
public AIAgent Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent session associated with the agent invocation.
|
||||
/// </summary>
|
||||
public AgentSession? Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the accumulated request messages (user input, chat history and any others provided by AI context providers)
|
||||
/// that were used by the agent for this invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
|
||||
/// This does not include any <see cref="ChatHistoryProvider"/> supplied messages.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing the response,
|
||||
/// or <see langword="null"/> if the invocation failed.
|
||||
/// </value>
|
||||
public IEnumerable<ChatMessage>? ResponseMessages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The exception that caused the invocation to fail, or <see langword="null"/> if the invocation succeeded.
|
||||
/// </value>
|
||||
public Exception? InvokeException { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Contains extension methods for <see cref="ChatMessage"/>
|
||||
/// </summary>
|
||||
public static class ChatMessageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the source type of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
|
||||
/// </summary>
|
||||
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source type.</param>
|
||||
/// <returns>An <see cref="AgentRequestMessageSourceType"/> value indicating the source type of the <see cref="ChatMessage"/>. Defaults to <see
|
||||
/// cref="AgentRequestMessageSourceType.External"/> if no explicit source is defined.</returns>
|
||||
public static AgentRequestMessageSourceType GetAgentRequestMessageSourceType(this ChatMessage message)
|
||||
{
|
||||
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var attribution) is true
|
||||
&& attribution is AgentRequestMessageSourceAttribution typedAttribution)
|
||||
{
|
||||
return typedAttribution.SourceType;
|
||||
}
|
||||
|
||||
return AgentRequestMessageSourceType.External;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source id of the provided <see cref="ChatMessage"/> in the context of messages passed into an agent run.
|
||||
/// </summary>
|
||||
/// <param name="message">The <see cref="ChatMessage"/> for which we need the source id.</param>
|
||||
/// <returns>An <see cref="string"/> value indicating the source id of the <see cref="ChatMessage"/>. Defaults to <see langword="null"/>
|
||||
/// if no explicit source id is defined.</returns>
|
||||
public static string? GetAgentRequestMessageSourceId(this ChatMessage message)
|
||||
{
|
||||
if (message.AdditionalProperties?.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var attribution) is true
|
||||
&& attribution is AgentRequestMessageSourceAttribution typedAttribution)
|
||||
{
|
||||
return typedAttribution.SourceId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that the provided message is tagged with the provided source type and source id in the context of a specific agent run.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to tag.</param>
|
||||
/// <param name="sourceType">The source type to tag the message with.</param>
|
||||
/// <param name="sourceId">The source id to tag the message with.</param>
|
||||
/// <returns>The tagged message.</returns>
|
||||
/// <remarks>
|
||||
/// If the message is already tagged with the provided source type and source id, it is returned as is.
|
||||
/// Otherwise, a cloned message is returned with the appropriate tagging in the AdditionalProperties.
|
||||
/// </remarks>
|
||||
public static ChatMessage WithAgentRequestMessageSource(this ChatMessage message, AgentRequestMessageSourceType sourceType, string? sourceId = null)
|
||||
{
|
||||
if (message.AdditionalProperties != null
|
||||
// Check if the message was already tagged with the required source type and source id
|
||||
&& message.AdditionalProperties.TryGetValue(AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, out var messageSourceAttribution)
|
||||
&& messageSourceAttribution is AgentRequestMessageSourceAttribution typedMessageSourceAttribution
|
||||
&& typedMessageSourceAttribution.SourceType == sourceType
|
||||
&& typedMessageSourceAttribution.SourceId == sourceId)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
message = message.Clone();
|
||||
message.AdditionalProperties ??= new();
|
||||
message.AdditionalProperties[AgentRequestMessageSourceAttribution.AdditionalPropertiesKey] =
|
||||
new AgentRequestMessageSourceAttribution(sourceType, sourceId);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for AI agents that delegate operations to an inner agent
|
||||
/// instance while allowing for extensibility and customization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="DelegatingAIAgent"/> implements the decorator pattern for <see cref="AIAgent"/>s, enabling the creation of agent pipelines
|
||||
/// where each layer can add functionality while delegating core operations to an underlying agent. This pattern is
|
||||
/// fundamental to building composable agent architectures.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation provides transparent pass-through behavior, forwarding all operations to the inner agent.
|
||||
/// Derived classes can override specific methods to add custom behavior while maintaining compatibility with the agent interface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class DelegatingAIAgent : AIAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelegatingAIAgent"/> class with the specified inner agent.
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The underlying agent instance that will handle the core operations.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="innerAgent"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// The inner agent serves as the foundation of the delegation chain. All operations not overridden by
|
||||
/// derived classes will be forwarded to this agent.
|
||||
/// </remarks>
|
||||
protected DelegatingAIAgent(AIAgent innerAgent)
|
||||
{
|
||||
this.InnerAgent = Throw.IfNull(innerAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inner agent instance that receives delegated operations.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The underlying <see cref="AIAgent"/> instance that handles core agent operations.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Derived classes can use this property to access the inner agent for custom delegation scenarios
|
||||
/// or to forward operations with additional processing.
|
||||
/// </remarks>
|
||||
protected AIAgent InnerAgent { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string? IdCore => this.InnerAgent.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Name => this.InnerAgent.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Description => this.InnerAgent.Description;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
_ = Throw.IfNull(serviceType);
|
||||
|
||||
// If the key is non-null, we don't know what it means so pass through to the inner service.
|
||||
return
|
||||
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
|
||||
this.InnerAgent.GetService(serviceType, serviceKey);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default) => this.InnerAgent.CreateSessionAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> this.InnerAgent.SerializeSessionAsync(session, jsonSerializerOptions, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
=> this.InnerAgent.DeserializeSessionAsync(serializedState, jsonSerializerOptions, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this.InnerAgent.RunAsync(messages, session, options, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-memory implementation of <see cref="ChatHistoryProvider"/> with support for message reduction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="InMemoryChatHistoryProvider"/> stores chat messages in the <see cref="AgentSession.StateBag"/>,
|
||||
/// providing fast access and manipulation capabilities integrated with session state management.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This <see cref="ChatHistoryProvider"/> maintains all messages in memory. For long-running conversations or high-volume scenarios, consider using
|
||||
/// message reduction strategies or alternative storage implementations.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
{
|
||||
private readonly ProviderSessionState<State> _sessionState;
|
||||
private IReadOnlyList<string>? _stateKeys;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">
|
||||
/// Optional configuration options that control the provider's behavior, including state initialization,
|
||||
/// message reduction, and serialization settings. If <see langword="null"/>, default settings will be used.
|
||||
/// </param>
|
||||
public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options = null)
|
||||
: base(
|
||||
options?.ProvideOutputMessageFilter,
|
||||
options?.StorageInputRequestMessageFilter,
|
||||
options?.StorageInputResponseMessageFilter)
|
||||
{
|
||||
this._sessionState = new ProviderSessionState<State>(
|
||||
options?.StateInitializer ?? (_ => new State()),
|
||||
options?.StateKey ?? this.GetType().Name,
|
||||
options?.JsonSerializerOptions);
|
||||
this.ChatReducer = options?.ChatReducer;
|
||||
this.ReducerTriggerEvent = options?.ReducerTriggerEvent ?? InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied.
|
||||
/// </summary>
|
||||
public IChatReducer? ChatReducer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event that triggers the reducer invocation in this provider.
|
||||
/// </summary>
|
||||
public InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent ReducerTriggerEvent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat messages stored for the specified session.
|
||||
/// </summary>
|
||||
/// <param name="session">The agent session containing the state.</param>
|
||||
/// <returns>A list of chat messages, or an empty list if no state is found.</returns>
|
||||
public List<ChatMessage> GetMessages(AgentSession? session)
|
||||
=> this._sessionState.GetOrInitializeState(session).Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the chat messages for the specified session.
|
||||
/// </summary>
|
||||
/// <param name="session">The agent session containing the state.</param>
|
||||
/// <param name="messages">The messages to store.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
public void SetMessages(AgentSession? session, List<ChatMessage> messages)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
|
||||
State state = this._sessionState.GetOrInitializeState(session);
|
||||
state.Messages = messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
|
||||
{
|
||||
// Apply pre-retrieval reduction if configured
|
||||
await ReduceMessagesAsync(this.ChatReducer, state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return state.Messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
// Add request and response messages to the provider
|
||||
var allNewMessages = (context.RequestMessages ?? []).Concat(context.ResponseMessages ?? []);
|
||||
state.Messages.AddRange(allNewMessages);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
{
|
||||
// Apply pre-write reduction strategy if configured
|
||||
await ReduceMessagesAsync(this.ChatReducer, state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReduceMessagesAsync(IChatReducer reducer, State state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
state.Messages = [.. await reducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of a <see cref="InMemoryChatHistoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
public sealed class State
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of chat messages.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messages")]
|
||||
public List<ChatMessage> Messages { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents configuration options for <see cref="InMemoryChatHistoryProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an optional delegate that initializes the provider state on the first invocation.
|
||||
/// If <see langword="null"/>, a default initializer that creates an empty state will be used.
|
||||
/// </summary>
|
||||
public Func<AgentSession?, InMemoryChatHistoryProvider.State>? StateInitializer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional <see cref="IChatReducer"/> instance used to process, reduce, or optimize chat messages.
|
||||
/// This can be used to implement strategies like message summarization, truncation, or cleanup.
|
||||
/// </summary>
|
||||
public IChatReducer? ChatReducer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets when the message reducer should be invoked.
|
||||
/// The default is <see cref="ChatReducerTriggerEvent.BeforeMessagesRetrieval"/>,
|
||||
/// which applies reduction logic when messages are retrieved for agent consumption.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Message reducers enable automatic management of message storage by implementing strategies to
|
||||
/// keep memory usage under control while preserving important conversation context.
|
||||
/// </remarks>
|
||||
public ChatReducerTriggerEvent ReducerTriggerEvent { get; set; } = ChatReducerTriggerEvent.BeforeMessagesRetrieval;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional key to use for storing the state in the <see cref="AgentSession.StateBag"/>.
|
||||
/// If <see langword="null"/>, a default key will be used.
|
||||
/// </summary>
|
||||
public string? StateKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional JSON serializer options for serializing the state of this provider.
|
||||
/// This is valuable for cases like when the chat history contains custom <see cref="AIContent"/> types
|
||||
/// and source generated serializers are required, or Native AOT / Trimming is required.
|
||||
/// </summary>
|
||||
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional filter function applied to request messages before they are added to storage
|
||||
/// during <see cref="ChatHistoryProvider.InvokedAsync"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// When <see langword="null"/>, the provider defaults to excluding messages with
|
||||
/// <see cref="AgentRequestMessageSourceType.ChatHistory"/> source type to avoid
|
||||
/// storing messages that came from chat history in the first place.
|
||||
/// Depending on your requirements, you could provide a different filter, that also excludes
|
||||
/// messages from e.g. AI context providers.
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? StorageInputRequestMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional filter function applied to response messages before they are added to storage
|
||||
/// during <see cref="ChatHistoryProvider.InvokedAsync"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// When <see langword="null"/>, no filtering is applied to response messages before they are stored.
|
||||
/// If you want to avoid persisting certain messages (for example, those with
|
||||
/// <see cref="AgentRequestMessageSourceType.ChatHistory"/> source type or produced by AI context providers),
|
||||
/// provide a filter that returns only the messages you want to keep.
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? StorageInputResponseMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional filter function applied to messages produced by this provider
|
||||
/// during <see cref="ChatHistoryProvider.InvokingAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This filter is only applied to the messages that the provider itself produces (from its internal storage).
|
||||
/// </remarks>
|
||||
/// <value>
|
||||
/// When <see langword="null"/>, no filtering is applied to the output messages.
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? ProvideOutputMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatHistoryProvider"/>.
|
||||
/// </summary>
|
||||
public enum ChatReducerTriggerEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger the reducer when a new message is added.
|
||||
/// <see cref="AIContextProvider.InvokedAsync"/> will only complete when reducer processing is done.
|
||||
/// </summary>
|
||||
AfterMessageAdded,
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the reducer before messages are retrieved from the provider.
|
||||
/// The reducer will process the messages before they are returned to the caller.
|
||||
/// </summary>
|
||||
BeforeMessagesRetrieval
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for components that enhance AI context during agent invocations by supplying additional chat messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A message AI context provider is a component that participates in the agent invocation lifecycle by:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Listening to changes in conversations</description></item>
|
||||
/// <item><description>Providing additional messages to agents during invocation</description></item>
|
||||
/// <item><description>Processing invocation results for state management or learning</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Context providers operate through a two-phase lifecycle: they are called at the start of invocation via
|
||||
/// <see cref="AIContextProvider.InvokingAsync"/> to provide context, and optionally called at the end of invocation via
|
||||
/// <see cref="AIContextProvider.InvokedAsync"/> to process results.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class MessageAIContextProvider : AIContextProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageAIContextProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="provideInputMessageFilter">An optional filter function to apply to input messages before providing messages via <see cref="ProvideMessagesAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
|
||||
/// <param name="storeInputRequestMessageFilter">An optional filter function to apply to request messages before storing messages via <see cref="AIContextProvider.StoreAIContextAsync"/>. If not set, defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages.</param>
|
||||
/// <param name="storeInputResponseMessageFilter">An optional filter function to apply to response messages before storing messages via <see cref="AIContextProvider.StoreAIContextAsync"/>. If not set, defaults to including all response messages (no filtering).</param>
|
||||
protected MessageAIContextProvider(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
|
||||
: base(provideInputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async ValueTask<AIContext> ProvideAIContextAsync(AIContextProvider.InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Call ProvideMessagesAsync directly to return only additional messages.
|
||||
// The base AIContextProvider.InvokingCoreAsync handles merging with the original input and stamping.
|
||||
#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
return new AIContext
|
||||
{
|
||||
Messages = await this.ProvideMessagesAsync(
|
||||
new InvokingContext(context.Agent, context.Session, context.AIContext.Messages ?? []),
|
||||
cancellationToken).ConfigureAwait(false)
|
||||
};
|
||||
#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide additional messages.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 the <see cref="IEnumerable{ChatMessage}"/> to be used by the agent during this invocation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementers can load any additional messages required at this time, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
|
||||
/// <item><description>Adding system instructions or prompts</description></item>
|
||||
/// <item><description>Injecting contextual messages from conversation history</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide additional messages.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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 the <see cref="IEnumerable{ChatMessage}"/> to be used by the agent during this invocation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Implementers can load any additional messages required at this time, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Retrieving relevant information from knowledge bases</description></item>
|
||||
/// <item><description>Adding system instructions or prompts</description></item>
|
||||
/// <item><description>Injecting contextual messages from conversation history</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The default implementation of this method filters the input messages using the configured provide-input message filter
|
||||
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
|
||||
/// then calls <see cref="ProvideMessagesAsync"/> to get additional messages,
|
||||
/// stamps any messages with <see cref="AgentRequestMessageSourceType.AIContextProvider"/> source attribution,
|
||||
/// and merges the returned messages with the original (unfiltered) input messages.
|
||||
/// For most scenarios, overriding <see cref="ProvideMessagesAsync"/> is sufficient to provide additional messages,
|
||||
/// while still benefiting from the default filtering, merging and source stamping behavior.
|
||||
/// However, for scenarios that require more control over message filtering, merging or source stamping, overriding this method
|
||||
/// allows you to directly control the full <see cref="IEnumerable{ChatMessage}"/> returned for the invocation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected virtual async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var inputMessages = context.RequestMessages;
|
||||
|
||||
// Create a filtered context for ProvideMessagesAsync, filtering input messages
|
||||
// to exclude non-external messages (e.g. chat history, other AI context provider messages).
|
||||
#pragma warning disable MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
var filteredContext = new InvokingContext(
|
||||
context.Agent,
|
||||
context.Session,
|
||||
this.ProvideInputMessageFilter(inputMessages));
|
||||
#pragma warning restore MAAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
var providedMessages = await this.ProvideMessagesAsync(filteredContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Stamp and merge provided messages.
|
||||
providedMessages = providedMessages.Select(m => m.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!));
|
||||
return inputMessages.Concat(providedMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, provides additional messages to be merged with the input messages for the current invocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method is called from <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
|
||||
/// Note that <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/> can be overridden to directly control messages merging and source stamping, in which case
|
||||
/// it is up to the implementer to call this method as needed to retrieve the additional messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In contrast with <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>, this method only returns additional messages to be merged with the input,
|
||||
/// while <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/> is responsible for returning the full merged <see cref="IEnumerable{ChatMessage}"/> for the invocation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</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="IEnumerable{ChatMessage}"/>
|
||||
/// with additional messages to be merged with the input messages.
|
||||
/// </returns>
|
||||
protected virtual ValueTask<IEnumerable<ChatMessage>> ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<IEnumerable<ChatMessage>>([]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the context information provided to <see cref="InvokingCoreAsync(InvokingContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides context about the invocation before the underlying AI model is invoked, including the messages
|
||||
/// that will be used. Message AI Context providers can use this information to determine what additional messages
|
||||
/// should be provided for the invocation.
|
||||
/// </remarks>
|
||||
public new sealed class InvokingContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent being invoked.</param>
|
||||
/// <param name="session">The session associated with the agent invocation.</param>
|
||||
/// <param name="requestMessages">The messages to be used by the agent for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agent"/> or <paramref name="requestMessages"/> is <see langword="null"/>.</exception>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public InvokingContext(
|
||||
AIAgent agent,
|
||||
AgentSession? session,
|
||||
IEnumerable<ChatMessage> requestMessages)
|
||||
{
|
||||
this.Agent = Throw.IfNull(agent);
|
||||
this.Session = session;
|
||||
this.RequestMessages = Throw.IfNull(requestMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent that is being invoked.
|
||||
/// </summary>
|
||||
public AIAgent Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent session associated with the agent invocation.
|
||||
/// </summary>
|
||||
public AgentSession? Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages that will be used by the agent for this invocation. <see cref="MessageAIContextProvider"/> instances can modify
|
||||
/// and return or return a new message list to add additional messages for the invocation.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A collection of <see cref="ChatMessage"/> instances representing the messages that will be used by the agent for this invocation.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If multiple <see cref="MessageAIContextProvider"/> instances are used in the same invocation, each <see cref="MessageAIContextProvider"/>
|
||||
/// will receive the messages returned by the previous <see cref="MessageAIContextProvider"/> allowing them to build on top of each other's context.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first <see cref="MessageAIContextProvider"/> in the invocation pipeline will receive the
|
||||
/// caller provided messages.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; set { field = Throw.IfNull(value); } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.Agents.AI</RootNamespace>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<IsReleased>true</IsReleased>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectSharedStructuredOutput>true</InjectSharedStructuredOutput>
|
||||
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
|
||||
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework Abstractions</Title>
|
||||
<Description>Provides Microsoft Agent Framework interfaces and abstractions.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.Abstractions.UnitTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides strongly-typed state management for providers, enabling reading and writing of provider-specific state
|
||||
/// to and from an <see cref="AgentSession"/>'s <see cref="AgentSessionStateBag"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">The type of the state to be maintained. Must be a reference type.</typeparam>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class encapsulates the logic for initializing, retrieving, and persisting provider state in the session's StateBag
|
||||
/// using a configurable key and JSON serialization options. It is intended to be used as a composed field within provider
|
||||
/// implementations (e.g., <see cref="AIContextProvider"/> or <see cref="ChatHistoryProvider"/> subclasses) to avoid
|
||||
/// duplicating state management logic across provider type hierarchies.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// State is stored in the <see cref="AgentSession.StateBag"/> using the <see cref="StateKey"/> property as the key,
|
||||
/// enabling multiple providers to maintain independent state within the same session.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class ProviderSessionState<TState>
|
||||
where TState : class
|
||||
{
|
||||
private readonly Func<AgentSession?, TState> _stateInitializer;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProviderSessionState{TState}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stateInitializer">A function to initialize the state when it is not yet present in the session's StateBag.</param>
|
||||
/// <param name="stateKey">The key used to store the state in the session's StateBag.</param>
|
||||
/// <param name="jsonSerializerOptions">Options for JSON serialization and deserialization of the state.</param>
|
||||
public ProviderSessionState(
|
||||
Func<AgentSession?, TState> stateInitializer,
|
||||
string stateKey,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
this._stateInitializer = Throw.IfNull(stateInitializer);
|
||||
this.StateKey = Throw.IfNullOrWhitespace(stateKey);
|
||||
this._jsonSerializerOptions = jsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key used to store the provider state in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
public string StateKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state from the session's StateBag, or initializes it using the state initializer if not present.
|
||||
/// </summary>
|
||||
/// <param name="session">The agent session containing the StateBag.</param>
|
||||
/// <returns>The provider state.</returns>
|
||||
public TState GetOrInitializeState(AgentSession? session)
|
||||
{
|
||||
if (session?.StateBag.TryGetValue<TState>(this.StateKey, out var state, this._jsonSerializerOptions) is true && state is not null)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
state = this._stateInitializer(session);
|
||||
if (session is not null)
|
||||
{
|
||||
session.StateBag.SetValue(this.StateKey, state, this._jsonSerializerOptions);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the specified state to the session's StateBag using the configured state key and JSON serializer options.
|
||||
/// If the session is null, this method does nothing.
|
||||
/// </summary>
|
||||
/// <param name="session">The agent session containing the StateBag.</param>
|
||||
/// <param name="state">The state to be saved.</param>
|
||||
public void SaveState(AgentSession? session, TState state)
|
||||
{
|
||||
if (session is not null)
|
||||
{
|
||||
session.StateBag.SetValue(this.StateKey, state, this._jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user