mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
co-authored by
Copilot
Stephen Toub
parent
35c938fb5b
commit
7c8ec5ec19
@@ -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] };
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
+36
@@ -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 };
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
Reference in New Issue
Block a user