.NET Port Agent Orchestration (#107)

* Checkpoint

* Checkpoint

* Namespaces

* Namespace

* Cleanup

* Namespace order

* Fix sync

* Formatting

* Formatting

* Namespace

* Namespace order

* Code convention

* Naming

* Naming

* Text handling

* Text handling

* Namespace

* Namespace order

* Namespace ordering

* Test

* ValueTask

* net472

* Test fix

* Fix namespace (net472)

* Namespace

* Fix conditional namespace

* Fix type expression

* Compatibility and cleanup

* Sample compatibility

* Sample compat

* Test compat

* modifier order

* Simply http-stub

* Formating fix for unit-test

* Fix test

* Real fix

* Test clean-up

* Update dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix build errors after merging

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Chris
2025-07-08 13:04:34 -04:00
committed by GitHub
co-authored by Copilot Stephen Toub
parent 35c938fb5b
commit 7c8ec5ec19
71 changed files with 5675 additions and 131 deletions
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null)
: base(
id,
runtime,
context,
VerifyDescription(agent),
logger)
{
this.Agent = agent;
this.Thread = this.Agent.GetNewThread();
}
/// <summary>
/// Gets the associated agent.
/// </summary>
protected Agent 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 regular (not streamed) response.
/// </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>
/// Override this method to customize the invocation of the agent.
/// </remarks>
protected virtual Task InvokeAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentRunOptions options,
CancellationToken cancellationToken = default) =>
this.Agent.RunAsync(
[.. messages],
this.Thread,
options,
cancellationToken);
/// <summary>
/// Invokes the agent for a streamed response.
/// </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>
/// Override this method to customize the invocation of the agent.
/// </remarks>
protected virtual IAsyncEnumerable<ChatResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
this.Agent.RunStreamingAsync(
messages,
this.Thread,
options,
cancellationToken);
/// <summary>
/// Invokes the agent with a single chat message.
/// This method sets the message role to <see cref="ChatRole.User"/> and delegates to the overload accepting multiple messages.
/// </summary>
/// <param name="input">The chat message content 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 ValueTask<ChatMessage> InvokeAsync(ChatMessage input, CancellationToken cancellationToken) =>
this.InvokeAsync([input], cancellationToken);
/// <summary>
/// Invokes 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> InvokeAsync(IEnumerable<ChatMessage> input, CancellationToken cancellationToken)
{
this.Context.Cancellation.ThrowIfCancellationRequested();
List<ChatMessage>? responseMessages = [];
ChatResponse response = new(responseMessages);
AgentRunOptions options =
new()
{
OnIntermediateMessages = HandleMessage,
};
if (this.Context.StreamingResponseCallback == null)
{
// No need to utilize streaming if no callback is provided
await this.InvokeAsync([.. input], options, cancellationToken).ConfigureAwait(false);
}
else
{
IAsyncEnumerable<ChatResponseUpdate> streamedResponses = this.InvokeStreamingAsync([.. input], options, cancellationToken);
ChatResponseUpdate? lastStreamedResponse = null;
await foreach (ChatResponseUpdate streamedResponse in streamedResponses.ConfigureAwait(false))
{
this.Context.Cancellation.ThrowIfCancellationRequested();
await HandleStreamedMessage(lastStreamedResponse, isFinal: false).ConfigureAwait(false);
lastStreamedResponse = streamedResponse;
}
await HandleStreamedMessage(lastStreamedResponse, isFinal: true).ConfigureAwait(false);
}
return response.Messages.Last();
async Task HandleMessage(IReadOnlyCollection<ChatMessage> messages)
{
responseMessages?.AddRange(messages);
if (this.Context.ResponseCallback is not null)
{
await this.Context.ResponseCallback.Invoke(messages).ConfigureAwait(false);
}
}
async ValueTask HandleStreamedMessage(ChatResponseUpdate? streamedResponse, bool isFinal)
{
if (this.Context.StreamingResponseCallback != null && streamedResponse != null)
{
await this.Context.StreamingResponseCallback.Invoke(streamedResponse, isFinal).ConfigureAwait(false);
}
}
}
private static string VerifyDescription(Agent agent)
{
return agent.Description ?? throw new ArgumentException($"Missing agent description: {agent.Name ?? agent.Id}", nameof(agent));
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
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, IHandle<TInput>
{
private readonly OrchestrationInputTransform<TInput> _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(
AgentId id,
IAgentRuntime runtime,
OrchestrationContext context,
OrchestrationInputTransform<TInput> 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;
}
/// <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>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
public async ValueTask HandleAsync(TInput item, MessageContext messageContext)
{
this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id);
try
{
IEnumerable<ChatMessage> input = await this._transform.Invoke(item).ConfigureAwait(false);
Task task = this._action.Invoke(input).AsTask();
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;
}
}
}
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
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, IHandle<TResult>
{
private readonly TaskCompletionSource<TOutput> _completionSource;
private readonly OrchestrationResultTransform<TResult> _transformResult;
private readonly OrchestrationOutputTransform<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(
AgentId id,
IAgentRuntime runtime,
OrchestrationContext context,
OrchestrationResultTransform<TResult> transformResult,
OrchestrationOutputTransform<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;
}
/// <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>
/// <returns>A ValueTask representing asynchronous operation.</returns>
public async ValueTask HandleAsync(TResult item, MessageContext messageContext)
{
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).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;
}
}
}
}
@@ -0,0 +1,257 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Called for every response is produced by any agent.
/// </summary>
/// <param name="response">The agent response</param>
public delegate ValueTask OrchestrationResponseCallback(IEnumerable<ChatMessage> response);
/// <summary>
/// Called to expose the streamed response produced by any agent.
/// </summary>
/// <param name="response">The agent response</param>
/// <param name="isFinal">Indicates if streamed content is final chunk of the message.</param>
public delegate ValueTask OrchestrationStreamingCallback(ChatResponseUpdate response, bool isFinal);
/// <summary>
/// Called when human interaction is requested.
/// </summary>
public delegate ValueTask<ChatMessage> OrchestrationInteractiveCallback();
/// <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 Agent[] members)
{
// Capture orchestration root name without generic parameters for use in
// agent type and topic formatting as well as logging.
this.OrchestrationLabel = this.GetType().Name.Split('`').First();
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 OrchestrationInputTransform<TInput> InputTransform { get; init; } = DefaultTransforms.FromInput<TInput>;
/// <summary>
/// Transforms the processed result into the final output form.
/// </summary>
public OrchestrationOutputTransform<TOutput> ResultTransform { get; init; } = DefaultTransforms.ToOutput<TOutput>;
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationResponseCallback? ResponseCallback { get; init; }
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationStreamingCallback? StreamingResponseCallback { get; init; }
/// <summary>
/// Gets the list of member targets involved in the orchestration.
/// </summary>
protected IReadOnlyList<Agent> 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,
CancellationToken cancellationToken = default)
{
Throw.IfNull(input, nameof(input));
TopicId topic = new($"{this.OrchestrationLabel}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}");
CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
OrchestrationContext context =
new(this.OrchestrationLabel,
topic,
this.ResponseCallback,
this.StreamingResponseCallback,
this.LoggerFactory,
cancellationToken);
ILogger logger = this.LoggerFactory.CreateLogger(this.GetType());
TaskCompletionSource<TOutput> completion = new();
AgentType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
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);
}
/// <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, AgentType? 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<AgentType?> 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 AgentType 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<AgentType> RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource<TOutput> completion, AgentType? 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);
AgentType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false);
// Register actor for orchestration entry-point
AgentType orchestrationEntry =
await runtime.RegisterOrchestrationAgentAsync(
this.FormatAgentType(context.Topic, "Boot"),
(agentId, runtime) =>
{
RequestActor actor =
new(agentId,
runtime,
context,
this.InputTransform,
completion,
StartAsync,
context.LoggerFactory.CreateLogger<RequestActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic);
return orchestrationEntry;
ValueTask StartAsync(IEnumerable<ChatMessage> input) => this.StartAsync(runtime, context.Topic, input, entryAgent);
}
/// <summary>
/// A context used during registration (<see cref="RegisterAsync"/>).
/// </summary>
public sealed class RegistrationContext(
AgentType agentType,
IAgentRuntime runtime,
OrchestrationContext context,
TaskCompletionSource<TOutput> completion,
OrchestrationOutputTransform<TOutput> outputTransform)
{
/// <summary>
/// Register the final result type.
/// </summary>
public async ValueTask<AgentType> RegisterResultTypeAsync<TResult>(OrchestrationResultTransform<TResult> resultTransform)
{
// Register actor for final result
AgentType registeredType =
await runtime.RegisterOrchestrationAgentAsync(
agentType,
(agentId, runtime) =>
{
ResultActor<TResult> actor =
new(agentId,
runtime,
context,
resultTransform,
outputTransform,
completion,
context.LoggerFactory.CreateLogger<ResultActor<TResult>>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
return registeredType;
}
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// An <see cref="AgentActor"/> used with the <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
internal sealed class ConcurrentActor : AgentActor, IHandle<ConcurrentMessages.Request>
{
private readonly AgentType _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="Agent"/>.</param>
/// <param name="resultActor">Identifies the actor collecting results.</param>
/// <param name="logger">The logger to use for the actor</param>
public ConcurrentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType resultActor, ILogger<ConcurrentActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
this._handoffActor = resultActor;
}
/// <inheritdoc/>
public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext)
{
this.Logger.LogConcurrentAgentInvoke(this.Id);
ChatMessage response = await this.InvokeAsync(item.Messages, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogConcurrentAgentResult(this.Id, response.Text);
await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// Common messages used by the <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
internal static class ConcurrentMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
public static readonly ChatMessage Empty = new();
/// <summary>
/// The input task for a <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
public sealed class Request
{
/// <summary>
/// The request input.
/// </summary>
public IList<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// A result from a <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
public sealed class Result
{
/// <summary>
/// The result message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Extension method to convert a <see cref="string"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this string text, ChatRole? role = null) => new() { Message = new ChatMessage(role ?? ChatRole.Assistant, text) };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this ChatMessage message) => new() { Message = message };
/// <summary>
/// Extension method to convert a collection of <see cref="ChatMessage"/> to a <see cref="ConcurrentMessages.Request"/>.
/// </summary>
public static Request AsInputMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = [.. messages] };
}
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <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 Agent[] members)
: base(members)
{
this.ResultTransform =
(response, cancellationToken) =>
{
string[] result = [.. response.Select(r => r.Text)];
#if !NETCOREAPP
return new ValueTask<string[]>(result);
#else
return ValueTask.FromResult(result);
#endif
};
}
}
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <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 Agent[] agents)
: base(agents)
{
}
/// <inheritdoc />
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
return runtime.PublishMessageAsync(input.AsInputMessage(), topic);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<ConcurrentMessages.Result[]>(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false);
// Register result actor
AgentType resultType = this.FormatAgentType(context.Topic, "Results");
await runtime.RegisterOrchestrationAgentAsync(
resultType,
(agentId, runtime) =>
{
ConcurrentResultActor actor = new(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger<ConcurrentResultActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, resultType, "RESULTS");
// Register member actors - All agents respond to the same message.
int agentCount = 0;
foreach (Agent agent in this.Members)
{
++agentCount;
AgentType agentType =
await runtime.RegisterAgentFactoryAsync(
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
(agentId, runtime) =>
{
ConcurrentActor actor = new(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger<ConcurrentActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
}
return null;
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// Actor for capturing each <see cref="ConcurrentMessages.Result"/> message.
/// </summary>
internal sealed class ConcurrentResultActor :
OrchestrationActor,
IHandle<ConcurrentMessages.Result>
{
private readonly ConcurrentQueue<ConcurrentMessages.Result> _results;
private readonly AgentType _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(
AgentId id,
IAgentRuntime runtime,
OrchestrationContext context,
AgentType orchestrationType,
int expectedCount,
ILogger logger)
: base(id, runtime, context, "Captures the results of the ConcurrentOrchestration", logger)
{
this._orchestrationType = orchestrationType;
this._expectedCount = expectedCount;
this._results = [];
}
/// <inheritdoc/>
public async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext)
{
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, messageContext.CancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Extensions;
/// <summary>
/// Extension methods for <see cref="IAgentRuntime"/>.
/// </summary>
public static class RuntimeExtensions
{
/// <summary>
/// Sends a message to the specified agent.
/// </summary>
public static async ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, AgentType agentType, CancellationToken cancellationToken = default)
{
await runtime.PublishMessageAsync(message, new TopicId(agentType), sender: null, messageId: null, cancellationToken).ConfigureAwait(false);
}
/// <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<AgentType> RegisterOrchestrationAgentAsync(this IAgentRuntime runtime, AgentType agentType, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc)
{
AgentType registeredType = await runtime.RegisterAgentFactoryAsync(agentType, factoryFunc).ConfigureAwait(false);
// Subscribe agent to its own unique topic
await runtime.SubscribeAsync(registeredType).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 async Task SubscribeAsync(this IAgentRuntime runtime, string agentType)
{
await runtime.AddSubscriptionAsync(new TypeSubscription(agentType, agentType)).ConfigureAwait(false);
}
/// <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, string agentType, params TopicId[] topics)
{
for (int index = 0; index < topics.Length; ++index)
{
await runtime.AddSubscriptionAsync(new TypeSubscription(topics[index].Type, agentType)).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An <see cref="AgentActor"/> used with the <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
/// </summary>
internal sealed class GroupChatAgentActor :
AgentActor,
IHandle<GroupChatMessages.Group>,
IHandle<GroupChatMessages.Reset>,
IHandle<GroupChatMessages.Speak>
{
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="Agent"/>.</param>
/// <param name="logger">The logger to use for the actor</param>
public GroupChatAgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger<GroupChatAgentActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
this._cache = [];
}
/// <inheritdoc/>
public ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext)
{
this._cache.AddRange(item.Messages);
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext)
{
this.ResetThread();
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext)
{
this.Logger.LogChatAgentInvoke(this.Id);
ChatMessage response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatAgentResult(this.Id, response.Text);
this._cache.Clear();
await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
}
}
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// Represents the result of a group chat manager operation, including a value and a reason.
/// </summary>
/// <typeparam name="TValue">The type of the value returned by the operation.</typeparam>
/// <param name="value">The value returned by the operation.</param>
public sealed class GroupChatManagerResult<TValue>(TValue value)
{
/// <summary>
/// The reason for the result, providing additional context or explanation.
/// </summary>
public string Reason { get; init; } = string.Empty;
/// <summary>
/// The value returned by the group chat manager operation.
/// </summary>
public TValue Value { get; } = value;
}
/// <summary>
/// A manager that manages the flow of a group chat.
/// </summary>
public abstract class GroupChatManager
{
private int _invocationCount;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatManager"/> class.
/// </summary>
protected GroupChatManager() { }
/// <summary>
/// Gets the number of times the group chat manager has been invoked.
/// </summary>
public int InvocationCount => this._invocationCount;
/// <summary>
/// Gets or sets the maximum number of invocations allowed for the group chat manager.
/// </summary>
public int MaximumInvocationCount { get; init; } = int.MaxValue;
/// <summary>
/// Gets or sets the callback to be invoked for interactive input.
/// </summary>
public OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
/// <summary>
/// Filters the results of the group chat based on the provided chat history.
/// </summary>
/// <param name="history">The chat history to filter.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> containing the filtered result as a string.</returns>
public abstract ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
/// <summary>
/// Selects the next agent to participate in the group chat based on the provided chat history and team.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="team">The group of agents participating in the chat.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> containing the identifier of the next agent as a string.</returns>
public abstract ValueTask<GroupChatManagerResult<string>> SelectNextAgent(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether user input should be requested based on the provided chat history.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> indicating whether user input should be requested.</returns>
public abstract ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the group chat should be terminated based on the provided chat history and invocation count.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> indicating whether the chat should be terminated.</returns>
public virtual ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref this._invocationCount);
bool resultValue = false;
string reason = "Maximum number of invocations has not been reached.";
if (this.InvocationCount > this.MaximumInvocationCount)
{
resultValue = true;
reason = "Maximum number of invocations reached.";
}
GroupChatManagerResult<bool> result = new(resultValue) { Reason = reason };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<bool>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
}
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An <see cref="OrchestrationActor"/> used to manage a <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
/// </summary>
internal sealed class GroupChatManagerActor :
OrchestrationActor,
IHandle<GroupChatMessages.InputTask>,
IHandle<GroupChatMessages.Group>
{
/// <summary>
/// A common description for the manager.
/// </summary>
public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task.";
private readonly AgentType _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(AgentId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, AgentType orchestrationType, ILogger? logger = null)
: base(id, runtime, context, DefaultDescription, logger)
{
this._chat = [];
this._manager = manager;
this._orchestrationType = orchestrationType;
this._team = team;
}
/// <inheritdoc/>
public async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext)
{
this.Logger.LogChatManagerInit(this.Id);
this._chat.AddRange(item.Messages);
await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
await this.ManageAsync(messageContext).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext)
{
this.Logger.LogChatManagerInvoke(this.Id);
this._chat.AddRange(item.Messages);
await this.ManageAsync(messageContext).ConfigureAwait(false);
}
private async ValueTask ManageAsync(MessageContext messageContext)
{
if (this._manager.InteractiveCallback != null)
{
GroupChatManagerResult<bool> inputResult = await this._manager.ShouldRequestUserInput(this._chat, messageContext.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(input.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
}
}
GroupChatManagerResult<bool> terminateResult = await this._manager.ShouldTerminate(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatManagerTerminate(this.Id, terminateResult.Value, terminateResult.Reason);
if (terminateResult.Value)
{
GroupChatManagerResult<string> filterResult = await this._manager.FilterResults(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatManagerResult(this.Id, filterResult.Value, filterResult.Reason);
await this.PublishMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false);
return;
}
GroupChatManagerResult<string> selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, messageContext.CancellationToken).ConfigureAwait(false);
AgentType selectionType = this._team[selectionResult.Value].Type;
this.Logger.LogChatManagerSelect(this.Id, selectionType);
await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, messageContext.CancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// Common messages used for agent chat patterns.
/// </summary>
public static class GroupChatMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
internal static readonly ChatMessage Empty = new();
/// <summary>
/// Broadcast a message to all <see cref="GroupChatAgentActor"/>.
/// </summary>
public sealed class Group
{
/// <summary>
/// The chat message being broadcast.
/// </summary>
public IEnumerable<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// Reset/clear the conversation history for all <see cref="GroupChatAgentActor"/>.
/// </summary>
public sealed class Reset;
/// <summary>
/// The final result.
/// </summary>
public sealed class Result
{
/// <summary>
/// The chat response message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Signal a <see cref="GroupChatAgentActor"/> to respond.
/// </summary>
public sealed class Speak;
/// <summary>
/// The input task.
/// </summary>
public sealed class InputTask
{
/// <summary>
/// A task that does not require any action.
/// </summary>
public static readonly InputTask None = new();
/// <summary>
/// The input that defines the task goal.
/// </summary>
public IEnumerable<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Group"/>.
/// </summary>
public static Group AsGroupMessage(this ChatMessage message) => new() { Messages = [message] };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Group"/>.
/// </summary>
public static Group AsGroupMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = messages };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static InputTask AsInputTaskMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = messages };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this string text) => new() { Message = new(ChatRole.Assistant, text) };
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <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 Agent[] members)
: base(manager, members)
{
}
}
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An orchestration that coordinates a group-chat.
/// </summary>
public class GroupChatOrchestration<TInput, TOutput> :
AgentOrchestration<TInput, TOutput>
{
internal const string DefaultAgentDescription = "A helpful agent.";
private readonly GroupChatManager _manager;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="manager">The manages the flow of the group-chat.</param>
/// <param name="agents">The agents participating in the orchestration.</param>
public GroupChatOrchestration(GroupChatManager manager, params Agent[] agents)
: base(agents)
{
Throw.IfNull(manager, nameof(manager));
this._manager = manager;
}
/// <inheritdoc />
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
if (!entryAgent.HasValue)
{
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
}
return runtime.PublishMessageAsync(input.AsInputTaskMessage(), entryAgent.Value);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<GroupChatMessages.Result>(response => [response.Message]).ConfigureAwait(false);
int agentCount = 0;
GroupChatTeam team = [];
foreach (Agent agent in this.Members)
{
++agentCount;
AgentType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false);
string name = agent.Name ?? agent.Id ?? agentType;
string? description = agent.Description;
team[name] = (agentType, description ?? DefaultAgentDescription);
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
}
AgentType managerType =
await runtime.RegisterOrchestrationAgentAsync(
this.FormatAgentType(context.Topic, "Manager"),
(agentId, runtime) =>
{
GroupChatManagerActor actor = new(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger<GroupChatManagerActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, managerType, "MANAGER");
await runtime.SubscribeAsync(managerType, context.Topic).ConfigureAwait(false);
return managerType;
ValueTask<AgentType> RegisterAgentAsync(Agent agent, int agentCount) =>
runtime.RegisterOrchestrationAgentAsync(
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
(agentId, runtime) =>
{
GroupChatAgentActor actor = new(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger<GroupChatAgentActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
});
}
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// Describes a team of agents participating in a group chat.
/// </summary>
public class GroupChatTeam : Dictionary<string, (string Type, string Description)>;
/// <summary>
/// Extensions for <see cref="GroupChatTeam"/>.
/// </summary>
public static class ChatGroupExtensions
{
/// <summary>
/// Format the names of the agents in the team as a comma delimimted list.
/// </summary>
/// <param name="team">The agent team</param>
/// <returns>A comma delimimted list of agent name.</returns>
public static string FormatNames(this GroupChatTeam team) => string.Join(",", team.Select(t => t.Key));
/// <summary>
/// Format the names and descriptions of the agents in the team as a markdown list.
/// </summary>
/// <param name="team">The agent team</param>
/// <returns>A markdown list of agent names and descriptions.</returns>
public static string FormatList(this GroupChatTeam team) => string.Join(Environment.NewLine, team.Select(t => $"- {t.Key}: {t.Value.Description}"));
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// A <see cref="GroupChatManager"/> that selects agents in a round-robin fashion.
/// </summary>
/// <remarks>
/// Subclass this class to customize filter and user interaction behavior.
/// </remarks>
public class RoundRobinGroupChatManager : GroupChatManager
{
private int _currentAgentIndex;
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<string> result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<string>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<string>> SelectNextAgent(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default)
{
string nextAgent = team.Skip(this._currentAgentIndex).First().Key;
this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count;
GroupChatManagerResult<string> result = new(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<string>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = new(false) { Reason = "The default round-robin group chat manager does not request user input." };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<bool>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
}
@@ -0,0 +1,211 @@
// 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.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// An actor used with the <see cref="HandoffOrchestration{TInput,TOutput}"/>.
/// </summary>
internal sealed class HandoffActor :
AgentActor,
IHandle<HandoffMessages.InputTask>,
IHandle<HandoffMessages.Request>,
IHandle<HandoffMessages.Response>
{
private readonly ChatClientAgent _chatAgent;
private readonly HandoffLookup _handoffs;
private readonly AgentType _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="Agent"/>.</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(AgentId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, AgentType resultHandoff, ILogger<HandoffActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
if (handoffs.ContainsKey(agent.Name ?? agent.Id))
{
throw new ArgumentException($"The agent {agent.Name ?? agent.Id} cannot have a handoff to itself.", nameof(handoffs));
}
this._cache = [];
this._chatAgent = agent;
this._handoffs = handoffs;
this._resultHandoff = resultHandoff;
this._options =
new ChatOptions
{
Tools = [.. this.CreateHandoffFunctions()],
ToolMode = ChatToolMode.Auto
};
}
/// <inheritdoc/>
protected override Task InvokeAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentRunOptions options,
CancellationToken cancellationToken = default) =>
this._chatAgent.RunAsync(
[.. messages],
this.Thread,
options,
this._options,
cancellationToken);
/// <inheritdoc/>
protected override IAsyncEnumerable<ChatResponseUpdate> InvokeStreamingAsync(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 OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
/// <inheritdoc/>
public ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext)
{
this._taskSummary = null;
this._cache.AddRange(item.Messages);
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext)
{
this._cache.Add(item.Message);
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext)
{
try
{
this.Logger.LogHandoffAgentInvoke(this.Id);
while (this._taskSummary == null)
{
ChatMessage response;
try
{
response = await this.InvokeAsync(this._cache, messageContext.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 { Message = response }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false);
}
if (this._handoffAgent != null)
{
AgentType handoffType = this._handoffs[this._handoffAgent].AgentType;
await this.PublishMessageAsync(new HandoffMessages.Request(), handoffType, messageContext.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 { Message = input }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false);
this._cache.Add(input);
continue;
}
await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", messageContext.CancellationToken).ConfigureAwait(false);
}
}
catch (Exception exception)
{
this.Logger.LogError(exception, "Failure");
throw;
}
}
private IEnumerable<AIFunction> CreateHandoffFunctions()
{
yield return AIFunctionFactory.Create(
this.EndAsync,
name: "end_task",
description: "Complete the task with a summary when no further requests are given.");
foreach (KeyValuePair<string, (AgentType _, string Description)> handoff in this._handoffs)
{
AIFunction handoffFunction =
AIFunctionFactory.Create(
() => this.Handoff(handoff.Key),
name: $"transfer_to_{handoff.Key}",
description: handoff.Value.Description);
yield return handoffFunction;
}
}
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 { Message = new ChatMessage(ChatRole.Assistant, summary) }, this._resultHandoff, cancellationToken).ConfigureAwait(false);
if (FunctionInvokingChatClient.CurrentContext is not null)
{
FunctionInvokingChatClient.CurrentContext.Terminate = true;
}
}
}
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// A message that describes the input task and captures results for a <see cref="HandoffOrchestration{TInput,TOutput}"/>.
/// </summary>
internal static class HandoffMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
internal static readonly ChatMessage Empty = new();
/// <summary>
/// The input message.
/// </summary>
public sealed class InputTask
{
/// <summary>
/// The orchestration input messages.
/// </summary>
public IList<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// The final result.
/// </summary>
public sealed class Result
{
/// <summary>
/// The orchestration result message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <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 class Response
{
/// <summary>
/// The chat response message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static InputTask AsInputTaskMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = [.. messages] };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this ChatMessage message) => new() { Message = message };
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <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 Agent[] members)
: base(handoffs, members)
{
}
}
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <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">The agents participating in the orchestration.</param>
public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agents)
: base(agents)
{
// Create list of distinct agent names
HashSet<string> agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal);
agentNames.Add(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 new ArgumentException($"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}", nameof(handoffs));
}
this._handoffs = handoffs;
}
/// <summary>
/// Gets or sets the callback to be invoked for interactive input.
/// </summary>
public OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
/// <inheritdoc />
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
if (!entryAgent.HasValue)
{
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
}
await runtime.PublishMessageAsync(input.AsInputTaskMessage(), topic).ConfigureAwait(false);
await runtime.PublishMessageAsync(new HandoffMessages.Request(), entryAgent.Value).ConfigureAwait(false);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<HandoffMessages.Result>(response => [response.Message]).ConfigureAwait(false);
// Each agent handsoff its result to the next agent.
Dictionary<string, AgentType> agentMap = [];
Dictionary<string, HandoffLookup> handoffMap = [];
AgentType agentType = outputType;
for (int index = this.Members.Count - 1; index >= 0; --index)
{
Agent 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
};
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).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 AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
}
@@ -0,0 +1,147 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// Defines the handoff relationships for a given agent.
/// Maps target agent names/IDs to handoff descriptions.
/// </summary>
public sealed class AgentHandoffs : Dictionary<string, string>
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentHandoffs"/> class with no handoff relationships.
/// </summary>
public AgentHandoffs() { }
/// <summary>
/// Initializes a new instance of the <see cref="AgentHandoffs"/> class with the specified handoff relationships.
/// </summary>
/// <param name="handoffs">A dictionary mapping target agent names/IDs to handoff descriptions.</param>
public AgentHandoffs(Dictionary<string, string> handoffs) : base(handoffs) { }
}
/// <summary>
/// Defines the orchestration handoff relationships for all agents in the system.
/// Maps source agent names/IDs to their <see cref="AgentHandoffs"/>.
/// </summary>
public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
{
/// <summary>
/// Initializes a new instance of the <see cref="OrchestrationHandoffs"/> class with no handoff relationships.
/// </summary>
/// <param name="firstAgent">The first agent to be invoked (prior to any handoff).</param>
public OrchestrationHandoffs(Agent firstAgent)
: this(firstAgent.Name ?? firstAgent.Id)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="OrchestrationHandoffs"/> class with no handoff relationships.
/// </summary>
/// <param name="firstAgentName">The name of the first agent to be invoked (prior to any handoff).</param>
public OrchestrationHandoffs(string firstAgentName)
{
Throw.IfNullOrWhitespace(firstAgentName, nameof(firstAgentName));
this.FirstAgentName = firstAgentName;
}
/// <summary>
/// The name of the first agent to be invoked (prior to any handoff).
/// </summary>
public string FirstAgentName { get; }
/// <summary>
/// Adds handoff relationships from a source agent to one or more target agents.
/// Each target agent's name or ID is mapped to its description.
/// </summary>
/// <param name="source">The source agent.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs StartWith(Agent source) => new(source);
}
/// <summary>
/// Extension methods for building and modifying <see cref="OrchestrationHandoffs"/> relationships.
/// </summary>
public static class OrchestrationHandoffsExtensions
{
/// <summary>
/// Adds handoff relationships from a source agent to one or more target agents.
/// Each target agent's name or ID is mapped to its description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="source">The source agent.</param>
/// <param name="targets">The target agents to add as handoff targets for the source agent.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, params Agent[] targets)
{
string key = source.Name ?? source.Id;
AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(key);
foreach (Agent target in targets)
{
agentHandoffs[target.Name ?? target.Id] = target.Description ?? string.Empty;
}
return handoffs;
}
/// <summary>
/// Adds a handoff relationship from a source agent to a target agent with a custom description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="source">The source agent.</param>
/// <param name="target">The target agent.</param>
/// <param name="description">The handoff description.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, Agent target, string description)
=> handoffs.Add(source.Name ?? source.Id, target.Name ?? target.Id, description);
/// <summary>
/// Adds a handoff relationship from a source agent to a target agent name/ID with a custom description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="source">The source agent.</param>
/// <param name="targetName">The target agent's name or ID.</param>
/// <param name="description">The handoff description.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, string targetName, string description)
=> handoffs.Add(source.Name ?? source.Id, targetName, description);
/// <summary>
/// Adds a handoff relationship from a source agent name/ID to a target agent name/ID with a custom description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="sourceName">The source agent's name or ID.</param>
/// <param name="targetName">The target agent's name or ID.</param>
/// <param name="description">The handoff description.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, string sourceName, string targetName, string description)
{
AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(sourceName);
agentHandoffs[targetName] = description;
return handoffs;
}
private static AgentHandoffs GetAgentHandoffs(this OrchestrationHandoffs handoffs, string key)
{
if (!handoffs.TryGetValue(key, out AgentHandoffs? agentHandoffs))
{
agentHandoffs = [];
handoffs[key] = agentHandoffs;
}
return agentHandoffs;
}
}
/// <summary>
/// Handoff relationships post-processed into a name-based lookup table that includes the agent type and handoff description.
/// Maps agent names/IDs to a tuple of <see cref="AgentType"/> and handoff description.
/// </summary>
internal sealed class HandoffLookup : Dictionary<string, (AgentType AgentType, string Description)>;
@@ -0,0 +1,157 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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(
EventId = 0,
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(
EventId = 0,
Level = LogLevel.Information,
Message = "REGISTER ACTOR {Orchestration} {label}: {AgentType}")]
public static partial void LogRegisterActor(
this ILogger logger,
string orchestration,
AgentType agentType,
string label);
/// <summary>
/// Logs agent actor registration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "REGISTER ACTOR {Orchestration} {label} #{Count}: {AgentType}")]
public static partial void LogRegisterActor(
this ILogger logger,
string orchestration,
AgentType agentType,
string label,
int count);
/// <summary>
/// Logs the end of the registration phase for an orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
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(
EventId = 0,
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(
EventId = 0,
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(
EventId = 0,
Level = LogLevel.Information,
Message = "START {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationStart(
this ILogger logger,
string orchestration,
AgentId agentId);
/// <summary>
/// Logs that orchestration request actor is active
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "INIT {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationRequestInvoke(
this ILogger logger,
string orchestration,
AgentId agentId);
/// <summary>
/// Logs that orchestration request actor experienced an unexpected failure.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "FAILURE {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationRequestFailure(
this ILogger logger,
string orchestration,
AgentId agentId,
Exception exception);
/// <summary>
/// Logs that orchestration result actor is active
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "EXIT {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationResultInvoke(
this ILogger logger,
string orchestration,
AgentId agentId);
/// <summary>
/// Logs that orchestration result actor experienced an unexpected failure.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "FAILURE {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationResultFailure(
this ILogger logger,
string orchestration,
AgentId agentId,
Exception exception);
}
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.Concurrent;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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(
EventId = 0,
Level = LogLevel.Trace,
Message = "REQUEST Concurrent agent [{AgentId}]")]
public static partial void LogConcurrentAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Concurrent agent [{AgentId}]: {Message}")]
public static partial void LogConcurrentAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
/// <summary>
/// Logs result capture.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")]
public static partial void LogConcurrentResultCapture(
this ILogger logger,
AgentId agentId,
int resultCount,
int expectedCount);
}
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.GroupChat;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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(
EventId = 0,
Level = LogLevel.Trace,
Message = "CHAT AGENT invoked [{AgentId}]")]
public static partial void LogChatAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "CHAT AGENT result [{AgentId}]: {Message}")]
public static partial void LogChatAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER initialized [{AgentId}]")]
public static partial void LogChatManagerInit(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER invoked [{AgentId}]")]
public static partial void LogChatManagerInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")]
public static partial void LogChatManagerTerminate(
this ILogger logger,
AgentId agentId,
bool result,
string reason);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")]
public static partial void LogChatManagerSelect(
this ILogger logger,
AgentId agentId,
AgentType nextAgent);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")]
public static partial void LogChatManagerResult(
this ILogger logger,
AgentId agentId,
string result,
string reason);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")]
public static partial void LogChatManagerInput(
this ILogger logger,
AgentId agentId,
bool result,
string reason);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "CHAT AGENT user-input [{AgentId}]: {Message}")]
public static partial void LogChatManagerUserInput(
this ILogger logger,
AgentId agentId,
string? message);
}
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.Handoff;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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(
EventId = 0,
Level = LogLevel.Trace,
Message = "REQUEST Handoff agent [{AgentId}]")]
public static partial void LogHandoffAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Handoff agent [{AgentId}]: {Message}")]
public static partial void LogHandoffAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "TOOL Handoff [{AgentId}]: {Name}")]
public static partial void LogHandoffFunctionCall(
this ILogger logger,
AgentId agentId,
string name);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Handoff summary [{AgentId}]: {Summary}")]
public static partial void LogHandoffSummary(
this ILogger logger,
AgentId agentId,
string? summary);
}
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="OrchestrationResult{TValue}"/>.
/// </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 OrchestrationResultLogMessages
{
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> awaiting the orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "AWAIT {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultAwait(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> timeout while awaiting the orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "TIMEOUT {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultTimeout(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> cancelled the orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "CANCELLED {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultCancelled(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> the awaited the orchestration has completed.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "COMPLETE {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultComplete(
this ILogger logger,
string orchestration,
TopicId topic);
}
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.Sequential;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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(
EventId = 0,
Level = LogLevel.Trace,
Message = "REQUEST Sequential agent [{AgentId}]")]
public static partial void LogSequentialAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Sequential agent [{AgentId}]: {Message}")]
public static partial void LogSequentialAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
#if !NET5_0_OR_GREATER
using System.ComponentModel;
namespace System.Runtime.CompilerServices;
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
#endif
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<RootNamespace>Microsoft.Agents.Orchestration</RootNamespace>
<VersionSuffix>alpha</VersionSuffix>
<IsAotCompatible>false</IsAotCompatible>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Orchestration Framework</Title>
<Description>Contains the Microsoft Agent Orchestration Framework.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.Abstractions" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.Core" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.Orchestration.UnitTests" />
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Base abstractions for any actor that participates in an orchestration.
/// </summary>
public abstract class OrchestrationActor : BaseAgent
{
/// <summary>
/// Initializes a new instance of the <see cref="OrchestrationActor"/> class.
/// </summary>
protected OrchestrationActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, string description, 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 async ValueTask PublishMessageAsync(
object message,
AgentType agentType,
CancellationToken cancellationToken = default)
{
await base.PublishMessageAsync(message, new TopicId(agentType), messageId: null, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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,
OrchestrationResponseCallback? responseCallback,
OrchestrationStreamingCallback? streamingCallback,
ILoggerFactory loggerFactory,
CancellationToken cancellation)
{
this.Orchestration = orchestration;
this.Topic = topic;
this.ResponseCallback = responseCallback;
this.StreamingResponseCallback = streamingCallback;
this.LoggerFactory = loggerFactory;
this.Cancellation = cancellation;
}
/// <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 Cancellation { 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 OrchestrationResponseCallback? ResponseCallback { get; }
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationStreamingCallback? StreamingResponseCallback { get; }
}
@@ -0,0 +1,125 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
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 class OrchestrationResult<TValue> : IDisposable
{
private readonly OrchestrationContext _context;
private readonly CancellationTokenSource _cancelSource;
private readonly TaskCompletionSource<TValue> _completion;
private readonly ILogger _logger;
private bool _isDisposed;
internal OrchestrationResult(OrchestrationContext context, TaskCompletionSource<TValue> completion, CancellationTokenSource orchestrationCancelSource, ILogger logger)
{
this._cancelSource = orchestrationCancelSource;
this._context = context;
this._completion = completion;
this._logger = logger;
}
/// <summary>
/// Releases all resources used by the <see cref="OrchestrationResult{TValue}"/> instance.
/// </summary>
public void Dispose()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <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>
/// Asynchronously retrieves the orchestration result value.
/// If a timeout is specified, the method will throw a <see cref="TimeoutException"/>
/// if the orchestration does not complete within the allotted time.
/// </summary>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the maximum wait duration.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{TValue}"/> representing the result of the orchestration.</returns>
/// <exception cref="ObjectDisposedException">Thrown if this instance has been disposed.</exception>
/// <exception cref="TimeoutException">Thrown if the orchestration does not complete within the specified timeout period.</exception>
public async ValueTask<TValue> GetValueAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
{
#if !NETCOREAPP
if (this._isDisposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
#else
ObjectDisposedException.ThrowIf(this._isDisposed, this);
#endif
this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic);
if (timeout.HasValue)
{
Task[] tasks = { this._completion.Task };
if (!Task.WaitAll(tasks, timeout.Value))
{
this._logger.LogOrchestrationResultTimeout(this.Orchestration, this.Topic);
throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout}).");
}
}
this._logger.LogOrchestrationResultComplete(this.Orchestration, this.Topic);
return await this._completion.Task.ConfigureAwait(false);
}
/// <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 !NETCOREAPP
if (this._isDisposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
#else
ObjectDisposedException.ThrowIf(this._isDisposed, this);
#endif
this._logger.LogOrchestrationResultCancelled(this.Orchestration, this.Topic);
this._cancelSource.Cancel();
this._completion.SetCanceled();
}
private void Dispose(bool disposing)
{
if (!this._isDisposed)
{
if (disposing)
{
this._cancelSource.Dispose();
}
this._isDisposed = true;
}
}
}
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <summary>
/// An actor used with the <see cref="SequentialOrchestration{TInput,TOutput}"/>.
/// </summary>
internal sealed class SequentialActor :
AgentActor,
IHandle<SequentialMessages.Request>,
IHandle<SequentialMessages.Response>
{
private readonly AgentType _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="Agent"/>.</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(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType nextAgent, ILogger<SequentialActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
logger?.LogInformation("ACTOR {ActorId} {NextAgent}", this.Id, nextAgent);
this._nextAgent = nextAgent;
}
/// <inheritdoc/>
public async ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext)
{
await this.InvokeAgentAsync(item.Messages, messageContext).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext)
{
await this.InvokeAgentAsync([item.Message], messageContext).ConfigureAwait(false);
}
private async ValueTask InvokeAgentAsync(IList<ChatMessage> input, MessageContext messageContext)
{
this.Logger.LogInformation("INVOKE {ActorId} {NextAgent}", this.Id, this._nextAgent);
this.Logger.LogSequentialAgentInvoke(this.Id);
ChatMessage response = await this.InvokeAsync(input, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogSequentialAgentResult(this.Id, response.Text);
await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent).ConfigureAwait(false);
}
}
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <summary>
/// A message that describes the input task and captures results for a <see cref="SequentialOrchestration{TInput,TOutput}"/>.
/// </summary>
internal static class SequentialMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
public static readonly ChatMessage Empty = new();
/// <summary>
/// Represents a request containing a sequence of chat messages to be processed by the sequential orchestration.
/// </summary>
public sealed class Request
{
/// <summary>
/// The request input.
/// </summary>
public IList<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// Represents a response containing the result message from the sequential orchestration.
/// </summary>
public sealed class Response
{
/// <summary>
/// The response message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="SequentialMessages.Request"/>.
/// </summary>
/// <param name="message">The chat message to include in the request.</param>
/// <returns>A <see cref="SequentialMessages.Request"/> containing the provided messages.</returns>
public static Request AsRequestMessage(this ChatMessage message) => new() { Messages = [message] };
/// <summary>
/// Extension method to convert a collection of <see cref="ChatMessage"/> to a <see cref="SequentialMessages.Request"/>.
/// </summary>
/// <param name="messages">The collection of chat messages to include in the request.</param>
/// <returns>A <see cref="SequentialMessages.Request"/> containing the provided messages.</returns>
public static Request AsRequestMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = [.. messages] };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="SequentialMessages.Response"/>.
/// </summary>
/// <param name="message">The chat message to include in the response.</param>
/// <returns>A <see cref="SequentialMessages.Response"/> containing the provided message.</returns>
public static Response AsResponseMessage(this ChatMessage message) => new() { Message = message };
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <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 Agent[] members)
: base(members)
{
}
}
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <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 Agent[] agents)
: base(agents)
{
}
/// <inheritdoc />
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
if (!entryAgent.HasValue)
{
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
}
await runtime.PublishMessageAsync(input.AsRequestMessage(), entryAgent.Value).ConfigureAwait(false);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<SequentialMessages.Response>(response => [response.Message]).ConfigureAwait(false);
// Each agent handsoff its result to the next agent.
AgentType nextAgent = outputType;
for (int index = this.Members.Count - 1; index >= 0; --index)
{
Agent agent = this.Members[index];
nextAgent = await RegisterAgentAsync(agent, index, nextAgent).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, nextAgent, "MEMBER", index + 1);
}
return nextAgent;
ValueTask<AgentType> RegisterAgentAsync(Agent agent, int index, AgentType nextAgent) =>
runtime.RegisterOrchestrationAgentAsync(
this.GetAgentType(context.Topic, index),
(agentId, runtime) =>
{
SequentialActor actor = new(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger<SequentialActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
});
}
private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
}
@@ -0,0 +1,79 @@
// 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;
namespace Microsoft.Agents.Orchestration.Transforms;
internal static class DefaultTransforms
{
public static ValueTask<IEnumerable<ChatMessage>> FromInput<TInput>(TInput input, CancellationToken cancellationToken = default)
{
#if !NETCOREAPP
return new ValueTask<IEnumerable<ChatMessage>>(TransformInput());
#else
return ValueTask.FromResult(TransformInput());
#endif
IEnumerable<ChatMessage> TransformInput() =>
input switch
{
IEnumerable<ChatMessage> messages => messages,
ChatMessage message => [message],
string text => [new ChatMessage(ChatRole.User, text)],
_ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input))]
};
}
public static ValueTask<TOutput> ToOutput<TOutput>(IList<ChatMessage> result, CancellationToken cancellationToken = default)
{
bool isSingleResult = result.Count == 1;
TOutput output =
GetDefaultOutput() ??
GetObjectOutput() ??
throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}.");
return new ValueTask<TOutput>(output);
TOutput? GetObjectOutput()
{
if (!isSingleResult)
{
return default;
}
try
{
return JsonSerializer.Deserialize<TOutput>(result[0].Text);
}
catch (JsonException)
{
return default;
}
}
TOutput? GetDefaultOutput()
{
object? output = null;
if (typeof(TOutput).IsAssignableFrom(result.GetType()))
{
output = (object)result;
}
else if (isSingleResult && typeof(ChatMessage).IsAssignableFrom(typeof(TOutput)))
{
output = (object)result[0];
}
else if (isSingleResult && typeof(string) == typeof(TOutput))
{
output = result[0].Text ?? string.Empty;
}
return (TOutput?)output;
}
}
}
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Transforms;
/// <summary>
/// Delegate for transforming an input of type <typeparamref name="TInput"/> into a collection of <see cref="ChatMessage"/>.
/// This is typically used to convert user or system input into a format suitable for chat orchestration.
/// </summary>
/// <param name="input">The input object to transform.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> containing an enumerable of <see cref="ChatMessage"/> representing the transformed input.</returns>
public delegate ValueTask<IEnumerable<ChatMessage>> OrchestrationInputTransform<TInput>(TInput input, CancellationToken cancellationToken = default);
/// <summary>
/// Delegate for transforming a <see cref="ChatMessage"/> into an output of type <typeparamref name="TOutput"/>.
/// This is typically used to convert a chat response into a desired output format.
/// </summary>
/// <param name="result">The result messages to transform.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> containing the transformed output of type <typeparamref name="TOutput"/>.</returns>
public delegate ValueTask<TOutput> OrchestrationOutputTransform<TOutput>(IList<ChatMessage> result, CancellationToken cancellationToken = default);
/// <summary>
/// Delegate for transforming the internal result message for an orchestration into a <see cref="ChatMessage"/>.
/// </summary>
/// <typeparam name="TResult">The result message type</typeparam>
/// <param name="result">The result messages</param>
/// <returns>The orchestration result as a <see cref="ChatMessage"/>.</returns>
public delegate IList<ChatMessage> OrchestrationResultTransform<TResult>(TResult result);
@@ -0,0 +1,58 @@
// 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.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration.Transforms;
/// <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="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, CancellationToken cancellationToken = default)
{
IEnumerable<ChatMessage> input =
[
new ChatMessage(ChatRole.System, this.Instructions),
.. messages,
];
ChatResponse<TOutput> response = await this._client.GetResponseAsync<TOutput>(input, this._options, useJsonSchemaResponseFormat: true, cancellationToken).ConfigureAwait(false);
return response.Result;
}
}
+22 -11
View File
@@ -80,7 +80,7 @@ public abstract class BaseSample : TextWriter
/// <param name="message">The text of the message to be sent. Cannot be null or empty.</param>
protected void WriteUserMessage(string message)
{
this.WriteResponseOutput(new ChatResponse(new ChatMessage(ChatRole.User, message)), printUsage: false);
this.WriteMessageOutput(new ChatMessage(ChatRole.User, message));
}
/// <summary>
@@ -101,8 +101,28 @@ public abstract class BaseSample : TextWriter
}
var message = chatResponse.Messages.Last();
this.WriteMessageOutput(message);
WriteUsage();
void WriteUsage()
{
if (!(printUsage ?? true) || chatResponse.Usage is null) { return; }
UsageDetails usageDetails = chatResponse.Usage;
Console.WriteLine($" [Usage] Tokens: {usageDetails.TotalTokenCount}, Input: {usageDetails.InputTokenCount}, Output: {usageDetails.OutputTokenCount}");
}
}
/// <summary>
/// Writes the given chat message to the console.
/// </summary>
/// <param name="message">The specified message</param>
protected void WriteMessageOutput(ChatMessage message)
{
string authorExpression = message.Role == ChatRole.User ? string.Empty : FormatAuthor();
string contentExpression = string.IsNullOrWhiteSpace(chatResponse.Text) ? string.Empty : chatResponse.Text;
string contentExpression = message.Text.Trim();
bool isCode = false; //message.AdditionalProperties?.ContainsKey(OpenAIAssistantAgent.CodeInterpreterMetadataKey) ?? false;
string codeMarker = isCode ? "\n [CODE]\n" : " ";
Console.WriteLine($"\n# {message.Role}{authorExpression}:{codeMarker}{contentExpression}");
@@ -124,16 +144,7 @@ public abstract class BaseSample : TextWriter
}
}
WriteUsage(chatResponse.Usage);
string FormatAuthor() => message.AuthorName is not null ? $" - {message.AuthorName ?? " * "}" : string.Empty;
void WriteUsage(UsageDetails? usageDetails)
{
if (!(printUsage ?? true) || usageDetails is null) { return; }
Console.WriteLine($" [Usage] Tokens: {usageDetails.TotalTokenCount}, Input: {usageDetails.InputTokenCount}, Output: {usageDetails.OutputTokenCount}");
}
}
/// <summary>
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAIClient = OpenAI.OpenAIClient;
namespace Microsoft.Shared.SampleUtilities;
/// <summary>
/// Provides a base class for orchestration samples that demonstrates agent orchestration scenarios.
/// Inherits from <see cref="BaseSample"/> and provides utility methods for creating agents, chat clients,
/// and writing responses to the console or test output.
/// </summary>
public abstract class OrchestrationSample : BaseSample
{
/// <summary>
/// This constant defines the timeout duration for result retrieval, measured in seconds.
/// </summary>
protected const int ResultTimeoutInSeconds = 30;
/// <summary>
/// Creates a new <see cref="ChatClientAgent"/> instance using the specified instructions, description, name, and functions.
/// </summary>
/// <param name="instructions">The instructions to provide to the agent.</param>
/// <param name="description">An optional description for the agent.</param>
/// <param name="name">An optional name for the agent.</param>
/// <param name="functions">A set of <see cref="AIFunction"/> instances to be used as tools by the agent.</param>
/// <returns>A new <see cref="ChatClientAgent"/> instance configured with the provided parameters.</returns>
protected ChatClientAgent CreateAgent(string instructions, string? description = null, string? name = null, params AIFunction[] functions)
{
// Get the chat client to use for the agent.
using IChatClient chatClient = CreateChatClient();
ChatClientAgentOptions options =
new()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = new() { Tools = functions, ToolMode = ChatToolMode.Auto }
};
return new ChatClientAgent(chatClient, options);
}
/// <summary>
/// Creates and configures a new <see cref="IChatClient"/> instance using the OpenAI client and test configuration.
/// </summary>
/// <returns>A configured <see cref="IChatClient"/> instance ready for use with agents.</returns>
protected IChatClient CreateChatClient()
{
return new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation()
.Build();
}
/// <summary>
/// Display the provided history.
/// </summary>
/// <param name="history">The history to display</param>
protected void DisplayHistory(IEnumerable<ChatMessage> history)
{
Console.WriteLine("\n\nORCHESTRATION HISTORY");
foreach (ChatMessage message in history)
{
this.WriteMessageOutput(message);
}
}
/// <summary>
/// Writes the provided chat response messages to the console or test output, including role and author information.
/// </summary>
/// <param name="response">An enumerable of <see cref="ChatMessage"/> objects to write.</param>
protected static void WriteResponse(IEnumerable<ChatMessage> response)
{
foreach (ChatMessage message in response)
{
if (!string.IsNullOrEmpty(message.Text))
{
System.Console.WriteLine($"\n# RESPONSE {message.Role}{(message.AuthorName is not null ? $" - {message.AuthorName}" : string.Empty)}: {message}");
}
}
}
/// <summary>
/// Writes the streamed chat response updates to the console or test output, including role and author information.
/// </summary>
/// <param name="streamedResponses">An enumerable of <see cref="ChatResponseUpdate"/> objects representing streamed responses.</param>
protected static void WriteStreamedResponse(IEnumerable<ChatResponseUpdate> streamedResponses)
{
string? authorName = null;
ChatRole? authorRole = null;
StringBuilder builder = new();
foreach (ChatResponseUpdate response in streamedResponses)
{
authorName ??= response.AuthorName;
authorRole ??= response.Role;
if (!string.IsNullOrEmpty(response.Text))
{
builder.Append($"({JsonSerializer.Serialize(response.Text)})");
}
}
if (builder.Length > 0)
{
System.Console.WriteLine($"\n# STREAMED {authorRole ?? ChatRole.Assistant}{(authorName is not null ? $" - {authorName}" : string.Empty)}: {builder}\n");
}
}
/// <summary>
/// Provides monitoring and callback functionality for orchestration scenarios, including tracking streamed responses and message history.
/// </summary>
protected sealed class OrchestrationMonitor
{
/// <summary>
/// Gets the list of streamed response updates received so far.
/// </summary>
public List<ChatResponseUpdate> StreamedResponses { get; } = [];
/// <summary>
/// Gets the list of chat messages representing the conversation history.
/// </summary>
public List<ChatMessage> History { get; } = [];
/// <summary>
/// Callback to handle a batch of chat messages, adding them to history and writing them to output.
/// </summary>
/// <param name="response">The collection of <see cref="ChatMessage"/> objects to process.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask ResponseCallback(IEnumerable<ChatMessage> response)
{
this.History.AddRange(response);
WriteResponse(response);
return new ValueTask();
}
/// <summary>
/// Callback to handle a streamed chat response update, adding it to the list and writing output if final.
/// </summary>
/// <param name="streamedResponse">The <see cref="ChatResponseUpdate"/> to process.</param>
/// <param name="isFinal">Indicates whether this is the final update in the stream.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask StreamingResultCallback(ChatResponseUpdate streamedResponse, bool isFinal)
{
this.StreamedResponses.Add(streamedResponse);
if (isFinal)
{
WriteStreamedResponse(this.StreamedResponses);
this.StreamedResponses.Clear();
}
return new ValueTask();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="BaseSample"/> class, setting up logging, configuration, and
/// optionally redirecting <see cref="System.Console"/> output to the test output.
/// </summary>
/// <remarks>This constructor initializes logging using an <see cref="XunitLogger"/> and sets up
/// configuration from multiple sources, including a JSON file, environment variables, and user secrets.
/// If <paramref name="redirectSystemConsoleOutput"/> is <see langword="true"/>, calls to <see cref="System.Console"/>
/// will be redirected to the test output provided by <paramref name="output"/>.
/// </remarks>
/// <param name="output">The <see cref="ITestOutputHelper"/> instance used to write test output.</param>
/// <param name="redirectSystemConsoleOutput">
/// A value indicating whether <see cref="System.Console"/> output should be redirected to the test output. <see langword="true"/> to redirect; otherwise, <see langword="false"/>.
/// </param>
protected OrchestrationSample(ITestOutputHelper output, bool redirectSystemConsoleOutput = true)
: base(output, redirectSystemConsoleOutput)
{
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Shared.Samples;
/// <summary>
/// Resource helper to load resources.
/// </summary>
internal static class Resources
{
private const string ResourceFolder = "Resources";
public static string Read(string fileName) => File.ReadAllText($"{ResourceFolder}/{fileName}");
}