mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Overhaul orchestration library with new approach (#199)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>Provides extensions for orchestrating <see cref="AIAgent"/>s.</summary>
|
||||
public static class AIAgentExtensions
|
||||
{
|
||||
private const string DefaultInstructions = "Respond with JSON that is populated by using the information in this conversation.";
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with the messages, then uses the chat client to process the agent's output and return a structured response.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result expected from the chat client response.</typeparam>
|
||||
/// <param name="agent">The AI agent to be run.</param>
|
||||
/// <param name="chatClient">The chat client used to process the messages.</param>
|
||||
/// <param name="message">The message to be processed.</param>
|
||||
/// <param name="thread">An optional thread context for the agent execution.</param>
|
||||
/// <param name="runOptions">Optional settings that influence the agent's execution.</param>
|
||||
/// <param name="serializerOptions">Optional serializer options to control how <typeparamref name="T"/> is deserialized.</param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation, with a result of type <typeparamref name="T"/> containing the
|
||||
/// structured response.</returns>
|
||||
public static ValueTask<T> RunAsync<T>(
|
||||
this AIAgent agent,
|
||||
IChatClient chatClient,
|
||||
string message,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? runOptions = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(chatClient);
|
||||
Throw.IfNull(message);
|
||||
|
||||
return RunAsync<T>(
|
||||
agent,
|
||||
chatClient,
|
||||
[new ChatMessage(ChatRole.User, message)],
|
||||
thread,
|
||||
runOptions,
|
||||
serializerOptions,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with the messages, then uses the chat client to process the agent's output and return a structured response.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result expected from the chat client response.</typeparam>
|
||||
/// <param name="agent">The AI agent to be run.</param>
|
||||
/// <param name="chatClient">The chat client used to process the messages.</param>
|
||||
/// <param name="messages">A collection of chat messages to be processed.</param>
|
||||
/// <param name="thread">An optional thread context for the agent execution.</param>
|
||||
/// <param name="runOptions">Optional settings that influence the agent's execution.</param>
|
||||
/// <param name="serializerOptions">Optional serializer options to control how <typeparamref name="T"/> is deserialized.</param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation, with a result of type <typeparamref name="T"/> containing the
|
||||
/// structured response.</returns>
|
||||
public static async ValueTask<T> RunAsync<T>(
|
||||
this AIAgent agent,
|
||||
IChatClient chatClient,
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? runOptions = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(chatClient);
|
||||
Throw.IfNull(messages);
|
||||
|
||||
// Invoke the agent.
|
||||
var response = await agent.RunAsync(messages, thread, runOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Pass the output messages to the chat client to get a structured response.
|
||||
var result = await chatClient.GetResponseAsync<T>(
|
||||
response.Messages,
|
||||
serializerOptions: serializerOptions ?? AIJsonUtilities.DefaultOptions,
|
||||
new ChatOptions() { Instructions = DefaultInstructions },
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Parse and return the results.
|
||||
return result.Result;
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An actor that represents an <see cref="Agent"/>.
|
||||
/// </summary>
|
||||
public abstract class AgentActor : OrchestrationActor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="agent">An <see cref="Agent"/>.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
protected AgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, AIAgent agent, ILogger? logger = null)
|
||||
: base(id, runtime, context, agent.Description, logger)
|
||||
{
|
||||
this.Agent = agent;
|
||||
this.Thread = this.Agent.GetNewThread();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated agent.
|
||||
/// </summary>
|
||||
protected AIAgent Agent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current conversation thread used during agent communication.
|
||||
/// </summary>
|
||||
protected AgentThread Thread { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reset the conversation thread.
|
||||
/// </summary>
|
||||
protected void ResetThread()
|
||||
{
|
||||
this.Thread = this.Agent.GetNewThread();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the agent for a non-streamed responses.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to send.</param>
|
||||
/// <param name="options">The options for running the agent.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <remarks>
|
||||
/// This method is not intended to be called directly; instead, use <see cref="RunAsync"/>.
|
||||
/// This method exists to be overridden in derived classes in order to customize the invocation of the agent by <see cref="RunAsync"/>.
|
||||
/// </remarks>
|
||||
protected virtual Task<AgentRunResponse> InvokeCoreAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages, AgentRunOptions? options, CancellationToken cancellationToken) =>
|
||||
this.Agent.RunAsync([.. messages], this.Thread, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the agent for a streamed responses.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to send.</param>
|
||||
/// <param name="options">The options for running the agent.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
/// <remarks>
|
||||
/// This method is not intended to be called directly; instead, use <see cref="RunAsync"/>.
|
||||
/// This method exists to be overridden in derived classes in order to customize the invocation of the agent by <see cref="RunAsync"/>.
|
||||
/// </remarks>
|
||||
protected virtual IAsyncEnumerable<AgentRunResponseUpdate> InvokeStreamingCoreAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages, AgentRunOptions? options, CancellationToken cancellationToken) =>
|
||||
this.Agent.RunStreamingAsync(messages, this.Thread, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with input messages and respond with both streamed and regular messages.
|
||||
/// </summary>
|
||||
/// <param name="input">The list of chat messages to send.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task that returns the response <see cref="ChatMessage"/>.</returns>
|
||||
protected async ValueTask<ChatMessage> RunAsync(IEnumerable<ChatMessage> input, CancellationToken cancellationToken)
|
||||
{
|
||||
using CancellationTokenSource combined = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this.Context.CancellationToken);
|
||||
cancellationToken = combined.Token;
|
||||
|
||||
// Utilize streaming iff a streaming callback is provided; otherwise, use the non-streaming API.
|
||||
AgentRunResponse response;
|
||||
if (this.Context.StreamingResponseCallback is { } streamingCallback)
|
||||
{
|
||||
// For streaming, enumerate all the updates, invoking the callback for each, and storing them all.
|
||||
// Then convert them all into a single response instance.
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in this.InvokeStreamingCoreAsync([.. input], options: null, cancellationToken).WithCancellation(this.Context.CancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates.Add(update);
|
||||
await streamingCallback(update).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
response = updates.ToAgentRunResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-streaming, just invoke the non-streaming method and get back the response.
|
||||
response = await this.InvokeCoreAsync([.. input], options: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Regardless of whether we invoked streaming callbacks for individual updates, invoke the non-streaming callback with the final response instance.
|
||||
// This can be used as an indication of completeness if someone otherwise only cares about the streaming updates.
|
||||
if (this.Context.ResponseCallback is { } responseCallback)
|
||||
{
|
||||
await responseCallback.Invoke(response.Messages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response.Messages.LastOrDefault() ?? new();
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// Actor responsible for receiving final message and transforming it into the output type.
|
||||
/// </summary>
|
||||
private sealed class RequestActor : OrchestrationActor
|
||||
{
|
||||
private readonly Func<TInput, JsonSerializerOptions?, CancellationToken, ValueTask<IEnumerable<ChatMessage>>> _transform;
|
||||
private readonly Func<IEnumerable<ChatMessage>, ValueTask> _action;
|
||||
private readonly TaskCompletionSource<TOutput> _completionSource;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentOrchestration{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="transform">A function that transforms an input of type TInput into a source type TSource.</param>
|
||||
/// <param name="completionSource">Optional TaskCompletionSource to signal orchestration completion.</param>
|
||||
/// <param name="action">An asynchronous function that processes the resulting source.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public RequestActor(
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
Func<TInput, JsonSerializerOptions?, CancellationToken, ValueTask<IEnumerable<ChatMessage>>> transform,
|
||||
TaskCompletionSource<TOutput> completionSource,
|
||||
Func<IEnumerable<ChatMessage>, ValueTask> action,
|
||||
ILogger<RequestActor>? logger = null)
|
||||
: base(id, runtime, context, $"{id.Type}_Actor", logger)
|
||||
{
|
||||
this._transform = transform;
|
||||
this._action = action;
|
||||
this._completionSource = completionSource;
|
||||
|
||||
this.RegisterMessageHandler<TInput>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the incoming message by transforming the input and executing the corresponding action asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="item">The input message of type TInput.</param>
|
||||
/// <param name="messageContext">The context of the message, providing additional details.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
private async ValueTask HandleAsync(TInput item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id);
|
||||
try
|
||||
{
|
||||
IEnumerable<ChatMessage> input = await this._transform.Invoke(item, messageContext.SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
var task = this._action.Invoke(input);
|
||||
this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id);
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Log exception details and allow orchestration to fail
|
||||
this.Logger.LogOrchestrationRequestFailure(this.Context.Orchestration, this.Id, exception);
|
||||
this._completionSource.SetException(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// Actor responsible for receiving the resultant message, transforming it, and handling further orchestration.
|
||||
/// </summary>
|
||||
private sealed class ResultActor<TResult> : OrchestrationActor
|
||||
{
|
||||
private readonly TaskCompletionSource<TOutput> _completionSource;
|
||||
private readonly Func<TResult, IList<ChatMessage>> _transformResult;
|
||||
private readonly Func<IList<ChatMessage>, JsonSerializerOptions?, CancellationToken, ValueTask<TOutput>> _transform;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentOrchestration{TInput, TOutput}.ResultActor{TResult}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="transformResult">A delegate that transforms a TResult instance into a ChatMessage.</param>
|
||||
/// <param name="transformOutput">A delegate that transforms a ChatMessage into a TOutput instance.</param>
|
||||
/// <param name="completionSource">Optional TaskCompletionSource to signal orchestration completion.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public ResultActor(
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
Func<TResult, IList<ChatMessage>> transformResult,
|
||||
Func<IList<ChatMessage>, JsonSerializerOptions?, CancellationToken, ValueTask<TOutput>> transformOutput,
|
||||
TaskCompletionSource<TOutput> completionSource,
|
||||
ILogger<ResultActor<TResult>>? logger = null)
|
||||
: base(id, runtime, context, $"{id.Type}_Actor", logger)
|
||||
{
|
||||
this._completionSource = completionSource;
|
||||
this._transformResult = transformResult;
|
||||
this._transform = transformOutput;
|
||||
|
||||
this.RegisterMessageHandler<TResult>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the received TResult message by transforming it into a TOutput message.
|
||||
/// If a CompletionTarget is defined, it sends the transformed message to the corresponding agent.
|
||||
/// Additionally, it signals completion via the provided TaskCompletionSource if available.
|
||||
/// </summary>
|
||||
/// <param name="item">The result item to process.</param>
|
||||
/// <param name="messageContext">The context associated with the message.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask representing asynchronous operation.</returns>
|
||||
private async ValueTask HandleAsync(TResult item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogOrchestrationResultInvoke(this.Context.Orchestration, this.Id);
|
||||
|
||||
try
|
||||
{
|
||||
if (!this._completionSource.Task.IsCompleted)
|
||||
{
|
||||
IList<ChatMessage> result = this._transformResult.Invoke(item);
|
||||
TOutput output = await this._transform.Invoke(result, messageContext.SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
this._completionSource.TrySetResult(output);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Log exception details and fail orchestration as per design.
|
||||
this.Logger.LogOrchestrationResultFailure(this.Context.Orchestration, this.Id, exception);
|
||||
this._completionSource.SetException(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime.InProcess;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for multi-agent agent orchestration patterns.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The type of the input to the orchestration.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of the result output by the orchestration.</typeparam>
|
||||
public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentOrchestration{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="members">Specifies the member agents or orchestrations participating in this orchestration.</param>
|
||||
protected AgentOrchestration(params AIAgent[] members)
|
||||
{
|
||||
_ = Throw.IfNull(members);
|
||||
|
||||
// Capture orchestration root name without generic parameters for use in
|
||||
// agent type and topic formatting as well as logging.
|
||||
string name = this.GetType().Name;
|
||||
int pos = name.IndexOf('`');
|
||||
if (pos > 0)
|
||||
{
|
||||
name = name.Substring(0, pos);
|
||||
}
|
||||
this.OrchestrationLabel = name;
|
||||
|
||||
this.Members = members;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the orchestration.
|
||||
/// </summary>
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the orchestration.
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated logger.
|
||||
/// </summary>
|
||||
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the orchestration input into a source input suitable for processing.
|
||||
/// </summary>
|
||||
public Func<TInput, JsonSerializerOptions?, CancellationToken, ValueTask<IEnumerable<ChatMessage>>>? InputTransform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the processed result into the final output form.
|
||||
/// </summary>
|
||||
public Func<IList<ChatMessage>, JsonSerializerOptions?, CancellationToken, ValueTask<TOutput>>? ResultTransform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback that is invoked for every agent response.
|
||||
/// </summary>
|
||||
public Func<IEnumerable<ChatMessage>, ValueTask>? ResponseCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback that is invoked for every agent update.
|
||||
/// </summary>
|
||||
public Func<AgentRunResponseUpdate, ValueTask>? StreamingResponseCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of member targets involved in the orchestration.
|
||||
/// </summary>
|
||||
protected IReadOnlyList<AIAgent> Members { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Orchestration identifier without generic parameters for use in
|
||||
/// agent type and topic formatting as well as logging.
|
||||
/// </summary>
|
||||
protected string OrchestrationLabel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initiates processing of the orchestration.
|
||||
/// </summary>
|
||||
/// <param name="input">The input message.</param>
|
||||
/// <param name="runtime">The runtime associated with the orchestration.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
public async ValueTask<OrchestrationResult<TOutput>> InvokeAsync(
|
||||
TInput input,
|
||||
IAgentRuntime? runtime = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(input, nameof(input));
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
TopicId topic = new($"{this.OrchestrationLabel}_{Guid.NewGuid():N}");
|
||||
|
||||
CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cancellationToken = orchestrationCancelSource.Token;
|
||||
|
||||
OrchestrationContext context =
|
||||
new(this.OrchestrationLabel,
|
||||
topic,
|
||||
this.ResponseCallback,
|
||||
this.StreamingResponseCallback,
|
||||
this.LoggerFactory,
|
||||
cancellationToken);
|
||||
|
||||
ILogger logger = this.LoggerFactory.CreateLogger(this.GetType());
|
||||
|
||||
TaskCompletionSource<TOutput> completion = new();
|
||||
|
||||
InProcessRuntime? temporaryRuntime = null;
|
||||
runtime ??= temporaryRuntime = InProcessRuntime.StartNew();
|
||||
|
||||
ActorType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false);
|
||||
|
||||
logger.LogOrchestrationInvoke(this.OrchestrationLabel, topic);
|
||||
|
||||
Task task = runtime.PublishMessageAsync(input, orchestrationType, cancellationToken).AsTask();
|
||||
|
||||
logger.LogOrchestrationYield(this.OrchestrationLabel, topic);
|
||||
|
||||
return new OrchestrationResult<TOutput>(context, completion, orchestrationCancelSource, logger, temporaryRuntime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates processing according to the orchestration pattern.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The runtime associated with the orchestration.</param>
|
||||
/// <param name="topic">The unique identifier for the orchestration session.</param>
|
||||
/// <param name="input">The input to be transformed and processed.</param>
|
||||
/// <param name="entryAgent">The initial agent type used for starting the orchestration.</param>
|
||||
protected abstract ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent);
|
||||
|
||||
/// <summary>
|
||||
/// Orchestration specific registration, including members and returns an optional entry agent.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The runtime targeted for registration.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="registrar">A registration context.</param>
|
||||
/// <param name="logger">The logger to use during registration</param>
|
||||
/// <returns>The entry AgentType for the orchestration, if any.</returns>
|
||||
protected abstract ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger);
|
||||
|
||||
/// <summary>
|
||||
/// Formats and returns a unique AgentType based on the provided topic and suffix.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic identifier used in formatting the agent type.</param>
|
||||
/// <param name="suffix">A suffix to differentiate the agent type.</param>
|
||||
/// <returns>A formatted AgentType object.</returns>
|
||||
protected ActorType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{suffix}");
|
||||
|
||||
/// <summary>
|
||||
/// Registers the orchestration's root and boot agents, setting up completion and target routing.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The runtime targeted for registration.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="completion">A TaskCompletionSource for the orchestration.</param>
|
||||
/// <param name="handoff">The actor type used for handoff. Only defined for nested orchestrations.</param>
|
||||
/// <returns>The AgentType representing the orchestration entry point.</returns>
|
||||
private async ValueTask<ActorType> RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource<TOutput> completion, ActorType? handoff)
|
||||
{
|
||||
// Create a logger for the orchestration registration.
|
||||
ILogger logger = context.LoggerFactory.CreateLogger(this.GetType());
|
||||
logger.LogOrchestrationRegistrationStart(context.Orchestration, context.Topic);
|
||||
|
||||
// Register orchestration
|
||||
RegistrationContext registrar = new(this.FormatAgentType(context.Topic, "Root"), runtime, context, completion, this.ResultTransform ?? DefaultTransforms.ToOutput<TOutput>);
|
||||
ActorType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false);
|
||||
|
||||
// Register actor for orchestration entry-point
|
||||
ActorType orchestrationEntry =
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
this.FormatAgentType(context.Topic, "Boot"),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
RequestActor actor =
|
||||
new(agentId,
|
||||
runtime,
|
||||
context,
|
||||
this.InputTransform ?? DefaultTransforms.FromInput<TInput>,
|
||||
completion,
|
||||
input => this.StartAsync(runtime, context.Topic, input, entryAgent),
|
||||
context.LoggerFactory.CreateLogger<RequestActor>());
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic);
|
||||
|
||||
return orchestrationEntry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A context used during registration (<see cref="RegisterAsync"/>).
|
||||
/// </summary>
|
||||
public sealed class RegistrationContext(
|
||||
ActorType agentType,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
TaskCompletionSource<TOutput> completion,
|
||||
Func<IList<ChatMessage>, JsonSerializerOptions?, CancellationToken, ValueTask<TOutput>> outputTransform)
|
||||
{
|
||||
/// <summary>
|
||||
/// Register the final result type.
|
||||
/// </summary>
|
||||
public async ValueTask<ActorType> RegisterResultTypeAsync<TResult>(Func<TResult, IList<ChatMessage>> resultTransform)
|
||||
{
|
||||
// Register actor for final result
|
||||
ActorType registeredType =
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
agentType,
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
ResultActor<TResult> actor =
|
||||
new(agentId,
|
||||
runtime,
|
||||
context,
|
||||
resultTransform,
|
||||
outputTransform,
|
||||
completion,
|
||||
context.LoggerFactory.CreateLogger<ResultActor<TResult>>());
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return registeredType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="AgentOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class AgentOrchestrationLogMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs the start of the registration phase for an orchestration.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "REGISTER {Orchestration} Start: {Topic}")]
|
||||
public static partial void LogOrchestrationRegistrationStart(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Logs pattern actor registration.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "REGISTER ACTOR {Orchestration} {label}: {AgentType}")]
|
||||
public static partial void LogRegisterActor(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorType agentType,
|
||||
string label);
|
||||
|
||||
/// <summary>
|
||||
/// Logs agent actor registration.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "REGISTER ACTOR {Orchestration} {label} #{Count}: {AgentType}")]
|
||||
public static partial void LogRegisterActor(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorType agentType,
|
||||
string label,
|
||||
int count);
|
||||
|
||||
/// <summary>
|
||||
/// Logs the end of the registration phase for an orchestration.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "REGISTER {Orchestration} Complete: {Topic}")]
|
||||
public static partial void LogOrchestrationRegistrationDone(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Logs an orchestration invocation
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "INVOKE {Orchestration}: {Topic}")]
|
||||
public static partial void LogOrchestrationInvoke(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that the orchestration has started successfully and
|
||||
/// yielded control back to the caller.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "YIELD {Orchestration}: {Topic}")]
|
||||
public static partial void LogOrchestrationYield(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Logs the start an orchestration (top/outer).
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "START {Orchestration}: {AgentId}")]
|
||||
public static partial void LogOrchestrationStart(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorId agentId);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration request actor is active
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "INIT {Orchestration}: {AgentId}")]
|
||||
public static partial void LogOrchestrationRequestInvoke(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorId agentId);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration request actor experienced an unexpected failure.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "FAILURE {Orchestration}: {AgentId}")]
|
||||
public static partial void LogOrchestrationRequestFailure(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorId agentId,
|
||||
Exception exception);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration result actor is active
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "EXIT {Orchestration}: {AgentId}")]
|
||||
public static partial void LogOrchestrationResultInvoke(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorId agentId);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration result actor experienced an unexpected failure.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "FAILURE {Orchestration}: {AgentId}")]
|
||||
public static partial void LogOrchestrationResultFailure(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
ActorId agentId,
|
||||
Exception exception);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IAgentRuntime"/>.
|
||||
/// </summary>
|
||||
internal static class AgentRuntimeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a message to the specified agent.
|
||||
/// </summary>
|
||||
public static ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, ActorType agentType, CancellationToken cancellationToken = default) =>
|
||||
runtime.PublishMessageAsync(message, new TopicId(agentType.Name), sender: null, messageId: null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Registers an agent factory for the specified agent type and associates it with the runtime.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The runtime targeted for registration.</param>
|
||||
/// <param name="agentType">The type of agent to register.</param>
|
||||
/// <param name="factoryFunc">The factory function for creating the agent.</param>
|
||||
/// <returns>The registered agent type.</returns>
|
||||
public static async ValueTask<ActorType> RegisterOrchestrationAgentAsync(this IAgentRuntime runtime, ActorType agentType, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc)
|
||||
{
|
||||
ActorType registeredType = await runtime.RegisterActorFactoryAsync(agentType, factoryFunc).ConfigureAwait(false);
|
||||
|
||||
// Subscribe agent to its own unique topic
|
||||
await runtime.SubscribeAsync(new(registeredType.Name)).ConfigureAwait(false);
|
||||
|
||||
return registeredType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes the specified agent type to its own dedicated topic.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The runtime for managing the subscription.</param>
|
||||
/// <param name="agentType">The agent type to subscribe.</param>
|
||||
public static Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType) =>
|
||||
runtime.AddSubscriptionAsync(new TypeSubscription(agentType.Name, agentType)).AsTask();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes the specified agent type to the provided topics.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The runtime for managing the subscription.</param>
|
||||
/// <param name="agentType">The agent type to subscribe.</param>
|
||||
/// <param name="topics">A variable list of topics for subscription.</param>
|
||||
public static async Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType, params TopicId[] topics)
|
||||
{
|
||||
for (int index = 0; index < topics.Length; ++index)
|
||||
{
|
||||
await runtime.AddSubscriptionAsync(new TypeSubscription(topics[index].Type, agentType)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AgentActor"/> used with the <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
internal sealed class ConcurrentActor : AgentActor
|
||||
{
|
||||
private readonly ActorType _handoffActor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="agent">An <see cref="AIAgent"/>.</param>
|
||||
/// <param name="resultActor">Identifies the actor collecting results.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public ConcurrentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, AIAgent agent, ActorType resultActor, ILogger<ConcurrentActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
this._handoffActor = resultActor;
|
||||
|
||||
this.RegisterMessageHandler<ConcurrentMessages.Request>(this.HandleAsync);
|
||||
}
|
||||
|
||||
private async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogConcurrentAgentInvoke(this.Id);
|
||||
|
||||
ChatMessage response = await this.RunAsync(item.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.Logger.LogConcurrentAgentResult(this.Id, response.Text);
|
||||
|
||||
await this.PublishMessageAsync(new ConcurrentMessages.Result(response), this._handoffActor, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Common messages used by the <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
internal static class ConcurrentMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// The input task for a <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
public sealed record Request(IList<ChatMessage> Messages);
|
||||
|
||||
/// <summary>
|
||||
/// A result from a <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
public sealed record Result(ChatMessage Message);
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that broadcasts the input message to each agent.
|
||||
/// </summary>
|
||||
public sealed class ConcurrentOrchestration : ConcurrentOrchestration<string, string[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentOrchestration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="members">The agents to be orchestrated.</param>
|
||||
public ConcurrentOrchestration(params AIAgent[] members)
|
||||
: base(members)
|
||||
{
|
||||
this.ResultTransform =
|
||||
(response, _, cancellationToken) =>
|
||||
{
|
||||
string[] result = [.. response.Select(r => r.Text)];
|
||||
return new ValueTask<string[]>(result);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that broadcasts the input message to each agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>TOutput</c> must be an array type for <see cref="ConcurrentOrchestration"/>.
|
||||
/// </remarks>
|
||||
public class ConcurrentOrchestration<TInput, TOutput>
|
||||
: AgentOrchestration<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentOrchestration{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agents">The agents participating in the orchestration.</param>
|
||||
public ConcurrentOrchestration(params AIAgent[] agents)
|
||||
: base(agents)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
return runtime.PublishMessageAsync(new ConcurrentMessages.Request([.. input]), topic);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
ActorType outputType = await registrar.RegisterResultTypeAsync<ConcurrentMessages.Result[]>(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false);
|
||||
|
||||
// Register result actor
|
||||
ActorType resultType = this.FormatAgentType(context.Topic, "Results");
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
resultType,
|
||||
async (agentId, runtime) =>
|
||||
{
|
||||
ConcurrentResultActor actor = new(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger<ConcurrentResultActor>());
|
||||
return actor;
|
||||
}).ConfigureAwait(false);
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, resultType, "RESULTS");
|
||||
|
||||
// Register member actors - All agents respond to the same message.
|
||||
int agentCount = 0;
|
||||
foreach (AIAgent agent in this.Members)
|
||||
{
|
||||
++agentCount;
|
||||
|
||||
ActorType agentType =
|
||||
await runtime.RegisterActorFactoryAsync(
|
||||
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
ConcurrentActor actor = new(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger<ConcurrentActor>());
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
|
||||
|
||||
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class ConcurrentOrchestrationLogMessages
|
||||
{
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "REQUEST Concurrent agent [{AgentId}]")]
|
||||
public static partial void LogConcurrentAgentInvoke(
|
||||
this ILogger logger,
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Concurrent agent [{AgentId}]: {Message}")]
|
||||
public static partial void LogConcurrentAgentResult(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
|
||||
/// <summary>
|
||||
/// Logs result capture.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Information,
|
||||
Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")]
|
||||
public static partial void LogConcurrentResultCapture(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
int resultCount,
|
||||
int expectedCount);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Actor for capturing each <see cref="ConcurrentMessages.Result"/> message.
|
||||
/// </summary>
|
||||
internal sealed class ConcurrentResultActor : OrchestrationActor
|
||||
{
|
||||
private readonly ConcurrentQueue<ConcurrentMessages.Result> _results;
|
||||
private readonly ActorType _orchestrationType;
|
||||
private readonly int _expectedCount;
|
||||
private int _resultCount;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentResultActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="orchestrationType">Identifies the orchestration agent.</param>
|
||||
/// <param name="expectedCount">The expected number of messages to be received.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public ConcurrentResultActor(
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
ActorType orchestrationType,
|
||||
int expectedCount,
|
||||
ILogger logger)
|
||||
: base(id, runtime, context, "Captures the results of the ConcurrentOrchestration", logger)
|
||||
{
|
||||
this._orchestrationType = orchestrationType;
|
||||
this._expectedCount = expectedCount;
|
||||
this._results = [];
|
||||
|
||||
this.RegisterMessageHandler<ConcurrentMessages.Result>(this.HandleAsync);
|
||||
}
|
||||
|
||||
private async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogConcurrentResultCapture(this.Id, this._resultCount + 1, this._expectedCount);
|
||||
|
||||
this._results.Enqueue(item);
|
||||
|
||||
if (Interlocked.Increment(ref this._resultCount) == this._expectedCount)
|
||||
{
|
||||
await this.PublishMessageAsync(this._results.ToArray(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>Provides an orchestrating agent that broadcasts the input message to each agent and then aggregates the result into a single response.</summary>
|
||||
public partial class ConcurrentOrchestration : OrchestratingAgent
|
||||
{
|
||||
private Func<AgentRunResponse[], CancellationToken, Task<AgentRunResponse>>? _aggregationFunc;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ConcurrentOrchestration"/> class.</summary>
|
||||
/// <param name="subagents">The agents participating in the orchestration.</param>
|
||||
public ConcurrentOrchestration(params AIAgent[] subagents) : base(subagents)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the function to use to aggregate an <see cref="AgentRunResponse"/> from each participating agent into a single <see cref="AgentRunResponse"/>.</summary>
|
||||
/// <remarks>The default function takes the last message from each response and puts those messages into a new response instance.</remarks>
|
||||
public Func<AgentRunResponse[], CancellationToken, Task<AgentRunResponse>> AggregationFunc
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this._aggregationFunc is { } f)
|
||||
{
|
||||
return f;
|
||||
}
|
||||
|
||||
return static async (responses, cancellationToken) =>
|
||||
new AgentRunResponse([.. responses.Where(r => r.Messages.Count > 0).Select(r =>
|
||||
{
|
||||
var messages = r.Messages;
|
||||
return messages.Count > 0 ? messages[messages.Count - 1] : new();
|
||||
})]);
|
||||
}
|
||||
set => this._aggregationFunc = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
this.ResumeAsync(messages, new AgentRunResponse?[this.Agents.Count], context, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.ConcurrentState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.Messages, state.Completed, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
private async Task<AgentRunResponse> ResumeAsync(
|
||||
IReadOnlyCollection<ChatMessage> input, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
List<Task> tasks = new(this.Agents.Count);
|
||||
for (int i = 0; i < this.Agents.Count; i++)
|
||||
{
|
||||
if (completed[i] is null)
|
||||
{
|
||||
int localI = i;
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
AIAgent agent = this.Agents[localI];
|
||||
this.LogOrchestrationSubagentRunning(context, agent);
|
||||
|
||||
completed[localI] = await RunAsync(agent, context, input, options: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.LogOrchestrationSubagentCompleted(context, agent);
|
||||
await this.CheckpointAsync(input, completed, context, cancellationToken).ConfigureAwait(false);
|
||||
}, cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: What do we want to do if one of the agents fails? As written, this waits for all to complete,
|
||||
// and then throws. And when resumption happens, it'll end up retrying failed agents. If we don't want that,
|
||||
// which we probably don't, we should checkpoint that failures happened, too.
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
Debug.Assert(Array.TrueForAll(completed, r => r is not null), "Expected all agents to have produced a result");
|
||||
return await this.AggregationFunc(completed!, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task CheckpointAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
context.Runtime is not null ? base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(messages, completed), OrchestrationJsonContext.Default.ConcurrentState), context, cancellationToken) :
|
||||
Task.CompletedTask;
|
||||
|
||||
internal sealed record ConcurrentState(IReadOnlyCollection<ChatMessage> Messages, AgentRunResponse?[] Completed);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
internal static class DefaultTransforms
|
||||
{
|
||||
public static ValueTask<IEnumerable<ChatMessage>> FromInput<TInput>(TInput input, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
return new(input switch
|
||||
{
|
||||
IEnumerable<ChatMessage> messages => messages,
|
||||
ChatMessage message => [message],
|
||||
string text => [new ChatMessage(ChatRole.User, text)],
|
||||
_ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input, serializerOptions.GetTypeInfo(typeof(TInput))))]
|
||||
});
|
||||
}
|
||||
|
||||
public static ValueTask<TOutput> ToOutput<TOutput>(IList<ChatMessage> result, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(result);
|
||||
|
||||
serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
bool isSingleResult = result.Count == 1;
|
||||
|
||||
if (result is TOutput)
|
||||
{
|
||||
return new((TOutput)(object)result);
|
||||
}
|
||||
|
||||
if (isSingleResult)
|
||||
{
|
||||
if (typeof(ChatMessage).IsAssignableFrom(typeof(TOutput)))
|
||||
{
|
||||
return new((TOutput)(object)result[0]);
|
||||
}
|
||||
|
||||
if (typeof(string) == typeof(TOutput))
|
||||
{
|
||||
return new((TOutput)(object)(result[0].Text ?? string.Empty));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new((TOutput)JsonSerializer.Deserialize(result[0].Text, serializerOptions.GetTypeInfo(typeof(TOutput)))!);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}.");
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AgentActor"/> used with the <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
internal sealed class GroupChatAgentActor : AgentActor
|
||||
{
|
||||
private readonly List<ChatMessage> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupChatAgentActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="agent">An <see cref="AIAgent"/>.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public GroupChatAgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, AIAgent agent, ILogger<GroupChatAgentActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
this._cache = [];
|
||||
|
||||
this.RegisterMessageHandler<GroupChatMessages.Group>((item, ctx) => this._cache.AddRange(item.Messages));
|
||||
this.RegisterMessageHandler<GroupChatMessages.Reset>((item, ctx) => this.ResetThread());
|
||||
this.RegisterMessageHandler<GroupChatMessages.Speak>(this.HandleAsync);
|
||||
}
|
||||
|
||||
private async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogChatAgentInvoke(this.Id);
|
||||
|
||||
ChatMessage response = await this.RunAsync(this._cache, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.Logger.LogChatAgentResult(this.Id, response.Text);
|
||||
|
||||
this._cache.Clear();
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Group([response]), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="OrchestrationActor"/> used to manage a <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
internal sealed class GroupChatManagerActor : OrchestrationActor
|
||||
{
|
||||
/// <summary>
|
||||
/// A common description for the manager.
|
||||
/// </summary>
|
||||
public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task.";
|
||||
|
||||
private readonly ActorType _orchestrationType;
|
||||
private readonly GroupChatManager _manager;
|
||||
private readonly List<ChatMessage> _chat;
|
||||
private readonly GroupChatTeam _team;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupChatManagerActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="manager">The manages the flow of the group-chat.</param>
|
||||
/// <param name="team">The team of agents being orchestrated</param>
|
||||
/// <param name="orchestrationType">Identifies the orchestration agent.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public GroupChatManagerActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, ActorType orchestrationType, ILogger? logger = null)
|
||||
: base(id, runtime, context, DefaultDescription, logger)
|
||||
{
|
||||
this._chat = [];
|
||||
this._manager = manager;
|
||||
this._orchestrationType = orchestrationType;
|
||||
this._team = team;
|
||||
|
||||
this.RegisterMessageHandler<GroupChatMessages.InputTask>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<GroupChatMessages.Group>(this.HandleAsync);
|
||||
}
|
||||
|
||||
private async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogChatManagerInit(this.Id);
|
||||
|
||||
this._chat.AddRange(item.Messages);
|
||||
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Group(item.Messages), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogChatManagerInvoke(this.Id);
|
||||
|
||||
this._chat.AddRange(item.Messages);
|
||||
|
||||
await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask ManageAsync(MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._manager.InteractiveCallback != null)
|
||||
{
|
||||
GroupChatManagerResult<bool> inputResult = await this._manager.ShouldRequestUserInput(this._chat, cancellationToken).ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerInput(this.Id, inputResult.Value, inputResult.Reason);
|
||||
if (inputResult.Value)
|
||||
{
|
||||
ChatMessage input = await this._manager.InteractiveCallback.Invoke().ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerUserInput(this.Id, input.Text);
|
||||
this._chat.Add(input);
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Group([input]), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
GroupChatManagerResult<bool> terminateResult = await this._manager.ShouldTerminate(this._chat, cancellationToken).ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerTerminate(this.Id, terminateResult.Value, terminateResult.Reason);
|
||||
if (terminateResult.Value)
|
||||
{
|
||||
GroupChatManagerResult<string> filterResult = await this._manager.FilterResults(this._chat, cancellationToken).ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerResult(this.Id, filterResult.Value, filterResult.Reason);
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Result(new(ChatRole.Assistant, filterResult.Value)), this._orchestrationType, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
GroupChatManagerResult<string> selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, cancellationToken).ConfigureAwait(false);
|
||||
ActorType selectionType = new(this._team[selectionResult.Value].Type);
|
||||
this.Logger.LogChatManagerSelect(this.Id, selectionType);
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Common messages used for agent chat patterns.
|
||||
/// </summary>
|
||||
internal static class GroupChatMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Broadcast a message to all <see cref="GroupChatAgentActor"/>.
|
||||
/// </summary>
|
||||
public sealed record Group(IEnumerable<ChatMessage> Messages);
|
||||
|
||||
/// <summary>
|
||||
/// Reset/clear the conversation history for all <see cref="GroupChatAgentActor"/>.
|
||||
/// </summary>
|
||||
public sealed class Reset;
|
||||
|
||||
/// <summary>
|
||||
/// The final result.
|
||||
/// </summary>
|
||||
public sealed record Result(ChatMessage Message);
|
||||
|
||||
/// <summary>
|
||||
/// Signal a <see cref="GroupChatAgentActor"/> to respond.
|
||||
/// </summary>
|
||||
public sealed class Speak;
|
||||
|
||||
/// <summary>
|
||||
/// The input task.
|
||||
/// </summary>
|
||||
public sealed record InputTask(IEnumerable<ChatMessage> Messages)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an input task that does not require any action.
|
||||
/// </summary>
|
||||
public static InputTask None { get; } = new([]);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that broadcasts the input message to each agent.
|
||||
/// </summary>
|
||||
public sealed class GroupChatOrchestration : GroupChatOrchestration<string, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupChatOrchestration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The manages the flow of the group-chat.</param>
|
||||
/// <param name="members">The agents to be orchestrated.</param>
|
||||
public GroupChatOrchestration(GroupChatManager manager, params AIAgent[] members)
|
||||
: base(manager, members)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,116 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that coordinates a group-chat.
|
||||
/// An orchestration that coordinates a group-chat using a manager to control conversation flow.
|
||||
/// </summary>
|
||||
public class GroupChatOrchestration<TInput, TOutput> :
|
||||
AgentOrchestration<TInput, TOutput>
|
||||
public sealed partial class GroupChatOrchestration : OrchestratingAgent
|
||||
{
|
||||
internal const string DefaultAgentDescription = "A helpful agent.";
|
||||
|
||||
private readonly GroupChatManager _manager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupChatOrchestration{TInput, TOutput}"/> class.
|
||||
/// Initializes a new instance of the <see cref="GroupChatOrchestration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The manages the flow of the group-chat.</param>
|
||||
/// <param name="manager">The manager that controls the flow of the group-chat.</param>
|
||||
/// <param name="agents">The agents participating in the orchestration.</param>
|
||||
public GroupChatOrchestration(GroupChatManager manager, params AIAgent[] agents)
|
||||
: base(agents)
|
||||
public GroupChatOrchestration(GroupChatManager manager, params AIAgent[] agents) : base(agents)
|
||||
{
|
||||
Throw.IfNull(manager, nameof(manager));
|
||||
this._manager = Throw.IfNull(manager);
|
||||
}
|
||||
|
||||
this._manager = manager;
|
||||
/// <summary>Gets or sets a callback invoked when user input is requested.</summary>
|
||||
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
List<ChatMessage> allMessages = [.. messages];
|
||||
int originalMessageCount = allMessages.Count;
|
||||
return this.ResumeAsync(allMessages, originalMessageCount, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!entryAgent.HasValue)
|
||||
{
|
||||
Throw.ArgumentException(nameof(entryAgent), "Entry agent is not defined.");
|
||||
}
|
||||
|
||||
return runtime.PublishMessageAsync(new GroupChatMessages.InputTask(input), entryAgent.Value);
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.GroupChatState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.AllMessages, state.OriginalMessageCount, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
private async Task<AgentRunResponse> ResumeAsync(
|
||||
List<ChatMessage> allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ActorType outputType = await registrar.RegisterResultTypeAsync<GroupChatMessages.Result>(response => [response.Message]).ConfigureAwait(false);
|
||||
|
||||
int agentCount = 0;
|
||||
GroupChatTeam team = [];
|
||||
foreach (AIAgent agent in this.Members)
|
||||
foreach (AIAgent agent in this.Agents)
|
||||
{
|
||||
++agentCount;
|
||||
ActorType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false);
|
||||
string name = agent.Name ?? agent.Id ?? agentType.Name;
|
||||
string? description = agent.Description;
|
||||
|
||||
team[name] = (agentType.Name, description ?? DefaultAgentDescription);
|
||||
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
|
||||
|
||||
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
|
||||
team[agent.DisplayName] = (agent.GetType().Name, agent.Description ?? agent.Name ?? "A helpful agent.");
|
||||
}
|
||||
|
||||
ActorType managerType =
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
this.FormatAgentType(context.Topic, "Manager"),
|
||||
(agentId, runtime) =>
|
||||
var interactiveCallback = this.InteractiveCallback ?? this._manager.InteractiveCallback;
|
||||
while (true)
|
||||
{
|
||||
// First, check if we should request user input.
|
||||
if (interactiveCallback is not null)
|
||||
{
|
||||
var userInputResult = await this._manager.ShouldRequestUserInput(allMessages, cancellationToken).ConfigureAwait(false);
|
||||
if (userInputResult.Value)
|
||||
{
|
||||
GroupChatManagerActor actor = new(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger<GroupChatManagerActor>());
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, managerType, "MANAGER");
|
||||
if (interactiveCallback is not null)
|
||||
{
|
||||
ChatMessage userMessage = await interactiveCallback().ConfigureAwait(false);
|
||||
allMessages.Add(userMessage);
|
||||
|
||||
await runtime.SubscribeAsync(managerType, context.Topic).ConfigureAwait(false);
|
||||
// Broadcast the user input
|
||||
if (this.ResponseCallback is not null)
|
||||
{
|
||||
await this.ResponseCallback([userMessage]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return managerType;
|
||||
await this.CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ValueTask<ActorType> RegisterAgentAsync(AIAgent agent, int agentCount) =>
|
||||
runtime.RegisterOrchestrationAgentAsync(
|
||||
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
GroupChatAgentActor actor = new(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger<GroupChatAgentActor>());
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
});
|
||||
// Check if we should terminate the conversation
|
||||
var terminateResult = await this._manager.ShouldTerminate(allMessages, cancellationToken).ConfigureAwait(false);
|
||||
if (terminateResult.Value)
|
||||
{
|
||||
// Filter and return final results
|
||||
var filterResult = await this._manager.FilterResults(allMessages, cancellationToken).ConfigureAwait(false);
|
||||
return new AgentRunResponse([new ChatMessage(ChatRole.Assistant, filterResult.Value) { AuthorName = this.DisplayName }]);
|
||||
}
|
||||
|
||||
// Select the next agent to speak
|
||||
var nextAgentResult = await this._manager.SelectNextAgent(allMessages, team, cancellationToken).ConfigureAwait(false);
|
||||
AIAgent nextAgent = this.FindAgentByName(nextAgentResult.Value) ??
|
||||
throw new InvalidOperationException($"AIAgent '{nextAgentResult.Value}' not found in the orchestration.");
|
||||
|
||||
// Run the selected agent with all messages.
|
||||
this.LogOrchestrationSubagentRunning(context, nextAgent);
|
||||
AgentRunResponse response = await RunAsync(nextAgent, context, allMessages, options: null, cancellationToken).ConfigureAwait(false);
|
||||
allMessages.AddRange(response.Messages); // Add the agent's response to the conversation.
|
||||
this.LogOrchestrationSubagentCompleted(context, nextAgent);
|
||||
|
||||
await this.CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private AIAgent? FindAgentByName(string name) => this.Agents.FirstOrDefault(a => a.DisplayName == name);
|
||||
|
||||
private Task CheckpointAsync(List<ChatMessage> allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
context.Runtime is not null ? base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(allMessages, originalMessageCount), OrchestrationJsonContext.Default.GroupChatState), context, cancellationToken) :
|
||||
Task.CompletedTask;
|
||||
|
||||
internal sealed record GroupChatState(List<ChatMessage> AllMessages, int OriginalMessageCount);
|
||||
}
|
||||
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class GroupChatOrchestrationLogMessages
|
||||
{
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "CHAT AGENT invoked [{AgentId}]")]
|
||||
public static partial void LogChatAgentInvoke(
|
||||
this ILogger logger,
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "CHAT AGENT result [{AgentId}]: {Message}")]
|
||||
public static partial void LogChatAgentResult(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER initialized [{AgentId}]")]
|
||||
public static partial void LogChatManagerInit(
|
||||
this ILogger logger,
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER invoked [{AgentId}]")]
|
||||
public static partial void LogChatManagerInvoke(
|
||||
this ILogger logger,
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")]
|
||||
public static partial void LogChatManagerTerminate(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
bool result,
|
||||
string reason);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")]
|
||||
public static partial void LogChatManagerSelect(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
ActorType nextAgent);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")]
|
||||
public static partial void LogChatManagerResult(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string result,
|
||||
string reason);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")]
|
||||
public static partial void LogChatManagerInput(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
bool result,
|
||||
string reason);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "CHAT AGENT user-input [{AgentId}]: {Message}")]
|
||||
public static partial void LogChatManagerUserInput(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An actor used with the <see cref="HandoffOrchestration{TInput,TOutput}"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class HandoffActor : AgentActor
|
||||
{
|
||||
private readonly ChatClientAgent _chatAgent;
|
||||
private readonly HandoffLookup _handoffs;
|
||||
private readonly ActorType _resultHandoff;
|
||||
private readonly List<ChatMessage> _cache;
|
||||
private readonly ChatOptions _options;
|
||||
|
||||
private string? _handoffAgent;
|
||||
private string? _taskSummary;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandoffActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="agent">An <see cref="AIAgent"/>.</param>>
|
||||
/// <param name="handoffs">The handoffs available to this agent</param>
|
||||
/// <param name="resultHandoff">The handoff agent for capturing the result.</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public HandoffActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, ActorType resultHandoff, ILogger<HandoffActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
Throw.IfNull(handoffs);
|
||||
Throw.IfNull(resultHandoff);
|
||||
|
||||
if (handoffs.ContainsKey(agent.Name ?? agent.Id))
|
||||
{
|
||||
Throw.ArgumentException(nameof(handoffs), $"The agent {agent.Name ?? agent.Id} cannot have a handoff to itself.");
|
||||
}
|
||||
|
||||
this._cache = [];
|
||||
this._chatAgent = agent;
|
||||
this._handoffs = handoffs;
|
||||
this._resultHandoff = resultHandoff;
|
||||
this._options = new() { Tools = this.CreateHandoffFunctions() };
|
||||
|
||||
this.RegisterMessageHandler<HandoffMessages.InputTask>(this.Handle);
|
||||
this.RegisterMessageHandler<HandoffMessages.Request>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<HandoffMessages.Response>(this.Handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<AgentRunResponse> InvokeCoreAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages, AgentRunOptions? options, CancellationToken cancellationToken) =>
|
||||
this._chatAgent.RunAsync([.. messages], this.Thread, options, this._options, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IAsyncEnumerable<AgentRunResponseUpdate> InvokeStreamingCoreAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages, AgentRunOptions? options, CancellationToken cancellationToken) =>
|
||||
this._chatAgent.RunStreamingAsync(messages, this.Thread, options, this._options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback to be invoked for interactive input.
|
||||
/// </summary>
|
||||
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; init; }
|
||||
|
||||
private void Handle(HandoffMessages.InputTask item, MessageContext messageContext)
|
||||
{
|
||||
this._taskSummary = null;
|
||||
this._cache.AddRange(item.Messages);
|
||||
}
|
||||
|
||||
private void Handle(HandoffMessages.Response item, MessageContext messageContext)
|
||||
{
|
||||
this._cache.Add(item.Message);
|
||||
}
|
||||
|
||||
private async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Logger.LogHandoffAgentInvoke(this.Id);
|
||||
|
||||
while (this._taskSummary == null)
|
||||
{
|
||||
ChatMessage response;
|
||||
try
|
||||
{
|
||||
response = await this.RunAsync(this._cache, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this.Logger.LogError(exception, "Failure");
|
||||
throw;
|
||||
}
|
||||
|
||||
this._cache.Clear();
|
||||
|
||||
this.Logger.LogHandoffAgentResult(this.Id, response.Text);
|
||||
|
||||
// The response can potentially be a TOOL message from the Handoff plugin due to the filter
|
||||
// which will terminate the conversation when a function from the handoff plugin is called.
|
||||
// Since we don't want to publish that message, so we only publish if the response is an ASSISTANT message.
|
||||
if (response.Role == ChatRole.Assistant)
|
||||
{
|
||||
await this.PublishMessageAsync(new HandoffMessages.Response(response), this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (this._handoffAgent != null)
|
||||
{
|
||||
ActorType handoffType = this._handoffs[this._handoffAgent].AgentType;
|
||||
await this.PublishMessageAsync(new HandoffMessages.Request(), handoffType, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._handoffAgent = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.InteractiveCallback != null && this._taskSummary == null)
|
||||
{
|
||||
ChatMessage input = await this.InteractiveCallback().ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(new HandoffMessages.Response(input), this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
this._cache.Add(input);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this.Logger.LogError(exception, "Failure");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private List<AITool> CreateHandoffFunctions()
|
||||
{
|
||||
List<AITool> functions = [];
|
||||
|
||||
functions.Add(AIFunctionFactory.Create(
|
||||
this.EndAsync,
|
||||
name: "end_task",
|
||||
description: "Complete the task with a summary when no further requests are given."));
|
||||
|
||||
foreach (KeyValuePair<string, (ActorType AgentType, string Description)> handoff in this._handoffs)
|
||||
{
|
||||
functions.Add(AIFunctionFactory.Create(
|
||||
() => this.Handoff(handoff.Key),
|
||||
name: $"transfer_to_{InvalidNameCharsRegex().Replace(handoff.Key, "_")}",
|
||||
description: handoff.Value.Description));
|
||||
}
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
private void Handoff(string agentName)
|
||||
{
|
||||
this.Logger.LogHandoffFunctionCall(this.Id, agentName);
|
||||
this._handoffAgent = agentName;
|
||||
|
||||
FunctionInvokingChatClient.CurrentContext!.Terminate = true;
|
||||
}
|
||||
|
||||
private async ValueTask EndAsync(string summary, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogHandoffSummary(this.Id, summary);
|
||||
this._taskSummary = summary;
|
||||
await this.PublishMessageAsync(new HandoffMessages.Result(new(ChatRole.Assistant, summary)), this._resultHandoff, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (FunctionInvokingChatClient.CurrentContext is not null)
|
||||
{
|
||||
FunctionInvokingChatClient.CurrentContext.Terminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
|
||||
#if NET
|
||||
[GeneratedRegex("[^0-9A-Za-z_]+")]
|
||||
private static partial Regex InvalidNameCharsRegex();
|
||||
#else
|
||||
private static Regex InvalidNameCharsRegex() => s_invalidNameCharsRegex;
|
||||
private static readonly Regex s_invalidNameCharsRegex = new("[^0-9A-Za-z_]+", RegexOptions.Compiled);
|
||||
#endif
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// A message that describes the input task and captures results for a <see cref="HandoffOrchestration{TInput,TOutput}"/>.
|
||||
/// </summary>
|
||||
internal static class HandoffMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// The input message.
|
||||
/// </summary>
|
||||
public sealed record InputTask(IList<ChatMessage> Messages);
|
||||
|
||||
/// <summary>
|
||||
/// The final result.
|
||||
/// </summary>
|
||||
public sealed record Result(ChatMessage Message);
|
||||
|
||||
/// <summary>
|
||||
/// Signals the handoff to another agent.
|
||||
/// </summary>
|
||||
public sealed class Request;
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast an agent response to all actors in the orchestration.
|
||||
/// </summary>
|
||||
public sealed record Response(ChatMessage Message);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that passes the input message to the first agent, and
|
||||
/// then the subsequent result to the next agent, etc...
|
||||
/// </summary>
|
||||
public sealed class HandoffOrchestration : HandoffOrchestration<string, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandoffOrchestration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="handoffs">Defines the handoff connections for each agent.</param>
|
||||
/// <param name="members">The agents to be orchestrated.</param>
|
||||
public HandoffOrchestration(OrchestrationHandoffs handoffs, params AIAgent[] members)
|
||||
: base(handoffs, members)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that provides the input message to the first agent
|
||||
/// and sequentially passes each agent result to the next agent.
|
||||
/// </summary>
|
||||
public class HandoffOrchestration<TInput, TOutput> : AgentOrchestration<TInput, TOutput>
|
||||
{
|
||||
private readonly OrchestrationHandoffs _handoffs;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandoffOrchestration{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="handoffs">Defines the handoff connections for each agent.</param>
|
||||
/// <param name="agents">Additional agents participating in the orchestration that weren't passed to <paramref name="handoffs"/>.</param>
|
||||
public HandoffOrchestration(OrchestrationHandoffs handoffs, params AIAgent[] agents) : base(
|
||||
agents is { Length: 0 } ? handoffs.Agents.ToArray() :
|
||||
handoffs.Agents is { Count: 0 } ? agents :
|
||||
handoffs.Agents.Concat(agents).Distinct().ToArray())
|
||||
{
|
||||
// Create list of distinct agent names
|
||||
HashSet<string> agentNames = new(base.Members.Select(a => a.Name ?? a.Id), StringComparer.Ordinal)
|
||||
{
|
||||
handoffs.FirstAgentName
|
||||
};
|
||||
|
||||
// Extract names from handoffs that don't align with a member agent.
|
||||
string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))];
|
||||
|
||||
// Fail fast if invalid names are present.
|
||||
if (badNames.Length > 0)
|
||||
{
|
||||
Throw.ArgumentException(nameof(handoffs), $"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}");
|
||||
}
|
||||
|
||||
this._handoffs = handoffs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback to be invoked for interactive input.
|
||||
/// </summary>
|
||||
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
Throw.IfNull(entryAgent);
|
||||
|
||||
await runtime.PublishMessageAsync(new HandoffMessages.InputTask([.. input]), topic).ConfigureAwait(false);
|
||||
await runtime.PublishMessageAsync(new HandoffMessages.Request(), entryAgent.Value).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
ActorType outputType = await registrar.RegisterResultTypeAsync<HandoffMessages.Result>(response => [response.Message]).ConfigureAwait(false);
|
||||
|
||||
// Each agent handsoff its result to the next agent.
|
||||
Dictionary<string, ActorType> agentMap = [];
|
||||
Dictionary<string, HandoffLookup> handoffMap = [];
|
||||
ActorType agentType = outputType;
|
||||
for (int index = this.Members.Count - 1; index >= 0; --index)
|
||||
{
|
||||
AIAgent agent = this.Members[index];
|
||||
HandoffLookup map = [];
|
||||
handoffMap[agent.Name ?? agent.Id] = map;
|
||||
agentType =
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
this.GetAgentType(context.Topic, index),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
HandoffActor actor =
|
||||
new(agentId, runtime, context, (ChatClientAgent)agent, map, outputType, context.LoggerFactory.CreateLogger<HandoffActor>())
|
||||
{
|
||||
InteractiveCallback = this.InteractiveCallback
|
||||
};
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
agentMap[agent.Name ?? agent.Id] = agentType;
|
||||
|
||||
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
|
||||
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", index + 1);
|
||||
}
|
||||
|
||||
// Complete the handoff model
|
||||
foreach (KeyValuePair<string, AgentHandoffs> handoffs in this._handoffs)
|
||||
{
|
||||
// Retrieve the map for the agent (every agent had an empty map created)
|
||||
HandoffLookup agentHandoffs = handoffMap[handoffs.Key];
|
||||
foreach (KeyValuePair<string, string> handoff in handoffs.Value)
|
||||
{
|
||||
// name = (type,description)
|
||||
agentHandoffs[handoff.Key] = (agentMap[handoff.Key], handoff.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return agentMap[this._handoffs.FirstAgentName];
|
||||
}
|
||||
|
||||
private ActorType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="HandoffOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class HandoffOrchestrationLogMessages
|
||||
{
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "REQUEST Handoff agent [{AgentId}]")]
|
||||
public static partial void LogHandoffAgentInvoke(
|
||||
this ILogger logger,
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Handoff agent [{AgentId}]: {Message}")]
|
||||
public static partial void LogHandoffAgentResult(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "TOOL Handoff [{AgentId}]: {Name}")]
|
||||
public static partial void LogHandoffFunctionCall(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string name);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Handoff summary [{AgentId}]: {Summary}")]
|
||||
public static partial void LogHandoffSummary(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string? summary);
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that provides the input message to the first agent
|
||||
/// and sequentially passes each agent result to the next agent.
|
||||
/// </summary>
|
||||
public sealed partial class HandoffOrchestration : OrchestratingAgent
|
||||
{
|
||||
private readonly OrchestrationHandoffs _handoffs;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandoffOrchestration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="handoffs">Defines the handoff connections for each agent.</param>
|
||||
/// <param name="agents">Additional agents participating in the orchestration that weren't passed to <paramref name="handoffs"/>.</param>
|
||||
public HandoffOrchestration(OrchestrationHandoffs handoffs, params AIAgent[] agents) : base(
|
||||
agents is { Length: 0 } ? [.. handoffs.Agents] :
|
||||
handoffs.Agents is { Count: 0 } ? agents :
|
||||
[.. handoffs.Agents.Concat(agents).Distinct()])
|
||||
{
|
||||
// Create list of distinct agent names
|
||||
HashSet<string> agentNames = [.. base.Agents.Select(a => a.DisplayName), handoffs.FirstAgentName];
|
||||
|
||||
// Extract names from handoffs that don't align with a member agent.
|
||||
// Fail fast if invalid names are present.
|
||||
string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))];
|
||||
if (badNames.Length > 0)
|
||||
{
|
||||
Throw.ArgumentException(nameof(handoffs), $"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}");
|
||||
}
|
||||
|
||||
this._handoffs = handoffs;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets a callback invoked when no next handoff is selected in order to supply </summary>
|
||||
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
List<ChatMessage> allMessages = [.. messages];
|
||||
int originalMessageCount = allMessages.Count;
|
||||
return this.ResumeAsync(this._handoffs.FirstAgentName, allMessages, originalMessageCount, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.HandoffState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.NextAgent, state.AllMessages, state.OriginalMessageCount, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
private async Task<AgentRunResponse> ResumeAsync(
|
||||
string? nextAgent, List<ChatMessage> allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
Debug.Assert(nextAgent is not null);
|
||||
AgentRunResponse? response = null;
|
||||
|
||||
while (nextAgent is not null)
|
||||
{
|
||||
AIAgent? agent =
|
||||
this.Agents.FirstOrDefault(a => a.Name == nextAgent || a.Id == nextAgent) ??
|
||||
throw new InvalidOperationException($"The agent '{nextAgent}' is not defined in the orchestration.");
|
||||
|
||||
this.LogOrchestrationSubagentRunning(context, agent);
|
||||
|
||||
if (!this._handoffs.TryGetValue(agent.DisplayName, out AgentHandoffs? handoffs) || handoffs.Count == 0)
|
||||
{
|
||||
// If no handoff is available, we can run the agent directly and return its response.
|
||||
response = await RunAsync(agent, context, allMessages, context.Options, cancellationToken).ConfigureAwait(false);
|
||||
allMessages.AddRange(response.Messages);
|
||||
nextAgent = null;
|
||||
await CheckpointAsync().ConfigureAwait(false);
|
||||
this.LogOrchestrationSubagentCompleted(context, agent);
|
||||
break;
|
||||
}
|
||||
|
||||
// Create the options for the next agent request, including handoff functions.
|
||||
HandoffContext handoffCtx = new(handoffs);
|
||||
ChatClientAgentRunOptions? options = null;
|
||||
List<AITool> handoffTools = handoffCtx.CreateHandoffFunctions(this.InteractiveCallback is not null);
|
||||
if (context.Options is ChatClientAgentRunOptions contextOptions)
|
||||
{
|
||||
ChatOptions chatOptions = contextOptions.ChatOptions?.Clone() ?? new();
|
||||
chatOptions.Tools = chatOptions.Tools is { Count: > 0 } ? [.. chatOptions.Tools, .. handoffTools] : handoffTools;
|
||||
options = new(chatOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
options = new(new() { Tools = handoffTools });
|
||||
}
|
||||
|
||||
// Invoke the next agent with all of the messages collected so far.
|
||||
response = await RunAsync(agent, context, allMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
allMessages.AddRange(response.Messages);
|
||||
nextAgent = handoffCtx.TargetedAgent;
|
||||
RemoveHandoffFunctionCalls(response, handoffTools);
|
||||
|
||||
if (this.InteractiveCallback is not null)
|
||||
{
|
||||
if (handoffCtx.EndTaskInvoked)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
nextAgent = agent.DisplayName;
|
||||
allMessages.Add(await this.InteractiveCallback().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
await CheckpointAsync().ConfigureAwait(false);
|
||||
this.LogOrchestrationSubagentCompleted(context, agent);
|
||||
}
|
||||
|
||||
allMessages.RemoveRange(0, originalMessageCount);
|
||||
response ??= new();
|
||||
response.Messages = allMessages;
|
||||
return response;
|
||||
|
||||
Task CheckpointAsync() => context.Runtime is not null ?
|
||||
base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(nextAgent, allMessages, originalMessageCount), OrchestrationJsonContext.Default.HandoffState), context, cancellationToken) :
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void RemoveHandoffFunctionCalls(AgentRunResponse response, List<AITool> handoffTools)
|
||||
{
|
||||
HashSet<string>? removeToolNames = null;
|
||||
HashSet<string>? callIds = null;
|
||||
|
||||
foreach (var message in response.Messages)
|
||||
{
|
||||
for (int i = message.Contents.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (message.Contents[i] is FunctionCallContent fcc)
|
||||
{
|
||||
removeToolNames ??= [.. handoffTools.Select(t => t.Name)];
|
||||
(callIds ??= new()).Add(fcc.CallId);
|
||||
|
||||
if (removeToolNames.Contains(fcc.Name))
|
||||
{
|
||||
message.Contents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callIds is not null)
|
||||
{
|
||||
foreach (var message in response.Messages)
|
||||
{
|
||||
for (int i = message.Contents.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (message.Contents[i] is FunctionResultContent frc && callIds.Contains(frc.CallId))
|
||||
{
|
||||
message.Contents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class HandoffContext(AgentHandoffs handoffs)
|
||||
{
|
||||
public string? TargetedAgent { get; set; }
|
||||
public bool EndTaskInvoked { get; set; }
|
||||
|
||||
public List<AITool> CreateHandoffFunctions(bool needsEndTask)
|
||||
{
|
||||
List<AITool> functions = [];
|
||||
|
||||
if (needsEndTask)
|
||||
{
|
||||
functions.Add(AIFunctionFactory.Create(
|
||||
() =>
|
||||
{
|
||||
this.EndTaskInvoked = true;
|
||||
Terminate();
|
||||
},
|
||||
name: "end_task",
|
||||
description: "Invoke this function when all work is completed and no further interactions are required."));
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> handoff in handoffs)
|
||||
{
|
||||
functions.Add(AIFunctionFactory.Create(
|
||||
() =>
|
||||
{
|
||||
this.TargetedAgent = handoff.Key;
|
||||
Terminate();
|
||||
},
|
||||
name: $"handoff_to_{InvalidNameCharsRegex().Replace(handoff.Key, "_")}",
|
||||
description: handoff.Value));
|
||||
}
|
||||
|
||||
return functions;
|
||||
|
||||
static void Terminate()
|
||||
{
|
||||
if (FunctionInvokingChatClient.CurrentContext is not { } ctx)
|
||||
{
|
||||
throw new NotSupportedException($"The agent is not configured with a {nameof(FunctionInvokingChatClient)}. Cease execution.");
|
||||
}
|
||||
|
||||
ctx.Terminate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record HandoffState(string? NextAgent, List<ChatMessage> AllMessages, int OriginalMessageCount);
|
||||
|
||||
/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
|
||||
#if NET
|
||||
[GeneratedRegex("[^0-9A-Za-z_]+")]
|
||||
private static partial Regex InvalidNameCharsRegex();
|
||||
#else
|
||||
private static Regex InvalidNameCharsRegex() => s_invalidNameCharsRegex;
|
||||
private static readonly Regex s_invalidNameCharsRegex = new("[^0-9A-Za-z_]+", RegexOptions.Compiled);
|
||||
#endif
|
||||
}
|
||||
+12
-5
@@ -36,7 +36,7 @@ public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
|
||||
/// </summary>
|
||||
/// <param name="firstAgent">The first agent to be invoked (prior to any handoff).</param>
|
||||
public OrchestrationHandoffs(AIAgent firstAgent)
|
||||
: this(firstAgent.Name ?? firstAgent.Id)
|
||||
: this(firstAgent.DisplayName)
|
||||
{
|
||||
this.Agents.Add(firstAgent);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
|
||||
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
|
||||
public OrchestrationHandoffs Add(AIAgent source, params AIAgent[] targets)
|
||||
{
|
||||
string key = source.Name ?? source.Id;
|
||||
string key = source.DisplayName;
|
||||
|
||||
AgentHandoffs agentHandoffs = this.GetAgentHandoffs(key);
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
|
||||
}
|
||||
|
||||
this.Agents.Add(target);
|
||||
agentHandoffs[target.Name ?? target.Id] = target.Description ?? target.Name!;
|
||||
agentHandoffs[target.DisplayName] = target.Description ?? target.Name!;
|
||||
}
|
||||
|
||||
this.Agents.Add(source);
|
||||
@@ -101,7 +101,11 @@ public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
|
||||
/// <param name="description">The handoff description.</param>
|
||||
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
|
||||
public OrchestrationHandoffs Add(AIAgent source, AIAgent target, string description)
|
||||
=> this.Add(source.Name ?? source.Id, target.Name ?? target.Id, description);
|
||||
{
|
||||
this.Agents.Add(source);
|
||||
this.Agents.Add(target);
|
||||
return this.Add(source.DisplayName, target.DisplayName, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handoff relationship from a source agent to a target agent name/ID with a custom description.
|
||||
@@ -111,7 +115,10 @@ public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
|
||||
/// <param name="description">The handoff description.</param>
|
||||
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
|
||||
public OrchestrationHandoffs Add(AIAgent source, string targetName, string description)
|
||||
=> this.Add(source.Name ?? source.Id, targetName, description);
|
||||
{
|
||||
this.Agents.Add(source);
|
||||
return this.Add(source.DisplayName, targetName, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handoff relationship from a source agent name/ID to a target agent name/ID with a custom description.
|
||||
@@ -0,0 +1,306 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for multi-agent agent orchestration patterns.
|
||||
/// </summary>
|
||||
public abstract partial class OrchestratingAgent : AIAgent
|
||||
{
|
||||
/// <summary>Key used to persist state with the runtime.</summary>
|
||||
private const string StateKey = "State";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OrchestratingAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agents">Specifies the agents participating in this orchestration.</param>
|
||||
protected OrchestratingAgent(IReadOnlyList<AIAgent> agents)
|
||||
{
|
||||
_ = Throw.IfNullOrEmpty(agents);
|
||||
|
||||
this.Agents = agents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of member targets involved in the orchestration.
|
||||
/// </summary>
|
||||
protected IReadOnlyList<AIAgent> Agents { get; }
|
||||
|
||||
/// <summary>Gets the serializer options to use by the orchestration.</summary>
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated logger.
|
||||
/// </summary>
|
||||
public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback that is invoked for every agent response.
|
||||
/// </summary>
|
||||
public Func<IEnumerable<ChatMessage>, ValueTask>? ResponseCallback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback that is invoked for every agent update.
|
||||
/// </summary>
|
||||
public Func<AgentRunResponseUpdate, ValueTask>? StreamingResponseCallback { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
if (thread is not null)
|
||||
{
|
||||
if (thread is not IMessagesRetrievableThread retrievableThread)
|
||||
{
|
||||
throw new InvalidOperationException($"The thread type '{thread.GetType().Name}' is not supported by this agent. Use {nameof(GetNewThread)} to create a thread when needed.");
|
||||
}
|
||||
|
||||
List<ChatMessage> messagesList = [];
|
||||
await foreach (var threadMessage in retrievableThread.GetMessagesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
messagesList.Add(threadMessage);
|
||||
}
|
||||
messagesList.AddRange(messages);
|
||||
messages = messagesList;
|
||||
}
|
||||
|
||||
var orchestrationResult = await this.RunAsync(messages, options, runtime: null, cancellationToken).ConfigureAwait(false);
|
||||
return await orchestrationResult.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: There should be a RunAsync overload that returns an OrchestratingAgentStreamingResponse, which this then delegates to.
|
||||
|
||||
var response = await this.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var update in response.ToAgentRunResponseUpdates())
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override AgentThread GetNewThread() => new ChatClientAgentThread();
|
||||
|
||||
/// <summary>
|
||||
/// Initiates processing of the orchestration.
|
||||
/// </summary>
|
||||
/// <param name="messages">The input message.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="runtime">The runtime associated with the orchestration.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public async ValueTask<OrchestratingAgentResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentRunOptions? options = null,
|
||||
IActorRuntimeContext? runtime = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages, nameof(messages));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
ILogger logger = this.LoggerFactory.CreateLogger(this.GetType().Name);
|
||||
|
||||
OrchestratingAgentContext context = new()
|
||||
{
|
||||
OrchestratingAgent = this,
|
||||
Runtime = runtime,
|
||||
Options = options,
|
||||
Logger = logger,
|
||||
};
|
||||
|
||||
LogOrchestrationInvoked(logger, this.DisplayName, context.Id);
|
||||
|
||||
CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cancellationToken = cts.Token;
|
||||
|
||||
JsonElement? checkpoint = await this.ReadCheckpointAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
Task<AgentRunResponse> completion = checkpoint is null ?
|
||||
this.RunCoreAsync(messages, context, cancellationToken) :
|
||||
this.ResumeCoreAsync(checkpoint.Value, context, cancellationToken);
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_ = LogCompletionAsync(logger, context, completion);
|
||||
}
|
||||
|
||||
return new OrchestratingAgentResponse(context, completion, cts, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates processing of the orchestration.
|
||||
/// </summary>
|
||||
/// <param name="messages">The input message.</param>
|
||||
/// <param name="context">The context for this operation.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
protected abstract Task<AgentRunResponse> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes processing of the orchestration.
|
||||
/// </summary>
|
||||
/// <param name="checkpointState">The last checkpoint state available from which to resume the operation.</param>
|
||||
/// <param name="context">The context for this operation.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
protected abstract Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the agent with input messages and respond with both streamed and regular messages.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent being run</param>
|
||||
/// <param name="context">The associated orchestration context for this run.</param>
|
||||
/// <param name="input">The list of chat messages to send.</param>
|
||||
/// <param name="options">Options to use when invoking the agent.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task that returns the response <see cref="ChatMessage"/>.</returns>
|
||||
protected static async ValueTask<AgentRunResponse> RunAsync(AIAgent agent, OrchestratingAgentContext context, IReadOnlyCollection<ChatMessage> input, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Utilize streaming iff a streaming callback is provided; otherwise, use the non-streaming API.
|
||||
AgentRunResponse response;
|
||||
if (context.OrchestratingAgent?.StreamingResponseCallback is { } streamingCallback)
|
||||
{
|
||||
// For streaming, enumerate all the updates, invoking the callback for each, and storing them all.
|
||||
// Then convert them all into a single response instance.
|
||||
List<AgentRunResponseUpdate> updates = [];
|
||||
|
||||
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, options: options ?? context.Options, cancellationToken: cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates.Add(update);
|
||||
await streamingCallback(update).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
response = updates.ToAgentRunResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-streaming, just invoke the non-streaming method and get back the response.
|
||||
response = await agent.RunAsync(input, options: options ?? context.Options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Regardless of whether we invoked streaming callbacks for individual updates, invoke the non-streaming callback with the final response instance.
|
||||
// This can be used as an indication of completeness if someone otherwise only cares about the streaming updates.
|
||||
if (context.OrchestratingAgent?.ResponseCallback is { } responseCallback)
|
||||
{
|
||||
await responseCallback.Invoke(response.Messages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected sealed override TThreadType ValidateOrCreateThreadType<TThreadType>(AgentThread? thread, Func<TThreadType> constructThread) =>
|
||||
base.ValidateOrCreateThreadType(thread, constructThread);
|
||||
|
||||
/// <summary>Writes the specified checkpoint state to the runtime.</summary>
|
||||
/// <param name="state">The state to persist.</param>
|
||||
/// <param name="context">The context for the orchestrating operation.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A Task that completes when the asynchronous operation quiesces.</returns>
|
||||
protected async Task WriteCheckpointAsync(JsonElement state, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
if (context.Runtime is not null)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var response = await context.Runtime.WriteAsync(
|
||||
new ActorWriteOperationBatch(context.ETag ?? "", [new SetValueOperation(StateKey, state)]),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// If the write failed, there was a concurrency conflict where someone else updated the state.
|
||||
// But we don't actually care about consistency between the previous checkpoint and the current one,
|
||||
// so we just retry the write with the new etag.
|
||||
context.ETag = response.ETag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Read checkpoint information, if it exists, for the specified context.</summary>
|
||||
/// <param name="context">The context for the orchestrating operation.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>The loaded state, or null if it doesn't exist.</returns>
|
||||
protected async ValueTask<JsonElement?> ReadCheckpointAsync(OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = Throw.IfNull(context);
|
||||
|
||||
if (context.Runtime is not null)
|
||||
{
|
||||
ReadResponse response = await context.Runtime.ReadAsync(
|
||||
new ActorReadOperationBatch([new GetValueOperation(StateKey)]),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
context.ETag = response.ETag;
|
||||
|
||||
if (response.Results is { } results &&
|
||||
results[results.Count - 1] is GetValueResult { Value: not null } getValueResult)
|
||||
{
|
||||
return getValueResult.Value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} started ('{Id}')")]
|
||||
private static partial void LogOrchestrationInvoked(ILogger logger, string orchestration, string id);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed ('{Id}'). Result: '{Result}'")]
|
||||
private static partial void LogOrchestrationResult(ILogger logger, string orchestration, string id, string result);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} cancellation requested ('{Id}')")]
|
||||
internal static partial void LogOrchestrationCancellationRequested(ILogger logger, string orchestration, string id);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} failed ('{Id}')")]
|
||||
private static partial void LogOrchestrationFailure(ILogger logger, string orchestration, string id, Exception error);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} invoking agent '{Agent}' ('{Id}')")]
|
||||
private static partial void LogOrchestrationSubagentRunning(ILogger logger, string orchestration, string id, string agent);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed agent '{Agent}' ('{Id}')")]
|
||||
private static partial void LogOrchestrationSubagentCompleted(ILogger logger, string orchestration, string id, string agent);
|
||||
|
||||
private protected void LogOrchestrationSubagentRunning(OrchestratingAgentContext context, AIAgent agent) =>
|
||||
LogOrchestrationSubagentRunning(context.Logger, context.ToString(), context.Id, agent.DisplayName);
|
||||
|
||||
private protected void LogOrchestrationSubagentCompleted(OrchestratingAgentContext context, AIAgent agent) =>
|
||||
LogOrchestrationSubagentCompleted(context.Logger, context.ToString(), context.Id, agent.DisplayName);
|
||||
|
||||
private static async Task LogCompletionAsync(ILogger logger, OrchestratingAgentContext context, Task<AgentRunResponse> completion)
|
||||
{
|
||||
try
|
||||
{
|
||||
AgentRunResponse result = await completion.ConfigureAwait(false);
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
JsonSerializerOptions jso = context.OrchestratingAgent?.SerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
LogOrchestrationResult(logger, context.ToString(), context.Id, JsonSerializer.Serialize(result, jso.GetTypeInfo(typeof(AgentRunResponse))));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogOrchestrationFailure(logger, context.ToString(), context.Id, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides contextual information for an orchestration operation, including logging, and response callback.
|
||||
/// </summary>
|
||||
public sealed class OrchestratingAgentContext
|
||||
{
|
||||
private ILogger? _logger;
|
||||
private string? _id;
|
||||
|
||||
/// <summary>Gets the orchestrating agent associated with this operation.</summary>
|
||||
public OrchestratingAgent? OrchestratingAgent { get; set; }
|
||||
|
||||
/// <summary>Gets the associated agent runtime, if one is being used.</summary>
|
||||
public IActorRuntimeContext? Runtime { get; set; }
|
||||
|
||||
/// <summary>Gets the options associated with the orchestration run.</summary>
|
||||
public AgentRunOptions? Options { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the last version number provided by the runtime for checkpoint state.</summary>
|
||||
public string? ETag { get; set; }
|
||||
|
||||
/// <summary>Gets or sets an ID to use for the orchestration operation.</summary>
|
||||
public string Id
|
||||
{
|
||||
get
|
||||
{
|
||||
this._id ??= this.Runtime?.ActorId.ToString() ?? Guid.NewGuid().ToString("N");
|
||||
return this._id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated logger for this operation.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get => this._logger ?? NullLogger.Instance;
|
||||
set => this._logger = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
this.OrchestratingAgent?.DisplayName ??
|
||||
nameof(OrchestratingAgentContext);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of an orchestrating agent.
|
||||
/// This class encapsulates the asynchronous completion of an orchestration process.
|
||||
/// </summary>
|
||||
public sealed partial class OrchestratingAgentResponse : IAsyncDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cancelSource;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
internal OrchestratingAgentResponse(
|
||||
OrchestratingAgentContext context,
|
||||
Task<AgentRunResponse> completion,
|
||||
CancellationTokenSource orchestrationCancelSource,
|
||||
ILogger logger)
|
||||
{
|
||||
this.Context = context;
|
||||
this._cancelSource = orchestrationCancelSource;
|
||||
this.Task = completion;
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>Gets the <see cref="OrchestratingAgentContext"/> associated with this response.</summary>
|
||||
public OrchestratingAgentContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the <see cref="OrchestratingAgentResponse"/> instance.
|
||||
/// </summary>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
this._cancelSource.Dispose();
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a task that represents the completion of the orchestration result.
|
||||
/// </summary>
|
||||
public Task<AgentRunResponse> Task { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Requests cancellation of the orchestration associated with this result.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if this instance has been disposed.</exception>
|
||||
public void Cancel()
|
||||
{
|
||||
OrchestratingAgent.LogOrchestrationCancellationRequested(this._logger, this.Context.ToString(), this.Context.Id);
|
||||
this._cancelSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>Enable directly awaiting an <see cref="OrchestratingAgentResponse"/> by using <see cref="Task"/>'s awaiter.</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public TaskAwaiter<AgentRunResponse> GetAwaiter() => this.Task.GetAwaiter();
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Base abstractions for any actor that participates in an orchestration.
|
||||
/// </summary>
|
||||
public abstract class OrchestrationActor : RuntimeActor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OrchestrationActor"/> class.
|
||||
/// </summary>
|
||||
protected OrchestrationActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, string? description = null, ILogger? logger = null)
|
||||
: base(id, runtime, description, logger)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The orchestration context.
|
||||
/// </summary>
|
||||
protected OrchestrationContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient agent-type through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to send.</param>
|
||||
/// <param name="agentType">The recipient agent's type.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>The agent identifier, if it exists.</returns>
|
||||
protected ValueTask PublishMessageAsync(
|
||||
object message,
|
||||
ActorType agentType,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
base.PublishMessageAsync(message, new TopicId(agentType.Name), messageId: null, cancellationToken);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides contextual information for an orchestration operation, including topic, cancellation, logging, and response callback.
|
||||
/// </summary>
|
||||
public sealed class OrchestrationContext
|
||||
{
|
||||
internal OrchestrationContext(
|
||||
string orchestration,
|
||||
TopicId topic,
|
||||
Func<IEnumerable<ChatMessage>, ValueTask>? responseCallback,
|
||||
Func<AgentRunResponseUpdate, ValueTask>? streamingCallback,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
this.Orchestration = orchestration;
|
||||
this.Topic = topic;
|
||||
this.ResponseCallback = responseCallback;
|
||||
this.StreamingResponseCallback = streamingCallback;
|
||||
this.LoggerFactory = loggerFactory;
|
||||
this.CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name or identifier of the orchestration.
|
||||
/// </summary>
|
||||
public string Orchestration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier associated with orchestration topic.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All orchestration actors are subscribed to this topic.
|
||||
/// </remarks>
|
||||
public TopicId Topic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token that can be used to observe cancellation requests for the orchestration.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated logger factory for creating loggers within the orchestration context.
|
||||
/// </summary>
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback that is invoked for every agent response.
|
||||
/// </summary>
|
||||
public Func<IEnumerable<ChatMessage>, ValueTask>? ResponseCallback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback that is invoked for every agent response.
|
||||
/// </summary>
|
||||
public Func<AgentRunResponseUpdate, ValueTask>? StreamingResponseCallback { get; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
[JsonSerializable(typeof(SequentialOrchestration.SequentialState))]
|
||||
[JsonSerializable(typeof(ConcurrentOrchestration.ConcurrentState))]
|
||||
[JsonSerializable(typeof(GroupChatOrchestration.GroupChatState))]
|
||||
[JsonSerializable(typeof(HandoffOrchestration.HandoffState))]
|
||||
internal sealed partial class OrchestrationJsonContext : JsonSerializerContext;
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of an orchestration operation that yields a value of type <typeparamref name="TValue"/>.
|
||||
/// This class encapsulates the asynchronous completion of an orchestration process.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the value produced by the orchestration.</typeparam>
|
||||
public sealed partial class OrchestrationResult<TValue> : IAsyncDisposable
|
||||
{
|
||||
private readonly OrchestrationContext _context;
|
||||
private readonly CancellationTokenSource _cancelSource;
|
||||
private readonly TaskCompletionSource<TValue> _completion;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IAsyncDisposable? _additionalDisposable;
|
||||
private bool _isDisposed;
|
||||
|
||||
internal OrchestrationResult(OrchestrationContext context, TaskCompletionSource<TValue> completion, CancellationTokenSource orchestrationCancelSource, ILogger logger, IAsyncDisposable? additionalDisposable = null)
|
||||
{
|
||||
this._cancelSource = orchestrationCancelSource;
|
||||
this._context = context;
|
||||
this._completion = completion;
|
||||
this._logger = logger;
|
||||
this._additionalDisposable = additionalDisposable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the <see cref="OrchestrationResult{TValue}"/> instance.
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!this._isDisposed)
|
||||
{
|
||||
this._isDisposed = true;
|
||||
|
||||
this._cancelSource.Dispose();
|
||||
|
||||
if (this._additionalDisposable is { } ad)
|
||||
{
|
||||
await ad.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the orchestration name associated with this orchestration result.
|
||||
/// </summary>
|
||||
public string Orchestration => this._context.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topic identifier associated with this orchestration result.
|
||||
/// </summary>
|
||||
public TopicId Topic => this._context.Topic;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a task that represents the completion of the orchestration result.
|
||||
/// </summary>
|
||||
public Task<TValue> Task => this._completion.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the orchestration associated with this result.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if this instance has been disposed.</exception>
|
||||
/// <remarks>
|
||||
/// Cancellation is not expected to immediately halt the orchestration. Messages that
|
||||
/// are already in-flight may still be processed.
|
||||
/// </remarks>
|
||||
public void Cancel()
|
||||
{
|
||||
#if NET
|
||||
ObjectDisposedException.ThrowIf(this._isDisposed, this);
|
||||
#else
|
||||
if (this._isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(this.GetType().Name);
|
||||
}
|
||||
#endif
|
||||
|
||||
this.LogOrchestrationResultCanceled(this.Orchestration, this.Topic);
|
||||
this._cancelSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>Enable directly awaiting an <see cref="OrchestrationResult{TValue}"/> by using <see cref="Task"/>'s awaiter.</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public TaskAwaiter<TValue> GetAwaiter() => this.Task.GetAwaiter();
|
||||
|
||||
/// <summary>
|
||||
/// Logs <see cref="OrchestrationResult{TValue}"/> canceled the orchestration.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Error,
|
||||
Message = "CANCELED {Orchestration}: {Topic}")]
|
||||
private partial void LogOrchestrationResultCanceled(
|
||||
string orchestration,
|
||||
TopicId topic);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An actor used with the <see cref="SequentialOrchestration{TInput,TOutput}"/>.
|
||||
/// </summary>
|
||||
internal sealed class SequentialActor : AgentActor
|
||||
{
|
||||
private readonly ActorType _nextAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SequentialActor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime associated with the agent.</param>
|
||||
/// <param name="context">The orchestration context.</param>
|
||||
/// <param name="agent">An <see cref="AIAgent"/>.</param>
|
||||
/// <param name="nextAgent">The identifier of the next agent for which to handoff the result</param>
|
||||
/// <param name="logger">The logger to use for the actor</param>
|
||||
public SequentialActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, AIAgent agent, ActorType nextAgent, ILogger<SequentialActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
logger?.LogInformation("ACTOR {ActorId} {NextAgent}", this.Id, nextAgent);
|
||||
this._nextAgent = nextAgent;
|
||||
|
||||
this.RegisterMessageHandler<SequentialMessages.Request>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<SequentialMessages.Response>(this.HandleAsync);
|
||||
}
|
||||
|
||||
public ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken) =>
|
||||
this.InvokeAgentAsync(item.Messages, messageContext, cancellationToken);
|
||||
|
||||
public ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext, CancellationToken cancellationToken) =>
|
||||
this.InvokeAgentAsync([item.Message], messageContext, cancellationToken);
|
||||
|
||||
private async ValueTask InvokeAgentAsync(IList<ChatMessage> input, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogInformation("INVOKE {ActorId} {NextAgent}", this.Id, this._nextAgent);
|
||||
|
||||
this.Logger.LogSequentialAgentInvoke(this.Id);
|
||||
|
||||
ChatMessage response = await this.RunAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.Logger.LogSequentialAgentResult(this.Id, response.Text);
|
||||
|
||||
await this.PublishMessageAsync(new SequentialMessages.Response(response), this._nextAgent, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// A message that describes the input task and captures results for a <see cref="SequentialOrchestration{TInput,TOutput}"/>.
|
||||
/// </summary>
|
||||
internal static class SequentialMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a request containing a sequence of chat messages to be processed by the sequential orchestration.
|
||||
/// </summary>
|
||||
public sealed record Request(IList<ChatMessage> Messages);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response containing the result message from the sequential orchestration.
|
||||
/// </summary>
|
||||
public sealed record Response(ChatMessage Message);
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that passes the input message to the first agent, and
|
||||
/// then the subsequent result to the next agent, etc...
|
||||
/// </summary>
|
||||
public sealed class SequentialOrchestration : SequentialOrchestration<string, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SequentialOrchestration"/> class.
|
||||
/// </summary>
|
||||
/// <param name="members">The agents to be orchestrated.</param>
|
||||
public SequentialOrchestration(params AIAgent[] members)
|
||||
: base(members)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// An orchestration that provides the input message to the first agent
|
||||
/// and sequentially passes each agent result to the next agent.
|
||||
/// </summary>
|
||||
public class SequentialOrchestration<TInput, TOutput> : AgentOrchestration<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SequentialOrchestration{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agents">The agents participating in the orchestration.</param>
|
||||
public SequentialOrchestration(params AIAgent[] agents)
|
||||
: base(agents)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
if (!entryAgent.HasValue)
|
||||
{
|
||||
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
|
||||
}
|
||||
await runtime.PublishMessageAsync(new SequentialMessages.Request([.. input]), entryAgent.Value).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
ActorType outputType = await registrar.RegisterResultTypeAsync<SequentialMessages.Response>(response => [response.Message]).ConfigureAwait(false);
|
||||
|
||||
// Each agent handsoff its result to the next agent.
|
||||
ActorType nextAgent = outputType;
|
||||
for (int index = this.Members.Count - 1; index >= 0; --index)
|
||||
{
|
||||
AIAgent agent = this.Members[index];
|
||||
nextAgent = await RegisterAgentAsync(agent, index, nextAgent).ConfigureAwait(false);
|
||||
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, nextAgent, "MEMBER", index + 1);
|
||||
}
|
||||
|
||||
return nextAgent;
|
||||
|
||||
ValueTask<ActorType> RegisterAgentAsync(AIAgent agent, int index, ActorType nextAgent) =>
|
||||
runtime.RegisterOrchestrationAgentAsync(
|
||||
this.GetAgentType(context.Topic, index),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
SequentialActor actor = new(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger<SequentialActor>());
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
});
|
||||
}
|
||||
|
||||
private ActorType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="SequentialOrchestration{TInput, TOutput}"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class SequentialOrchestrationLogMessages
|
||||
{
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "REQUEST Sequential agent [{AgentId}]")]
|
||||
public static partial void LogSequentialAgentInvoke(
|
||||
this ILogger logger,
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Sequential agent [{AgentId}]: {Message}")]
|
||||
public static partial void LogSequentialAgentResult(
|
||||
this ILogger logger,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>Provides an orchestration that passes messages sequentially through a series of agents.</summary>
|
||||
public sealed partial class SequentialOrchestration : OrchestratingAgent
|
||||
{
|
||||
/// <summary>Initializes a new instance of the <see cref="SequentialOrchestration"/> class.</summary>
|
||||
/// <param name="agents">The agents participating in the orchestration.</param>
|
||||
public SequentialOrchestration(params AIAgent[] agents) : base(agents)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> RunCoreAsync(IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
this.ResumeAsync(0, messages, context, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.SequentialState) ?? throw new InvalidOperationException("The checkpoint state is invalid.");
|
||||
return this.ResumeAsync(state.Index, state.Messages, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
private async Task<AgentRunResponse> ResumeAsync(int i, IReadOnlyCollection<ChatMessage> input, OrchestratingAgentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
AgentRunResponse? response = null;
|
||||
for (; i < this.Agents.Count; i++)
|
||||
{
|
||||
this.LogOrchestrationSubagentRunning(context, this.Agents[i]);
|
||||
|
||||
response = await RunAsync(this.Agents[i], context, input, options: null, cancellationToken).ConfigureAwait(false);
|
||||
input = response.Messages as IReadOnlyCollection<ChatMessage> ?? [.. response.Messages];
|
||||
|
||||
await this.CheckpointAsync(i + 1, input, context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Debug.Assert(response is not null, "Response should not be null after processing a positive number of agents.");
|
||||
return response!;
|
||||
}
|
||||
|
||||
private Task CheckpointAsync(int index, IReadOnlyCollection<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
|
||||
context.Runtime is not null ? base.WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(index, messages), OrchestrationJsonContext.Default.SequentialState), context, cancellationToken) :
|
||||
Task.CompletedTask;
|
||||
|
||||
internal sealed record SequentialState(int Index, IReadOnlyCollection<ChatMessage> Messages);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.Orchestration;
|
||||
|
||||
/// <summary>
|
||||
/// Populates the target result type <see cref="ChatMessage"/> into a structured output.
|
||||
/// </summary>
|
||||
/// <typeparam name="TOutput">The .NET type of the structured-output to deserialization target.</typeparam>
|
||||
public sealed class StructuredOutputTransform<TOutput>
|
||||
{
|
||||
internal const string DefaultInstructions = "Respond with JSON that is populated by using the information in this conversation.";
|
||||
|
||||
private readonly IChatClient _client;
|
||||
private readonly ChatOptions? _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StructuredOutputTransform{TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The chat completion service to use for generating responses.</param>
|
||||
/// <param name="chatOptions">The prompt execution settings to use for the chat completion service.</param>
|
||||
public StructuredOutputTransform(IChatClient client, ChatOptions? chatOptions = null)
|
||||
{
|
||||
Throw.IfNull(client, nameof(client));
|
||||
|
||||
this._client = client;
|
||||
this._options = chatOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the instructions to be used as the system message for the chat completion.
|
||||
/// </summary>
|
||||
public string Instructions { get; init; } = DefaultInstructions;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the provided <see cref="ChatMessage"/> into a strongly-typed structured output by invoking the chat completion service and deserializing the response.
|
||||
/// </summary>
|
||||
/// <param name="messages">The chat messages to process.</param>
|
||||
/// <param name="serializerOptions">The JSON serializer options to use when performing any JSON serialization.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
|
||||
/// <returns>The structured output of type <typeparamref name="TOutput"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the response cannot be deserialized into <typeparamref name="TOutput"/>.</exception>
|
||||
public async ValueTask<TOutput> TransformAsync(IList<ChatMessage> messages, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
|
||||
ChatResponse<TOutput> response = await this._client.GetResponseAsync<TOutput>(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, this.Instructions),
|
||||
.. messages,
|
||||
],
|
||||
serializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions,
|
||||
this._options,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response.Result;
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,7 @@ public abstract class AIAgent
|
||||
/// <summary>
|
||||
/// Gets a display name for the agent, which is either the <see cref="Name"/> or <see cref="Id"/> if the name is not set.
|
||||
/// </summary>
|
||||
public virtual string DisplayName
|
||||
{
|
||||
get => this.Name ?? this.Id;
|
||||
}
|
||||
public virtual string DisplayName => this.Name ?? this.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the agent (optional).
|
||||
|
||||
+3
-1
@@ -39,7 +39,7 @@ public static partial class AgentAbstractionsJsonUtilities
|
||||
// Copy the configuration from the source generated context.
|
||||
JsonSerializerOptions options = new(JsonContext.Default.Options);
|
||||
|
||||
// Chain with all supported types from MEAI.
|
||||
// Chain with all supported types from Microsoft.Extensions.AI.Abstractions.
|
||||
options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
|
||||
|
||||
options.MakeReadOnly();
|
||||
@@ -54,7 +54,9 @@ public static partial class AgentAbstractionsJsonUtilities
|
||||
// Agent abstraction types
|
||||
[JsonSerializable(typeof(AgentRunOptions))]
|
||||
[JsonSerializable(typeof(AgentRunResponse))]
|
||||
[JsonSerializable(typeof(AgentRunResponse[]))]
|
||||
[JsonSerializable(typeof(AgentRunResponseUpdate))]
|
||||
[JsonSerializable(typeof(AgentRunResponseUpdate[]))]
|
||||
[JsonSerializable(typeof(AgentThread))]
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
|
||||
+18
-18
@@ -6,36 +6,36 @@ using System.Text.Json.Serialization;
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated JSON type information for use by all Actor abstractions.
|
||||
/// Source-generated JSON type information for use by all agent runtime abstractions.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(ActorId))]
|
||||
[JsonSerializable(typeof(ActorMessage))]
|
||||
[JsonSerializable(typeof(ActorRequestMessage))]
|
||||
[JsonSerializable(typeof(ActorResponseMessage))]
|
||||
[JsonSerializable(typeof(ActorWriteOperation))]
|
||||
[JsonSerializable(typeof(SetValueOperation))]
|
||||
[JsonSerializable(typeof(RemoveKeyOperation))]
|
||||
[JsonSerializable(typeof(SendRequestOperation))]
|
||||
[JsonSerializable(typeof(UpdateRequestOperation))]
|
||||
[JsonSerializable(typeof(ActorReadOperation))]
|
||||
[JsonSerializable(typeof(ListKeysOperation))]
|
||||
[JsonSerializable(typeof(GetValueOperation))]
|
||||
[JsonSerializable(typeof(ActorReadOperationBatch))]
|
||||
[JsonSerializable(typeof(ActorReadResult))]
|
||||
[JsonSerializable(typeof(ListKeysResult))]
|
||||
[JsonSerializable(typeof(GetValueResult))]
|
||||
[JsonSerializable(typeof(ActorRequest))]
|
||||
[JsonSerializable(typeof(ActorRequestMessage))]
|
||||
[JsonSerializable(typeof(ActorRequestUpdate))]
|
||||
[JsonSerializable(typeof(ActorResponse))]
|
||||
[JsonSerializable(typeof(ActorId))]
|
||||
[JsonSerializable(typeof(RequestStatus))]
|
||||
[JsonSerializable(typeof(ActorWriteOperationBatch))]
|
||||
[JsonSerializable(typeof(ActorReadOperationBatch))]
|
||||
[JsonSerializable(typeof(ReadResponse))]
|
||||
[JsonSerializable(typeof(WriteResponse))]
|
||||
[JsonSerializable(typeof(ActorResponseMessage))]
|
||||
[JsonSerializable(typeof(ActorType))]
|
||||
[JsonSerializable(typeof(ActorWriteOperation))]
|
||||
[JsonSerializable(typeof(ActorWriteOperationBatch))]
|
||||
[JsonSerializable(typeof(GetValueOperation))]
|
||||
[JsonSerializable(typeof(GetValueResult))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(ListKeysOperation))]
|
||||
[JsonSerializable(typeof(ListKeysResult))]
|
||||
[JsonSerializable(typeof(ReadResponse))]
|
||||
[JsonSerializable(typeof(RemoveKeyOperation))]
|
||||
[JsonSerializable(typeof(RequestStatus))]
|
||||
[JsonSerializable(typeof(SendRequestOperation))]
|
||||
[JsonSerializable(typeof(SetValueOperation))]
|
||||
[JsonSerializable(typeof(UpdateRequestOperation))]
|
||||
[JsonSerializable(typeof(WriteResponse))]
|
||||
internal sealed partial class ActorJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata associated with an actor, including its type, unique key, and description.
|
||||
/// </summary>
|
||||
public readonly struct ActorMetadata : IEquatable<ActorMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActorMetadata"/> class with the specified type, key, and description.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the actor.</param>
|
||||
/// <param name="key">The unique key associated with the actor.</param>
|
||||
/// <param name="description">A brief description of the actor.</param>
|
||||
public ActorMetadata(ActorType type, string key, string? description = null)
|
||||
{
|
||||
if (!ActorId.IsValidKey(key))
|
||||
{
|
||||
throw new ArgumentException("Invalid actor key.", nameof(key));
|
||||
}
|
||||
|
||||
this.Type = type;
|
||||
this.Key = key;
|
||||
this.Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an identifier that associates an actor with a specific factory function.
|
||||
/// </summary>
|
||||
public ActorType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A unique key identifying the actor instance.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A brief description of the actor's purpose or functionality.
|
||||
/// </summary>
|
||||
public string? Description { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is ActorMetadata actorMetadata && this.Equals(actorMetadata);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(ActorMetadata other) =>
|
||||
this.Type == other.Type &&
|
||||
this.Key == other.Key &&
|
||||
this.Description == other.Description;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(this.Type, this.Key, this.Description);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(ActorMetadata left, ActorMetadata right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(ActorMetadata left, ActorMetadata right) =>
|
||||
!(left == right);
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// Represents a batch of read operations to be performed on an actor.
|
||||
/// </summary>
|
||||
/// <param name="operations">The collection of read operations to perform.</param>
|
||||
public class ActorReadOperationBatch(IReadOnlyList<ActorReadOperation> operations)
|
||||
public sealed class ActorReadOperationBatch(IReadOnlyList<ActorReadOperation> operations)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of read operations to perform.
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// <summary>
|
||||
/// Represents a request to be sent to an actor.
|
||||
/// </summary>
|
||||
public class ActorRequest(ActorId actorId, string messageId, string method, JsonElement @params)
|
||||
public sealed class ActorRequest(ActorId actorId, string messageId, string method, JsonElement @params)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier of the target actor.
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// <summary>
|
||||
/// Represents an update to an actor request's status and data.
|
||||
/// </summary>
|
||||
public class ActorRequestUpdate(RequestStatus status, JsonElement data)
|
||||
public sealed class ActorRequestUpdate(RequestStatus status, JsonElement data)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the updated status of the request.
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// <summary>
|
||||
/// Represents a response handle for an actor request, providing access to the result and status updates.
|
||||
/// </summary>
|
||||
public class ActorResponse
|
||||
public sealed class ActorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the identifier of the actor that is processing the request.
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// </summary>
|
||||
/// <param name="eTag">The ETag for optimistic concurrency control.</param>
|
||||
/// <param name="operations">The collection of write operations to perform.</param>
|
||||
public class ActorWriteOperationBatch(string eTag, IReadOnlyCollection<ActorWriteOperation> operations)
|
||||
public sealed class ActorWriteOperationBatch(string eTag, IReadOnlyCollection<ActorWriteOperation> operations)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of write operations to perform.
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for the agent runtime.
|
||||
/// </summary>
|
||||
public static class AgentRuntimeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an actor by its type.
|
||||
/// </summary>
|
||||
/// <param name="agentRuntime">The agent runtime.</param>
|
||||
/// <param name="actorType">The type of the actor.</param>
|
||||
/// <param name="key">An optional key to specify variations of the actor. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
|
||||
public static ValueTask<ActorId> GetActorAsync(this IAgentRuntime agentRuntime, ActorType actorType, string? key = null, bool lazy = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agentRuntime);
|
||||
|
||||
return agentRuntime.GetActorAsync(actorType.Name, key, lazy, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an actor by its string representation.
|
||||
/// </summary>
|
||||
/// <param name="agentRuntime">The agent runtime.</param>
|
||||
/// <param name="actor">The string representation of the actor.</param>
|
||||
/// <param name="key">An optional key to specify variations of the actor. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
|
||||
public static ValueTask<ActorId> GetActorAsync(this IAgentRuntime agentRuntime, string actor, string? key = null, bool lazy = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(agentRuntime);
|
||||
|
||||
return agentRuntime.GetActorAsync(new ActorId(actor, key ?? "default"), lazy, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an actor factory with the runtime, associating it with a specific actor type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TActor">The type of actor created by the factory.</typeparam>
|
||||
/// <param name="agentRuntime">The agent runtime.</param>
|
||||
/// <param name="type">The actor type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the actor instance.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered actor type.</returns>
|
||||
public static ValueTask<ActorType> RegisterActorFactoryAsync<TActor>(
|
||||
this IAgentRuntime agentRuntime,
|
||||
ActorType type,
|
||||
Func<ActorId, IAgentRuntime, ValueTask<TActor>> factoryFunc,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TActor : IRuntimeActor
|
||||
{
|
||||
Throw.IfNull(agentRuntime);
|
||||
Throw.IfNull(factoryFunc);
|
||||
|
||||
return agentRuntime.RegisterActorFactoryAsync(
|
||||
type,
|
||||
async ValueTask<IRuntimeActor> (actorId, runtime) => await factoryFunc(actorId, runtime).ConfigureAwait(false),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// Represents a request to read a value from the actor's state by its key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key corresponding to the value to read from the actor's state.</param>
|
||||
public class GetValueOperation(string key) : ActorStateReadOperation
|
||||
public sealed class GetValueOperation(string key) : ActorStateReadOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key corresponding to the value to read from the actor's state.
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// Represents the result of a get value operation containing the retrieved value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value retrieved from the actor's state, or null if not found.</param>
|
||||
public class GetValueResult(JsonElement? value) : ActorReadResult
|
||||
public sealed class GetValueResult(JsonElement? value) : ActorReadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value retrieved from the actor's state.
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the runtime environment for actors, managing message sending, subscriptions, actor resolution, and state persistence,
|
||||
/// all in support of agent-based architectures.
|
||||
/// </summary>
|
||||
public interface IAgentRuntime : ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a message to an actor and gets a response.
|
||||
/// This method should be used to communicate directly with an actor.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="recipient">The actor to send the message to.</param>
|
||||
/// <param name="sender">The actor sending the message. Should be <c>null</c> if sent from an external source.</param>
|
||||
/// <param name="messageId">A unique identifier for the message. If <c>null</c>, a new ID will be generated.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the response from the actor.</returns>
|
||||
ValueTask<object?> SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all agents subscribed to the given topic.
|
||||
/// No responses are expected from publishing.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to publish.</param>
|
||||
/// <param name="topic">The topic to publish the message to.</param>
|
||||
/// <param name="sender">The actor sending the message. Defaults to <c>null</c>.</param>
|
||||
/// <param name="messageId">A unique message ID. If <c>null</c>, a new one will be generated.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an actor by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The unique identifier of the actor.</param>
|
||||
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
|
||||
ValueTask<ActorId> GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the state of an actor.
|
||||
/// The result must be JSON serializable.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The ID of the actor whose state is being saved.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning a dictionary of the saved state.</returns>
|
||||
ValueTask<JsonElement> SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the saved state into an actor.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The ID of the actor whose state is being restored.</param>
|
||||
/// <param name="state">The state dictionary to restore.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves metadata for an actor.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The ID of the actor.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's metadata.</returns>
|
||||
ValueTask<ActorMetadata> GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new subscription for the runtime to handle when processing published messages.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to add.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a subscription from the runtime.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The unique identifier of the subscription to remove.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if the subscription does not exist.</exception>
|
||||
ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers an actor factory with the runtime, associating it with a specific actor type.
|
||||
/// The type must be unique.
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the actor instance.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered <see cref="ActorType"/>.</returns>
|
||||
ValueTask<ActorType> RegisterActorFactoryAsync(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve an <see cref="IdProxyActor"/> for the specified actor.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The ID of the actor.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning an <see cref="IdProxyActor"/> if successful.</returns>
|
||||
ValueTask<IdProxyActor?> TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an actor within the runtime that can process messages, maintain state, and be closed when no longer needed.
|
||||
/// </summary>
|
||||
public interface IRuntimeActor : ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the actor.
|
||||
/// </summary>
|
||||
ActorId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets metadata associated with the actor.
|
||||
/// </summary>
|
||||
ActorMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message for the actor.
|
||||
/// This should only be called by the runtime, not by other actors.
|
||||
/// </summary>
|
||||
/// <param name="message">The received message. The type should match one of the expected subscription types.</param>
|
||||
/// <param name="messageContext">The context of the message, providing additional metadata.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation, returning a response to the message.
|
||||
/// The response can be <c>null</c> if no reply is necessary.
|
||||
/// </returns>
|
||||
/// <exception cref="OperationCanceledException">Thrown if the message was canceled.</exception>
|
||||
ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
// TODO: Why is this interface needed? It's inherited by IAgentRuntime and IRuntimeActor.
|
||||
// Is the former needed (does IAgentRuntime need to not only persist every actor but do so via
|
||||
// this interface)? If not, these methods could be moved to IRuntimeActor.
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for saving and loading the state of an object as JSON.
|
||||
/// </summary>
|
||||
public interface ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the current state of the object.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation, returning a dictionary
|
||||
/// containing the saved state. The structure of the state is implementation-defined
|
||||
/// but must be JSON serializable.
|
||||
/// </returns>
|
||||
ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a previously saved state into the object.
|
||||
/// </summary>
|
||||
/// <param name="state">
|
||||
/// A dictionary representing the saved state. The structure of the state
|
||||
/// is implementation-defined but must be JSON serializable.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default);
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a subscription that matches topics and maps them to actors.
|
||||
/// </summary>
|
||||
public interface ISubscriptionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the subscription.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
bool Equals([NotNullWhen(true)] object? obj);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified subscription is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="other">The subscription to compare.</param>
|
||||
/// <returns><c>true</c> if the subscriptions are equal; otherwise, <c>false</c>.</returns>
|
||||
bool Equals(ISubscriptionDefinition? other);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this subscription.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the subscription.</returns>
|
||||
int GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given <see cref="TopicId"/> matches the subscription.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to check.</param>
|
||||
/// <returns><c>true</c> if the topic matches the subscription; otherwise, <c>false</c>.</returns>
|
||||
bool Matches(TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="ActorId"/>.
|
||||
/// Should only be called if <see cref="Matches"/> returns <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to map.</param>
|
||||
/// <returns>The <see cref="ActorId"/> that should handle the topic.</returns>
|
||||
ActorId MapToActor(TopicId topic);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an actor proxy that allows you to use an <see cref="ActorId"/> in place of its associated <see cref="IRuntimeActor"/>.
|
||||
/// </summary>
|
||||
public sealed class IdProxyActor : IRuntimeActor
|
||||
{
|
||||
/// <summary>The runtime instance used to interact with actors.</summary>
|
||||
private readonly IAgentRuntime _runtime;
|
||||
/// <summary>The metadata for the actor, lazy-loaded.</summary>
|
||||
private ActorMetadata? _metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IdProxyActor"/> class.
|
||||
/// </summary>
|
||||
public IdProxyActor(IAgentRuntime runtime, ActorId actorId)
|
||||
{
|
||||
Throw.IfNull(runtime);
|
||||
|
||||
this.Id = actorId;
|
||||
this._runtime = runtime;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ActorId Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ActorMetadata Metadata =>
|
||||
this._metadata ??=
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
this._runtime.GetActorMetadataAsync(this.Id).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<object?> SendMessageAsync(object message, ActorId sender, string? messageId = null, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.LoadActorStateAsync(this.Id, state, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default) =>
|
||||
this._runtime.SaveActorStateAsync(this.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<object?> IRuntimeActor.OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken) =>
|
||||
new((object?)null);
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess;
|
||||
|
||||
/// <summary>Provides an in-process/in-memory implementation of the agent runtime.</summary>
|
||||
public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
{
|
||||
private static readonly UnboundedChannelOptions s_singleReaderOptions = new();
|
||||
|
||||
private readonly Dictionary<ActorType, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>>> _actorFactories = [];
|
||||
private readonly Dictionary<string, ISubscriptionDefinition> _subscriptions = [];
|
||||
private readonly Channel<MessageToProcess> _messages = Channel.CreateUnbounded<MessageToProcess>(s_singleReaderOptions);
|
||||
private readonly CancellationTokenSource _shutdownTokenSource = new();
|
||||
|
||||
private Task? _messageDeliveryTask;
|
||||
private int _remainingWork = 1; // initial count of 1 represents overall operation, decremented when shutting down.
|
||||
private int _signaledCompletion = 0;
|
||||
|
||||
// Internal for testing purposes.
|
||||
internal readonly Dictionary<ActorId, IRuntimeActor> _actorInstances = [];
|
||||
|
||||
/// <summary>Initializes a new instance of the in-memory runtime.</summary>
|
||||
public InProcessRuntime() { }
|
||||
|
||||
/// <summary>Gets the number of pending work items.</summary>
|
||||
/// <remarks>Internal for testing purposes.</remarks>
|
||||
internal int MessageCountForTesting => this._remainingWork - (1 - this._signaledCompletion);
|
||||
|
||||
/// <summary>Creates and starts a new <see cref="InProcessRuntime"/> instance.</summary>
|
||||
/// <returns>The started runtime.</returns>
|
||||
public static InProcessRuntime StartNew()
|
||||
{
|
||||
InProcessRuntime runtime = new();
|
||||
runtime.Start();
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/// <summary>Starts the runtime.</summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the runtime is already started.</exception>
|
||||
public void Start()
|
||||
{
|
||||
ThrowIfInvalid(this._signaledCompletion != 0 || this._messageDeliveryTask is not null, "Runtime was already started or shutdown.");
|
||||
|
||||
CancellationToken ct = this._shutdownTokenSource.Token;
|
||||
this._messageDeliveryTask = Task.Run(() => this.RunAsync(ct));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Interlocked.Exchange(ref this._signaledCompletion, 1) == 0 && this._messageDeliveryTask is not null)
|
||||
{
|
||||
this.DecrementRemainingWork();
|
||||
this._shutdownTokenSource.Cancel();
|
||||
this._shutdownTokenSource.Dispose();
|
||||
await this._messageDeliveryTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(message);
|
||||
|
||||
MessageToProcess m = new(this, message, messageId, sender, topic, cancellationToken);
|
||||
|
||||
this.IncrementRemainingWork();
|
||||
this._messages.Writer.TryWrite(m);
|
||||
|
||||
return new(m.ResultTcs.Task);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<object?> SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(message);
|
||||
|
||||
MessageToProcess m = new(this, message, messageId, sender, recipient, cancellationToken);
|
||||
|
||||
this.IncrementRemainingWork();
|
||||
this._messages.Writer.TryWrite(m);
|
||||
|
||||
return new(m.ResultTcs.Task);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ActorId> GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!lazy)
|
||||
{
|
||||
await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return actorId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ActorMetadata> GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
return actor.Metadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<TActor> TryGetUnderlyingActorInstanceAsync<TActor>(ActorId actorId, CancellationToken cancellationToken = default) where TActor : IRuntimeActor
|
||||
{
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (actor is not TActor concreteActor)
|
||||
{
|
||||
throw new InvalidOperationException($"Actor with name {actorId.Type} is not of type {typeof(TActor).Name}.");
|
||||
}
|
||||
|
||||
return concreteActor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
await actor.LoadStateAsync(state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<JsonElement> SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
return await actor.SaveStateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(subscription);
|
||||
ThrowIfInvalid(this._subscriptions.ContainsKey(subscription.Id), "Subscription with the specified ID already exists.");
|
||||
|
||||
this._subscriptions.Add(subscription.Id, subscription);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(subscriptionId);
|
||||
ThrowIfInvalid(!this._subscriptions.ContainsKey(subscriptionId), "Subscription with the specified ID does not exist.");
|
||||
|
||||
this._subscriptions.Remove(subscriptionId);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (JsonProperty actorIdStr in state.EnumerateObject())
|
||||
{
|
||||
ActorId actorId = ActorId.Parse(actorIdStr.Name);
|
||||
|
||||
if (this._actorFactories.ContainsKey(actorId.Type))
|
||||
{
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
await actor.LoadStateAsync(actorIdStr.Value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Dictionary<string, JsonElement> state = [];
|
||||
foreach (KeyValuePair<ActorId, IRuntimeActor> actor in this._actorInstances)
|
||||
{
|
||||
state[actor.Key.ToString()] = await actor.Value.SaveStateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return JsonSerializer.SerializeToElement(state, InProcessRuntimeContext.Default.DictionaryStringJsonElement);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ActorType> RegisterActorFactoryAsync(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(factoryFunc);
|
||||
ThrowIfInvalid(this._actorFactories.ContainsKey(type), "Actor type already registered.");
|
||||
|
||||
this._actorFactories.Add(type, factoryFunc);
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<IdProxyActor?> TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default) =>
|
||||
new(this, actorId);
|
||||
|
||||
private async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<long, Task> pendingTasks = [];
|
||||
|
||||
long currentId = 0;
|
||||
await foreach (MessageToProcess message in this._messages.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
this.DecrementRemainingWork();
|
||||
|
||||
ValueTask processTask = message.InvokeAsync(cancellationToken);
|
||||
if (!processTask.IsCompleted)
|
||||
{
|
||||
currentId++;
|
||||
Task t = WaitAndRemoveAsync(currentId, processTask);
|
||||
lock (pendingTasks)
|
||||
{
|
||||
if (!t.IsCompleted)
|
||||
{
|
||||
pendingTasks.Add(currentId, t);
|
||||
}
|
||||
}
|
||||
|
||||
async Task WaitAndRemoveAsync(long taskId, ValueTask processTask)
|
||||
{
|
||||
try
|
||||
{
|
||||
await processTask.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (pendingTasks)
|
||||
{
|
||||
pendingTasks.Remove(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(pendingTasks.Values).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignore cancellation exceptions, as they are expected when the runtime is shutting down.
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var actor in this._actorInstances)
|
||||
{
|
||||
if (actor.Value is IAsyncDisposable closeableActor)
|
||||
{
|
||||
await closeableActor.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Func<MessageToProcess, CancellationToken, ValueTask<object?>> s_publishServicer =
|
||||
async (MessageToProcess message, CancellationToken cancellationToken) =>
|
||||
{
|
||||
Debug.Assert(message.Topic.HasValue);
|
||||
|
||||
List<Task>? tasks = null;
|
||||
TopicId topic = message.Topic!.Value;
|
||||
foreach (KeyValuePair<string, ISubscriptionDefinition> subscription in message.Runtime._subscriptions)
|
||||
{
|
||||
if (subscription.Value.Matches(topic))
|
||||
{
|
||||
(tasks ??= []).Add(ProcessSubscriptionAsync(message, subscription.Value, topic, cancellationToken));
|
||||
}
|
||||
|
||||
static async Task ProcessSubscriptionAsync(
|
||||
MessageToProcess message, ISubscriptionDefinition subscription, TopicId topic, CancellationToken cancellationToken)
|
||||
{
|
||||
using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(message.Cancellation, cancellationToken);
|
||||
combinedSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
ActorId actorId = subscription.MapToActor(topic);
|
||||
ActorId? sender = message.Sender;
|
||||
if (sender is null || sender != actorId)
|
||||
{
|
||||
IRuntimeActor actor = await message.Runtime.EnsureActorAsync(actorId, combinedSource.Token).ConfigureAwait(false);
|
||||
await actor.OnMessageAsync(message.Message, new()
|
||||
{
|
||||
MessageId = message.MessageId,
|
||||
Sender = sender,
|
||||
Topic = topic,
|
||||
}, combinedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks is not null)
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// This method is effectively void, with the result never being used. But it's typed the same as SendMessageServicerAsync
|
||||
// in order to be able to share the same consuming code.
|
||||
return null;
|
||||
};
|
||||
|
||||
private static readonly Func<MessageToProcess, CancellationToken, ValueTask<object?>> s_sendServicer =
|
||||
async (MessageToProcess message, CancellationToken cancellationToken) =>
|
||||
{
|
||||
Debug.Assert(message.Receiver.HasValue);
|
||||
|
||||
using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(message.Cancellation, cancellationToken);
|
||||
|
||||
IRuntimeActor actor = await message.Runtime.EnsureActorAsync(message.Receiver!.Value, combinedSource.Token).ConfigureAwait(false);
|
||||
return await actor.OnMessageAsync(message.Message, new()
|
||||
{
|
||||
MessageId = message.MessageId,
|
||||
Sender = message.Sender,
|
||||
}, combinedSource.Token).ConfigureAwait(false);
|
||||
};
|
||||
|
||||
private async ValueTask<IRuntimeActor> EnsureActorAsync(ActorId actorId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!this._actorInstances.TryGetValue(actorId, out IRuntimeActor? actor))
|
||||
{
|
||||
this._actorFactories.TryGetValue(actorId.Type, out Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>>? factoryFunc);
|
||||
ThrowIfInvalid(factoryFunc is null, "Actor with the specified name not found.");
|
||||
|
||||
actor = await factoryFunc(actorId, this).ConfigureAwait(false);
|
||||
this._actorInstances.Add(actorId, actor);
|
||||
}
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
private void IncrementRemainingWork()
|
||||
{
|
||||
int current;
|
||||
do
|
||||
{
|
||||
current = this._remainingWork;
|
||||
ThrowIfInvalid(current <= 0, "Runtime has already shut down.");
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref this._remainingWork, current + 1, current) != current);
|
||||
}
|
||||
|
||||
private void DecrementRemainingWork()
|
||||
{
|
||||
int current;
|
||||
do
|
||||
{
|
||||
current = this._remainingWork;
|
||||
ThrowIfInvalid(current <= 0, "Runtime has already shut down.");
|
||||
}
|
||||
while (Interlocked.CompareExchange(ref this._remainingWork, current - 1, current) != current);
|
||||
|
||||
if (current == 1)
|
||||
{
|
||||
this._messages.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfInvalid([DoesNotReturnIf(true)] bool isInvalid, string message)
|
||||
{
|
||||
if (isInvalid)
|
||||
{
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
|
||||
private sealed partial class InProcessRuntimeContext : JsonSerializerContext;
|
||||
|
||||
private sealed class MessageToProcess
|
||||
{
|
||||
public MessageToProcess(InProcessRuntime runtime, object message, string? messageId, ActorId? sender, ActorId receiver, CancellationToken cancellationToken) :
|
||||
this(runtime, message, messageId, sender, s_sendServicer, cancellationToken)
|
||||
{
|
||||
this.Receiver = Throw.IfNull(receiver);
|
||||
}
|
||||
|
||||
public MessageToProcess(InProcessRuntime runtime, object message, string? messageId, ActorId? sender, TopicId topic, CancellationToken cancellationToken) :
|
||||
this(runtime, message, messageId, sender, s_publishServicer, cancellationToken)
|
||||
{
|
||||
this.Topic = Throw.IfNull(topic);
|
||||
}
|
||||
|
||||
private MessageToProcess(InProcessRuntime runtime, object message, string? messageId, ActorId? sender, Func<MessageToProcess, CancellationToken, ValueTask<object?>> servicer, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Runtime = runtime;
|
||||
this.Message = message;
|
||||
this.MessageId = messageId ?? Guid.NewGuid().ToString();
|
||||
this.Sender = sender;
|
||||
this.Servicer = servicer;
|
||||
this.Cancellation = cancellationToken;
|
||||
}
|
||||
|
||||
public InProcessRuntime Runtime { get; }
|
||||
public object Message { get; }
|
||||
public string MessageId { get; }
|
||||
public ActorId? Sender { get; }
|
||||
public TopicId? Topic { get; }
|
||||
public ActorId? Receiver { get; }
|
||||
public CancellationToken Cancellation { get; }
|
||||
public TaskCompletionSource<object?> ResultTcs { get; } = new();
|
||||
private Func<MessageToProcess, CancellationToken, ValueTask<object?>> Servicer { get; }
|
||||
|
||||
public async ValueTask InvokeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.ResultTcs.SetResult(await this.Servicer(this, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
catch (OperationCanceledException exception)
|
||||
{
|
||||
this.ResultTcs.TrySetCanceled(exception.CancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
this.ResultTcs.SetException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// </summary>
|
||||
/// <param name="continuationToken">Optional token for pagination to continue listing from a previous operation.</param>
|
||||
/// <param name="keyPrefix">Optional prefix to filter keys. Only keys starting with this prefix will be returned.</param>
|
||||
public class ListKeysOperation(string? continuationToken, string? keyPrefix = null) : ActorStateReadOperation
|
||||
public sealed class ListKeysOperation(string? continuationToken, string? keyPrefix = null) : ActorStateReadOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the continuation token for pagination.
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// </summary>
|
||||
/// <param name="keys">The collection of keys found in the actor's state.</param>
|
||||
/// <param name="continuationToken">Optional token for pagination to retrieve additional keys.</param>
|
||||
public class ListKeysResult(IReadOnlyCollection<string> keys, string? continuationToken) : ActorReadResult
|
||||
public sealed class ListKeysResult(IReadOnlyCollection<string> keys, string? continuationToken) : ActorReadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the collection of keys found in the actor's state.
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the context of a message being sent within the agent runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This includes metadata such as the sender, topic, ahd RPC status.
|
||||
/// </remarks>
|
||||
public sealed class MessageContext
|
||||
{
|
||||
private string? _messageId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this message.
|
||||
/// </summary>
|
||||
public string MessageId
|
||||
{
|
||||
get => this._messageId ?? Interlocked.CompareExchange(ref this._messageId, Guid.NewGuid().ToString(), null) ?? this._messageId;
|
||||
set => this._messageId = Throw.IfNullOrEmpty(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender of the message.
|
||||
/// If <c>null</c>, the sender is unspecified.
|
||||
/// </summary>
|
||||
public ActorId? Sender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic associated with the message.
|
||||
/// If <c>null</c>, the message is not tied to a specific topic.
|
||||
/// </summary>
|
||||
public TopicId? Topic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this message is part of an RPC (Remote Procedure Call).
|
||||
/// </summary>
|
||||
public bool IsRpc { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the serializer options to be used when performing JSON serialization associated with this message.</summary>
|
||||
public JsonSerializerOptions? SerializerOptions { get; set; }
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// </summary>
|
||||
/// <param name="eTag">The actor's last-known ETag value.</param>
|
||||
/// <param name="results">The ordered collection of results.</param>
|
||||
public class ReadResponse(string eTag, IReadOnlyList<ActorReadResult> results)
|
||||
public sealed class ReadResponse(string eTag, IReadOnlyList<ActorReadResult> results)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the version of the state update.
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base implementation of <see cref="IRuntimeActor"/>.
|
||||
/// </summary>
|
||||
public abstract class RuntimeActor : IRuntimeActor
|
||||
{
|
||||
private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement;
|
||||
|
||||
/// <summary>
|
||||
/// The activity source for tracing.
|
||||
/// </summary>
|
||||
public static readonly ActivitySource TraceSource = new($"{typeof(IRuntimeActor).Namespace}");
|
||||
|
||||
private readonly Dictionary<Type, HandlerInvoker> _handlerInvokers = [];
|
||||
private readonly IAgentRuntime _runtime;
|
||||
|
||||
private delegate ValueTask<object?> HandlerInvoker(object? message, MessageContext messageContext, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Provides logging capabilities used for diagnostic and operational information.
|
||||
/// </summary>
|
||||
protected internal ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the actor.
|
||||
/// </summary>
|
||||
public ActorId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata of the actor.
|
||||
/// </summary>
|
||||
public ActorMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RuntimeActor class with the specified identifier, runtime, description, and optional logger.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the actor.</param>
|
||||
/// <param name="runtime">The runtime environment in which the actor operates.</param>
|
||||
/// <param name="description">A brief description of the actor's purpose.</param>
|
||||
/// <param name="logger">An optional logger for recording diagnostic information.</param>
|
||||
protected RuntimeActor(
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
string? description = null,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
Throw.IfNull(runtime);
|
||||
|
||||
this.Id = id;
|
||||
this._runtime = runtime;
|
||||
this.Logger = logger ?? NullLogger.Instance;
|
||||
|
||||
this.Metadata = new ActorMetadata(this.Id.Type, this.Id.Key, description);
|
||||
}
|
||||
|
||||
/// <summary>Registers a handler for <typeparamref name="TInput"/>.</summary>
|
||||
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
|
||||
/// <param name="messageHandler">The handler function that processes the message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
|
||||
/// <remarks>
|
||||
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
|
||||
/// </remarks>
|
||||
protected void RegisterMessageHandler<TInput>(Action<TInput, MessageContext> messageHandler)
|
||||
{
|
||||
_ = Throw.IfNull(messageHandler);
|
||||
|
||||
this.RegisterMessageHandler<TInput, object?>(async (input, ctx, cancellationToken) =>
|
||||
{
|
||||
messageHandler(input, ctx);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Registers a handler for <typeparamref name="TInput"/>.</summary>
|
||||
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
|
||||
/// <param name="messageHandler">The handler function that processes the message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
|
||||
/// <remarks>
|
||||
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
|
||||
/// </remarks>
|
||||
protected void RegisterMessageHandler<TInput>(Func<TInput, MessageContext, CancellationToken, ValueTask> messageHandler)
|
||||
{
|
||||
_ = Throw.IfNull(messageHandler);
|
||||
|
||||
this.RegisterMessageHandler<TInput, object?>(async (input, ctx, cancellationToken) =>
|
||||
{
|
||||
await messageHandler(input, ctx, cancellationToken).ConfigureAwait(false);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Registers a handler for <typeparamref name="TInput"/>.</summary>
|
||||
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of the output message for the handler.</typeparam>
|
||||
/// <param name="messageHandler">The handler function that processes the message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
|
||||
/// <remarks>
|
||||
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
|
||||
/// </remarks>
|
||||
protected void RegisterMessageHandler<TInput, TOutput>(Func<TInput, MessageContext, TOutput> messageHandler)
|
||||
{
|
||||
_ = Throw.IfNull(messageHandler);
|
||||
|
||||
this.RegisterMessageHandler<TInput, object?>(async (input, ctx, cancellationToken) => messageHandler(input, ctx));
|
||||
}
|
||||
|
||||
/// <summary>Registers a handler for <typeparamref name="TInput"/> that produces a <typeparamref name="TOutput"/>.</summary>
|
||||
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of the output message for the handler.</typeparam>
|
||||
/// <param name="messageHandler">The handler function that processes the message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
|
||||
/// <remarks>
|
||||
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
|
||||
/// </remarks>
|
||||
protected void RegisterMessageHandler<TInput, TOutput>(Func<TInput, MessageContext, CancellationToken, ValueTask<TOutput>> messageHandler)
|
||||
{
|
||||
_ = Throw.IfNull(messageHandler);
|
||||
|
||||
if (this._handlerInvokers.ContainsKey(typeof(TInput)))
|
||||
{
|
||||
throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered.");
|
||||
}
|
||||
|
||||
this._handlerInvokers.Add(
|
||||
typeof(TInput),
|
||||
async (message, messageContext, cancellationToken) => await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message by determining its type and invoking the corresponding handler method if available.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to be handled.</param>
|
||||
/// <param name="messageContext">The context associated with the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, containing the response object or null.</returns>
|
||||
public ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the handler for the message type, and invoke it, if it exists.
|
||||
if (message is not null && this._handlerInvokers.TryGetValue(message.GetType(), out HandlerInvoker? handlerInvoker))
|
||||
{
|
||||
return handlerInvoker(message, messageContext, cancellationToken);
|
||||
}
|
||||
|
||||
return new((object?)null);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default) =>
|
||||
new(s_emptyElement);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient actor through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="actor">The requested actor's type.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected async ValueTask<ActorId?> GetActorAsync(ActorType actor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._runtime.GetActorAsync(actor, lazy: false, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient actor through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to send.</param>
|
||||
/// <param name="recipient">The recipient actor's identifier.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected ValueTask<object?> SendMessageAsync(object message, ActorId recipient, string? messageId = null, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all actors subscribed to a specific topic through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to publish.</param>
|
||||
/// <param name="topic">The topic identifier to which the message is published.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous publish operation.</returns>
|
||||
protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a topic identifier that defines the scope of a broadcast message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The agent runtime implements a publish-subscribe model through its broadcast API,
|
||||
/// where messages must be published with a specific topic.
|
||||
/// </remarks>
|
||||
public readonly partial struct TopicId : IEquatable<TopicId>
|
||||
{
|
||||
private const string TypePattern = @"^[\w-.:=]+$";
|
||||
|
||||
#if NET
|
||||
[GeneratedRegex(TypePattern)]
|
||||
private static partial Regex TypeRegex();
|
||||
#else
|
||||
private static Regex TypeRegex() => s_typeRegex;
|
||||
private static readonly Regex s_typeRegex = new(TypePattern, RegexOptions.Compiled);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the topic. Must match the pattern: <c>^[\w-.:=]+$</c></param>
|
||||
/// <param name="source">The source of the event.</param>
|
||||
public TopicId(string type, string? source = null)
|
||||
{
|
||||
Throw.IfNull(type);
|
||||
|
||||
if (!TypeRegex().IsMatch(type))
|
||||
{
|
||||
Throw.ArgumentException(nameof(type), "Invalid type format.");
|
||||
}
|
||||
|
||||
// TODO: What validation should be performed on source? The cited cloudevents spec suggests it should be a URI reference.
|
||||
|
||||
this.Type = type;
|
||||
this.Source = source ?? "default";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the event that this <see cref="TopicId"/> represents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adheres to the CloudEvents specification.
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type</see>.
|
||||
/// </remarks>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source that identifies the context in which an event happened.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This adheres to the CloudEvents specification.
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#source-1">CloudEvents Source</see>.
|
||||
/// </remarks>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string of the format "type/key" into an <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="TopicId">The actor ID string.</param>
|
||||
/// <returns>An instance of <see cref="TopicId"/>.</returns>
|
||||
public static TopicId Parse(string TopicId)
|
||||
{
|
||||
if (!KeyValueParser.TryParse(TopicId, out string? type, out string? key))
|
||||
{
|
||||
throw new FormatException($"Invalid TopicId format: '{TopicId}'. Expected format is 'type/key'.");
|
||||
}
|
||||
|
||||
return new TopicId(type, key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly string ToString() => $"{this.Type}/{this.Source}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj) =>
|
||||
obj is TopicId other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(TopicId other) =>
|
||||
this.Type == other.Type && this.Source == other.Source;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(this.Type, this.Source);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator ==(TopicId left, TopicId right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator !=(TopicId left, TopicId right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
// TODO: Implement < for wildcard matching (type, *)
|
||||
//public readonly bool IsWildcardMatch(TopicId other)
|
||||
//{
|
||||
// return this.Type == other.Type;
|
||||
//}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// This subscription matches on topics based on the exact type and maps to actors using the source of the topic as the actor key.
|
||||
/// This subscription causes each source to have its own actor instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
/// <code>
|
||||
/// var subscription = new TypeSubscription("t1", "a1");
|
||||
/// </code>
|
||||
/// In this case:
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s1"` will be handled by an actor of type `"a1"` with key `"s1"`.
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s2"` will be handled by an actor of type `"a1"` with key `"s2"`.
|
||||
/// </remarks>
|
||||
public sealed class TypeSubscription : ISubscriptionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypeSubscription"/> class.
|
||||
/// </summary>
|
||||
/// <param name="topicType">The exact topic type to match against.</param>
|
||||
/// <param name="actorType">Actor type to handle this subscription.</param>
|
||||
/// <param name="id">Unique identifier for the subscription. If not provided, a new UUID will be generated.</param>
|
||||
public TypeSubscription(string topicType, ActorType actorType, string? id = null)
|
||||
{
|
||||
Throw.IfNullOrEmpty(topicType);
|
||||
|
||||
this.TopicType = topicType;
|
||||
this.ActorType = actorType;
|
||||
this.Id = id ?? Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the subscription.
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exact topic type used for matching.
|
||||
/// </summary>
|
||||
public string TopicType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actor type that handles this subscription.
|
||||
/// </summary>
|
||||
public ActorType ActorType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given <see cref="TopicId"/> matches the subscription based on an exact type match.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to check.</param>
|
||||
/// <returns><c>true</c> if the topic's type matches exactly, <c>false</c> otherwise.</returns>
|
||||
public bool Matches(TopicId topic) => topic.Type == this.TopicType;
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="ActorId"/>. Should only be called if <see cref="Matches"/> returns true.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to map.</param>
|
||||
/// <returns>An <see cref="ActorId"/> representing the actor that should handle the topic.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the topic does not match the subscription.</exception>
|
||||
public ActorId MapToActor(TopicId topic)
|
||||
{
|
||||
if (!this.Matches(topic))
|
||||
{
|
||||
throw new InvalidOperationException("TopicId does not match the subscription.");
|
||||
}
|
||||
|
||||
return new ActorId(this.ActorType, topic.Source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj) =>
|
||||
obj is TypeSubscription other &&
|
||||
(this.Id == other.Id || (this.ActorType == other.ActorType && this.TopicType == other.TopicType));
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified subscription is equal to the current subscription.
|
||||
/// </summary>
|
||||
/// <param name="other">The subscription to compare.</param>
|
||||
/// <returns><c>true</c> if the subscriptions are equal; otherwise, <c>false</c>.</returns>
|
||||
public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures.</returns>
|
||||
public override int GetHashCode() => HashCode.Combine(this.Id, this.ActorType, this.TopicType);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// </summary>
|
||||
/// <param name="eTag">The actor's updated ETag value after the write operation.</param>
|
||||
/// <param name="success">Whether the write operation was successful.</param>
|
||||
public class WriteResponse(string eTag, bool success)
|
||||
public sealed class WriteResponse(string eTag, bool success)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the version of the state update.
|
||||
|
||||
@@ -11,6 +11,4 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(string))]
|
||||
internal sealed partial class ActorRuntimeJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
internal sealed partial class ActorRuntimeJsonContext : JsonSerializerContext;
|
||||
|
||||
+12
-3
@@ -5,14 +5,23 @@ namespace Microsoft.Extensions.AI.Agents;
|
||||
/// <summary>
|
||||
/// Chat client agent run options.
|
||||
/// </summary>
|
||||
internal sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
public sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentRunOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatOptions">Optional chat options to pass to the agent's invocation.</param>
|
||||
public ChatClientAgentRunOptions(ChatOptions? chatOptions = null) :
|
||||
this(null, chatOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentRunOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="source">Optional source <see cref="AgentRunOptions"/> to clone.</param>
|
||||
/// <param name="chatOptions">Optional chat options to pass to the agent's invocation.</param>
|
||||
internal ChatClientAgentRunOptions(AgentRunOptions? source = null, ChatOptions? chatOptions = null)
|
||||
internal ChatClientAgentRunOptions(AgentRunOptions? source, ChatOptions? chatOptions = null)
|
||||
{
|
||||
this.ChatOptions = chatOptions;
|
||||
}
|
||||
@@ -20,5 +29,5 @@ internal sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
/// <summary>
|
||||
/// Gets or sets optional chat options to pass to the agent's invocation
|
||||
/// </summary>
|
||||
internal ChatOptions? ChatOptions { get; }
|
||||
public ChatOptions? ChatOptions { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user