mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'microsoft:main' into features/ai-project-2.0.0-update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
@@ -28,7 +28,7 @@ internal sealed class ExperimentalAttribute : Attribute
|
||||
/// <param name="diagnosticId">Human readable explanation for marking experimental API.</param>
|
||||
public ExperimentalAttribute(string diagnosticId)
|
||||
{
|
||||
DiagnosticId = diagnosticId;
|
||||
this.DiagnosticId = diagnosticId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -63,7 +63,16 @@ public sealed class A2AAgent : AIAgent
|
||||
/// <param name="contextId">The context id to continue.</param>
|
||||
/// <returns>A value task representing the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance.</returns>
|
||||
public ValueTask<AgentSession> CreateSessionAsync(string contextId)
|
||||
=> new(new A2AAgentSession() { ContextId = contextId });
|
||||
=> new(new A2AAgentSession() { ContextId = Throw.IfNullOrWhitespace(contextId) });
|
||||
|
||||
/// <summary>
|
||||
/// Get a new <see cref="AgentSession"/> instance using an existing context id and task id, to resume that conversation from a specific task.
|
||||
/// </summary>
|
||||
/// <param name="contextId">The context id to continue.</param>
|
||||
/// <param name="taskId">The task id to resume from.</param>
|
||||
/// <returns>A value task representing the asynchronous operation. The task result contains a new <see cref="AgentSession"/> instance.</returns>
|
||||
public ValueTask<AgentSession> CreateSessionAsync(string contextId, string taskId)
|
||||
=> new(new A2AAgentSession() { ContextId = Throw.IfNullOrWhitespace(contextId), TaskId = Throw.IfNullOrWhitespace(taskId) });
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
@@ -72,7 +81,7 @@ public sealed class A2AAgent : AIAgent
|
||||
|
||||
if (session is not A2AAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(A2AAgentSession)}' can be serialized by this agent.");
|
||||
}
|
||||
|
||||
return new(typedSession.Serialize(jsonSerializerOptions));
|
||||
@@ -247,7 +256,7 @@ public sealed class A2AAgent : AIAgent
|
||||
|
||||
if (session is not A2AAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException($"The provided session type {session.GetType()} is not compatible with the agent. Only A2A agent created sessions are supported.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(A2AAgentSession)}' can be used by this agent.");
|
||||
}
|
||||
|
||||
return typedSession;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -10,7 +11,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for components that enhance AI context management during agent invocations.
|
||||
/// Provides an abstract base class for components that enhance AI context during agent invocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -30,6 +31,32 @@ namespace Microsoft.Agents.AI;
|
||||
/// </remarks>
|
||||
public abstract class AIContextProvider
|
||||
{
|
||||
private static IEnumerable<ChatMessage> DefaultExternalOnlyFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);
|
||||
|
||||
/// <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="storeInputMessageFilter">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>
|
||||
protected AIContextProvider(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
|
||||
{
|
||||
this.ProvideInputMessageFilter = provideInputMessageFilter ?? DefaultExternalOnlyFilter;
|
||||
this.StoreInputMessageFilter = storeInputMessageFilter ?? DefaultExternalOnlyFilter;
|
||||
}
|
||||
|
||||
/// <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>> StoreInputMessageFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key used to store the provider state in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
@@ -58,7 +85,7 @@ public abstract class AIContextProvider
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokingCoreAsync(context, cancellationToken);
|
||||
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide additional context.
|
||||
@@ -76,8 +103,96 @@ public abstract class AIContextProvider
|
||||
/// <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 abstract ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default);
|
||||
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).
|
||||
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
|
||||
});
|
||||
|
||||
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>
|
||||
/// </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.
|
||||
@@ -106,7 +221,7 @@ public abstract class AIContextProvider
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokedCoreAsync(context, cancellationToken);
|
||||
=> this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of the agent invocation to process the invocation results.
|
||||
@@ -128,9 +243,50 @@ public abstract class AIContextProvider
|
||||
/// 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 message filter
|
||||
/// (which defaults to including only <see cref="AgentRequestMessageSourceType.External"/> messages),
|
||||
/// 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)
|
||||
=> default;
|
||||
{
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var subContext = new InvokedContext(context.Agent, context.Session, this.StoreInputMessageFilter(context.RequestMessages), context.ResponseMessages!);
|
||||
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>
|
||||
/// </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>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -173,6 +174,7 @@ public class AgentResponse
|
||||
/// to poll for completion.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public ResponseContinuationToken? ContinuationToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// 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;
|
||||
@@ -50,6 +52,7 @@ public class AgentRunOptions
|
||||
/// 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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -39,6 +40,25 @@ namespace Microsoft.Agents.AI;
|
||||
/// </remarks>
|
||||
public abstract class ChatHistoryProvider
|
||||
{
|
||||
private static IEnumerable<ChatMessage> DefaultExcludeChatHistoryFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
|
||||
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _provideOutputMessageFilter;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storeInputMessageFilter;
|
||||
|
||||
/// <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="storeInputMessageFilter">An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type <see cref="AgentRequestMessageSourceType.ChatHistory"/>.</param>
|
||||
protected ChatHistoryProvider(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
|
||||
{
|
||||
this._provideOutputMessageFilter = provideOutputMessageFilter;
|
||||
this._storeInputMessageFilter = storeInputMessageFilter ?? DefaultExcludeChatHistoryFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key used to store the provider state in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
@@ -50,20 +70,16 @@ public abstract class ChatHistoryProvider
|
||||
public virtual string StateKey => this.GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide messages from the chat history as context for the next agent invocation.
|
||||
/// 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 in ascending chronological order (oldest first).
|
||||
/// instances that will be used for the agent invocation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <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>
|
||||
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
|
||||
/// storage constraints, such as:
|
||||
/// <list type="bullet">
|
||||
@@ -75,23 +91,19 @@ public abstract class ChatHistoryProvider
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
=> this.InvokingCoreAsync(context, cancellationToken);
|
||||
=> this.InvokingCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the start of agent invocation to provide messages from the chat history as context for the next agent invocation.
|
||||
/// 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 in ascending chronological order (oldest first).
|
||||
/// instances that will be used for the agent invocation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <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>
|
||||
/// If the total message history becomes very large, implementations should apply appropriate strategies to manage
|
||||
/// storage constraints, such as:
|
||||
/// <list type="bullet">
|
||||
@@ -102,11 +114,54 @@ public abstract class ChatHistoryProvider
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each <see cref="ChatHistoryProvider"/> instance should be associated with a single <see cref="AgentSession"/> to ensure proper message isolation
|
||||
/// and context management.
|
||||
/// 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 abstract ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default);
|
||||
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>
|
||||
/// </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.
|
||||
@@ -134,7 +189,7 @@ public abstract class ChatHistoryProvider
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
|
||||
this.InvokedCoreAsync(context, cancellationToken);
|
||||
this.InvokedCoreAsync(Throw.IfNull(context), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Called at the end of the agent invocation to add new messages to the chat history.
|
||||
@@ -160,8 +215,59 @@ public abstract class ChatHistoryProvider
|
||||
/// 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 message filter
|
||||
/// 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 abstract ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default);
|
||||
protected virtual ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var subContext = new InvokedContext(context.Agent, context.Session, this._storeInputMessageFilter(context.RequestMessages), context.ResponseMessages!);
|
||||
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>
|
||||
/// </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>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -27,14 +26,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// </remarks>
|
||||
public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
{
|
||||
private static IEnumerable<ChatMessage> DefaultExcludeChatHistoryFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
|
||||
|
||||
private readonly string _stateKey;
|
||||
private readonly Func<AgentSession?, State> _stateInitializer;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storageInputMessageFilter;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _retrievalOutputMessageFilter;
|
||||
private readonly ProviderSessionState<State> _sessionState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InMemoryChatHistoryProvider"/> class.
|
||||
@@ -44,18 +36,20 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
/// message reduction, and serialization settings. If <see langword="null"/>, default settings will be used.
|
||||
/// </param>
|
||||
public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options = null)
|
||||
: base(
|
||||
options?.ProvideOutputMessageFilter,
|
||||
options?.StorageInputMessageFilter)
|
||||
{
|
||||
this._stateInitializer = options?.StateInitializer ?? (_ => new State());
|
||||
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;
|
||||
this._stateKey = options?.StateKey ?? base.StateKey;
|
||||
this._jsonSerializerOptions = options?.JsonSerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
this._storageInputMessageFilter = options?.StorageInputMessageFilter ?? DefaultExcludeChatHistoryFilter;
|
||||
this._retrievalOutputMessageFilter = options?.RetrievalOutputMessageFilter;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string StateKey => this._stateKey;
|
||||
public override string StateKey => this._sessionState.StateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied.
|
||||
@@ -73,7 +67,7 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
/// <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.GetOrInitializeState(session).Messages;
|
||||
=> this._sessionState.GetOrInitializeState(session).Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the chat messages for the specified session.
|
||||
@@ -85,67 +79,30 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
var state = this.GetOrInitializeState(session);
|
||||
var state = this._sessionState.GetOrInitializeState(session);
|
||||
state.Messages = messages;
|
||||
}
|
||||
|
||||
/// <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, or null if no session is available.</returns>
|
||||
private State GetOrInitializeState(AgentSession? session)
|
||||
{
|
||||
if (session?.StateBag.TryGetValue<State>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
|
||||
{
|
||||
state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
|
||||
IEnumerable<ChatMessage> output = state.Messages;
|
||||
if (this._retrievalOutputMessageFilter is not null)
|
||||
{
|
||||
output = this._retrievalOutputMessageFilter(output);
|
||||
}
|
||||
return output
|
||||
.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!))
|
||||
.Concat(context.RequestMessages);
|
||||
return state.Messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
// Add request and response messages to the provider
|
||||
var allNewMessages = this._storageInputMessageFilter(context.RequestMessages).Concat(context.ResponseMessages ?? []);
|
||||
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
|
||||
state.Messages.AddRange(allNewMessages);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
|
||||
@@ -71,7 +71,7 @@ public sealed class InMemoryChatHistoryProviderOptions
|
||||
/// <value>
|
||||
/// When <see langword="null"/>, no filtering is applied to the output messages.
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? RetrievalOutputMessageFilter { get; set; }
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? ProvideOutputMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatHistoryProvider"/>.
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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 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="storeInputMessageFilter">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>
|
||||
protected MessageAIContextProvider(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
|
||||
: base(provideInputMessageFilter, storeInputMessageFilter)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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.
|
||||
return new AIContext
|
||||
{
|
||||
Messages = await this.ProvideMessagesAsync(
|
||||
new InvokingContext(context.Agent, context.Session, context.AIContext.Messages ?? []),
|
||||
cancellationToken).ConfigureAwait(false)
|
||||
};
|
||||
}
|
||||
|
||||
/// <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).
|
||||
var filteredContext = new InvokingContext(
|
||||
context.Agent,
|
||||
context.Session,
|
||||
this.ProvideInputMessageFilter(inputMessages));
|
||||
|
||||
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>
|
||||
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); } }
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.Agents.AI</RootNamespace>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -14,6 +14,8 @@
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
|
||||
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.OpenAI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
namespace Microsoft.Agents.AI.AzureAI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using
|
||||
/// Azure-specific agent capabilities.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
internal sealed class AzureAIProjectChatClient : DelegatingChatClient
|
||||
{
|
||||
private readonly ChatClientMetadata? _metadata;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System.ClientModel;
|
||||
using System.ClientModel.Primitives;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -12,18 +13,17 @@ using Azure.AI.Projects.OpenAI;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.AzureAI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
|
||||
namespace Azure.AI.Projects;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="AIProjectClient"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static partial class AzureAIProjectChatClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.AI.Projects.OpenAI" />
|
||||
|
||||
@@ -60,7 +60,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
|
||||
if (session is not CopilotStudioAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(CopilotStudioAgentSession)}' can be serialized by this agent.");
|
||||
}
|
||||
|
||||
return new(typedSession.Serialize(jsonSerializerOptions));
|
||||
@@ -84,7 +84,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (session is not CopilotStudioAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be used.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(CopilotStudioAgentSession)}' can be used by this agent.");
|
||||
}
|
||||
|
||||
typedSession.ConversationId ??= await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -123,7 +123,7 @@ public class CopilotStudioAgent : AIAgent
|
||||
session ??= await this.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (session is not CopilotStudioAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be used.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(CopilotStudioAgentSession)}' can be used by this agent.");
|
||||
}
|
||||
|
||||
typedSession.ConversationId ??= await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -21,14 +21,10 @@ namespace Microsoft.Agents.AI;
|
||||
[RequiresDynamicCode("The CosmosChatHistoryProvider uses JSON serialization which is incompatible with NativeAOT.")]
|
||||
public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
{
|
||||
private static IEnumerable<ChatMessage> DefaultExcludeChatHistoryFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
|
||||
|
||||
private readonly ProviderSessionState<State> _sessionState;
|
||||
private readonly CosmosClient _cosmosClient;
|
||||
private readonly Container _container;
|
||||
private readonly bool _ownsClient;
|
||||
private readonly string _stateKey;
|
||||
private readonly Func<AgentSession?, State> _stateInitializer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
@@ -46,9 +42,6 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string StateKey => this._stateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to return in a single query batch.
|
||||
/// Default is 100 for optimal performance.
|
||||
@@ -84,25 +77,6 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
/// </summary>
|
||||
public string ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A filter function applied to request messages before they are stored
|
||||
/// during <see cref="ChatHistoryProvider.InvokedAsync"/>. The default filter excludes messages with the
|
||||
/// <see cref="AgentRequestMessageSourceType.ChatHistory"/> source type.
|
||||
/// </summary>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> StorageInputMessageFilter { get; set { field = Throw.IfNull(value); } } = DefaultExcludeChatHistoryFilter;
|
||||
|
||||
/// <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>>? RetrievalOutputMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class.
|
||||
/// </summary>
|
||||
@@ -112,6 +86,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
/// <param name="stateInitializer">A delegate that initializes the provider state on the first invocation, providing the conversation routing info (conversationId, tenantId, userId).</param>
|
||||
/// <param name="ownsClient">Whether this instance owns the CosmosClient and should dispose it.</param>
|
||||
/// <param name="stateKey">An optional key to use for storing the state in the <see cref="AgentSession.StateBag"/>.</param>
|
||||
/// <param name="provideOutputMessageFilter">An optional filter function to apply to messages when retrieving them from the chat history.</param>
|
||||
/// <param name="storeInputMessageFilter">An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type <see cref="AgentRequestMessageSourceType.ChatHistory"/>.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> or <paramref name="stateInitializer"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
|
||||
public CosmosChatHistoryProvider(
|
||||
@@ -120,17 +96,24 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
string containerId,
|
||||
Func<AgentSession?, State> stateInitializer,
|
||||
bool ownsClient = false,
|
||||
string? stateKey = null)
|
||||
string? stateKey = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
|
||||
: base(provideOutputMessageFilter, storeInputMessageFilter)
|
||||
{
|
||||
this._sessionState = new ProviderSessionState<State>(
|
||||
Throw.IfNull(stateInitializer),
|
||||
stateKey ?? this.GetType().Name);
|
||||
this._cosmosClient = Throw.IfNull(cosmosClient);
|
||||
this.DatabaseId = Throw.IfNullOrWhitespace(databaseId);
|
||||
this.ContainerId = Throw.IfNullOrWhitespace(containerId);
|
||||
this._container = this._cosmosClient.GetContainer(databaseId, containerId);
|
||||
this._stateInitializer = Throw.IfNull(stateInitializer);
|
||||
this._ownsClient = ownsClient;
|
||||
this._stateKey = stateKey ?? base.StateKey;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string StateKey => this._sessionState.StateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CosmosChatHistoryProvider"/> class using a connection string.
|
||||
/// </summary>
|
||||
@@ -139,6 +122,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
|
||||
/// <param name="stateInitializer">A delegate that initializes the provider state on the first invocation.</param>
|
||||
/// <param name="stateKey">An optional key to use for storing the state in the <see cref="AgentSession.StateBag"/>.</param>
|
||||
/// <param name="provideOutputMessageFilter">An optional filter function to apply to messages when retrieving them from the chat history.</param>
|
||||
/// <param name="storeInputMessageFilter">An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type <see cref="AgentRequestMessageSourceType.ChatHistory"/>.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
|
||||
public CosmosChatHistoryProvider(
|
||||
@@ -146,8 +131,10 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
string databaseId,
|
||||
string containerId,
|
||||
Func<AgentSession?, State> stateInitializer,
|
||||
string? stateKey = null)
|
||||
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey)
|
||||
string? stateKey = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
|
||||
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputMessageFilter)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -160,6 +147,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
/// <param name="containerId">The identifier of the Cosmos DB container.</param>
|
||||
/// <param name="stateInitializer">A delegate that initializes the provider state on the first invocation.</param>
|
||||
/// <param name="stateKey">An optional key to use for storing the state in the <see cref="AgentSession.StateBag"/>.</param>
|
||||
/// <param name="provideOutputMessageFilter">An optional filter function to apply to messages when retrieving them from the chat history.</param>
|
||||
/// <param name="storeInputMessageFilter">An optional filter function to apply to messages before storing them in the chat history. If not set, defaults to excluding messages with source type <see cref="AgentRequestMessageSourceType.ChatHistory"/>.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
|
||||
public CosmosChatHistoryProvider(
|
||||
@@ -168,32 +157,13 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
string databaseId,
|
||||
string containerId,
|
||||
Func<AgentSession?, State> stateInitializer,
|
||||
string? stateKey = null)
|
||||
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey)
|
||||
string? stateKey = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
|
||||
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputMessageFilter)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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, or null if no session is available.</returns>
|
||||
private State GetOrInitializeState(AgentSession? session)
|
||||
{
|
||||
if (session?.StateBag.TryGetValue<State>(this._stateKey, out var state, AgentAbstractionsJsonUtilities.DefaultOptions) is true && state is not null)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
state = this._stateInitializer(session);
|
||||
if (session is not null)
|
||||
{
|
||||
session.StateBag.SetValue(this._stateKey, state, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether hierarchical partitioning should be used based on the state.
|
||||
/// </summary>
|
||||
@@ -218,7 +188,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
|
||||
if (this._disposed)
|
||||
@@ -227,9 +197,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
}
|
||||
#pragma warning restore CA1513
|
||||
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
var partitionKey = BuildPartitionKey(state);
|
||||
|
||||
// Fetch most recent messages in descending order when limit is set, then reverse to ascending
|
||||
@@ -279,22 +247,12 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
messages.Reverse();
|
||||
}
|
||||
|
||||
return (this.RetrievalOutputMessageFilter is not null ? this.RetrievalOutputMessageFilter(messages) : messages)
|
||||
.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!))
|
||||
.Concat(context.RequestMessages);
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(context);
|
||||
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
// Do not store messages if there was an exception during invocation
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
|
||||
if (this._disposed)
|
||||
{
|
||||
@@ -302,8 +260,8 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
}
|
||||
#pragma warning restore CA1513
|
||||
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var messageList = this.StorageInputMessageFilter(context.RequestMessages).Concat(context.ResponseMessages ?? []).ToList();
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
var messageList = context.RequestMessages.Concat(context.ResponseMessages ?? []).ToList();
|
||||
if (messageList.Count == 0)
|
||||
{
|
||||
return;
|
||||
@@ -473,11 +431,11 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
}
|
||||
#pragma warning restore CA1513
|
||||
|
||||
var state = this.GetOrInitializeState(session);
|
||||
var state = this._sessionState.GetOrInitializeState(session);
|
||||
var partitionKey = BuildPartitionKey(state);
|
||||
|
||||
// Efficient count query
|
||||
var query = new QueryDefinition("SELECT VALUE COUNT(1) FROM c WHERE c.conversationId = @conversationId AND c.Type = @type")
|
||||
var query = new QueryDefinition("SELECT VALUE COUNT(1) FROM c WHERE c.conversationId = @conversationId AND c.type = @type")
|
||||
.WithParameter("@conversationId", state.ConversationId)
|
||||
.WithParameter("@type", "ChatMessage");
|
||||
|
||||
@@ -507,11 +465,11 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
|
||||
}
|
||||
#pragma warning restore CA1513
|
||||
|
||||
var state = this.GetOrInitializeState(session);
|
||||
var state = this._sessionState.GetOrInitializeState(session);
|
||||
var partitionKey = BuildPartitionKey(state);
|
||||
|
||||
// Batch delete for efficiency
|
||||
var query = new QueryDefinition("SELECT VALUE c.id FROM c WHERE c.conversationId = @conversationId AND c.Type = @type")
|
||||
var query = new QueryDefinition("SELECT VALUE c.id FROM c WHERE c.conversationId = @conversationId AND c.type = @type")
|
||||
.WithParameter("@conversationId", state.ConversationId)
|
||||
.WithParameter("@type", "ChatMessage");
|
||||
|
||||
|
||||
@@ -95,11 +95,11 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
public string ContainerId => this._container.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<CheckpointInfo> CreateCheckpointAsync(string runId, JsonElement value, CheckpointInfo? parent = null)
|
||||
public override async ValueTask<CheckpointInfo> CreateCheckpointAsync(string sessionId, JsonElement value, CheckpointInfo? parent = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
if (string.IsNullOrWhiteSpace(sessionId))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or whitespace", nameof(runId));
|
||||
throw new ArgumentException("Cannot be null or whitespace", nameof(sessionId));
|
||||
}
|
||||
|
||||
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
|
||||
@@ -110,28 +110,28 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
#pragma warning restore CA1513
|
||||
|
||||
var checkpointId = Guid.NewGuid().ToString("N");
|
||||
var checkpointInfo = new CheckpointInfo(runId, checkpointId);
|
||||
var checkpointInfo = new CheckpointInfo(sessionId, checkpointId);
|
||||
|
||||
var document = new CosmosCheckpointDocument
|
||||
{
|
||||
Id = $"{runId}_{checkpointId}",
|
||||
RunId = runId,
|
||||
Id = $"{sessionId}_{checkpointId}",
|
||||
SessionId = sessionId,
|
||||
CheckpointId = checkpointId,
|
||||
Value = JToken.Parse(value.GetRawText()),
|
||||
ParentCheckpointId = parent?.CheckpointId,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
await this._container.CreateItemAsync(document, new PartitionKey(runId)).ConfigureAwait(false);
|
||||
await this._container.CreateItemAsync(document, new PartitionKey(sessionId)).ConfigureAwait(false);
|
||||
return checkpointInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<JsonElement> RetrieveCheckpointAsync(string runId, CheckpointInfo key)
|
||||
public override async ValueTask<JsonElement> RetrieveCheckpointAsync(string sessionId, CheckpointInfo key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
if (string.IsNullOrWhiteSpace(sessionId))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or whitespace", nameof(runId));
|
||||
throw new ArgumentException("Cannot be null or whitespace", nameof(sessionId));
|
||||
}
|
||||
|
||||
if (key is null)
|
||||
@@ -146,26 +146,26 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
}
|
||||
#pragma warning restore CA1513
|
||||
|
||||
var id = $"{runId}_{key.CheckpointId}";
|
||||
var id = $"{sessionId}_{key.CheckpointId}";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await this._container.ReadItemAsync<CosmosCheckpointDocument>(id, new PartitionKey(runId)).ConfigureAwait(false);
|
||||
var response = await this._container.ReadItemAsync<CosmosCheckpointDocument>(id, new PartitionKey(sessionId)).ConfigureAwait(false);
|
||||
using var document = JsonDocument.Parse(response.Resource.Value.ToString());
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new InvalidOperationException($"Checkpoint with ID '{key.CheckpointId}' for run '{runId}' not found.");
|
||||
throw new InvalidOperationException($"Checkpoint with ID '{key.CheckpointId}' for session '{sessionId}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
|
||||
public override async ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(runId))
|
||||
if (string.IsNullOrWhiteSpace(sessionId))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or whitespace", nameof(runId));
|
||||
throw new ArgumentException("Cannot be null or whitespace", nameof(sessionId));
|
||||
}
|
||||
|
||||
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
|
||||
@@ -176,10 +176,10 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
#pragma warning restore CA1513
|
||||
|
||||
QueryDefinition query = withParent == null
|
||||
? new QueryDefinition("SELECT c.runId, c.checkpointId FROM c WHERE c.runId = @runId ORDER BY c.timestamp ASC")
|
||||
.WithParameter("@runId", runId)
|
||||
: new QueryDefinition("SELECT c.runId, c.checkpointId FROM c WHERE c.runId = @runId AND c.parentCheckpointId = @parentCheckpointId ORDER BY c.timestamp ASC")
|
||||
.WithParameter("@runId", runId)
|
||||
? new QueryDefinition("SELECT c.sessionId, c.checkpointId FROM c WHERE c.sessionId = @sessionId ORDER BY c.timestamp ASC")
|
||||
.WithParameter("@sessionId", sessionId)
|
||||
: new QueryDefinition("SELECT c.sessionId, c.checkpointId FROM c WHERE c.sessionId = @sessionId AND c.parentCheckpointId = @parentCheckpointId ORDER BY c.timestamp ASC")
|
||||
.WithParameter("@sessionId", sessionId)
|
||||
.WithParameter("@parentCheckpointId", withParent.CheckpointId);
|
||||
|
||||
var iterator = this._container.GetItemQueryIterator<CheckpointQueryResult>(query);
|
||||
@@ -188,7 +188,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
while (iterator.HasMoreResults)
|
||||
{
|
||||
var response = await iterator.ReadNextAsync().ConfigureAwait(false);
|
||||
checkpoints.AddRange(response.Select(r => new CheckpointInfo(r.RunId, r.CheckpointId)));
|
||||
checkpoints.AddRange(response.Select(r => new CheckpointInfo(r.SessionId, r.CheckpointId)));
|
||||
}
|
||||
|
||||
return checkpoints;
|
||||
@@ -223,8 +223,8 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; } = string.Empty;
|
||||
[JsonProperty("sessionId")]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("checkpointId")]
|
||||
public string CheckpointId { get; set; } = string.Empty;
|
||||
@@ -245,7 +245,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
|
||||
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Cosmos DB query deserialization")]
|
||||
private sealed class CheckpointQueryResult
|
||||
{
|
||||
public string RunId { get; set; } = string.Empty;
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public string CheckpointId { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<RootNamespace>Microsoft.Agents.AI</RootNamespace>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -32,7 +32,7 @@ public static class MicrosoftAgentAIDevUIServiceCollectionsExtensions
|
||||
var workflow = sp.GetKeyedService<Workflow>(keyAsStr);
|
||||
if (workflow is not null)
|
||||
{
|
||||
return workflow.AsAgent(name: workflow.Name);
|
||||
return workflow.AsAIAgent(name: workflow.Name);
|
||||
}
|
||||
|
||||
// another thing we can do is resolve a non-keyed workflow.
|
||||
@@ -41,7 +41,7 @@ public static class MicrosoftAgentAIDevUIServiceCollectionsExtensions
|
||||
workflow = sp.GetService<Workflow>();
|
||||
if (workflow is not null && workflow.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true)
|
||||
{
|
||||
return workflow.AsAgent(name: workflow.Name);
|
||||
return workflow.AsAIAgent(name: workflow.Name);
|
||||
}
|
||||
|
||||
// and it's possible to lookup at the default-registered AIAgent
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- Changed AIAgent.SerializeSession to AIAgent.SerializeSessionAsync ([#3879](https://github.com/microsoft/agent-framework/pull/3879))
|
||||
- Changed ChatHistory and AIContext Providers to have pipeline semantics ([#3806](https://github.com/microsoft/agent-framework/pull/3806))
|
||||
- Marked all `RunAsync<T>` overloads as `new`, added missing ones, and added support for primitives and arrays ([#3803](https://github.com/microsoft/agent-framework/pull/3803))
|
||||
- Improve session cast error message quality and consistency ([#3973](https://github.com/microsoft/agent-framework/pull/3973))
|
||||
|
||||
## v1.0.0-preview.251204.1
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class DurableAIAgent : AIAgent
|
||||
|
||||
if (session is not DurableAgentSession durableSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(DurableAgentSession)}' can be serialized by this agent.");
|
||||
}
|
||||
|
||||
return new(durableSession.Serialize(jsonSerializerOptions));
|
||||
|
||||
@@ -20,7 +20,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
|
||||
|
||||
if (session is not DurableAgentSession durableSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(DurableAgentSession)}' can be serialized by this agent.");
|
||||
}
|
||||
|
||||
return new(durableSession.Serialize(jsonSerializerOptions));
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- CA2007: This rule should generally be suppressed in Durable Task libraries -->
|
||||
<!-- MEAI001: UserInputRequestContent is experimental but used in source-generated code for AgentResponse -->
|
||||
<NoWarn>$(NoWarn);CA2007;MEAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Projects;
|
||||
|
||||
namespace Microsoft.Agents.AI.FoundryMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Internal extension methods for <see cref="AIProjectClient"/> to provide MemoryStores helper operations.
|
||||
/// </summary>
|
||||
internal static class AIProjectClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a memory store if it doesn't already exist.
|
||||
/// </summary>
|
||||
internal static async Task<bool> CreateMemoryStoreIfNotExistsAsync(
|
||||
this AIProjectClient client,
|
||||
string memoryStoreName,
|
||||
string? description,
|
||||
string chatModel,
|
||||
string embeddingModel,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.MemoryStores.GetMemoryStoreAsync(memoryStoreName, cancellationToken).ConfigureAwait(false);
|
||||
return false; // Store already exists
|
||||
}
|
||||
catch (ClientResultException ex) when (ex.Status == 404)
|
||||
{
|
||||
// Store doesn't exist, create it
|
||||
}
|
||||
|
||||
MemoryStoreDefaultDefinition definition = new(chatModel, embeddingModel);
|
||||
await client.MemoryStores.CreateMemoryStoreAsync(memoryStoreName, definition, description, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.FoundryMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Provides JSON serialization utilities for the Foundry Memory provider.
|
||||
/// </summary>
|
||||
internal static class FoundryMemoryJsonUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default JSON serializer options for Foundry Memory operations.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false,
|
||||
TypeInfoResolver = FoundryMemoryJsonContext.Default
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON serialization context for Foundry Memory types.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.General,
|
||||
UseStringEnumConverter = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(FoundryMemoryProviderScope))]
|
||||
[JsonSerializable(typeof(FoundryMemoryProvider.State))]
|
||||
internal partial class FoundryMemoryJsonContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,440 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ClientModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Projects;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.FoundryMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an Azure AI Foundry Memory backed <see cref="AIContextProvider"/> that persists conversation messages as memories
|
||||
/// and retrieves related memories to augment the agent invocation context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provider stores user, assistant and system messages as Foundry memories and retrieves relevant memories
|
||||
/// for new invocations using the memory search endpoint. Retrieved memories are injected as user messages
|
||||
/// to the model, prefixed by a configurable context prompt.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public sealed class FoundryMemoryProvider : AIContextProvider
|
||||
{
|
||||
private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:";
|
||||
|
||||
private readonly ProviderSessionState<State> _sessionState;
|
||||
private readonly string _contextPrompt;
|
||||
private readonly string _memoryStoreName;
|
||||
private readonly int _maxMemories;
|
||||
private readonly int _updateDelay;
|
||||
private readonly bool _enableSensitiveTelemetryData;
|
||||
|
||||
private readonly AIProjectClient _client;
|
||||
private readonly ILogger<FoundryMemoryProvider>? _logger;
|
||||
|
||||
private string? _lastPendingUpdateId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FoundryMemoryProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The Azure AI Project client configured for your Foundry project.</param>
|
||||
/// <param name="memoryStoreName">The name of the memory store in Azure AI Foundry.</param>
|
||||
/// <param name="stateInitializer">A delegate that initializes the provider state on the first invocation, providing the scope for memory storage and retrieval.</param>
|
||||
/// <param name="options">Provider options.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="client"/> or <paramref name="stateInitializer"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="memoryStoreName"/> is null or whitespace.</exception>
|
||||
public FoundryMemoryProvider(
|
||||
AIProjectClient client,
|
||||
string memoryStoreName,
|
||||
Func<AgentSession?, State> stateInitializer,
|
||||
FoundryMemoryProviderOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
: base(options?.SearchInputMessageFilter, options?.StorageInputMessageFilter)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhitespace(memoryStoreName);
|
||||
|
||||
this._sessionState = new ProviderSessionState<State>(
|
||||
ValidateStateInitializer(Throw.IfNull(stateInitializer)),
|
||||
options?.StateKey ?? this.GetType().Name,
|
||||
FoundryMemoryJsonUtilities.DefaultOptions);
|
||||
|
||||
FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
|
||||
|
||||
this._logger = loggerFactory?.CreateLogger<FoundryMemoryProvider>();
|
||||
this._client = client;
|
||||
|
||||
this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
|
||||
this._memoryStoreName = memoryStoreName;
|
||||
this._maxMemories = effectiveOptions.MaxMemories;
|
||||
this._updateDelay = effectiveOptions.UpdateDelay;
|
||||
this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string StateKey => this._sessionState.StateKey;
|
||||
|
||||
private static Func<AgentSession?, State> ValidateStateInitializer(Func<AgentSession?, State> stateInitializer) =>
|
||||
session =>
|
||||
{
|
||||
State state = stateInitializer(session);
|
||||
|
||||
if (state is null)
|
||||
{
|
||||
throw new InvalidOperationException("State initializer must return a non-null state.");
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(context);
|
||||
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
FoundryMemoryProviderScope scope = state.Scope;
|
||||
|
||||
List<ResponseItem> messageItems = (context.AIContext.Messages ?? [])
|
||||
.Where(m => !string.IsNullOrWhiteSpace(m.Text))
|
||||
.Select(m => (ResponseItem)ToResponseItem(m.Role, m.Text!))
|
||||
.ToList();
|
||||
|
||||
if (messageItems.Count == 0)
|
||||
{
|
||||
return new AIContext();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
MemorySearchOptions searchOptions = new(scope.Scope)
|
||||
{
|
||||
ResultOptions = new MemorySearchResultOptions { MaxMemories = this._maxMemories }
|
||||
};
|
||||
|
||||
foreach (ResponseItem item in messageItems)
|
||||
{
|
||||
searchOptions.Items.Add(item);
|
||||
}
|
||||
|
||||
ClientResult<MemoryStoreSearchResponse> result = await this._client.MemoryStores.SearchMemoriesAsync(
|
||||
this._memoryStoreName,
|
||||
searchOptions,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
MemoryStoreSearchResponse response = result.Value;
|
||||
|
||||
List<string> memories = response.Memories
|
||||
.Select(m => m.MemoryItem?.Content ?? string.Empty)
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c))
|
||||
.ToList();
|
||||
|
||||
string? outputMessageText = memories.Count == 0
|
||||
? null
|
||||
: $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
|
||||
|
||||
if (this._logger?.IsEnabled(LogLevel.Information) is true)
|
||||
{
|
||||
this._logger.LogInformation(
|
||||
"FoundryMemoryProvider: Retrieved {Count} memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
|
||||
memories.Count,
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope));
|
||||
|
||||
if (outputMessageText is not null && this._logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
this._logger.LogTrace(
|
||||
"FoundryMemoryProvider: Search Results\nOutput:{MessageText}\nMemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
|
||||
this.SanitizeLogData(outputMessageText),
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope));
|
||||
}
|
||||
}
|
||||
|
||||
return new AIContext
|
||||
{
|
||||
Messages = [new ChatMessage(ChatRole.User, outputMessageText)]
|
||||
};
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (this._logger?.IsEnabled(LogLevel.Error) is true)
|
||||
{
|
||||
this._logger.LogError(
|
||||
ex,
|
||||
"FoundryMemoryProvider: Failed to search for memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope));
|
||||
}
|
||||
|
||||
return new AIContext();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
FoundryMemoryProviderScope scope = state.Scope;
|
||||
|
||||
try
|
||||
{
|
||||
List<ResponseItem> messageItems = context.RequestMessages
|
||||
.Concat(context.ResponseMessages ?? [])
|
||||
.Where(m => IsAllowedRole(m.Role) && !string.IsNullOrWhiteSpace(m.Text))
|
||||
.Select(m => (ResponseItem)ToResponseItem(m.Role, m.Text!))
|
||||
.ToList();
|
||||
|
||||
if (messageItems.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MemoryUpdateOptions updateOptions = new(scope.Scope)
|
||||
{
|
||||
UpdateDelay = this._updateDelay
|
||||
};
|
||||
|
||||
foreach (ResponseItem item in messageItems)
|
||||
{
|
||||
updateOptions.Items.Add(item);
|
||||
}
|
||||
|
||||
ClientResult<MemoryUpdateResult> result = await this._client.MemoryStores.UpdateMemoriesAsync(
|
||||
this._memoryStoreName,
|
||||
updateOptions,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
MemoryUpdateResult response = result.Value;
|
||||
|
||||
if (response.UpdateId is not null)
|
||||
{
|
||||
Interlocked.Exchange(ref this._lastPendingUpdateId, response.UpdateId);
|
||||
}
|
||||
|
||||
if (this._logger?.IsEnabled(LogLevel.Information) is true)
|
||||
{
|
||||
this._logger.LogInformation(
|
||||
"FoundryMemoryProvider: Sent {Count} messages to update memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}', UpdateId: '{UpdateId}'.",
|
||||
messageItems.Count,
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope),
|
||||
response.UpdateId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (this._logger?.IsEnabled(LogLevel.Error) is true)
|
||||
{
|
||||
this._logger.LogError(
|
||||
ex,
|
||||
"FoundryMemoryProvider: Failed to send messages to update memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all stored memories for the configured scope are deleted.
|
||||
/// This method handles cases where the scope doesn't exist (no memories stored yet).
|
||||
/// </summary>
|
||||
/// <param name="session">The session containing the scope state to clear memories for.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task EnsureStoredMemoriesDeletedAsync(AgentSession session, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(session);
|
||||
State state = this._sessionState.GetOrInitializeState(session);
|
||||
FoundryMemoryProviderScope scope = state.Scope;
|
||||
|
||||
try
|
||||
{
|
||||
await this._client.MemoryStores.DeleteScopeAsync(this._memoryStoreName, scope.Scope, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (this._logger?.IsEnabled(LogLevel.Information) is true)
|
||||
{
|
||||
this._logger.LogInformation(
|
||||
"FoundryMemoryProvider: Deleted stored memories for scope. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope));
|
||||
}
|
||||
}
|
||||
catch (ClientResultException ex) when (ex.Status == 404)
|
||||
{
|
||||
// Scope doesn't exist (no memories stored yet), nothing to delete
|
||||
if (this._logger?.IsEnabled(LogLevel.Debug) is true)
|
||||
{
|
||||
this._logger.LogDebug(
|
||||
"FoundryMemoryProvider: No memories to delete for scope. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
|
||||
this._memoryStoreName,
|
||||
this.SanitizeLogData(scope.Scope));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the memory store exists, creating it if necessary.
|
||||
/// </summary>
|
||||
/// <param name="chatModel">The deployment name of the chat model for memory processing.</param>
|
||||
/// <param name="embeddingModel">The deployment name of the embedding model for memory search.</param>
|
||||
/// <param name="description">Optional description for the memory store.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public async Task EnsureMemoryStoreCreatedAsync(
|
||||
string chatModel,
|
||||
string embeddingModel,
|
||||
string? description = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool created = await this._client.CreateMemoryStoreIfNotExistsAsync(
|
||||
this._memoryStoreName,
|
||||
description,
|
||||
chatModel,
|
||||
embeddingModel,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (created)
|
||||
{
|
||||
if (this._logger?.IsEnabled(LogLevel.Information) is true)
|
||||
{
|
||||
this._logger.LogInformation(
|
||||
"FoundryMemoryProvider: Created memory store '{MemoryStoreName}'.",
|
||||
this._memoryStoreName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this._logger?.IsEnabled(LogLevel.Debug) is true)
|
||||
{
|
||||
this._logger.LogDebug(
|
||||
"FoundryMemoryProvider: Memory store '{MemoryStoreName}' already exists.",
|
||||
this._memoryStoreName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for all pending memory update operations to complete.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Memory extraction in Azure AI Foundry is asynchronous. This method polls the latest pending update
|
||||
/// and returns when it has completed, failed, or been superseded. Since updates are processed in order,
|
||||
/// completion of the latest update implies all prior updates have also been processed.
|
||||
/// </remarks>
|
||||
/// <param name="pollingInterval">The interval between status checks. Defaults to 5 seconds.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the update operation failed.</exception>
|
||||
public async Task WhenUpdatesCompletedAsync(
|
||||
TimeSpan? pollingInterval = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? updateId = Volatile.Read(ref this._lastPendingUpdateId);
|
||||
if (updateId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan interval = pollingInterval ?? TimeSpan.FromSeconds(5);
|
||||
await this.WaitForUpdateAsync(updateId, interval, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Only clear the pending update ID after successful completion
|
||||
Interlocked.CompareExchange(ref this._lastPendingUpdateId, null, updateId);
|
||||
}
|
||||
|
||||
private async Task WaitForUpdateAsync(string updateId, TimeSpan interval, CancellationToken cancellationToken)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
ClientResult<MemoryUpdateResult> result = await this._client.MemoryStores.GetUpdateResultAsync(
|
||||
this._memoryStoreName,
|
||||
updateId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
MemoryUpdateResult response = result.Value;
|
||||
MemoryStoreUpdateStatus status = response.Status;
|
||||
|
||||
if (this._logger?.IsEnabled(LogLevel.Debug) is true)
|
||||
{
|
||||
this._logger.LogDebug(
|
||||
"FoundryMemoryProvider: Update status for '{UpdateId}': {Status}",
|
||||
updateId,
|
||||
status);
|
||||
}
|
||||
|
||||
if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (status == MemoryStoreUpdateStatus.Failed)
|
||||
{
|
||||
throw new InvalidOperationException($"Memory update operation '{updateId}' failed: {response.ErrorDetails}");
|
||||
}
|
||||
|
||||
if (status == MemoryStoreUpdateStatus.Queued || status == MemoryStoreUpdateStatus.InProgress)
|
||||
{
|
||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown update status '{status}' for update '{updateId}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageResponseItem ToResponseItem(ChatRole role, string text)
|
||||
{
|
||||
if (role == ChatRole.Assistant)
|
||||
{
|
||||
return ResponseItem.CreateAssistantMessageItem(text);
|
||||
}
|
||||
|
||||
if (role == ChatRole.System)
|
||||
{
|
||||
return ResponseItem.CreateSystemMessageItem(text);
|
||||
}
|
||||
|
||||
return ResponseItem.CreateUserMessageItem(text);
|
||||
}
|
||||
|
||||
private static bool IsAllowedRole(ChatRole role) =>
|
||||
role == ChatRole.User || role == ChatRole.Assistant || role == ChatRole.System;
|
||||
|
||||
private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "<redacted>";
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of a <see cref="FoundryMemoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
public sealed class State
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="State"/> class with the specified scope.
|
||||
/// </summary>
|
||||
/// <param name="scope">The scope to use for memory storage and retrieval.</param>
|
||||
[JsonConstructor]
|
||||
public State(FoundryMemoryProviderScope scope)
|
||||
{
|
||||
this.Scope = Throw.IfNull(scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scope used for memory storage and retrieval.
|
||||
/// </summary>
|
||||
public FoundryMemoryProviderScope Scope { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.FoundryMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Options for configuring the <see cref="FoundryMemoryProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class FoundryMemoryProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// When providing memories to the model, this string is prefixed to the retrieved memories to supply context.
|
||||
/// </summary>
|
||||
/// <value>Defaults to "## Memories\nConsider the following memories when answering user questions:".</value>
|
||||
public string? ContextPrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of memories to retrieve during search.
|
||||
/// </summary>
|
||||
/// <value>Defaults to 5.</value>
|
||||
public int MaxMemories { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay in seconds before memory updates are processed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting to 0 triggers updates immediately without waiting for inactivity.
|
||||
/// Higher values allow the service to batch multiple updates together.
|
||||
/// </remarks>
|
||||
/// <value>Defaults to 0 (immediate).</value>
|
||||
public int UpdateDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
|
||||
/// </summary>
|
||||
/// <value>Defaults to <see langword="false"/>.</value>
|
||||
public bool EnableSensitiveTelemetryData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key used to store the provider state in the session's <see cref="AgentSessionStateBag"/>.
|
||||
/// </summary>
|
||||
/// <value>Defaults to the provider's type name.</value>
|
||||
public string? StateKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional filter function applied to request messages when building the search text to use when
|
||||
/// searching for relevant memories during <see cref="AIContextProvider.InvokingAsync"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// When <see langword="null"/>, the provider defaults to including only
|
||||
/// <see cref="AgentRequestMessageSourceType.External"/> messages.
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? SearchInputMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional filter function applied to request messages when determining which messages to
|
||||
/// extract memories from during <see cref="AIContextProvider.InvokedAsync"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// When <see langword="null"/>, the provider defaults to including only
|
||||
/// <see cref="AgentRequestMessageSourceType.External"/> messages.
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? StorageInputMessageFilter { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.FoundryMemory;
|
||||
|
||||
/// <summary>
|
||||
/// Allows scoping of memories for the <see cref="FoundryMemoryProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Azure AI Foundry memories are scoped by a single string identifier that you control.
|
||||
/// Common patterns include using a user ID, team ID, or other unique identifier
|
||||
/// to partition memories across different contexts.
|
||||
/// </remarks>
|
||||
public sealed class FoundryMemoryProviderScope
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FoundryMemoryProviderScope"/> class with the specified scope identifier.
|
||||
/// </summary>
|
||||
/// <param name="scope">The scope identifier used to partition memories. Must not be null or whitespace.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="scope"/> is null or whitespace.</exception>
|
||||
public FoundryMemoryProviderScope(string scope)
|
||||
{
|
||||
Throw.IfNullOrWhitespace(scope);
|
||||
this.Scope = scope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scope identifier used to partition memories.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value controls how memory is partitioned in the memory store.
|
||||
/// Each unique scope maintains its own isolated collection of memory items.
|
||||
/// For example, use a user ID to ensure each user has their own individual memory.
|
||||
/// </remarks>
|
||||
public string Scope { get; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
<PropertyGroup>
|
||||
<!-- Disable packing until we are ready to release this as a nuget -->
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework - Azure AI Foundry Memory integration</Title>
|
||||
<Description>Provides Azure AI Foundry Memory integration for Microsoft Agent Framework.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.FoundryMemory.UnitTests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -104,7 +104,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
|
||||
|
||||
if (session is not GitHubCopilotAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException("The provided session is not compatible with the agent. Only sessions created by the agent can be serialized.");
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(GitHubCopilotAgentSession)}' can be serialized by this agent.");
|
||||
}
|
||||
|
||||
return new(typedSession.Serialize(jsonSerializerOptions));
|
||||
@@ -139,7 +139,7 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
|
||||
if (session is not GitHubCopilotAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The provided session type {session.GetType()} is not compatible with the agent. Only GitHub Copilot agent created sessions are supported.");
|
||||
$"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(GitHubCopilotAgentSession)}' can be used by this agent.");
|
||||
}
|
||||
|
||||
// Ensure the client is started
|
||||
|
||||
+142
-3
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using Microsoft.Agents.AI;
|
||||
@@ -10,12 +11,14 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring A2A (Agent2Agent) communication in a host application builder.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -33,6 +36,20 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path)
|
||||
=> endpoints.MapA2A(agentBuilder, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentBuilder">The configuration builder for <see cref="AIAgent"/>.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
@@ -43,6 +60,21 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
|
||||
=> endpoints.MapA2A(agentName, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, _ => { }, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
@@ -109,6 +141,37 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agentName, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentBuilder">The configuration builder for <see cref="AIAgent"/>.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agentBuilder);
|
||||
return endpoints.MapA2A(agentBuilder.Name, path, agentCard, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, agentCard, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
@@ -144,10 +207,28 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
=> endpoints.MapA2A(agentName, path, agentCard, configureTaskManager, AgentRunMode.DisallowBackground);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, agentCard, configureTaskManager);
|
||||
return endpoints.MapA2A(agent, path, agentCard, configureTaskManager, agentRunMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,6 +241,17 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
|
||||
=> endpoints.MapA2A(agent, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentRunMode agentRunMode)
|
||||
=> endpoints.MapA2A(agent, path, _ => { }, agentRunMode);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
@@ -169,13 +261,25 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager)
|
||||
=> endpoints.MapA2A(agent, path, configureTaskManager, AgentRunMode.DisallowBackground);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
|
||||
var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentSessionStore: agentSessionStore);
|
||||
var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentSessionStore: agentSessionStore, runMode: agentRunMode);
|
||||
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
|
||||
|
||||
configureTaskManager(taskManager);
|
||||
@@ -198,6 +302,23 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, AgentRunMode agentRunMode)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, _ => { }, agentRunMode);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
@@ -213,13 +334,31 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, configureTaskManager, AgentRunMode.DisallowBackground);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <param name="agentRunMode">Controls the response behavior of the agent run.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager, AgentRunMode agentRunMode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(endpoints);
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, agentSessionStore: agentSessionStore, loggerFactory: loggerFactory);
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, agentSessionStore: agentSessionStore, loggerFactory: loggerFactory, runMode: agentRunMode);
|
||||
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
|
||||
|
||||
configureTaskManager(taskManager);
|
||||
|
||||
+6
@@ -8,6 +8,12 @@
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="A2A.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Provides JSON serialization options for A2A Hosting APIs to support AOT and trimming.
|
||||
/// </summary>
|
||||
public static class A2AHostingJsonUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for A2A Hosting serialization.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
|
||||
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
JsonSerializerOptions options = new(global::A2A.A2AJsonUtilities.DefaultOptions);
|
||||
|
||||
// Chain in the resolvers from both AgentAbstractionsJsonUtilities and the A2A SDK context.
|
||||
// AgentAbstractionsJsonUtilities is first to ensure M.E.AI types (e.g. ResponseContinuationToken)
|
||||
// are handled via its resolver, followed by the A2A SDK resolver for protocol types.
|
||||
options.TypeInfoResolverChain.Clear();
|
||||
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
|
||||
options.TypeInfoResolverChain.Add(global::A2A.A2AJsonUtilities.DefaultOptions.TypeInfoResolver!);
|
||||
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Provides context for a custom A2A run mode decision.
|
||||
/// </summary>
|
||||
public sealed class A2ARunDecisionContext
|
||||
{
|
||||
internal A2ARunDecisionContext(MessageSendParams messageSendParams)
|
||||
{
|
||||
this.MessageSendParams = messageSendParams;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters of the incoming A2A message that triggered this run.
|
||||
/// </summary>
|
||||
public MessageSendParams MessageSendParams { get; }
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for attaching A2A (Agent2Agent) messaging capabilities to an <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public static class AIAgentExtensions
|
||||
{
|
||||
// Metadata key used to store continuation tokens for long-running background operations
|
||||
// in the AgentTask.Metadata dictionary, persisted by the task store.
|
||||
private const string ContinuationTokenMetadataKey = "__a2a__continuationToken";
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) messaging capabilities via Message processing to the specified <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
@@ -21,49 +31,45 @@ public static class AIAgentExtensions
|
||||
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
|
||||
/// <param name="agentSessionStore">The store to store session contents and metadata.</param>
|
||||
/// <param name="runMode">Controls the response behavior of the agent run.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional <see cref="JsonSerializerOptions"/> for serializing and deserializing continuation tokens. Use this when the agent's continuation token contains custom types not registered in the default options. Falls back to <see cref="A2AHostingJsonUtilities.DefaultOptions"/> if not provided.</param>
|
||||
/// <returns>The configured <see cref="TaskManager"/>.</returns>
|
||||
public static ITaskManager MapA2A(
|
||||
this AIAgent agent,
|
||||
ITaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
AgentSessionStore? agentSessionStore = null)
|
||||
AgentSessionStore? agentSessionStore = null,
|
||||
AgentRunMode? runMode = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
ArgumentNullException.ThrowIfNull(agent.Name);
|
||||
|
||||
runMode ??= AgentRunMode.DisallowBackground;
|
||||
|
||||
var hostAgent = new AIHostAgent(
|
||||
innerAgent: agent,
|
||||
sessionStore: agentSessionStore ?? new NoopAgentSessionStore());
|
||||
|
||||
taskManager ??= new TaskManager();
|
||||
taskManager.OnMessageReceived += OnMessageReceivedAsync;
|
||||
|
||||
// Resolve the JSON serializer options for continuation token serialization. May be custom for the user's agent.
|
||||
JsonSerializerOptions continuationTokenJsonOptions = jsonSerializerOptions ?? A2AHostingJsonUtilities.DefaultOptions;
|
||||
|
||||
// OnMessageReceived handles both message-only and task-based flows.
|
||||
// The A2A SDK prioritizes OnMessageReceived over OnTaskCreated when both are set,
|
||||
// so we consolidate all initial message handling here and return either
|
||||
// an AgentMessage or AgentTask depending on the agent response.
|
||||
// When the agent returns a ContinuationToken (long-running operation), a task is
|
||||
// created for stateful tracking. Otherwise a lightweight AgentMessage is returned.
|
||||
// See https://github.com/a2aproject/a2a-dotnet/issues/275
|
||||
taskManager.OnMessageReceived += (p, ct) => OnMessageReceivedAsync(p, hostAgent, runMode, taskManager, continuationTokenJsonOptions, ct);
|
||||
|
||||
// Task flow for subsequent updates and cancellations
|
||||
taskManager.OnTaskUpdated += (t, ct) => OnTaskUpdatedAsync(t, hostAgent, taskManager, continuationTokenJsonOptions, ct);
|
||||
taskManager.OnTaskCancelled += OnTaskCancelledAsync;
|
||||
|
||||
return taskManager;
|
||||
|
||||
async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
|
||||
{
|
||||
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
var options = messageSendParams.Metadata is not { Count: > 0 }
|
||||
? null
|
||||
: new AgentRunOptions { AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
messageSendParams.ToChatMessages(),
|
||||
session: session,
|
||||
options: options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
var parts = response.Messages.ToParts();
|
||||
return new AgentMessage
|
||||
{
|
||||
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
ContextId = contextId,
|
||||
Role = MessageRole.Agent,
|
||||
Parts = parts,
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,15 +80,19 @@ public static class AIAgentExtensions
|
||||
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
|
||||
/// <param name="agentSessionStore">The store to store session contents and metadata.</param>
|
||||
/// <param name="runMode">Controls the response behavior of the agent run.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional <see cref="JsonSerializerOptions"/> for serializing and deserializing continuation tokens. Use this when the agent's continuation token contains custom types not registered in the default options. Falls back to <see cref="A2AHostingJsonUtilities.DefaultOptions"/> if not provided.</param>
|
||||
/// <returns>The configured <see cref="TaskManager"/>.</returns>
|
||||
public static ITaskManager MapA2A(
|
||||
this AIAgent agent,
|
||||
AgentCard agentCard,
|
||||
ITaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
AgentSessionStore? agentSessionStore = null)
|
||||
AgentSessionStore? agentSessionStore = null,
|
||||
AgentRunMode? runMode = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
taskManager = agent.MapA2A(taskManager, loggerFactory, agentSessionStore);
|
||||
taskManager = agent.MapA2A(taskManager, loggerFactory, agentSessionStore, runMode, jsonSerializerOptions);
|
||||
|
||||
taskManager.OnAgentCardQuery += (context, query) =>
|
||||
{
|
||||
@@ -97,4 +107,203 @@ public static class AIAgentExtensions
|
||||
};
|
||||
return taskManager;
|
||||
}
|
||||
|
||||
private static async Task<A2AResponse> OnMessageReceivedAsync(
|
||||
MessageSendParams messageSendParams,
|
||||
AIHostAgent hostAgent,
|
||||
AgentRunMode runMode,
|
||||
ITaskManager taskManager,
|
||||
JsonSerializerOptions continuationTokenJsonOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// AIAgent does not support resuming from arbitrary prior tasks.
|
||||
// Throw explicitly so the client gets a clear error rather than a response
|
||||
// that silently ignores the referenced task context.
|
||||
// Follow-ups on the *same* task are handled via OnTaskUpdated instead.
|
||||
if (messageSendParams.Message.ReferenceTaskIds is { Count: > 0 })
|
||||
{
|
||||
throw new NotSupportedException("ReferenceTaskIds is not supported. AIAgent cannot resume from arbitrary prior task context. Use OnTaskUpdated for follow-ups on the same task.");
|
||||
}
|
||||
|
||||
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Decide whether to run in background based on user preferences and agent capabilities
|
||||
var decisionContext = new A2ARunDecisionContext(messageSendParams);
|
||||
var allowBackgroundResponses = await runMode.ShouldRunInBackgroundAsync(decisionContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var options = messageSendParams.Metadata is not { Count: > 0 }
|
||||
? new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses }
|
||||
: new AgentRunOptions { AllowBackgroundResponses = allowBackgroundResponses, AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
messageSendParams.ToChatMessages(),
|
||||
session: session,
|
||||
options: options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.ContinuationToken is null)
|
||||
{
|
||||
return CreateMessageFromResponse(contextId, response);
|
||||
}
|
||||
|
||||
var agentTask = await InitializeTaskAsync(contextId, messageSendParams.Message, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
StoreContinuationToken(agentTask, response.ContinuationToken, continuationTokenJsonOptions);
|
||||
await TransitionToWorkingAsync(agentTask.Id, contextId, response, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
return agentTask;
|
||||
}
|
||||
|
||||
private static async Task OnTaskUpdatedAsync(
|
||||
AgentTask agentTask,
|
||||
AIHostAgent hostAgent,
|
||||
ITaskManager taskManager,
|
||||
JsonSerializerOptions continuationTokenJsonOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var contextId = agentTask.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var session = await hostAgent.GetOrCreateSessionAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// Discard any stale continuation token — the incoming user message supersedes
|
||||
// any previous background operation. AF agents don't support updating existing
|
||||
// background responses (long-running operations); we start a fresh run from the
|
||||
// existing session using the full chat history (which includes the new message).
|
||||
agentTask.Metadata?.Remove(ContinuationTokenMetadataKey);
|
||||
|
||||
await taskManager.UpdateStatusAsync(agentTask.Id, TaskState.Working, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
ExtractChatMessagesFromTaskHistory(agentTask),
|
||||
session: session,
|
||||
options: new AgentRunOptions { AllowBackgroundResponses = true },
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveSessionAsync(contextId, session, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.ContinuationToken is not null)
|
||||
{
|
||||
StoreContinuationToken(agentTask, response.ContinuationToken, continuationTokenJsonOptions);
|
||||
await TransitionToWorkingAsync(agentTask.Id, contextId, response, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CompleteWithArtifactAsync(agentTask.Id, response, taskManager, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await taskManager.UpdateStatusAsync(
|
||||
agentTask.Id,
|
||||
TaskState.Failed,
|
||||
final: true,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Task OnTaskCancelledAsync(AgentTask agentTask, CancellationToken cancellationToken)
|
||||
{
|
||||
// Remove the continuation token from metadata if present.
|
||||
// The task has already been marked as cancelled by the TaskManager.
|
||||
agentTask.Metadata?.Remove(ContinuationTokenMetadataKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static AgentMessage CreateMessageFromResponse(string contextId, AgentResponse response) =>
|
||||
new()
|
||||
{
|
||||
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
ContextId = contextId,
|
||||
Role = MessageRole.Agent,
|
||||
Parts = response.Messages.ToParts(),
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
// Task outputs should be returned as artifacts rather than messages:
|
||||
// https://a2a-protocol.org/latest/specification/#37-messages-and-artifacts
|
||||
private static Artifact CreateArtifactFromResponse(AgentResponse response) =>
|
||||
new()
|
||||
{
|
||||
ArtifactId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
Parts = response.Messages.ToParts(),
|
||||
Metadata = response.AdditionalProperties?.ToA2AMetadata()
|
||||
};
|
||||
|
||||
private static async Task<AgentTask> InitializeTaskAsync(
|
||||
string contextId,
|
||||
AgentMessage originalMessage,
|
||||
ITaskManager taskManager,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
AgentTask agentTask = await taskManager.CreateTaskAsync(contextId, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Add the original user message to the task history.
|
||||
// The A2A SDK does this internally when it creates tasks via OnTaskCreated.
|
||||
agentTask.History ??= [];
|
||||
agentTask.History.Add(originalMessage);
|
||||
|
||||
// Notify subscribers of the Submitted state per the A2A spec: https://a2a-protocol.org/latest/specification/#413-taskstate
|
||||
await taskManager.UpdateStatusAsync(agentTask.Id, TaskState.Submitted, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return agentTask;
|
||||
}
|
||||
|
||||
private static void StoreContinuationToken(
|
||||
AgentTask agentTask,
|
||||
ResponseContinuationToken token,
|
||||
JsonSerializerOptions continuationTokenJsonOptions)
|
||||
{
|
||||
// Serialize the continuation token into the task's metadata so it survives
|
||||
// across requests and is cleaned up with the task itself.
|
||||
agentTask.Metadata ??= [];
|
||||
agentTask.Metadata[ContinuationTokenMetadataKey] = JsonSerializer.SerializeToElement(
|
||||
token,
|
||||
continuationTokenJsonOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
|
||||
}
|
||||
|
||||
private static async Task TransitionToWorkingAsync(
|
||||
string taskId,
|
||||
string contextId,
|
||||
AgentResponse response,
|
||||
ITaskManager taskManager,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Include any intermediate progress messages from the response as a status message.
|
||||
AgentMessage? progressMessage = response.Messages.Count > 0 ? CreateMessageFromResponse(contextId, response) : null;
|
||||
await taskManager.UpdateStatusAsync(taskId, TaskState.Working, message: progressMessage, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task CompleteWithArtifactAsync(
|
||||
string taskId,
|
||||
AgentResponse response,
|
||||
ITaskManager taskManager,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var artifact = CreateArtifactFromResponse(response);
|
||||
await taskManager.ReturnArtifactAsync(taskId, artifact, cancellationToken).ConfigureAwait(false);
|
||||
await taskManager.UpdateStatusAsync(taskId, TaskState.Completed, final: true, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static List<ChatMessage> ExtractChatMessagesFromTaskHistory(AgentTask agentTask)
|
||||
{
|
||||
if (agentTask.History is not { Count: > 0 })
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var chatMessages = new List<ChatMessage>(agentTask.History.Count);
|
||||
foreach (var message in agentTask.History)
|
||||
{
|
||||
chatMessages.Add(message.ToChatMessage());
|
||||
}
|
||||
|
||||
return chatMessages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how the A2A hosting layer determines whether to run <see cref="AIAgent"/> in background or not.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
||||
public sealed class AgentRunMode : IEquatable<AgentRunMode>
|
||||
{
|
||||
private const string MessageValue = "message";
|
||||
private const string TaskValue = "task";
|
||||
private const string DynamicValue = "dynamic";
|
||||
|
||||
private readonly string _value;
|
||||
private readonly Func<A2ARunDecisionContext, CancellationToken, ValueTask<bool>>? _runInBackground;
|
||||
|
||||
private AgentRunMode(string value, Func<A2ARunDecisionContext, CancellationToken, ValueTask<bool>>? runInBackground = null)
|
||||
{
|
||||
this._value = value;
|
||||
this._runInBackground = runInBackground;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dissallows the background responses from the agent. Is equivalent to configuring <see cref="AgentRunOptions.AllowBackgroundResponses"/> as <c>false</c>.
|
||||
/// In the A2A protocol terminology will make responses be returned as <c>AgentMessage</c>.
|
||||
/// </summary>
|
||||
public static AgentRunMode DisallowBackground => new(MessageValue);
|
||||
|
||||
/// <summary>
|
||||
/// Allows the background responses from the agent. Is equivalent to configuring <see cref="AgentRunOptions.AllowBackgroundResponses"/> as <c>true</c>.
|
||||
/// In the A2A protocol terminology will make responses be returned as <c>AgentTask</c> if the agent supports background responses, and as <c>AgentMessage</c> otherwise.
|
||||
/// </summary>
|
||||
public static AgentRunMode AllowBackgroundIfSupported => new(TaskValue);
|
||||
|
||||
/// <summary>
|
||||
/// The agent run mode is decided by the supplied <paramref name="runInBackground"/> delegate.
|
||||
/// The delegate receives an <see cref="A2ARunDecisionContext"/> with the incoming
|
||||
/// message and returns a boolean specifying whether to run the agent in background mode.
|
||||
/// <see langword="true"/> indicates that the agent should run in background mode and return an
|
||||
/// <c>AgentTask</c> if the agent supports background mode; otherwise, it returns an <c>AgentMessage</c>
|
||||
/// if the mode is not supported. <see langword="false"/> indicates that the agent should run in
|
||||
/// non-background mode and return an <c>AgentMessage</c>.
|
||||
/// </summary>
|
||||
/// <param name="runInBackground">
|
||||
/// An async delegate that decides whether the response should be wrapped in an <c>AgentTask</c>.
|
||||
/// </param>
|
||||
public static AgentRunMode AllowBackgroundWhen(Func<A2ARunDecisionContext, CancellationToken, ValueTask<bool>> runInBackground)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runInBackground);
|
||||
return new(DynamicValue, runInBackground);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the agent response should be returned as an <c>AgentTask</c>.
|
||||
/// </summary>
|
||||
internal ValueTask<bool> ShouldRunInBackgroundAsync(A2ARunDecisionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.Equals(this._value, MessageValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ValueTask.FromResult(false);
|
||||
}
|
||||
|
||||
if (string.Equals(this._value, TaskValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
// Dynamic: delegate to custom callback.
|
||||
if (this._runInBackground is not null)
|
||||
{
|
||||
return this._runInBackground(context, cancellationToken);
|
||||
}
|
||||
|
||||
// No delegate provided — fall back to "message" behavior.
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(AgentRunMode? other) =>
|
||||
other is not null && string.Equals(this._value, other._value, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => this.Equals(obj as AgentRunMode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(this._value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this._value;
|
||||
|
||||
/// <summary>Determines whether two <see cref="AgentRunMode"/> instances are equal.</summary>
|
||||
public static bool operator ==(AgentRunMode? left, AgentRunMode? right) =>
|
||||
left?.Equals(right) ?? right is null;
|
||||
|
||||
/// <summary>Determines whether two <see cref="AgentRunMode"/> instances are not equal.</summary>
|
||||
public static bool operator !=(AgentRunMode? left, AgentRunMode? right) =>
|
||||
!(left == right);
|
||||
}
|
||||
+1
-2
@@ -2,7 +2,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
@@ -37,7 +36,7 @@ internal static class AdditionalPropertiesDictionaryExtensions
|
||||
continue;
|
||||
}
|
||||
|
||||
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
||||
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
|
||||
}
|
||||
|
||||
return metadata;
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);OPENAI001;MEAI001</NoWarn>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
<RootNamespace>Microsoft.Agents.AI.Hosting.OpenAI</RootNamespace>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsNamespaces>
|
||||
|
||||
@@ -30,6 +30,6 @@ public static class HostedWorkflowBuilderExtensions
|
||||
var agentName = name ?? workflowName;
|
||||
|
||||
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
|
||||
sp.GetRequiredKeyedService<Workflow>(workflowName).AsAgent(name: key));
|
||||
sp.GetRequiredKeyedService<Workflow>(workflowName).AsAIAgent(name: key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace Microsoft.Agents.AI.Mem0;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a Mem0 backed <see cref="AIContextProvider"/> that persists conversation messages as memories
|
||||
/// Provides a Mem0 backed <see cref="MessageAIContextProvider"/> that persists conversation messages as memories
|
||||
/// and retrieves related memories to augment the agent invocation context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
@@ -22,19 +22,13 @@ namespace Microsoft.Agents.AI.Mem0;
|
||||
/// for new invocations using a semantic search endpoint. Retrieved memories are injected as user messages
|
||||
/// to the model, prefixed by a configurable context prompt.
|
||||
/// </remarks>
|
||||
public sealed class Mem0Provider : AIContextProvider
|
||||
public sealed class Mem0Provider : MessageAIContextProvider
|
||||
{
|
||||
private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:";
|
||||
|
||||
private static IEnumerable<ChatMessage> DefaultExternalOnlyFilter(IEnumerable<ChatMessage> messages)
|
||||
=> messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);
|
||||
|
||||
private readonly ProviderSessionState<State> _sessionState;
|
||||
private readonly string _contextPrompt;
|
||||
private readonly bool _enableSensitiveTelemetryData;
|
||||
private readonly string _stateKey;
|
||||
private readonly Func<AgentSession?, State> _stateInitializer;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _searchInputMessageFilter;
|
||||
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>> _storageInputMessageFilter;
|
||||
|
||||
private readonly Mem0Client _client;
|
||||
private readonly ILogger<Mem0Provider>? _logger;
|
||||
@@ -58,70 +52,56 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public Mem0Provider(HttpClient httpClient, Func<AgentSession?, State> stateInitializer, Mem0ProviderOptions? options = null, ILoggerFactory? loggerFactory = null)
|
||||
: base(options?.SearchInputMessageFilter, options?.StorageInputMessageFilter)
|
||||
{
|
||||
this._sessionState = new ProviderSessionState<State>(
|
||||
ValidateStateInitializer(Throw.IfNull(stateInitializer)),
|
||||
options?.StateKey ?? this.GetType().Name,
|
||||
Mem0JsonUtilities.DefaultOptions);
|
||||
Throw.IfNull(httpClient);
|
||||
if (string.IsNullOrWhiteSpace(httpClient.BaseAddress?.AbsoluteUri))
|
||||
{
|
||||
throw new ArgumentException("The HttpClient BaseAddress must be set for Mem0 operations.", nameof(httpClient));
|
||||
}
|
||||
|
||||
this._stateInitializer = Throw.IfNull(stateInitializer);
|
||||
this._logger = loggerFactory?.CreateLogger<Mem0Provider>();
|
||||
this._client = new Mem0Client(httpClient);
|
||||
|
||||
this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt;
|
||||
this._enableSensitiveTelemetryData = options?.EnableSensitiveTelemetryData ?? false;
|
||||
this._stateKey = options?.StateKey ?? base.StateKey;
|
||||
this._searchInputMessageFilter = options?.SearchInputMessageFilter ?? DefaultExternalOnlyFilter;
|
||||
this._storageInputMessageFilter = options?.StorageInputMessageFilter ?? DefaultExternalOnlyFilter;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string StateKey => this._stateKey;
|
||||
public override string StateKey => this._sessionState.StateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state from the session's StateBag, or initializes it using the StateInitializer if not present.
|
||||
/// </summary>
|
||||
/// <param name="session">The agent session containing the StateBag.</param>
|
||||
/// <returns>The provider state, or null if no session is available.</returns>
|
||||
private State? GetOrInitializeState(AgentSession? session)
|
||||
{
|
||||
if (session?.StateBag.TryGetValue<State>(this._stateKey, out var state, Mem0JsonUtilities.DefaultOptions) is true && state is not null)
|
||||
private static Func<AgentSession?, State> ValidateStateInitializer(Func<AgentSession?, State> stateInitializer) =>
|
||||
session =>
|
||||
{
|
||||
var state = stateInitializer(session);
|
||||
|
||||
if (state is null
|
||||
|| state.StorageScope is null
|
||||
|| (state.StorageScope.AgentId is null && state.StorageScope.ThreadId is null && state.StorageScope.UserId is null && state.StorageScope.ApplicationId is null)
|
||||
|| state.SearchScope is null
|
||||
|| (state.SearchScope.AgentId is null && state.SearchScope.ThreadId is null && state.SearchScope.UserId is null && state.SearchScope.ApplicationId is null))
|
||||
{
|
||||
throw new InvalidOperationException("State initializer must return a non-null state with valid storage and search scopes, where at least one scoping parameter is set for each.");
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
state = this._stateInitializer(session);
|
||||
|
||||
if (state is null
|
||||
|| state.StorageScope is null
|
||||
|| (state.StorageScope.AgentId is null && state.StorageScope.ThreadId is null && state.StorageScope.UserId is null && state.StorageScope.ApplicationId is null)
|
||||
|| state.SearchScope is null
|
||||
|| (state.SearchScope.AgentId is null && state.SearchScope.ThreadId is null && state.SearchScope.UserId is null && state.SearchScope.ApplicationId is null))
|
||||
{
|
||||
throw new InvalidOperationException("State initializer must return a non-null state with valid storage and search scopes, where at lest one scoping parameter is set for each.");
|
||||
}
|
||||
|
||||
if (session is not null)
|
||||
{
|
||||
session.StateBag.SetValue(this._stateKey, state, Mem0JsonUtilities.DefaultOptions);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(context);
|
||||
|
||||
var inputContext = context.AIContext;
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var searchScope = state?.SearchScope ?? new Mem0ProviderScope();
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
var searchScope = state.SearchScope;
|
||||
|
||||
string queryText = string.Join(
|
||||
Environment.NewLine,
|
||||
this._searchInputMessageFilter(inputContext.Messages ?? [])
|
||||
context.RequestMessages
|
||||
.Where(m => !string.IsNullOrWhiteSpace(m.Text))
|
||||
.Select(m => m.Text));
|
||||
|
||||
@@ -138,9 +118,6 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
var outputMessageText = memories.Count == 0
|
||||
? null
|
||||
: $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
|
||||
var outputMessage = memories.Count == 0
|
||||
? null
|
||||
: new ChatMessage(ChatRole.User, outputMessageText!).WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, this.GetType().FullName!);
|
||||
|
||||
if (this._logger?.IsEnabled(LogLevel.Information) is true)
|
||||
{
|
||||
@@ -165,14 +142,9 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
}
|
||||
}
|
||||
|
||||
return new AIContext
|
||||
{
|
||||
Instructions = inputContext.Instructions,
|
||||
Messages =
|
||||
(inputContext.Messages ?? [])
|
||||
.Concat(outputMessage is not null ? [outputMessage] : []),
|
||||
Tools = inputContext.Tools
|
||||
};
|
||||
return outputMessageText is not null
|
||||
? [new ChatMessage(ChatRole.User, outputMessageText)]
|
||||
: [];
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@@ -190,27 +162,23 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
searchScope.ThreadId,
|
||||
this.SanitizeLogData(searchScope.UserId));
|
||||
}
|
||||
return inputContext;
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
protected override async ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (context.InvokeException is not null)
|
||||
{
|
||||
return; // Do not update memory on failed invocations.
|
||||
}
|
||||
|
||||
var state = this.GetOrInitializeState(context.Session);
|
||||
var storageScope = state?.StorageScope ?? new Mem0ProviderScope();
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
var storageScope = state.StorageScope;
|
||||
|
||||
try
|
||||
{
|
||||
// Persist request and response messages after invocation.
|
||||
await this.PersistMessagesAsync(
|
||||
storageScope,
|
||||
this._storageInputMessageFilter(context.RequestMessages)
|
||||
context.RequestMessages
|
||||
.Concat(context.ResponseMessages ?? []),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -237,13 +205,8 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
public Task ClearStoredMemoriesAsync(AgentSession session, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(session);
|
||||
var state = this.GetOrInitializeState(session);
|
||||
var storageScope = state?.StorageScope;
|
||||
|
||||
if (storageScope is null)
|
||||
{
|
||||
return Task.CompletedTask; // Nothing to clear if there is no state.
|
||||
}
|
||||
var state = this._sessionState.GetOrInitializeState(session);
|
||||
var storageScope = state.StorageScope;
|
||||
|
||||
return this._client.ClearMemoryAsync(
|
||||
storageScope.ApplicationId,
|
||||
|
||||
+3
@@ -1,10 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using OpenAI.Responses;
|
||||
|
||||
namespace Microsoft.Agents.AI.OpenAI;
|
||||
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
internal sealed class AsyncStreamingResponseUpdateCollectionResult : AsyncCollectionResult<StreamingResponseUpdate>
|
||||
{
|
||||
private readonly IAsyncEnumerable<AgentResponseUpdate> _updates;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Agents.AI.OpenAI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Chat;
|
||||
using OpenAI.Responses;
|
||||
@@ -18,6 +20,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// The methods handle the conversion between OpenAI chat message types and Microsoft Extensions AI types,
|
||||
/// and return OpenAI <see cref="ChatCompletion"/> objects directly from the agent's <see cref="AgentResponse"/>.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class AIAgentWithOpenAIExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI.Chat;
|
||||
using OpenAI.Responses;
|
||||
@@ -11,6 +13,7 @@ namespace Microsoft.Agents.AI;
|
||||
/// Provides extension methods for <see cref="AgentResponse"/> and <see cref="AgentResponseUpdate"/> instances to
|
||||
/// create or extract native OpenAI response objects from the Microsoft Agent Framework responses.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class AgentResponseExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace OpenAI.Assistants;
|
||||
@@ -18,6 +20,7 @@ namespace OpenAI.Assistants;
|
||||
/// The methods handle the conversion from OpenAI clients to <see cref="IChatClient"/> instances and then wrap them
|
||||
/// in <see cref="ChatClientAgent"/> objects that implement the <see cref="AIAgent"/> interface.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIAssistants)]
|
||||
public static class OpenAIAssistantClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace OpenAI.Responses;
|
||||
@@ -17,6 +19,7 @@ namespace OpenAI.Responses;
|
||||
/// The methods handle the conversion from OpenAI clients to <see cref="IChatClient"/> instances and then wrap them
|
||||
/// in <see cref="ChatClientAgent"/> objects that implement the <see cref="AIAgent"/> interface.
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
|
||||
public static class OpenAIResponseClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -89,4 +92,23 @@ public static class OpenAIResponseClientExtensions
|
||||
|
||||
return new ChatClientAgent(chatClient, options, loggerFactory, services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IChatClient"/> for use with this <see cref="ResponsesClient"/> that does not store responses for later retrieval.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This corresponds to setting the "store" property in the JSON representation to false.
|
||||
/// </remarks>
|
||||
/// <param name="responseClient">The client.</param>
|
||||
/// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="ResponsesClient"/> that does not store responses for later retrieval.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="responseClient"/> is <see langword="null"/>.</exception>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public static IChatClient AsIChatClientWithStoredOutputDisabled(this ResponsesClient responseClient)
|
||||
{
|
||||
return Throw.IfNull(responseClient)
|
||||
.AsIChatClient()
|
||||
.AsBuilder()
|
||||
.ConfigureOptions(x => x.RawRepresentationFactory = _ => new CreateResponseOptions() { StoredOutputEnabled = false })
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<NoWarn>$(NoWarn);OPENAI001;</NoWarn>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
|
||||
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
+3
-1
@@ -111,7 +111,9 @@ internal static class JsonDocumentExtensions
|
||||
VariableType? currentType =
|
||||
element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => VariableType.Record(targetType.Schema?.Select(kvp => (kvp.Key, kvp.Value)) ?? []),
|
||||
JsonValueKind.Object => targetType.HasSchema
|
||||
? VariableType.Record(targetType.Schema!.Select(kvp => (kvp.Key, kvp.Value)))
|
||||
: VariableType.RecordType,
|
||||
JsonValueKind.String => typeof(string),
|
||||
JsonValueKind.True => typeof(bool),
|
||||
JsonValueKind.False => typeof(bool),
|
||||
|
||||
+9
@@ -39,6 +39,14 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
|
||||
this.Model = model;
|
||||
}
|
||||
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
return base.ConfigureProtocol(protocolBuilder)
|
||||
// We chain to HandleAsync, so let the protocol know we have additional Send/Yield types that may not be
|
||||
// available on the HandleAsync override.
|
||||
.AddDelegateAttributeTypes(this.ExecuteAsync);
|
||||
}
|
||||
|
||||
public DialogAction Model { get; }
|
||||
|
||||
public string ParentId { get => field ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root(); }
|
||||
@@ -60,6 +68,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SendsMessage(typeof(ActionExecutorResult))]
|
||||
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.Model.Disabled)
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
@@ -25,6 +26,7 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
|
||||
return default;
|
||||
}
|
||||
|
||||
[SendsMessage(typeof(ActionExecutorResult))]
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// No state to restore if we're starting from the beginning.
|
||||
|
||||
+16
@@ -34,12 +34,28 @@ internal class DelegateActionExecutor<TMessage> : Executor<TMessage>, IResettabl
|
||||
this._emitResult = emitResult;
|
||||
}
|
||||
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
ProtocolBuilder baseBuilder = base.ConfigureProtocol(protocolBuilder);
|
||||
|
||||
if (this._emitResult)
|
||||
{
|
||||
baseBuilder.SendsMessage<TMessage>();
|
||||
}
|
||||
|
||||
// We chain to the provided delegate, so let the protocol know we have additional Send/Yield types that may not be
|
||||
// available on the HandleAsync override.
|
||||
return (this._action != null) ? baseBuilder.AddDelegateAttributeTypes(this._action)
|
||||
: baseBuilder;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ResetAsync()
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
[SendsMessage(typeof(ActionExecutorResult))]
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._action is not null)
|
||||
|
||||
+21
@@ -390,6 +390,27 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeFunctionTool item)
|
||||
{
|
||||
this.Trace(item);
|
||||
|
||||
// Entry point to invoke function tool - always yields for external execution
|
||||
InvokeFunctionToolExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
|
||||
this.ContinueWith(action);
|
||||
|
||||
// Define request-port for function tool invocation (always requires external input)
|
||||
string externalInputPortId = InvokeFunctionToolExecutor.Steps.ExternalInput(action.Id);
|
||||
RequestPortAction externalInputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(externalInputPortId));
|
||||
this._workflowModel.AddNode(externalInputPort, action.ParentId);
|
||||
this._workflowModel.AddLinkFromPeer(action.ParentId, externalInputPortId);
|
||||
|
||||
// Capture response when external input is received
|
||||
string resumeId = InvokeFunctionToolExecutor.Steps.Resume(action.Id);
|
||||
this.ContinueWith(
|
||||
new DelegateActionExecutor<ExternalInputResponse>(resumeId, this._workflowState, action.CaptureResponseAsync),
|
||||
action.ParentId);
|
||||
}
|
||||
|
||||
protected override void Visit(InvokeAzureResponse item)
|
||||
{
|
||||
this.NotSupported(item);
|
||||
|
||||
+2
@@ -365,6 +365,8 @@ internal sealed class WorkflowTemplateVisitor : DialogActionVisitor
|
||||
|
||||
#region Not supported
|
||||
|
||||
protected override void Visit(InvokeFunctionTool item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item);
|
||||
|
||||
protected override void Visit(DeleteActivity item) => this.NotSupported(item);
|
||||
|
||||
@@ -73,6 +73,7 @@ public abstract class ActionExecutor<TMessage> : Executor<TMessage>, IResettable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SendsMessage(typeof(ActionExecutorResult))]
|
||||
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -54,6 +54,7 @@ public abstract class RootExecutor<TInput> : Executor<TInput>, IResettableExecut
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SendsMessage(typeof(ActionExecutorResult))]
|
||||
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
DeclarativeWorkflowContext declarativeContext = new(context, this._state);
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
+2
-1
@@ -22,11 +22,12 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
|
||||
{
|
||||
return conditionItem.Id;
|
||||
}
|
||||
|
||||
int index = model.Conditions.IndexOf(conditionItem);
|
||||
return $"{model.Id}_Items{index}";
|
||||
}
|
||||
|
||||
public static string Else(ConditionGroup model) => model.ElseActions.Id.Value ?? $"{model.Id}_Else";
|
||||
public static string Else(ConditionGroup model) => model.ElseActions.Id.Value;
|
||||
}
|
||||
|
||||
public ConditionGroupExecutor(ConditionGroup model, WorkflowFormulaState state)
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
[SendsMessage(typeof(ExternalInputRequest))]
|
||||
internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, ResponseAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<InvokeAzureAgent>(model, state)
|
||||
{
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Executor for the <see cref="InvokeFunctionTool"/> action.
|
||||
/// This executor yields to the caller for function execution and resumes when results are provided.
|
||||
/// </summary>
|
||||
internal sealed class InvokeFunctionToolExecutor(
|
||||
InvokeFunctionTool model,
|
||||
ResponseAgentProvider agentProvider,
|
||||
WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<InvokeFunctionTool>(model, state)
|
||||
{
|
||||
/// <summary>
|
||||
/// Step identifiers for the function tool invocation workflow.
|
||||
/// </summary>
|
||||
public static class Steps
|
||||
{
|
||||
/// <summary>
|
||||
/// Step for waiting for external input (function result).
|
||||
/// </summary>
|
||||
public static string ExternalInput(string id) => $"{id}_{nameof(ExternalInput)}";
|
||||
|
||||
/// <summary>
|
||||
/// Step for resuming after receiving function result.
|
||||
/// </summary>
|
||||
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool EmitResultEvent => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsDiscreteAction => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SendsMessage(typeof(ExternalInputRequest))]
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string functionName = this.GetFunctionName();
|
||||
bool requireApproval = this.GetRequireApproval();
|
||||
Dictionary<string, object?>? arguments = this.GetArguments();
|
||||
|
||||
// Create the function call content to send to the caller
|
||||
FunctionCallContent functionCall = new(
|
||||
callId: this.Id,
|
||||
name: functionName,
|
||||
arguments: arguments);
|
||||
|
||||
// Build the response with the function call request
|
||||
ChatMessage requestMessage = new(ChatRole.Tool, [functionCall]);
|
||||
|
||||
// If approval is required, add user input request content
|
||||
if (requireApproval)
|
||||
{
|
||||
requestMessage.Contents.Add(new FunctionApprovalRequestContent(this.Id, functionCall));
|
||||
}
|
||||
|
||||
AgentResponse agentResponse = new([requestMessage]);
|
||||
|
||||
// Yield to the caller - workflow halts here until external input is received
|
||||
ExternalInputRequest inputRequest = new(agentResponse);
|
||||
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the function result and stores in output variables.
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <param name="response">The external input response containing the function result.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
|
||||
public async ValueTask CaptureResponseAsync(
|
||||
IWorkflowContext context,
|
||||
ExternalInputResponse response,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
bool autoSend = this.GetAutoSendValue();
|
||||
string? conversationId = this.GetConversationId();
|
||||
|
||||
// Extract function results from the response
|
||||
IEnumerable<FunctionResultContent> functionResults = response.Messages
|
||||
.SelectMany(m => m.Contents)
|
||||
.OfType<FunctionResultContent>();
|
||||
|
||||
FunctionResultContent? matchingResult = functionResults
|
||||
.FirstOrDefault(r => r.CallId == this.Id);
|
||||
|
||||
if (matchingResult is not null)
|
||||
{
|
||||
// Store the result in output variable
|
||||
await this.AssignResultAsync(context, matchingResult).ConfigureAwait(false);
|
||||
|
||||
// Auto-send the result if configured
|
||||
if (autoSend)
|
||||
{
|
||||
AgentResponse resultResponse = new([new ChatMessage(ChatRole.Tool, [matchingResult])]);
|
||||
await context.AddEventAsync(new AgentResponseEvent(this.Id, resultResponse), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Store messages if output path is configured
|
||||
if (this.Model.Output?.Messages is not null)
|
||||
{
|
||||
await this.AssignAsync(this.Model.Output.Messages?.Path, response.Messages.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Add messages to conversation if conversationId is provided
|
||||
// Note: We transform messages containing FunctionResultContent or FunctionCallContent
|
||||
// to assistant text messages because workflow-generated CallIds don't correspond to
|
||||
// actual AI-generated tool calls and would be rejected by the API.
|
||||
if (conversationId is not null)
|
||||
{
|
||||
foreach (ChatMessage message in TransformConversationMessages(response.Messages))
|
||||
{
|
||||
await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Completes the action after processing the function result.
|
||||
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms messages containing function-related content to assistant text messages.
|
||||
/// Messages with FunctionResultContent are converted to assistant messages with the result as text.
|
||||
/// Messages with only FunctionCallContent are excluded as they have no informational value.
|
||||
/// </summary>
|
||||
private static IEnumerable<ChatMessage> TransformConversationMessages(IEnumerable<ChatMessage> messages)
|
||||
{
|
||||
foreach (ChatMessage message in messages)
|
||||
{
|
||||
// Check if message contains function content
|
||||
bool hasFunctionResult = message.Contents.OfType<FunctionResultContent>().Any();
|
||||
bool hasFunctionCall = message.Contents.OfType<FunctionCallContent>().Any();
|
||||
|
||||
if (hasFunctionResult)
|
||||
{
|
||||
// Convert function results to assistant text message
|
||||
List<AIContent> updatedContents = [];
|
||||
foreach (AIContent content in message.Contents)
|
||||
{
|
||||
if (content is FunctionResultContent functionResult)
|
||||
{
|
||||
string? resultText = functionResult.Result?.ToString();
|
||||
if (!string.IsNullOrEmpty(resultText))
|
||||
{
|
||||
updatedContents.Add(new TextContent($"[Function {functionResult.CallId} result]: {resultText}"));
|
||||
}
|
||||
}
|
||||
else if (content is not FunctionCallContent)
|
||||
{
|
||||
// Keep non-function content as-is
|
||||
updatedContents.Add(content);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContents.Count > 0)
|
||||
{
|
||||
yield return new ChatMessage(ChatRole.Assistant, updatedContents);
|
||||
}
|
||||
}
|
||||
else if (!hasFunctionCall)
|
||||
{
|
||||
// Pass through messages without function content
|
||||
yield return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask AssignResultAsync(IWorkflowContext context, FunctionResultContent result)
|
||||
{
|
||||
if (this.Model.Output?.Result is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
object? resultValue = result.Result;
|
||||
|
||||
// Attempt to parse as JSON if it's a string
|
||||
if (resultValue is string jsonString)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument jsonDocument = JsonDocument.Parse(jsonString);
|
||||
// Handle different JSON value kinds
|
||||
object? parsedValue = jsonDocument.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => jsonDocument.ParseRecord(VariableType.RecordType),
|
||||
JsonValueKind.Array => jsonDocument.ParseList(CreateListTypeFromJson(jsonDocument.RootElement)),
|
||||
JsonValueKind.String => jsonDocument.RootElement.GetString(),
|
||||
JsonValueKind.Number => jsonDocument.RootElement.TryGetInt64(out long l) ? l : jsonDocument.RootElement.GetDouble(),
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
_ => jsonString,
|
||||
};
|
||||
await this.AssignAsync(this.Model.Output.Result?.Path, parsedValue.ToFormula(), context).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Not a valid JSON
|
||||
}
|
||||
}
|
||||
|
||||
await this.AssignAsync(this.Model.Output.Result?.Path, resultValue.ToFormula(), context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a VariableType.List with schema inferred from the first object element in the array.
|
||||
/// </summary>
|
||||
private static VariableType CreateListTypeFromJson(JsonElement arrayElement)
|
||||
{
|
||||
// Find the first object element to infer schema
|
||||
foreach (JsonElement element in arrayElement.EnumerateArray())
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
// Build schema from the object's properties
|
||||
List<(string Key, VariableType Type)> fields = [];
|
||||
foreach (JsonProperty property in element.EnumerateObject())
|
||||
{
|
||||
VariableType fieldType = property.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => typeof(string),
|
||||
JsonValueKind.Number => typeof(decimal),
|
||||
JsonValueKind.True or JsonValueKind.False => typeof(bool),
|
||||
JsonValueKind.Object => VariableType.RecordType,
|
||||
JsonValueKind.Array => VariableType.ListType,
|
||||
_ => typeof(string),
|
||||
};
|
||||
fields.Add((property.Name, fieldType));
|
||||
}
|
||||
|
||||
return VariableType.List(fields);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for arrays of primitives or empty arrays
|
||||
return VariableType.ListType;
|
||||
}
|
||||
|
||||
private string GetFunctionName() =>
|
||||
this.Evaluator.GetValue(
|
||||
Throw.IfNull(
|
||||
this.Model.FunctionName,
|
||||
$"{nameof(this.Model)}.{nameof(this.Model.FunctionName)}")).Value;
|
||||
|
||||
private string? GetConversationId()
|
||||
{
|
||||
if (this.Model.ConversationId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string conversationIdValue = this.Evaluator.GetValue(this.Model.ConversationId).Value;
|
||||
return conversationIdValue.Length == 0 ? null : conversationIdValue;
|
||||
}
|
||||
|
||||
private bool GetRequireApproval()
|
||||
{
|
||||
if (this.Model.RequireApproval is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.Evaluator.GetValue(this.Model.RequireApproval).Value;
|
||||
}
|
||||
|
||||
private bool GetAutoSendValue()
|
||||
{
|
||||
if (this.Model.Output?.AutoSend is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.Evaluator.GetValue(this.Model.Output.AutoSend).Value;
|
||||
}
|
||||
|
||||
private Dictionary<string, object?>? GetArguments()
|
||||
{
|
||||
if (this.Model.Arguments is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> result = [];
|
||||
foreach (KeyValuePair<string, ValueExpression> argument in this.Model.Arguments)
|
||||
{
|
||||
result[argument.Key] = this.Evaluator.GetValue(argument.Value).Value.ToObject();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+16
-11
@@ -16,6 +16,8 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
[SendsMessage(typeof(ExternalInputRequest))]
|
||||
[SendsMessage(typeof(ExternalInputResponse))]
|
||||
internal sealed class QuestionExecutor(Question model, ResponseAgentProvider agentProvider, WorkflowFormulaState state) :
|
||||
DeclarativeActionExecutor<Question>(model, state)
|
||||
{
|
||||
@@ -44,18 +46,17 @@ internal sealed class QuestionExecutor(Question model, ResponseAgentProvider age
|
||||
await this._promptCount.WriteAsync(context, 0).ConfigureAwait(false);
|
||||
|
||||
InitializablePropertyPath variable = Throw.IfNull(this.Model.Variable);
|
||||
bool hasValue = context.ReadState(variable.Path) is BlankValue;
|
||||
bool alwaysPrompt = this.Evaluator.GetValue(this.Model.AlwaysPrompt).Value;
|
||||
bool isValueUndefined = context.ReadState(variable.Path) is BlankValue;
|
||||
bool proceed = this.Evaluator.GetValue(this.Model.AlwaysPrompt).Value;
|
||||
|
||||
bool proceed = !alwaysPrompt || hasValue;
|
||||
if (proceed)
|
||||
if (!proceed)
|
||||
{
|
||||
SkipQuestionMode mode = this.Evaluator.GetValue(this.Model.SkipQuestionMode).Value;
|
||||
proceed =
|
||||
mode switch
|
||||
{
|
||||
SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue => !await this._hasExecuted.ReadAsync(context).ConfigureAwait(false),
|
||||
SkipQuestionMode.AlwaysSkipIfVariableHasValue => hasValue,
|
||||
SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue => isValueUndefined && !await this._hasExecuted.ReadAsync(context).ConfigureAwait(false),
|
||||
SkipQuestionMode.AlwaysSkipIfVariableHasValue => isValueUndefined,
|
||||
SkipQuestionMode.AlwaysAsk => true,
|
||||
_ => true,
|
||||
};
|
||||
@@ -86,7 +87,7 @@ internal sealed class QuestionExecutor(Question model, ResponseAgentProvider age
|
||||
FormulaValue? extractedValue = null;
|
||||
if (!response.HasMessages)
|
||||
{
|
||||
string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt);
|
||||
string unrecognizedResponse = this.Model.UnrecognizedPrompt is not null ? this.FormatPrompt(this.Model.UnrecognizedPrompt) : "Invalid response";
|
||||
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -128,7 +129,7 @@ internal sealed class QuestionExecutor(Question model, ResponseAgentProvider age
|
||||
}
|
||||
}
|
||||
|
||||
await this.AssignAsync(this.Model.Variable?.Path, extractedValue, context).ConfigureAwait(false);
|
||||
await this.AssignAsync(Throw.IfNull(this.Model.Variable).Path, extractedValue, context).ConfigureAwait(false);
|
||||
await this._hasExecuted.WriteAsync(context, true).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -145,9 +146,13 @@ internal sealed class QuestionExecutor(Question model, ResponseAgentProvider age
|
||||
int actualCount = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
|
||||
if (actualCount >= repeatCount)
|
||||
{
|
||||
ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue);
|
||||
DataValue defaultValue = this.Evaluator.GetValue(defaultValueExpression).Value;
|
||||
await this.AssignAsync(this.Model.Variable?.Path, defaultValue.ToFormula(), context).ConfigureAwait(false);
|
||||
DataValue defaultValue = DataValue.Blank();
|
||||
if (this.Model.DefaultValue is not null)
|
||||
{
|
||||
ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue);
|
||||
defaultValue = this.Evaluator.GetValue(defaultValueExpression).Value;
|
||||
}
|
||||
await this.AssignAsync(Throw.IfNull(this.Model.Variable).Path, defaultValue.ToFormula(), context).ConfigureAwait(false);
|
||||
string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse);
|
||||
await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim()), cancellationToken).ConfigureAwait(false);
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
+4
@@ -12,6 +12,8 @@ using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
[SendsMessage(typeof(ExternalInputRequest))]
|
||||
[SendsMessage(typeof(ExternalInputResponse))]
|
||||
internal sealed class RequestExternalInputExecutor(RequestExternalInput model, ResponseAgentProvider agentProvider, WorkflowFormulaState state)
|
||||
: DeclarativeActionExecutor<RequestExternalInput>(model, state)
|
||||
{
|
||||
@@ -45,5 +47,7 @@ internal sealed class RequestExternalInputExecutor(RequestExternalInput model, R
|
||||
}
|
||||
await context.SetLastMessageAsync(response.Messages.Last()).ConfigureAwait(false);
|
||||
await this.AssignAsync(this.Model.Variable?.Path, response.Messages.ToFormula(), context).ConfigureAwait(false);
|
||||
|
||||
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ internal static class SemanticAnalyzer
|
||||
string classKey = GetClassKey(classSymbol);
|
||||
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
|
||||
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
|
||||
bool hasManualConfigureRoutes = HasConfigureRoutesDefined(classSymbol);
|
||||
bool configureProtocol = HasConfigureProtocolDefined(classSymbol);
|
||||
|
||||
// Extract class metadata
|
||||
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
|
||||
@@ -78,7 +78,7 @@ internal static class SemanticAnalyzer
|
||||
string? genericParameters = GetGenericParameters(classSymbol);
|
||||
bool isNested = classSymbol.ContainingType != null;
|
||||
string containingTypeChain = GetContainingTypeChain(classSymbol);
|
||||
bool baseHasConfigureRoutes = BaseHasConfigureRoutes(classSymbol);
|
||||
bool baseHasConfigureProtocol = BaseHasConfigureProtocol(classSymbol);
|
||||
ImmutableEquatableArray<string> classSendTypes = GetClassLevelTypes(classSymbol, SendsMessageAttributeName);
|
||||
ImmutableEquatableArray<string> classYieldTypes = GetClassLevelTypes(classSymbol, YieldsOutputAttributeName);
|
||||
|
||||
@@ -96,8 +96,8 @@ internal static class SemanticAnalyzer
|
||||
|
||||
return new MethodAnalysisResult(
|
||||
classKey, @namespace, className, genericParameters, isNested, containingTypeChain,
|
||||
baseHasConfigureRoutes, classSendTypes, classYieldTypes,
|
||||
isPartialClass, derivesFromExecutor, hasManualConfigureRoutes,
|
||||
baseHasConfigureProtocol, classSendTypes, classYieldTypes,
|
||||
isPartialClass, derivesFromExecutor, configureProtocol,
|
||||
classLocation,
|
||||
handler,
|
||||
Diagnostics: new ImmutableEquatableArray<DiagnosticInfo>(methodDiagnostics.ToImmutable()));
|
||||
@@ -152,7 +152,7 @@ internal static class SemanticAnalyzer
|
||||
if (first.HasManualConfigureRoutes)
|
||||
{
|
||||
allDiagnostics.Add(Diagnostic.Create(
|
||||
DiagnosticDescriptors.ConfigureRoutesAlreadyDefined,
|
||||
DiagnosticDescriptors.ConfigureProtocolAlreadyDefined,
|
||||
classLocation,
|
||||
first.ClassName));
|
||||
return AnalysisResult.WithDiagnostics(allDiagnostics.ToImmutable());
|
||||
@@ -175,7 +175,7 @@ internal static class SemanticAnalyzer
|
||||
first.GenericParameters,
|
||||
first.IsNested,
|
||||
first.ContainingTypeChain,
|
||||
first.BaseHasConfigureRoutes,
|
||||
first.BaseHasConfigureProtocol,
|
||||
new ImmutableEquatableArray<HandlerInfo>(handlers),
|
||||
first.ClassSendTypes,
|
||||
first.ClassYieldTypes);
|
||||
@@ -211,7 +211,7 @@ internal static class SemanticAnalyzer
|
||||
string classKey = GetClassKey(classSymbol);
|
||||
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
|
||||
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
|
||||
bool hasManualConfigureRoutes = HasConfigureRoutesDefined(classSymbol);
|
||||
bool hasManualConfigureProtocol = HasConfigureProtocolDefined(classSymbol);
|
||||
|
||||
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
|
||||
? null
|
||||
@@ -240,7 +240,7 @@ internal static class SemanticAnalyzer
|
||||
containingTypeChain,
|
||||
isPartialClass,
|
||||
derivesFromExecutor,
|
||||
hasManualConfigureRoutes,
|
||||
hasManualConfigureProtocol,
|
||||
classLocation,
|
||||
typeName,
|
||||
attributeKind));
|
||||
@@ -251,12 +251,16 @@ internal static class SemanticAnalyzer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines ClassProtocolInfo results into an AnalysisResult for classes that only have protocol attributes
|
||||
/// (no [MessageHandler] methods). This generates only ConfigureSentTypes/ConfigureYieldTypes overrides.
|
||||
/// Combines ClassProtocolInfo results into an AnalysisResult for classes that only have IO attributes
|
||||
/// (no [MessageHandler] methods). This generates only .SendsMessage/.YieldsMessage calls in the protocol
|
||||
/// configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is likely to be seen combined with the basic one-method <c>Executor%lt;TIn></c> or <c>Executor<TIn, TOut></c>
|
||||
/// </remarks>
|
||||
/// <param name="protocolInfos">The protocol info entries for the class.</param>
|
||||
/// <returns>The combined analysis result.</returns>
|
||||
public static AnalysisResult CombineProtocolOnlyResults(IEnumerable<ClassProtocolInfo> protocolInfos)
|
||||
public static AnalysisResult CombineOutputOnlyResults(IEnumerable<ClassProtocolInfo> protocolInfos)
|
||||
{
|
||||
List<ClassProtocolInfo> protocols = protocolInfos.ToList();
|
||||
if (protocols.Count == 0)
|
||||
@@ -317,7 +321,7 @@ internal static class SemanticAnalyzer
|
||||
first.GenericParameters,
|
||||
first.IsNested,
|
||||
first.ContainingTypeChain,
|
||||
BaseHasConfigureRoutes: false, // Not relevant for protocol-only
|
||||
BaseHasConfigureProtocol: false, // Not relevant for protocol-only
|
||||
Handlers: ImmutableEquatableArray<HandlerInfo>.Empty,
|
||||
ClassSendTypes: new ImmutableEquatableArray<string>(sendTypes.ToImmutable()),
|
||||
ClassYieldTypes: new ImmutableEquatableArray<string>(yieldTypes.ToImmutable()));
|
||||
@@ -394,12 +398,12 @@ internal static class SemanticAnalyzer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this class directly defines ConfigureRoutes (not inherited).
|
||||
/// Checks if this class directly defines ConfigureProtocol (not inherited).
|
||||
/// If so, we skip generation to avoid conflicting with user's manual implementation.
|
||||
/// </summary>
|
||||
private static bool HasConfigureRoutesDefined(INamedTypeSymbol classSymbol)
|
||||
private static bool HasConfigureProtocolDefined(INamedTypeSymbol classSymbol)
|
||||
{
|
||||
foreach (var member in classSymbol.GetMembers("ConfigureRoutes"))
|
||||
foreach (var member in classSymbol.GetMembers("ConfigureProtocol"))
|
||||
{
|
||||
if (member is IMethodSymbol method && !method.IsAbstract &&
|
||||
SymbolEqualityComparer.Default.Equals(method.ContainingType, classSymbol))
|
||||
@@ -412,22 +416,22 @@ internal static class SemanticAnalyzer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any base class (between this class and Executor) defines ConfigureRoutes.
|
||||
/// If so, generated code should call base.ConfigureRoutes() to preserve inherited handlers.
|
||||
/// Checks if any base class (between this class and Executor) defines ConfigureProtocol.
|
||||
/// If so, generated code should call base.ConfigureProtocol() to preserve inherited handlers.
|
||||
/// </summary>
|
||||
private static bool BaseHasConfigureRoutes(INamedTypeSymbol classSymbol)
|
||||
private static bool BaseHasConfigureProtocol(INamedTypeSymbol classSymbol)
|
||||
{
|
||||
INamedTypeSymbol? baseType = classSymbol.BaseType;
|
||||
while (baseType != null)
|
||||
{
|
||||
string fullName = baseType.OriginalDefinition.ToDisplayString();
|
||||
// Stop at Executor - its ConfigureRoutes is abstract/empty
|
||||
// Stop at Executor - its ConfigureProtocol is abstract/empty
|
||||
if (fullName == ExecutorTypeName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var member in baseType.GetMembers("ConfigureRoutes"))
|
||||
foreach (var member in baseType.GetMembers("ConfigureProtocol"))
|
||||
{
|
||||
if (member is IMethodSymbol method && !method.IsAbstract)
|
||||
{
|
||||
|
||||
+3
-3
@@ -86,10 +86,10 @@ internal static class DiagnosticDescriptors
|
||||
/// <summary>
|
||||
/// MAFGENWF006: ConfigureRoutes already defined.
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor ConfigureRoutesAlreadyDefined = Register(new(
|
||||
public static readonly DiagnosticDescriptor ConfigureProtocolAlreadyDefined = Register(new(
|
||||
id: "MAFGENWF006",
|
||||
title: "ConfigureRoutes already defined",
|
||||
messageFormat: "Class '{0}' already defines ConfigureRoutes; [MessageHandler] methods will be ignored",
|
||||
title: "ConfigureProtocol already defined",
|
||||
messageFormat: "Class '{0}' already defines ConfigureProtocol; [MessageHandler] methods will be ignored",
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Info,
|
||||
isEnabledByDefault: true));
|
||||
|
||||
@@ -120,7 +120,7 @@ public sealed class ExecutorRouteGenerator : IIncrementalGenerator
|
||||
{
|
||||
if (!processedClasses.Contains(kvp.Key))
|
||||
{
|
||||
yield return SemanticAnalyzer.CombineProtocolOnlyResults(kvp.Value);
|
||||
yield return SemanticAnalyzer.CombineOutputOnlyResults(kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Agents.AI.Workflows.Generators.Models;
|
||||
|
||||
@@ -16,6 +17,8 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Generation;
|
||||
/// </remarks>
|
||||
internal static class SourceBuilder
|
||||
{
|
||||
internal const string IndentUnit = " ";
|
||||
|
||||
/// <summary>
|
||||
/// Generates the complete source file for an executor's generated partial class.
|
||||
/// </summary>
|
||||
@@ -53,7 +56,8 @@ internal static class SourceBuilder
|
||||
{
|
||||
sb.AppendLine($"{indent}partial class {containingType}");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
indent += " ";
|
||||
|
||||
indent += IndentUnit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,30 +65,49 @@ internal static class SourceBuilder
|
||||
sb.AppendLine($"{indent}partial class {info.ClassName}{info.GenericParameters}");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string memberIndent = indent + " ";
|
||||
bool hasContent = false;
|
||||
string memberIndent = indent + IndentUnit;
|
||||
|
||||
// Only generate ConfigureRoutes if there are handlers
|
||||
if (info.Handlers.Count > 0)
|
||||
// ConfigureProtocol
|
||||
sb.AppendLine($"{memberIndent}protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)");
|
||||
sb.AppendLine($"{memberIndent}{{");
|
||||
|
||||
string bodyIndent = memberIndent + IndentUnit;
|
||||
|
||||
if (info.BaseHasConfigureProtocol)
|
||||
{
|
||||
GenerateConfigureRoutes(sb, info, memberIndent);
|
||||
hasContent = true;
|
||||
sb.Append($"{bodyIndent}return base.ConfigureProtocol(protocolBuilder)");
|
||||
bodyIndent += " ";
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"{bodyIndent}return protocolBuilder");
|
||||
}
|
||||
|
||||
// Only generate protocol overrides if [SendsMessage] or [YieldsOutput] attributes are present.
|
||||
// Without these attributes, we rely on the base class defaults.
|
||||
if (info.ShouldGenerateProtocolOverrides)
|
||||
if (info.ShouldGenerateSentMessageRegistrations)
|
||||
{
|
||||
if (hasContent)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
GenerateConfigureSentTypes(sb, info, memberIndent);
|
||||
sb.AppendLine();
|
||||
GenerateConfigureYieldTypes(sb, info, memberIndent);
|
||||
GenerateConfigureSentTypes(sb, info, bodyIndent);
|
||||
}
|
||||
|
||||
if (info.ShouldGenerateYieldedOutputRegistrations)
|
||||
{
|
||||
GenerateConfigureYieldTypes(sb, info, bodyIndent);
|
||||
}
|
||||
|
||||
// Only generate ConfigureRoutes if there are handlers
|
||||
if (info.Handlers.Count > 0)
|
||||
{
|
||||
GenerateConfigureRoutes(sb, info, bodyIndent);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(";");
|
||||
}
|
||||
|
||||
// Close ConfigureProtocol
|
||||
sb.AppendLine($"{memberIndent}}}");
|
||||
|
||||
// Close class
|
||||
sb.AppendLine($"{indent}}}");
|
||||
|
||||
@@ -107,24 +130,19 @@ internal static class SourceBuilder
|
||||
/// </summary>
|
||||
private static void GenerateConfigureRoutes(StringBuilder sb, ExecutorInfo info, string indent)
|
||||
{
|
||||
sb.AppendLine($"{indent}protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)");
|
||||
sb.AppendLine(".ConfigureRoutes(ConfigureRoutes);");
|
||||
|
||||
sb.AppendLine($"{indent}void ConfigureRoutes(RouteBuilder routeBuilder)");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string bodyIndent = indent + " ";
|
||||
|
||||
// If a base class has its own ConfigureRoutes, chain to it first to preserve inherited handlers.
|
||||
if (info.BaseHasConfigureRoutes)
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}routeBuilder = base.ConfigureRoutes(routeBuilder);");
|
||||
sb.AppendLine();
|
||||
}
|
||||
string bodyIndent = indent + IndentUnit;
|
||||
|
||||
// Generate handler registrations using fluent AddHandler calls.
|
||||
// RouteBuilder.AddHandler<TIn> registers a void handler; AddHandler<TIn, TOut> registers one with a return value.
|
||||
if (info.Handlers.Count == 1)
|
||||
{
|
||||
HandlerInfo handler = info.Handlers[0];
|
||||
sb.AppendLine($"{bodyIndent}return routeBuilder");
|
||||
sb.AppendLine($"{bodyIndent}routeBuilder");
|
||||
sb.Append($"{bodyIndent} .AddHandler");
|
||||
AppendHandlerGenericArgs(sb, handler);
|
||||
sb.AppendLine($"(this.{handler.MethodName});");
|
||||
@@ -132,7 +150,7 @@ internal static class SourceBuilder
|
||||
else
|
||||
{
|
||||
// Multiple handlers: chain fluent calls, semicolon only on the last one.
|
||||
sb.AppendLine($"{bodyIndent}return routeBuilder");
|
||||
sb.AppendLine($"{bodyIndent}routeBuilder");
|
||||
|
||||
for (int i = 0; i < info.Handlers.Count; i++)
|
||||
{
|
||||
@@ -178,28 +196,24 @@ internal static class SourceBuilder
|
||||
/// </remarks>
|
||||
private static void GenerateConfigureSentTypes(StringBuilder sb, ExecutorInfo info, string indent)
|
||||
{
|
||||
sb.AppendLine($"{indent}protected override ISet<Type> ConfigureSentTypes()");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
// Track types to avoid emitting duplicate Add calls (the set handles runtime dedup,
|
||||
// but cleaner generated code is easier to read).
|
||||
var addedTypes = new HashSet<string>();
|
||||
|
||||
string bodyIndent = indent + " ";
|
||||
|
||||
sb.AppendLine($"{bodyIndent}var types = base.ConfigureSentTypes();");
|
||||
|
||||
foreach (var type in info.ClassSendTypes)
|
||||
foreach (var type in info.ClassSendTypes.Where(type => addedTypes.Add(type)))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
sb.AppendLine($".SendsMessage<{type}>()");
|
||||
sb.Append(indent);
|
||||
}
|
||||
|
||||
foreach (var handler in info.Handlers)
|
||||
{
|
||||
foreach (var type in handler.SendTypes)
|
||||
foreach (var type in handler.SendTypes.Where(type => addedTypes.Add(type)))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
sb.AppendLine($".SendsMessage<{type}>()");
|
||||
sb.Append(indent);
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($"{bodyIndent}return types;");
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -211,43 +225,23 @@ internal static class SourceBuilder
|
||||
/// </remarks>
|
||||
private static void GenerateConfigureYieldTypes(StringBuilder sb, ExecutorInfo info, string indent)
|
||||
{
|
||||
sb.AppendLine($"{indent}protected override ISet<Type> ConfigureYieldTypes()");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
string bodyIndent = indent + " ";
|
||||
|
||||
sb.AppendLine($"{bodyIndent}var types = base.ConfigureYieldTypes();");
|
||||
|
||||
// Track types to avoid emitting duplicate Add calls (the set handles runtime dedup,
|
||||
// but cleaner generated code is easier to read).
|
||||
var addedTypes = new HashSet<string>();
|
||||
|
||||
foreach (var type in info.ClassYieldTypes)
|
||||
foreach (var type in info.ClassYieldTypes.Where(type => addedTypes.Add(type)))
|
||||
{
|
||||
if (addedTypes.Add(type))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
}
|
||||
sb.AppendLine($".YieldsOutput<{type}>()");
|
||||
sb.Append(indent);
|
||||
}
|
||||
|
||||
foreach (var handler in info.Handlers)
|
||||
{
|
||||
foreach (var type in handler.YieldTypes)
|
||||
foreach (var type in handler.YieldTypes.Where(type => addedTypes.Add(type)))
|
||||
{
|
||||
if (addedTypes.Add(type))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({type}));");
|
||||
}
|
||||
}
|
||||
|
||||
// Handler return types (ValueTask<T>) are implicitly yielded.
|
||||
if (handler.HasOutput && handler.OutputTypeName != null && addedTypes.Add(handler.OutputTypeName))
|
||||
{
|
||||
sb.AppendLine($"{bodyIndent}types.Add(typeof({handler.OutputTypeName}));");
|
||||
sb.AppendLine($".YieldsOutput<{type}>()");
|
||||
sb.Append(indent);
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($"{bodyIndent}return types;");
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
|
||||
/// <param name="GenericParameters">The generic type parameters of the class (e.g., "<T, U>"), or null if not generic.</param>
|
||||
/// <param name="IsNested">Whether the class is nested inside another class.</param>
|
||||
/// <param name="ContainingTypeChain">The chain of containing types for nested classes (e.g., "OuterClass.InnerClass"). Empty string if not nested.</param>
|
||||
/// <param name="BaseHasConfigureRoutes">Whether the base class has a ConfigureRoutes method that should be called.</param>
|
||||
/// <param name="BaseHasConfigureProtocol">Whether the base class has a ConfigureRoutes method that should be called.</param>
|
||||
/// <param name="Handlers">The list of handler methods to register.</param>
|
||||
/// <param name="ClassSendTypes">The types declared via class-level [SendsMessage] attributes.</param>
|
||||
/// <param name="ClassYieldTypes">The types declared via class-level [YieldsOutput] attributes.</param>
|
||||
@@ -21,19 +21,20 @@ internal sealed record ExecutorInfo(
|
||||
string? GenericParameters,
|
||||
bool IsNested,
|
||||
string ContainingTypeChain,
|
||||
bool BaseHasConfigureRoutes,
|
||||
bool BaseHasConfigureProtocol,
|
||||
ImmutableEquatableArray<HandlerInfo> Handlers,
|
||||
ImmutableEquatableArray<string> ClassSendTypes,
|
||||
ImmutableEquatableArray<string> ClassYieldTypes)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether any protocol type overrides should be generated.
|
||||
/// Gets whether any "Sent" message type registrations should be generated.
|
||||
/// </summary>
|
||||
public bool ShouldGenerateProtocolOverrides =>
|
||||
!this.ClassSendTypes.IsEmpty ||
|
||||
!this.ClassYieldTypes.IsEmpty ||
|
||||
this.HasHandlerWithSendTypes ||
|
||||
this.HasHandlerWithYieldTypes;
|
||||
public bool ShouldGenerateSentMessageRegistrations => !this.ClassSendTypes.IsEmpty || this.HasHandlerWithSendTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether any "Yielded" output type registrations should be generated.
|
||||
/// </summary>
|
||||
public bool ShouldGenerateYieldedOutputRegistrations => !this.ClassYieldTypes.IsEmpty || this.HasHandlerWithYieldTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether any handler has explicit Send types.
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed record MethodAnalysisResult(
|
||||
string? GenericParameters,
|
||||
bool IsNested,
|
||||
string ContainingTypeChain,
|
||||
bool BaseHasConfigureRoutes,
|
||||
bool BaseHasConfigureProtocol,
|
||||
ImmutableEquatableArray<string> ClassSendTypes,
|
||||
ImmutableEquatableArray<string> ClassYieldTypes,
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// <summary>
|
||||
/// Represents an event triggered when an agent produces a response.
|
||||
/// </summary>
|
||||
public class AgentResponseEvent : ExecutorEvent
|
||||
public sealed class AgentResponseEvent : WorkflowOutputEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentResponseEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The identifier of the executor that generated this event.</param>
|
||||
/// <param name="response">The agent response.</param>
|
||||
public AgentResponseEvent(string executorId, AgentResponse response) : base(executorId, data: response)
|
||||
public AgentResponseEvent(string executorId, AgentResponse response) : base(response, executorId)
|
||||
{
|
||||
this.Response = Throw.IfNull(response);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// <summary>
|
||||
/// Represents an event triggered when an agent run produces an update.
|
||||
/// </summary>
|
||||
public class AgentResponseUpdateEvent : ExecutorEvent
|
||||
public sealed class AgentResponseUpdateEvent : WorkflowOutputEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentResponseUpdateEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The identifier of the executor that generated this event.</param>
|
||||
/// <param name="update">The agent run response update.</param>
|
||||
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) : base(executorId, data: update)
|
||||
public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) : base(update, executorId)
|
||||
{
|
||||
this.Update = Throw.IfNull(update);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ public static partial class AgentWorkflowBuilder
|
||||
|
||||
ExecutorBinding end = endFactory.BindExecutor(ConcurrentEndExecutor.ExecutorId);
|
||||
|
||||
builder.AddFanInEdge(accumulators, end);
|
||||
builder.AddFanInBarrierEdge(accumulators, end);
|
||||
|
||||
builder = builder.WithOutputFrom(end);
|
||||
if (workflowName is not null)
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class SendsMessageAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class YieldsOutputAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -34,19 +34,29 @@ public sealed class ChatForwardingExecutor(string id, ChatForwardingExecutorOpti
|
||||
private readonly ChatRole? _stringMessageChatRole = options?.StringMessageChatRole;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
if (this._stringMessageChatRole.HasValue)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)));
|
||||
}
|
||||
return protocolBuilder.ConfigureRoutes(ConfigureRoutes)
|
||||
.SendsMessage<ChatMessage>()
|
||||
.SendsMessage<List<ChatMessage>>()
|
||||
.SendsMessage<ChatMessage[]>()
|
||||
.SendsMessage<TurnToken>();
|
||||
|
||||
return routeBuilder.AddHandler<ChatMessage>(ForwardMessageAsync)
|
||||
.AddHandler<IEnumerable<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<ChatMessage[]>(ForwardMessagesAsync)
|
||||
.AddHandler<List<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<TurnToken>(ForwardTurnTokenAsync);
|
||||
void ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
{
|
||||
if (this._stringMessageChatRole.HasValue)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)));
|
||||
}
|
||||
|
||||
routeBuilder.AddHandler<ChatMessage>(ForwardMessageAsync)
|
||||
.AddHandler<IEnumerable<ChatMessage>>(ForwardMessagesAsync)
|
||||
// remove this once we internalize the typecheck logic
|
||||
.AddHandler<ChatMessage[]>(ForwardMessagesAsync)
|
||||
//.AddHandler<List<ChatMessage>>(ForwardMessagesAsync)
|
||||
.AddHandler<TurnToken>(ForwardTurnTokenAsync);
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueTask ForwardMessageAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
|
||||
@@ -26,7 +26,7 @@ public static class ChatProtocolExtensions
|
||||
/// langword="false"/>.</returns>
|
||||
public static bool IsChatProtocol(this ProtocolDescriptor descriptor, bool allowCatchAll = false)
|
||||
{
|
||||
bool foundListChatMessageInput = false;
|
||||
bool foundIEnumerableChatMessageInput = false;
|
||||
bool foundTurnTokenInput = false;
|
||||
|
||||
if (allowCatchAll && descriptor.AcceptsAll)
|
||||
@@ -40,9 +40,9 @@ public static class ChatProtocolExtensions
|
||||
// output type.
|
||||
foreach (Type inputType in descriptor.Accepts)
|
||||
{
|
||||
if (inputType == typeof(List<ChatMessage>))
|
||||
if (inputType == typeof(IEnumerable<ChatMessage>))
|
||||
{
|
||||
foundListChatMessageInput = true;
|
||||
foundIEnumerableChatMessageInput = true;
|
||||
}
|
||||
else if (inputType == typeof(TurnToken))
|
||||
{
|
||||
@@ -50,7 +50,7 @@ public static class ChatProtocolExtensions
|
||||
}
|
||||
}
|
||||
|
||||
return foundListChatMessageInput && foundTurnTokenInput;
|
||||
return foundIEnumerableChatMessageInput && foundTurnTokenInput;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -67,19 +67,26 @@ public abstract class ChatProtocolExecutor : StatefulExecutor<List<ChatMessage>>
|
||||
protected bool AutoSendTurnToken => this._options.AutoSendTurnToken;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
|
||||
{
|
||||
if (this.SupportsStringMessage)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => this.AddMessageAsync(new(this.StringMessageChatRole.Value, message), context));
|
||||
}
|
||||
return protocolBuilder.ConfigureRoutes(ConfigureRoutes)
|
||||
.SendsMessage<List<ChatMessage>>()
|
||||
.SendsMessage<TurnToken>();
|
||||
|
||||
return routeBuilder.AddHandler<ChatMessage>(this.AddMessageAsync)
|
||||
.AddHandler<IEnumerable<ChatMessage>>(this.AddMessagesAsync)
|
||||
.AddHandler<ChatMessage[]>(this.AddMessagesAsync)
|
||||
.AddHandler<List<ChatMessage>>(this.AddMessagesAsync)
|
||||
.AddHandler<TurnToken>(this.TakeTurnAsync);
|
||||
void ConfigureRoutes(RouteBuilder routeBuilder)
|
||||
{
|
||||
if (this.SupportsStringMessage)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => this.AddMessageAsync(new(this.StringMessageChatRole.Value, message), context));
|
||||
}
|
||||
|
||||
routeBuilder.AddHandler<ChatMessage>(this.AddMessageAsync)
|
||||
.AddHandler<IEnumerable<ChatMessage>>(this.AddMessagesAsync)
|
||||
.AddHandler<ChatMessage[]>(this.AddMessagesAsync)
|
||||
//.AddHandler<List<ChatMessage>>(this.AddMessagesAsync)
|
||||
.AddHandler<TurnToken>(this.TakeTurnAsync);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,14 +7,14 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a checkpoint with a unique identifier and a timestamp indicating when it was created.
|
||||
/// Represents a checkpoint with a unique identifier.
|
||||
/// </summary>
|
||||
public sealed class CheckpointInfo : IEquatable<CheckpointInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for the current run.
|
||||
/// Gets the unique identifier for the current session.
|
||||
/// </summary>
|
||||
public string RunId { get; }
|
||||
public string SessionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier for the checkpoint.
|
||||
@@ -22,37 +22,34 @@ public sealed class CheckpointInfo : IEquatable<CheckpointInfo>
|
||||
public string CheckpointId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CheckpointInfo"/> class with a unique identifier and the current
|
||||
/// UTC timestamp.
|
||||
/// Initializes a new instance of the <see cref="CheckpointInfo"/> class with a unique identifier.
|
||||
/// </summary>
|
||||
/// <remarks>This constructor generates a new unique identifier using a GUID in a 32-character, lowercase,
|
||||
/// hexadecimal format and sets the timestamp to the current UTC time.</remarks>
|
||||
internal CheckpointInfo(string runId) : this(runId, Guid.NewGuid().ToString("N")) { }
|
||||
internal CheckpointInfo(string sessionId) : this(sessionId, Guid.NewGuid().ToString("N")) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the CheckpointInfo class with the specified run and checkpoint identifiers.
|
||||
/// Initializes a new instance of the CheckpointInfo class with the specified session and checkpoint identifiers.
|
||||
/// </summary>
|
||||
/// <param name="runId">The unique identifier for the run. Cannot be null or empty.</param>
|
||||
/// <param name="sessionId">The unique identifier for the session. Cannot be null or empty.</param>
|
||||
/// <param name="checkpointId">The unique identifier for the checkpoint. Cannot be null or empty.</param>
|
||||
[JsonConstructor]
|
||||
public CheckpointInfo(string runId, string checkpointId)
|
||||
public CheckpointInfo(string sessionId, string checkpointId)
|
||||
{
|
||||
this.RunId = Throw.IfNullOrEmpty(runId);
|
||||
this.SessionId = Throw.IfNullOrEmpty(sessionId);
|
||||
this.CheckpointId = Throw.IfNullOrEmpty(checkpointId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(CheckpointInfo? other) =>
|
||||
other is not null &&
|
||||
this.RunId == other.RunId &&
|
||||
this.SessionId == other.SessionId &&
|
||||
this.CheckpointId == other.CheckpointId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => this.Equals(obj as CheckpointInfo);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(this.RunId, this.CheckpointId);
|
||||
public override int GetHashCode() => HashCode.Combine(this.SessionId, this.CheckpointId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"CheckpointInfo(RunId: {this.RunId}, CheckpointId: {this.CheckpointId})";
|
||||
public override string ToString() => $"CheckpointInfo(SessionId: {this.SessionId}, CheckpointId: {this.CheckpointId})";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
@@ -49,9 +50,12 @@ public sealed class CheckpointManager : ICheckpointManager
|
||||
return new(CreateImpl(marshaller, store));
|
||||
}
|
||||
|
||||
ValueTask<CheckpointInfo> ICheckpointManager.CommitCheckpointAsync(string runId, Checkpoint checkpoint)
|
||||
=> this._impl.CommitCheckpointAsync(runId, checkpoint);
|
||||
ValueTask<CheckpointInfo> ICheckpointManager.CommitCheckpointAsync(string sessionId, Checkpoint checkpoint)
|
||||
=> this._impl.CommitCheckpointAsync(sessionId, checkpoint);
|
||||
|
||||
ValueTask<Checkpoint> ICheckpointManager.LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo)
|
||||
=> this._impl.LookupCheckpointAsync(runId, checkpointInfo);
|
||||
ValueTask<Checkpoint> ICheckpointManager.LookupCheckpointAsync(string sessionId, CheckpointInfo checkpointInfo)
|
||||
=> this._impl.LookupCheckpointAsync(sessionId, checkpointInfo);
|
||||
|
||||
ValueTask<IEnumerable<CheckpointInfo>> ICheckpointManager.RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent)
|
||||
=> this._impl.RetrieveIndexAsync(sessionId, withParent);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a base object for a workflow run that may support checkpointing.
|
||||
/// </summary>
|
||||
public abstract class CheckpointableRunBase
|
||||
{
|
||||
// TODO: Rename Context?
|
||||
private readonly ICheckpointingHandle _checkpointingHandle;
|
||||
|
||||
internal CheckpointableRunBase(ICheckpointingHandle checkpointingHandle)
|
||||
{
|
||||
this._checkpointingHandle = checkpointingHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICheckpointingHandle.IsCheckpointingEnabled"/>
|
||||
public bool IsCheckpointingEnabled => this._checkpointingHandle.IsCheckpointingEnabled;
|
||||
|
||||
/// <inheritdoc cref="ICheckpointingHandle.Checkpoints"/>
|
||||
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints ?? [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most recent checkpoint information.
|
||||
/// </summary>
|
||||
public CheckpointInfo? LastCheckpoint
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!this.IsCheckpointingEnabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var checkpoints = this.Checkpoints;
|
||||
return checkpoints.Count > 0 ? checkpoints[checkpoints.Count - 1] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICheckpointingHandle.RestoreCheckpointAsync"/>
|
||||
public ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
|
||||
=> this._checkpointingHandle.RestoreCheckpointAsync(checkpointInfo, cancellationToken);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a workflow run that supports checkpointing.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRun">The type of the underlying workflow run handle.</typeparam>
|
||||
/// <seealso cref="Run"/>
|
||||
/// <seealso cref="StreamingRun"/>
|
||||
public sealed class Checkpointed<TRun> : IAsyncDisposable
|
||||
{
|
||||
private readonly ICheckpointingHandle _runner;
|
||||
|
||||
internal Checkpointed(TRun run, ICheckpointingHandle runner)
|
||||
{
|
||||
this.Run = Throw.IfNull(run);
|
||||
this._runner = Throw.IfNull(runner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the workflow run associated with this <see cref="Checkpointed{TRun}"/> instance.
|
||||
/// </summary>
|
||||
/// <seealso cref="Run"/>
|
||||
/// <seealso cref="StreamingRun"/>
|
||||
public TRun Run { get; }
|
||||
|
||||
/// <inheritdoc cref="ICheckpointingHandle.Checkpoints"/>
|
||||
public IReadOnlyList<CheckpointInfo> Checkpoints => this._runner.Checkpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the most recent checkpoint information.
|
||||
/// </summary>
|
||||
public CheckpointInfo? LastCheckpoint
|
||||
{
|
||||
get
|
||||
{
|
||||
var checkpoints = this.Checkpoints;
|
||||
return checkpoints.Count > 0 ? checkpoints[checkpoints.Count - 1] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (this.Run is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (this.Run is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICheckpointingHandle.RestoreCheckpointAsync"/>
|
||||
public ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
|
||||
=> this._runner.RestoreCheckpointAsync(checkpointInfo, cancellationToken);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal sealed partial class CheckpointInfoConverter() : JsonConverterDictionar
|
||||
protected override JsonTypeInfo<CheckpointInfo> TypeInfo
|
||||
=> WorkflowsJsonUtilities.JsonContext.Default.CheckpointInfo;
|
||||
|
||||
private const string CheckpointInfoPropertyNamePattern = @"^(?<runId>(((\|\|)|([^\|]))*))\|(?<checkpointId>(((\|\|)|([^\|]))*)?)$";
|
||||
private const string CheckpointInfoPropertyNamePattern = @"^(?<sessionId>(((\|\|)|([^\|]))*))\|(?<checkpointId>(((\|\|)|([^\|]))*)?)$";
|
||||
#if NET
|
||||
[GeneratedRegex(CheckpointInfoPropertyNamePattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture)]
|
||||
public static partial Regex CheckpointInfoPropertyNameRegex();
|
||||
@@ -33,17 +33,17 @@ internal sealed partial class CheckpointInfoConverter() : JsonConverterDictionar
|
||||
throw new JsonException($"Invalid CheckpointInfo property name format. Got '{propertyName}'.");
|
||||
}
|
||||
|
||||
string runId = scopeKeyPatternMatch.Groups["runId"].Value;
|
||||
string sessionId = scopeKeyPatternMatch.Groups["sessionId"].Value;
|
||||
string checkpointId = scopeKeyPatternMatch.Groups["checkpointId"].Value;
|
||||
|
||||
return new(Unescape(runId)!, Unescape(checkpointId)!);
|
||||
return new(Unescape(sessionId)!, Unescape(checkpointId)!);
|
||||
}
|
||||
|
||||
protected override string Stringify([DisallowNull] CheckpointInfo value)
|
||||
{
|
||||
string? runIdEscaped = Escape(value.RunId);
|
||||
string? sessionIdEscaped = Escape(value.SessionId);
|
||||
string? checkpointIdEscaped = Escape(value.CheckpointId);
|
||||
|
||||
return $"{runIdEscaped}|{checkpointIdEscaped}";
|
||||
return $"{sessionIdEscaped}|{checkpointIdEscaped}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
@@ -15,16 +16,19 @@ internal sealed class CheckpointManagerImpl<TStoreObject> : ICheckpointManager
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
public ValueTask<CheckpointInfo> CommitCheckpointAsync(string runId, Checkpoint checkpoint)
|
||||
public ValueTask<CheckpointInfo> CommitCheckpointAsync(string sessionId, Checkpoint checkpoint)
|
||||
{
|
||||
TStoreObject storeObject = this._marshaller.Marshal(checkpoint);
|
||||
|
||||
return this._store.CreateCheckpointAsync(runId, storeObject, checkpoint.Parent);
|
||||
return this._store.CreateCheckpointAsync(sessionId, storeObject, checkpoint.Parent);
|
||||
}
|
||||
|
||||
public async ValueTask<Checkpoint> LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo)
|
||||
public async ValueTask<Checkpoint> LookupCheckpointAsync(string sessionId, CheckpointInfo checkpointInfo)
|
||||
{
|
||||
TStoreObject result = await this._store.RetrieveCheckpointAsync(runId, checkpointInfo).ConfigureAwait(false);
|
||||
TStoreObject result = await this._store.RetrieveCheckpointAsync(sessionId, checkpointInfo).ConfigureAwait(false);
|
||||
return this._marshaller.Marshal<Checkpoint>(result);
|
||||
}
|
||||
|
||||
public ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null)
|
||||
=> this._store.RetrieveIndexAsync(sessionId, withParent);
|
||||
}
|
||||
|
||||
+10
-10
@@ -93,15 +93,15 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFileNameForCheckpoint(string runId, CheckpointInfo key)
|
||||
=> Path.Combine(this.Directory.FullName, $"{runId}_{key.CheckpointId}.json");
|
||||
private string GetFileNameForCheckpoint(string sessionId, CheckpointInfo key)
|
||||
=> Path.Combine(this.Directory.FullName, $"{sessionId}_{key.CheckpointId}.json");
|
||||
|
||||
private CheckpointInfo GetUnusedCheckpointInfo(string runId)
|
||||
private CheckpointInfo GetUnusedCheckpointInfo(string sessionId)
|
||||
{
|
||||
CheckpointInfo key;
|
||||
do
|
||||
{
|
||||
key = new(runId);
|
||||
key = new(sessionId);
|
||||
} while (!this.CheckpointIndex.Add(key));
|
||||
|
||||
return key;
|
||||
@@ -110,12 +110,12 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
|
||||
/// <inheritdoc/>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1835:Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'",
|
||||
Justification = "Memory-based overload is missing for 4.7.2")]
|
||||
public override async ValueTask<CheckpointInfo> CreateCheckpointAsync(string runId, JsonElement value, CheckpointInfo? parent = null)
|
||||
public override async ValueTask<CheckpointInfo> CreateCheckpointAsync(string sessionId, JsonElement value, CheckpointInfo? parent = null)
|
||||
{
|
||||
this.CheckDisposed();
|
||||
|
||||
CheckpointInfo key = this.GetUnusedCheckpointInfo(runId);
|
||||
string fileName = this.GetFileNameForCheckpoint(runId, key);
|
||||
CheckpointInfo key = this.GetUnusedCheckpointInfo(sessionId);
|
||||
string fileName = this.GetFileNameForCheckpoint(sessionId, key);
|
||||
try
|
||||
{
|
||||
using Stream checkpointStream = File.Open(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
@@ -145,10 +145,10 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<JsonElement> RetrieveCheckpointAsync(string runId, CheckpointInfo key)
|
||||
public override async ValueTask<JsonElement> RetrieveCheckpointAsync(string sessionId, CheckpointInfo key)
|
||||
{
|
||||
this.CheckDisposed();
|
||||
string fileName = this.GetFileNameForCheckpoint(runId, key);
|
||||
string fileName = this.GetFileNameForCheckpoint(sessionId, key);
|
||||
|
||||
if (!this.CheckpointIndex.Contains(key) ||
|
||||
!File.Exists(fileName))
|
||||
@@ -163,7 +163,7 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
|
||||
public override ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null)
|
||||
{
|
||||
this.CheckDisposed();
|
||||
|
||||
|
||||
@@ -13,18 +13,30 @@ internal interface ICheckpointManager
|
||||
/// <summary>
|
||||
/// Commits the specified checkpoint and returns information that can be used to retrieve it later.
|
||||
/// </summary>
|
||||
/// <param name="runId">The identifier for the current run or execution context.</param>
|
||||
/// <param name="sessionId">The identifier for the current session or execution context.</param>
|
||||
/// <param name="checkpoint">The checkpoint to commit.</param>
|
||||
/// <returns>A <see cref="CheckpointInfo"/> representing the incoming checkpoint.</returns>
|
||||
ValueTask<CheckpointInfo> CommitCheckpointAsync(string runId, Checkpoint checkpoint);
|
||||
ValueTask<CheckpointInfo> CommitCheckpointAsync(string sessionId, Checkpoint checkpoint);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the checkpoint associated with the specified checkpoint information.
|
||||
/// </summary>
|
||||
/// <param name="runId">The identifier for the current run of execution context.</param>
|
||||
/// <param name="sessionId">The identifier for the current session of execution context.</param>
|
||||
/// <param name="checkpointInfo">The information used to identify the checkpoint.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> representing the asynchronous operation. The result contains the <see
|
||||
/// cref="Checkpoint"/> associated with the specified <paramref name="checkpointInfo"/>.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if the checkpoint is not found.</exception>
|
||||
ValueTask<Checkpoint> LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo);
|
||||
ValueTask<Checkpoint> LookupCheckpointAsync(string sessionId, CheckpointInfo checkpointInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves the collection of checkpoint information for the specified session identifier, optionally
|
||||
/// filtered by a parent checkpoint.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The unique identifier of the session for which to retrieve checkpoint information. Cannot be null or empty.</param>
|
||||
/// <param name="withParent">An optional parent checkpoint to filter the results. If specified, only checkpoints with the given parent are
|
||||
/// returned; otherwise, all checkpoints for the session are included.</param>
|
||||
/// <returns>A value task representing the asynchronous operation. The result contains a collection of <see
|
||||
/// cref="CheckpointInfo"/> objects associated with the specified session. The collection is empty if no checkpoints are
|
||||
/// found.</returns>
|
||||
ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null);
|
||||
}
|
||||
|
||||
@@ -6,44 +6,41 @@ using System.Threading.Tasks;
|
||||
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for storing and retrieving checkpoints associated with a specific run and key.
|
||||
/// Defines a contract for storing and retrieving checkpoints associated with a specific session and key.
|
||||
/// </summary>
|
||||
/// <remarks>Implementations of this interface enable durable or in-memory storage of checkpoints, which can be
|
||||
/// used to resume or audit long-running processes. The interface is generic to support different storage object types
|
||||
/// depending on the application's requirements.</remarks>
|
||||
/// <typeparam name="TStoreObject">The type of object to be stored as the value for each checkpoint.</typeparam>
|
||||
public interface ICheckpointStore<TStoreObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves the collection of checkpoint information for the specified run identifier, optionally
|
||||
/// Asynchronously retrieves the collection of checkpoint information for the specified session identifier, optionally
|
||||
/// filtered by a parent checkpoint.
|
||||
/// </summary>
|
||||
/// <param name="runId">The unique identifier of the run for which to retrieve checkpoint information. Cannot be null or empty.</param>
|
||||
/// <param name="sessionId">The unique identifier of the session for which to retrieve checkpoint information. Cannot be null or empty.</param>
|
||||
/// <param name="withParent">An optional parent checkpoint to filter the results. If specified, only checkpoints with the given parent are
|
||||
/// returned; otherwise, all checkpoints for the run are included.</param>
|
||||
/// returned; otherwise, all checkpoints for the session are included.</param>
|
||||
/// <returns>A value task representing the asynchronous operation. The result contains a collection of <see
|
||||
/// cref="CheckpointInfo"/> objects associated with the specified run. The collection is empty if no checkpoints are
|
||||
/// cref="CheckpointInfo"/> objects associated with the specified session. The collection is empty if no checkpoints are
|
||||
/// found.</returns>
|
||||
ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null);
|
||||
ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously creates a checkpoint for the specified run and key, associating it with the provided value and
|
||||
/// Asynchronously creates a checkpoint for the specified session and key, associating it with the provided value and
|
||||
/// optional parent checkpoint.
|
||||
/// </summary>
|
||||
/// <param name="runId">The unique identifier of the run for which the checkpoint is being created. Cannot be null or empty.</param>
|
||||
/// <param name="sessionId">The unique identifier of the session for which the checkpoint is being created. Cannot be null or empty.</param>
|
||||
/// <param name="value">The value to associate with the checkpoint. Cannot be null.</param>
|
||||
/// <param name="parent">The optional parent checkpoint information. If specified, the new checkpoint will be linked as a child of this
|
||||
/// parent.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation. The result contains the <see cref="CheckpointInfo"/>
|
||||
/// object representing this stored checkpoint.</returns>
|
||||
ValueTask<CheckpointInfo> CreateCheckpointAsync(string runId, TStoreObject value, CheckpointInfo? parent = null);
|
||||
ValueTask<CheckpointInfo> CreateCheckpointAsync(string sessionId, TStoreObject value, CheckpointInfo? parent = null);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves a checkpoint object associated with the specified run and checkpoint key.
|
||||
/// Asynchronously retrieves a checkpoint object associated with the specified session and checkpoint key.
|
||||
/// </summary>
|
||||
/// <param name="runId">The unique identifier of the run for which the checkpoint is to be retrieved. Cannot be null or empty.</param>
|
||||
/// <param name="sessionId">The unique identifier of the session for which the checkpoint is to be retrieved. Cannot be null or empty.</param>
|
||||
/// <param name="key">The key identifying the specific checkpoint to retrieve. Cannot be null.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation. The result contains the checkpoint object associated
|
||||
/// with the specified run and key.</returns>
|
||||
ValueTask<TStoreObject> RetrieveCheckpointAsync(string runId, CheckpointInfo key);
|
||||
/// with the specified session and key.</returns>
|
||||
ValueTask<TStoreObject> RetrieveCheckpointAsync(string sessionId, CheckpointInfo key);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,21 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
internal interface ICheckpointingHandle
|
||||
{
|
||||
// TODO: Convert this to a multi-timeline (e.g.: Live timeline + forks for orphaned checkpoints due to timetravel)
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether checkpointing is enabled for the current operation or process.
|
||||
/// </summary>
|
||||
bool IsCheckpointingEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only list of checkpoint information associated with the current context.
|
||||
/// </summary>
|
||||
IReadOnlyList<CheckpointInfo> Checkpoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Restores the system state from the specified checkpoint asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="checkpointInfo">The checkpoint information that identifies the state to restore. Cannot be null.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the restore operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> that represents the asynchronous restore operation.</returns>
|
||||
ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
+19
-16
@@ -13,51 +13,54 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
internal sealed class InMemoryCheckpointManager : ICheckpointManager
|
||||
{
|
||||
[JsonInclude]
|
||||
internal Dictionary<string, RunCheckpointCache<Checkpoint>> Store { get; } = [];
|
||||
internal Dictionary<string, SessionCheckpointCache<Checkpoint>> Store { get; } = [];
|
||||
|
||||
public InMemoryCheckpointManager() { }
|
||||
|
||||
[JsonConstructor]
|
||||
internal InMemoryCheckpointManager(Dictionary<string, RunCheckpointCache<Checkpoint>> store)
|
||||
internal InMemoryCheckpointManager(Dictionary<string, SessionCheckpointCache<Checkpoint>> store)
|
||||
{
|
||||
this.Store = store;
|
||||
}
|
||||
|
||||
private RunCheckpointCache<Checkpoint> GetRunStore(string runId)
|
||||
private SessionCheckpointCache<Checkpoint> GetSessionStore(string sessionId)
|
||||
{
|
||||
if (!this.Store.TryGetValue(runId, out RunCheckpointCache<Checkpoint>? runStore))
|
||||
if (!this.Store.TryGetValue(sessionId, out SessionCheckpointCache<Checkpoint>? sessionStore))
|
||||
{
|
||||
runStore = this.Store[runId] = new();
|
||||
sessionStore = this.Store[sessionId] = new();
|
||||
}
|
||||
|
||||
return runStore;
|
||||
return sessionStore;
|
||||
}
|
||||
|
||||
public ValueTask<CheckpointInfo> CommitCheckpointAsync(string runId, Checkpoint checkpoint)
|
||||
public ValueTask<CheckpointInfo> CommitCheckpointAsync(string sessionId, Checkpoint checkpoint)
|
||||
{
|
||||
RunCheckpointCache<Checkpoint> runStore = this.GetRunStore(runId);
|
||||
SessionCheckpointCache<Checkpoint> sessionStore = this.GetSessionStore(sessionId);
|
||||
|
||||
CheckpointInfo key;
|
||||
do
|
||||
{
|
||||
key = new(runId);
|
||||
} while (!runStore.Add(key, checkpoint));
|
||||
key = new(sessionId);
|
||||
} while (!sessionStore.Add(key, checkpoint));
|
||||
|
||||
return new(key);
|
||||
}
|
||||
|
||||
public ValueTask<Checkpoint> LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo)
|
||||
public ValueTask<Checkpoint> LookupCheckpointAsync(string sessionId, CheckpointInfo checkpointInfo)
|
||||
{
|
||||
if (!this.GetRunStore(runId).TryGet(checkpointInfo, out Checkpoint? value))
|
||||
if (!this.GetSessionStore(sessionId).TryGet(checkpointInfo, out Checkpoint? value))
|
||||
{
|
||||
throw new KeyNotFoundException($"Could not retrieve checkpoint with id {checkpointInfo.CheckpointId} for run {runId}");
|
||||
throw new KeyNotFoundException($"Could not retrieve checkpoint with id {checkpointInfo.CheckpointId} for session {sessionId}");
|
||||
}
|
||||
|
||||
return new(value);
|
||||
}
|
||||
|
||||
internal bool HasCheckpoints(string runId) => this.GetRunStore(runId).HasCheckpoints;
|
||||
internal bool HasCheckpoints(string sessionId) => this.GetSessionStore(sessionId).HasCheckpoints;
|
||||
|
||||
public bool TryGetLastCheckpoint(string runId, [NotNullWhen(true)] out CheckpointInfo? checkpoint)
|
||||
=> this.GetRunStore(runId).TryGetLastCheckpointInfo(out checkpoint);
|
||||
public bool TryGetLastCheckpoint(string sessionId, [NotNullWhen(true)] out CheckpointInfo? checkpoint)
|
||||
=> this.GetSessionStore(sessionId).TryGetLastCheckpointInfo(out checkpoint);
|
||||
|
||||
public ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null)
|
||||
=> new(this.GetSessionStore(sessionId).CheckpointIndex.AsReadOnly());
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ public abstract class JsonCheckpointStore : ICheckpointStore<JsonElement>
|
||||
protected static JsonTypeInfo<CheckpointInfo> KeyTypeInfo => WorkflowsJsonUtilities.JsonContext.Default.CheckpointInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract ValueTask<CheckpointInfo> CreateCheckpointAsync(string runId, JsonElement value, CheckpointInfo? parent = null);
|
||||
public abstract ValueTask<CheckpointInfo> CreateCheckpointAsync(string sessionId, JsonElement value, CheckpointInfo? parent = null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract ValueTask<JsonElement> RetrieveCheckpointAsync(string runId, CheckpointInfo key);
|
||||
public abstract ValueTask<JsonElement> RetrieveCheckpointAsync(string sessionId, CheckpointInfo key);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null);
|
||||
public abstract ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ internal sealed class PortableMessageEnvelope
|
||||
{
|
||||
this.MessageType = envelope.MessageType;
|
||||
this.Message = new PortableValue(envelope.Message);
|
||||
this.Source = envelope.Source;
|
||||
this.TargetId = envelope.TargetId;
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
internal sealed class RunCheckpointCache<TStoreObject>
|
||||
internal sealed class SessionCheckpointCache<TStoreObject>
|
||||
{
|
||||
[JsonInclude]
|
||||
internal List<CheckpointInfo> CheckpointIndex { get; } = [];
|
||||
@@ -14,10 +14,10 @@ internal sealed class RunCheckpointCache<TStoreObject>
|
||||
[JsonInclude]
|
||||
internal Dictionary<CheckpointInfo, TStoreObject> Cache { get; } = [];
|
||||
|
||||
public RunCheckpointCache() { }
|
||||
public SessionCheckpointCache() { }
|
||||
|
||||
[JsonConstructor]
|
||||
internal RunCheckpointCache(List<CheckpointInfo> checkpointIndex, Dictionary<CheckpointInfo, TStoreObject> cache)
|
||||
internal SessionCheckpointCache(List<CheckpointInfo> checkpointIndex, Dictionary<CheckpointInfo, TStoreObject> cache)
|
||||
{
|
||||
this.CheckpointIndex = checkpointIndex;
|
||||
this.Cache = cache;
|
||||
@@ -29,13 +29,13 @@ internal sealed class RunCheckpointCache<TStoreObject>
|
||||
public bool IsInIndex(CheckpointInfo key) => this.Cache.ContainsKey(key);
|
||||
public bool TryGet(CheckpointInfo key, [MaybeNullWhen(false)] out TStoreObject value) => this.Cache.TryGetValue(key, out value);
|
||||
|
||||
public CheckpointInfo Add(string runId, TStoreObject value)
|
||||
public CheckpointInfo Add(string sessionId, TStoreObject value)
|
||||
{
|
||||
CheckpointInfo key;
|
||||
|
||||
do
|
||||
{
|
||||
key = new(runId);
|
||||
key = new(sessionId);
|
||||
} while (!this.Add(key, value));
|
||||
|
||||
return key;
|
||||
@@ -16,7 +16,7 @@ public static class ConfigurationExtensions
|
||||
/// <param name="configured">The existing configuration for the subject type to be upcast to its parent type. Cannot be null.</param>
|
||||
/// <returns>A new <see cref="Configured{TParent}"/> instance that applies the original configuration logic to the parent type.</returns>
|
||||
public static Configured<TParent> Super<TSubject, TParent>(this Configured<TSubject> configured) where TSubject : TParent
|
||||
=> new(async (config, runId) => await configured.FactoryAsync(config, runId).ConfigureAwait(false), configured.Id, configured.Raw);
|
||||
=> new(async (config, sessionId) => await configured.FactoryAsync(config, sessionId).ConfigureAwait(false), configured.Id, configured.Raw);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new configuration that treats the subject as its base type, allowing configuration to be applied at
|
||||
|
||||
@@ -79,7 +79,7 @@ public class Configured<TSubject>(Func<Config, string, ValueTask<TSubject>> fact
|
||||
/// Gets a "partially" applied factory function that only requires no parameters to create an instance of
|
||||
/// <typeparamref name="TSubject"/> with the provided <see cref="Configuration"/> instance.
|
||||
/// </summary>
|
||||
internal Func<string, ValueTask<TSubject>> BoundFactoryAsync => (runId) => this.FactoryAsync(this.Configuration, runId);
|
||||
internal Func<string, ValueTask<TSubject>> BoundFactoryAsync => (sessionId) => this.FactoryAsync(this.Configuration, sessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,20 +122,20 @@ public class Configured<TSubject, TOptions>(Func<Config<TOptions>, string, Value
|
||||
/// Gets a "partially" applied factory function that only requires no parameters to create an instance of
|
||||
/// <typeparamref name="TSubject"/> with the provided <see cref="Configuration"/> instance.
|
||||
/// </summary>
|
||||
internal Func<string, ValueTask<TSubject>> BoundFactoryAsync => (runId) => this.CreateValidatingMemoizedFactory()(this.Configuration, runId);
|
||||
internal Func<string, ValueTask<TSubject>> BoundFactoryAsync => (sessionId) => this.CreateValidatingMemoizedFactory()(this.Configuration, sessionId);
|
||||
|
||||
private Func<Config, string, ValueTask<TSubject>> CreateValidatingMemoizedFactory()
|
||||
{
|
||||
return FactoryAsync;
|
||||
|
||||
async ValueTask<TSubject> FactoryAsync(Config configuration, string runId)
|
||||
async ValueTask<TSubject> FactoryAsync(Config configuration, string sessionId)
|
||||
{
|
||||
if (this.Id != configuration.Id)
|
||||
{
|
||||
throw new InvalidOperationException($"Requested instance ID '{configuration.Id}' does not match configured ID '{this.Id}'.");
|
||||
}
|
||||
|
||||
TSubject subject = await this.FactoryAsync(this.Configuration, runId).ConfigureAwait(false);
|
||||
TSubject subject = await this.FactoryAsync(this.Configuration, sessionId).ConfigureAwait(false);
|
||||
|
||||
if (this.Id is not null && subject is IIdentified identified && identified.Id != this.Id)
|
||||
{
|
||||
|
||||
@@ -44,7 +44,9 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public string RunId => this._stepRunner.RunId;
|
||||
public string SessionId => this._stepRunner.SessionId;
|
||||
|
||||
public bool IsCheckpointingEnabled => this._checkpointingHandle.IsCheckpointingEnabled;
|
||||
|
||||
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -8,12 +7,6 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal static class AsyncRunHandleExtensions
|
||||
{
|
||||
public static async ValueTask<Checkpointed<TRunType>> WithCheckpointingAsync<TRunType>(this AsyncRunHandle runHandle, Func<ValueTask<TRunType>> prepareFunc)
|
||||
{
|
||||
TRunType run = await prepareFunc().ConfigureAwait(false);
|
||||
return new Checkpointed<TRunType>(run, runHandle);
|
||||
}
|
||||
|
||||
public static async ValueTask<StreamingRun> EnqueueAndStreamAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
@@ -9,10 +10,7 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
internal sealed class DirectEdgeRunner(IRunnerContext runContext, DirectEdgeData edgeData) :
|
||||
EdgeRunner<DirectEdgeData>(runContext, edgeData)
|
||||
{
|
||||
private async ValueTask<Executor> FindRouterAsync(IStepTracer? tracer) => await this.RunContext.EnsureExecutorAsync(this.EdgeData.SinkId, tracer)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
protected internal override async ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer)
|
||||
protected internal override async ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer, CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
@@ -35,8 +33,11 @@ internal sealed class DirectEdgeRunner(IRunnerContext runContext, DirectEdgeData
|
||||
return null;
|
||||
}
|
||||
|
||||
Executor target = await this.FindRouterAsync(stepTracer).ConfigureAwait(false);
|
||||
if (target.CanHandle(envelope.MessageType))
|
||||
Type? messageType = await this.GetMessageRuntimeTypeAsync(envelope, stepTracer, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Executor target = await this.RunContext.EnsureExecutorAsync(this.EdgeData.SinkId, stepTracer, cancellationToken).ConfigureAwait(false);
|
||||
if (CanHandle(target, messageType))
|
||||
{
|
||||
activity?.SetEdgeRunnerDeliveryStatus(EdgeRunnerDeliveryStatus.Delivered);
|
||||
return new DeliveryMapping(envelope, target);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
|
||||
@@ -65,7 +66,7 @@ internal sealed class EdgeMap
|
||||
this._stepTracer = stepTracer;
|
||||
}
|
||||
|
||||
public ValueTask<DeliveryMapping?> PrepareDeliveryForEdgeAsync(Edge edge, MessageEnvelope message)
|
||||
public ValueTask<DeliveryMapping?> PrepareDeliveryForEdgeAsync(Edge edge, MessageEnvelope message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EdgeId id = edge.Data.Id;
|
||||
if (!this._edgeRunners.TryGetValue(id, out EdgeRunner? edgeRunner))
|
||||
@@ -73,25 +74,25 @@ internal sealed class EdgeMap
|
||||
throw new InvalidOperationException($"Edge {edge} not found in the edge map.");
|
||||
}
|
||||
|
||||
return edgeRunner.ChaseEdgeAsync(message, this._stepTracer);
|
||||
return edgeRunner.ChaseEdgeAsync(message, this._stepTracer, cancellationToken);
|
||||
}
|
||||
|
||||
public bool TryRegisterPort(IRunnerContext runContext, string executorId, RequestPort port)
|
||||
=> this._portEdgeRunners.TryAdd(port.Id, ResponseEdgeRunner.ForPort(runContext, executorId, port));
|
||||
|
||||
public ValueTask<DeliveryMapping?> PrepareDeliveryForInputAsync(MessageEnvelope message)
|
||||
public ValueTask<DeliveryMapping?> PrepareDeliveryForInputAsync(MessageEnvelope message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._inputRunner.ChaseEdgeAsync(message, this._stepTracer);
|
||||
return this._inputRunner.ChaseEdgeAsync(message, this._stepTracer, cancellationToken);
|
||||
}
|
||||
|
||||
public ValueTask<DeliveryMapping?> PrepareDeliveryForResponseAsync(ExternalResponse response)
|
||||
public ValueTask<DeliveryMapping?> PrepareDeliveryForResponseAsync(ExternalResponse response, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!this._portEdgeRunners.TryGetValue(response.PortInfo.PortId, out ResponseEdgeRunner? portRunner))
|
||||
{
|
||||
throw new InvalidOperationException($"Port {response.PortInfo.PortId} not found in the edge map.");
|
||||
}
|
||||
|
||||
return portRunner.ChaseEdgeAsync(new MessageEnvelope(response, ExecutorIdentity.None), this._stepTracer);
|
||||
return portRunner.ChaseEdgeAsync(new MessageEnvelope(response, ExecutorIdentity.None), this._stepTracer, cancellationToken);
|
||||
}
|
||||
|
||||
internal async ValueTask<Dictionary<EdgeId, PortableValue>> ExportStateAsync()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user