mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
First round of cleanup of runtime abstractions (#156)
This commit is contained in:
committed by
GitHub
Unverified
parent
a233d31813
commit
fbf1f10a8a
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
@@ -39,7 +39,7 @@ internal sealed class DynamicallyAccessedMembersAttribute : Attribute
|
||||
/// <param name="memberTypes">The types of members dynamically accessed.</param>
|
||||
public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
|
||||
{
|
||||
MemberTypes = memberTypes;
|
||||
this.MemberTypes = memberTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed class RequiresDynamicCodeAttribute : Attribute
|
||||
/// </param>
|
||||
public RequiresDynamicCodeAttribute(string message)
|
||||
{
|
||||
Message = message;
|
||||
this.Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
@@ -23,7 +23,7 @@ internal sealed class RequiresUnreferencedCodeAttribute : Attribute
|
||||
/// </param>
|
||||
public RequiresUnreferencedCodeAttribute(string message)
|
||||
{
|
||||
Message = message;
|
||||
this.Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
@@ -23,8 +23,8 @@ internal sealed class UnconditionalSuppressMessageAttribute : Attribute
|
||||
/// <param name="checkId">The identifier of the analysis rule the attribute applies to.</param>
|
||||
public UnconditionalSuppressMessageAttribute(string category, string checkId)
|
||||
{
|
||||
Category = category;
|
||||
CheckId = checkId;
|
||||
this.Category = category;
|
||||
this.CheckId = checkId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ public abstract class AgentActor : OrchestrationActor
|
||||
/// <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)
|
||||
protected AgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null)
|
||||
: base(
|
||||
id,
|
||||
runtime,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Orchestration.Transforms;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -15,7 +16,7 @@ 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 sealed class RequestActor : OrchestrationActor
|
||||
{
|
||||
private readonly OrchestrationInputTransform<TInput> _transform;
|
||||
private readonly Func<IEnumerable<ChatMessage>, ValueTask> _action;
|
||||
@@ -32,7 +33,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <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,
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
OrchestrationInputTransform<TInput> transform,
|
||||
@@ -44,6 +45,8 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
this._transform = transform;
|
||||
this._action = action;
|
||||
this._completionSource = completionSource;
|
||||
|
||||
this.RegisterMessageHandler<TInput>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,13 +54,14 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// </summary>
|
||||
/// <param name="item">The input message of type TInput.</param>
|
||||
/// <param name="messageContext">The context of the message, providing additional details.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
public async ValueTask HandleAsync(TInput item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(TInput item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id);
|
||||
try
|
||||
{
|
||||
IEnumerable<ChatMessage> input = await this._transform.Invoke(item).ConfigureAwait(false);
|
||||
IEnumerable<ChatMessage> input = await this._transform.Invoke(item, cancellationToken).ConfigureAwait(false);
|
||||
Task task = this._action.Invoke(input).AsTask();
|
||||
this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id);
|
||||
await task.ConfigureAwait(false);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Orchestration.Transforms;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -15,7 +16,7 @@ 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 sealed class ResultActor<TResult> : OrchestrationActor
|
||||
{
|
||||
private readonly TaskCompletionSource<TOutput> _completionSource;
|
||||
private readonly OrchestrationResultTransform<TResult> _transformResult;
|
||||
@@ -32,7 +33,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <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,
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
OrchestrationResultTransform<TResult> transformResult,
|
||||
@@ -44,6 +45,8 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
this._completionSource = completionSource;
|
||||
this._transformResult = transformResult;
|
||||
this._transform = transformOutput;
|
||||
|
||||
this.RegisterMessageHandler<TResult>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -53,8 +56,9 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// </summary>
|
||||
/// <param name="item">The result item to process.</param>
|
||||
/// <param name="messageContext">The context associated with the message.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask representing asynchronous operation.</returns>
|
||||
public async ValueTask HandleAsync(TResult item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(TResult item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogOrchestrationResultInvoke(this.Context.Orchestration, this.Id);
|
||||
|
||||
@@ -63,7 +67,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
if (!this._completionSource.Task.IsCompleted)
|
||||
{
|
||||
IList<ChatMessage> result = this._transformResult.Invoke(item);
|
||||
TOutput output = await this._transform.Invoke(result).ConfigureAwait(false);
|
||||
TOutput output = await this._transform.Invoke(result, cancellationToken).ConfigureAwait(false);
|
||||
this._completionSource.TrySetResult(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
|
||||
TaskCompletionSource<TOutput> completion = new();
|
||||
|
||||
AgentType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false);
|
||||
ActorType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -149,7 +149,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <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);
|
||||
protected abstract ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent);
|
||||
|
||||
/// <summary>
|
||||
/// Orchestration specific registration, including members and returns an optional entry agent.
|
||||
@@ -159,7 +159,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <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);
|
||||
protected abstract ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger);
|
||||
|
||||
/// <summary>
|
||||
/// Formats and returns a unique AgentType based on the provided topic and suffix.
|
||||
@@ -167,7 +167,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <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}");
|
||||
protected ActorType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{suffix}");
|
||||
|
||||
/// <summary>
|
||||
/// Registers the orchestration's root and boot agents, setting up completion and target routing.
|
||||
@@ -177,7 +177,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <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)
|
||||
private async ValueTask<ActorType> RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource<TOutput> completion, ActorType? handoff)
|
||||
{
|
||||
// Create a logger for the orchestration registration.
|
||||
ILogger logger = context.LoggerFactory.CreateLogger(this.GetType());
|
||||
@@ -185,10 +185,10 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
|
||||
// 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);
|
||||
ActorType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false);
|
||||
|
||||
// Register actor for orchestration entry-point
|
||||
AgentType orchestrationEntry =
|
||||
ActorType orchestrationEntry =
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
this.FormatAgentType(context.Topic, "Boot"),
|
||||
(agentId, runtime) =>
|
||||
@@ -201,11 +201,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
completion,
|
||||
StartAsync,
|
||||
context.LoggerFactory.CreateLogger<RequestActor>());
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<IHostableAgent>(actor);
|
||||
#else
|
||||
return ValueTask.FromResult<IHostableAgent>(actor);
|
||||
#endif
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic);
|
||||
@@ -219,7 +215,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// A context used during registration (<see cref="RegisterAsync"/>).
|
||||
/// </summary>
|
||||
public sealed class RegistrationContext(
|
||||
AgentType agentType,
|
||||
ActorType agentType,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
TaskCompletionSource<TOutput> completion,
|
||||
@@ -228,10 +224,10 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
/// <summary>
|
||||
/// Register the final result type.
|
||||
/// </summary>
|
||||
public async ValueTask<AgentType> RegisterResultTypeAsync<TResult>(OrchestrationResultTransform<TResult> resultTransform)
|
||||
public async ValueTask<ActorType> RegisterResultTypeAsync<TResult>(OrchestrationResultTransform<TResult> resultTransform)
|
||||
{
|
||||
// Register actor for final result
|
||||
AgentType registeredType =
|
||||
ActorType registeredType =
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
agentType,
|
||||
(agentId, runtime) =>
|
||||
@@ -244,11 +240,7 @@ public abstract partial class AgentOrchestration<TInput, TOutput>
|
||||
outputTransform,
|
||||
completion,
|
||||
context.LoggerFactory.CreateLogger<ResultActor<TResult>>());
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<IHostableAgent>(actor);
|
||||
#else
|
||||
return ValueTask.FromResult<IHostableAgent>(actor);
|
||||
#endif
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return registeredType;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
@@ -11,9 +12,9 @@ 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>
|
||||
internal sealed class ConcurrentActor : AgentActor
|
||||
{
|
||||
private readonly AgentType _handoffActor;
|
||||
private readonly ActorType _handoffActor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConcurrentActor"/> class.
|
||||
@@ -24,21 +25,22 @@ internal sealed class ConcurrentActor : AgentActor, IHandle<ConcurrentMessages.R
|
||||
/// <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)
|
||||
public ConcurrentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ActorType resultActor, ILogger<ConcurrentActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
this._handoffActor = resultActor;
|
||||
|
||||
this.RegisterMessageHandler<ConcurrentMessages.Request>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogConcurrentAgentInvoke(this.Id);
|
||||
|
||||
ChatMessage response = await this.InvokeAsync(item.Messages, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
ChatMessage response = await this.InvokeAsync(item.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.Logger.LogConcurrentAgentResult(this.Id, response.Text);
|
||||
|
||||
await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,18 @@ public class ConcurrentOrchestration<TInput, TOutput>
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
|
||||
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
return runtime.PublishMessageAsync(input.AsInputMessage(), topic);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
AgentType outputType = await registrar.RegisterResultTypeAsync<ConcurrentMessages.Result[]>(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false);
|
||||
ActorType outputType = await registrar.RegisterResultTypeAsync<ConcurrentMessages.Result[]>(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false);
|
||||
|
||||
// Register result actor
|
||||
AgentType resultType = this.FormatAgentType(context.Topic, "Results");
|
||||
ActorType resultType = this.FormatAgentType(context.Topic, "Results");
|
||||
await runtime.RegisterOrchestrationAgentAsync(
|
||||
resultType,
|
||||
async (agentId, runtime) =>
|
||||
@@ -57,17 +57,13 @@ public class ConcurrentOrchestration<TInput, TOutput>
|
||||
{
|
||||
++agentCount;
|
||||
|
||||
AgentType agentType =
|
||||
await runtime.RegisterAgentFactoryAsync(
|
||||
ActorType agentType =
|
||||
await runtime.RegisterActorFactoryAsync(
|
||||
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
ConcurrentActor actor = new(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger<ConcurrentActor>());
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<IHostableAgent>(actor);
|
||||
#else
|
||||
return ValueTask.FromResult<IHostableAgent>(actor);
|
||||
#endif
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
|
||||
|
||||
@@ -11,12 +11,10 @@ namespace Microsoft.Agents.Orchestration.Concurrent;
|
||||
/// <summary>
|
||||
/// Actor for capturing each <see cref="ConcurrentMessages.Result"/> message.
|
||||
/// </summary>
|
||||
internal sealed class ConcurrentResultActor :
|
||||
OrchestrationActor,
|
||||
IHandle<ConcurrentMessages.Result>
|
||||
internal sealed class ConcurrentResultActor : OrchestrationActor
|
||||
{
|
||||
private readonly ConcurrentQueue<ConcurrentMessages.Result> _results;
|
||||
private readonly AgentType _orchestrationType;
|
||||
private readonly ActorType _orchestrationType;
|
||||
private readonly int _expectedCount;
|
||||
private int _resultCount;
|
||||
|
||||
@@ -30,10 +28,10 @@ internal sealed class ConcurrentResultActor :
|
||||
/// <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,
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
OrchestrationContext context,
|
||||
AgentType orchestrationType,
|
||||
ActorType orchestrationType,
|
||||
int expectedCount,
|
||||
ILogger logger)
|
||||
: base(id, runtime, context, "Captures the results of the ConcurrentOrchestration", logger)
|
||||
@@ -41,10 +39,11 @@ internal sealed class ConcurrentResultActor :
|
||||
this._orchestrationType = orchestrationType;
|
||||
this._expectedCount = expectedCount;
|
||||
this._results = [];
|
||||
|
||||
this.RegisterMessageHandler<ConcurrentMessages.Result>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogConcurrentResultCapture(this.Id, this._resultCount + 1, this._expectedCount);
|
||||
|
||||
@@ -52,7 +51,7 @@ internal sealed class ConcurrentResultActor :
|
||||
|
||||
if (Interlocked.Increment(ref this._resultCount) == this._expectedCount)
|
||||
{
|
||||
await this.PublishMessageAsync(this._results.ToArray(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(this._results.ToArray(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ 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)
|
||||
public static async ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, ActorType agentType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await runtime.PublishMessageAsync(message, new TopicId(agentType), sender: null, messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
await runtime.PublishMessageAsync(message, new TopicId(agentType.Name), sender: null, messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,12 +27,12 @@ public static class RuntimeExtensions
|
||||
/// <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)
|
||||
public static async ValueTask<ActorType> RegisterOrchestrationAgentAsync(this IAgentRuntime runtime, ActorType agentType, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc)
|
||||
{
|
||||
AgentType registeredType = await runtime.RegisterAgentFactoryAsync(agentType, factoryFunc).ConfigureAwait(false);
|
||||
ActorType registeredType = await runtime.RegisterActorFactoryAsync(agentType, factoryFunc).ConfigureAwait(false);
|
||||
|
||||
// Subscribe agent to its own unique topic
|
||||
await runtime.SubscribeAsync(registeredType).ConfigureAwait(false);
|
||||
await runtime.SubscribeAsync(new(registeredType.Name)).ConfigureAwait(false);
|
||||
|
||||
return registeredType;
|
||||
}
|
||||
@@ -42,9 +42,9 @@ public static class RuntimeExtensions
|
||||
/// </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)
|
||||
public static async Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType)
|
||||
{
|
||||
await runtime.AddSubscriptionAsync(new TypeSubscription(agentType, agentType)).ConfigureAwait(false);
|
||||
await runtime.AddSubscriptionAsync(new TypeSubscription(agentType.Name, agentType)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -53,7 +53,7 @@ public static class RuntimeExtensions
|
||||
/// <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)
|
||||
public static async Task SubscribeAsync(this IAgentRuntime runtime, ActorType agentType, params TopicId[] topics)
|
||||
{
|
||||
for (int index = 0; index < topics.Length; ++index)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
@@ -12,11 +13,7 @@ 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>
|
||||
internal sealed class GroupChatAgentActor : AgentActor
|
||||
{
|
||||
private readonly List<ChatMessage> _cache;
|
||||
|
||||
@@ -28,46 +25,37 @@ internal sealed class GroupChatAgentActor :
|
||||
/// <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)
|
||||
public GroupChatAgentActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger<GroupChatAgentActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
this._cache = [];
|
||||
|
||||
this.RegisterMessageHandler<GroupChatMessages.Group>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<GroupChatMessages.Reset>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<GroupChatMessages.Speak>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext)
|
||||
private ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this._cache.AddRange(item.Messages);
|
||||
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask();
|
||||
#else
|
||||
return ValueTask.CompletedTask;
|
||||
#endif
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext)
|
||||
private ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.ResetThread();
|
||||
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask();
|
||||
#else
|
||||
return ValueTask.CompletedTask;
|
||||
#endif
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogChatAgentInvoke(this.Id);
|
||||
|
||||
ChatMessage response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
ChatMessage response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.Logger.LogChatAgentResult(this.Id, response.Text);
|
||||
|
||||
this._cache.Clear();
|
||||
await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +96,6 @@ public abstract class GroupChatManager
|
||||
}
|
||||
|
||||
GroupChatManagerResult<bool> result = new(resultValue) { Reason = reason };
|
||||
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<GroupChatManagerResult<bool>>(result);
|
||||
#else
|
||||
return ValueTask.FromResult(result);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents.Runtime;
|
||||
@@ -11,17 +12,14 @@ 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>
|
||||
internal sealed class GroupChatManagerActor : OrchestrationActor
|
||||
{
|
||||
/// <summary>
|
||||
/// A common description for the manager.
|
||||
/// </summary>
|
||||
public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task.";
|
||||
|
||||
private readonly AgentType _orchestrationType;
|
||||
private readonly ActorType _orchestrationType;
|
||||
private readonly GroupChatManager _manager;
|
||||
private readonly List<ChatMessage> _chat;
|
||||
private readonly GroupChatTeam _team;
|
||||
@@ -36,65 +34,67 @@ internal sealed class GroupChatManagerActor :
|
||||
/// <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)
|
||||
public GroupChatManagerActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, ActorType orchestrationType, ILogger? logger = null)
|
||||
: base(id, runtime, context, DefaultDescription, logger)
|
||||
{
|
||||
this._chat = [];
|
||||
this._manager = manager;
|
||||
this._orchestrationType = orchestrationType;
|
||||
this._team = team;
|
||||
|
||||
this.RegisterMessageHandler<GroupChatMessages.InputTask>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<GroupChatMessages.Group>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogChatManagerInit(this.Id);
|
||||
|
||||
this._chat.AddRange(item.Messages);
|
||||
|
||||
await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await this.ManageAsync(messageContext).ConfigureAwait(false);
|
||||
await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogChatManagerInvoke(this.Id);
|
||||
|
||||
this._chat.AddRange(item.Messages);
|
||||
|
||||
await this.ManageAsync(messageContext).ConfigureAwait(false);
|
||||
await this.ManageAsync(messageContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask ManageAsync(MessageContext messageContext)
|
||||
private async ValueTask ManageAsync(MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._manager.InteractiveCallback != null)
|
||||
{
|
||||
GroupChatManagerResult<bool> inputResult = await this._manager.ShouldRequestUserInput(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
GroupChatManagerResult<bool> inputResult = await this._manager.ShouldRequestUserInput(this._chat, cancellationToken).ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerInput(this.Id, inputResult.Value, inputResult.Reason);
|
||||
if (inputResult.Value)
|
||||
{
|
||||
ChatMessage input = await this._manager.InteractiveCallback.Invoke().ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerUserInput(this.Id, input.Text);
|
||||
this._chat.Add(input);
|
||||
await this.PublishMessageAsync(input.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(input.AsGroupMessage(), this.Context.Topic, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
GroupChatManagerResult<bool> terminateResult = await this._manager.ShouldTerminate(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
GroupChatManagerResult<bool> terminateResult = await this._manager.ShouldTerminate(this._chat, cancellationToken).ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerTerminate(this.Id, terminateResult.Value, terminateResult.Reason);
|
||||
if (terminateResult.Value)
|
||||
{
|
||||
GroupChatManagerResult<string> filterResult = await this._manager.FilterResults(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
GroupChatManagerResult<string> filterResult = await this._manager.FilterResults(this._chat, cancellationToken).ConfigureAwait(false);
|
||||
this.Logger.LogChatManagerResult(this.Id, filterResult.Value, filterResult.Reason);
|
||||
await this.PublishMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, 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;
|
||||
GroupChatManagerResult<string> selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, cancellationToken).ConfigureAwait(false);
|
||||
ActorType selectionType = new(this._team[selectionResult.Value].Type);
|
||||
this.Logger.LogChatManagerSelect(this.Id, selectionType);
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class GroupChatOrchestration<TInput, TOutput> :
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
|
||||
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
if (!entryAgent.HasValue)
|
||||
{
|
||||
@@ -46,37 +46,33 @@ public class GroupChatOrchestration<TInput, TOutput> :
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
AgentType outputType = await registrar.RegisterResultTypeAsync<GroupChatMessages.Result>(response => [response.Message]).ConfigureAwait(false);
|
||||
ActorType 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;
|
||||
ActorType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false);
|
||||
string name = agent.Name ?? agent.Id ?? agentType.Name;
|
||||
string? description = agent.Description;
|
||||
|
||||
team[name] = (agentType, description ?? DefaultAgentDescription);
|
||||
team[name] = (agentType.Name, description ?? DefaultAgentDescription);
|
||||
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
|
||||
|
||||
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
AgentType managerType =
|
||||
ActorType 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
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
logger.LogRegisterActor(this.OrchestrationLabel, managerType, "MANAGER");
|
||||
|
||||
@@ -84,17 +80,13 @@ public class GroupChatOrchestration<TInput, TOutput> :
|
||||
|
||||
return managerType;
|
||||
|
||||
ValueTask<AgentType> RegisterAgentAsync(Agent agent, int agentCount) =>
|
||||
ValueTask<ActorType> 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
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,7 @@ public class RoundRobinGroupChatManager : GroupChatManager
|
||||
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/>
|
||||
@@ -35,21 +31,13 @@ public class RoundRobinGroupChatManager : GroupChatManager
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,11 @@ 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>
|
||||
internal sealed class HandoffActor : AgentActor
|
||||
{
|
||||
private readonly ChatClientAgent _chatAgent;
|
||||
private readonly HandoffLookup _handoffs;
|
||||
private readonly AgentType _resultHandoff;
|
||||
private readonly ActorType _resultHandoff;
|
||||
private readonly List<ChatMessage> _cache;
|
||||
private readonly ChatOptions _options;
|
||||
|
||||
@@ -39,7 +35,7 @@ internal sealed class HandoffActor :
|
||||
/// <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)
|
||||
public HandoffActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, ActorType resultHandoff, ILogger<HandoffActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
if (handoffs.ContainsKey(agent.Name ?? agent.Id))
|
||||
@@ -57,6 +53,10 @@ internal sealed class HandoffActor :
|
||||
Tools = [.. this.CreateHandoffFunctions()],
|
||||
ToolMode = ChatToolMode.Auto
|
||||
};
|
||||
|
||||
this.RegisterMessageHandler<HandoffMessages.InputTask>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<HandoffMessages.Request>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<HandoffMessages.Response>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -85,33 +85,20 @@ internal sealed class HandoffActor :
|
||||
/// </summary>
|
||||
public OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext)
|
||||
private ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this._taskSummary = null;
|
||||
this._cache.AddRange(item.Messages);
|
||||
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask();
|
||||
#else
|
||||
return ValueTask.CompletedTask;
|
||||
#endif
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext)
|
||||
private ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this._cache.Add(item.Message);
|
||||
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask();
|
||||
#else
|
||||
return ValueTask.CompletedTask;
|
||||
#endif
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext)
|
||||
private async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -122,7 +109,7 @@ internal sealed class HandoffActor :
|
||||
ChatMessage response;
|
||||
try
|
||||
{
|
||||
response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -139,13 +126,13 @@ internal sealed class HandoffActor :
|
||||
// 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);
|
||||
await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, 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);
|
||||
ActorType handoffType = this._handoffs[this._handoffAgent].AgentType;
|
||||
await this.PublishMessageAsync(new HandoffMessages.Request(), handoffType, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this._handoffAgent = null;
|
||||
break;
|
||||
@@ -154,12 +141,12 @@ internal sealed class HandoffActor :
|
||||
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);
|
||||
await this.PublishMessageAsync(new HandoffMessages.Response { Message = input }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
this._cache.Add(input);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", messageContext.CancellationToken).ConfigureAwait(false);
|
||||
await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -176,7 +163,7 @@ internal sealed class HandoffActor :
|
||||
name: "end_task",
|
||||
description: "Complete the task with a summary when no further requests are given.");
|
||||
|
||||
foreach (KeyValuePair<string, (AgentType AgentType, string Description)> handoff in this._handoffs)
|
||||
foreach (KeyValuePair<string, (ActorType AgentType, string Description)> handoff in this._handoffs)
|
||||
{
|
||||
AIFunction handoffFunction =
|
||||
AIFunctionFactory.Create(
|
||||
|
||||
@@ -50,7 +50,7 @@ public class HandoffOrchestration<TInput, TOutput> : AgentOrchestration<TInput,
|
||||
public OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
|
||||
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
if (!entryAgent.HasValue)
|
||||
{
|
||||
@@ -61,14 +61,14 @@ public class HandoffOrchestration<TInput, TOutput> : AgentOrchestration<TInput,
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
AgentType outputType = await registrar.RegisterResultTypeAsync<HandoffMessages.Result>(response => [response.Message]).ConfigureAwait(false);
|
||||
ActorType 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, ActorType> agentMap = [];
|
||||
Dictionary<string, HandoffLookup> handoffMap = [];
|
||||
AgentType agentType = outputType;
|
||||
ActorType agentType = outputType;
|
||||
for (int index = this.Members.Count - 1; index >= 0; --index)
|
||||
{
|
||||
Agent agent = this.Members[index];
|
||||
@@ -84,11 +84,7 @@ public class HandoffOrchestration<TInput, TOutput> : AgentOrchestration<TInput,
|
||||
{
|
||||
InteractiveCallback = this.InteractiveCallback
|
||||
};
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<IHostableAgent>(actor);
|
||||
#else
|
||||
return ValueTask.FromResult<IHostableAgent>(actor);
|
||||
#endif
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
}).ConfigureAwait(false);
|
||||
agentMap[agent.Name ?? agent.Id] = agentType;
|
||||
|
||||
@@ -112,5 +108,5 @@ public class HandoffOrchestration<TInput, TOutput> : AgentOrchestration<TInput,
|
||||
return agentMap[this._handoffs.FirstAgentName];
|
||||
}
|
||||
|
||||
private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
|
||||
private ActorType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
|
||||
}
|
||||
|
||||
@@ -142,6 +142,6 @@ public static class OrchestrationHandoffsExtensions
|
||||
|
||||
/// <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.
|
||||
/// Maps agent names/IDs to a tuple of <see cref="ActorType"/> and handoff description.
|
||||
/// </summary>
|
||||
internal sealed class HandoffLookup : Dictionary<string, (AgentType AgentType, string Description)>;
|
||||
internal sealed class HandoffLookup : Dictionary<string, (ActorType AgentType, string Description)>;
|
||||
|
||||
@@ -37,7 +37,7 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogRegisterActor(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentType agentType,
|
||||
ActorType agentType,
|
||||
string label);
|
||||
|
||||
/// <summary>
|
||||
@@ -49,7 +49,7 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogRegisterActor(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentType agentType,
|
||||
ActorType agentType,
|
||||
string label,
|
||||
int count);
|
||||
|
||||
@@ -96,7 +96,7 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogOrchestrationStart(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration request actor is active
|
||||
@@ -107,7 +107,7 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogOrchestrationRequestInvoke(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration request actor experienced an unexpected failure.
|
||||
@@ -118,7 +118,7 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogOrchestrationRequestFailure(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
Exception exception);
|
||||
|
||||
/// <summary>
|
||||
@@ -130,7 +130,7 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogOrchestrationResultInvoke(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
/// <summary>
|
||||
/// Logs that orchestration result actor experienced an unexpected failure.
|
||||
@@ -141,6 +141,6 @@ internal static partial class AgentOrchestrationLogMessages
|
||||
public static partial void LogOrchestrationResultFailure(
|
||||
this ILogger logger,
|
||||
string orchestration,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
Exception exception);
|
||||
}
|
||||
|
||||
+3
-3
@@ -22,14 +22,14 @@ internal static partial class ConcurrentOrchestrationLogMessages
|
||||
Message = "REQUEST Concurrent agent [{AgentId}]")]
|
||||
public static partial void LogConcurrentAgentInvoke(
|
||||
this ILogger logger,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Concurrent agent [{AgentId}]: {Message}")]
|
||||
public static partial void LogConcurrentAgentResult(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
|
||||
/// <summary>
|
||||
@@ -40,7 +40,7 @@ internal static partial class ConcurrentOrchestrationLogMessages
|
||||
Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")]
|
||||
public static partial void LogConcurrentResultCapture(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
int resultCount,
|
||||
int expectedCount);
|
||||
}
|
||||
|
||||
+10
-10
@@ -22,14 +22,14 @@ internal static partial class GroupChatOrchestrationLogMessages
|
||||
Message = "CHAT AGENT invoked [{AgentId}]")]
|
||||
public static partial void LogChatAgentInvoke(
|
||||
this ILogger logger,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "CHAT AGENT result [{AgentId}]: {Message}")]
|
||||
public static partial void LogChatAgentResult(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
|
||||
[LoggerMessage(
|
||||
@@ -37,21 +37,21 @@ internal static partial class GroupChatOrchestrationLogMessages
|
||||
Message = "CHAT MANAGER initialized [{AgentId}]")]
|
||||
public static partial void LogChatManagerInit(
|
||||
this ILogger logger,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER invoked [{AgentId}]")]
|
||||
public static partial void LogChatManagerInvoke(
|
||||
this ILogger logger,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")]
|
||||
public static partial void LogChatManagerTerminate(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
bool result,
|
||||
string reason);
|
||||
|
||||
@@ -60,15 +60,15 @@ internal static partial class GroupChatOrchestrationLogMessages
|
||||
Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")]
|
||||
public static partial void LogChatManagerSelect(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
AgentType nextAgent);
|
||||
ActorId agentId,
|
||||
ActorType nextAgent);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Debug,
|
||||
Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")]
|
||||
public static partial void LogChatManagerResult(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string result,
|
||||
string reason);
|
||||
|
||||
@@ -77,7 +77,7 @@ internal static partial class GroupChatOrchestrationLogMessages
|
||||
Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")]
|
||||
public static partial void LogChatManagerInput(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
bool result,
|
||||
string reason);
|
||||
|
||||
@@ -86,6 +86,6 @@ internal static partial class GroupChatOrchestrationLogMessages
|
||||
Message = "CHAT AGENT user-input [{AgentId}]: {Message}")]
|
||||
public static partial void LogChatManagerUserInput(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
}
|
||||
|
||||
+4
-4
@@ -22,14 +22,14 @@ internal static partial class HandoffOrchestrationLogMessages
|
||||
Message = "REQUEST Handoff agent [{AgentId}]")]
|
||||
public static partial void LogHandoffAgentInvoke(
|
||||
this ILogger logger,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Handoff agent [{AgentId}]: {Message}")]
|
||||
public static partial void LogHandoffAgentResult(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
|
||||
[LoggerMessage(
|
||||
@@ -37,7 +37,7 @@ internal static partial class HandoffOrchestrationLogMessages
|
||||
Message = "TOOL Handoff [{AgentId}]: {Name}")]
|
||||
public static partial void LogHandoffFunctionCall(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string name);
|
||||
|
||||
[LoggerMessage(
|
||||
@@ -45,6 +45,6 @@ internal static partial class HandoffOrchestrationLogMessages
|
||||
Message = "RESULT Handoff summary [{AgentId}]: {Summary}")]
|
||||
public static partial void LogHandoffSummary(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string? summary);
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,13 +22,13 @@ internal static partial class SequentialOrchestrationLogMessages
|
||||
Message = "REQUEST Sequential agent [{AgentId}]")]
|
||||
public static partial void LogSequentialAgentInvoke(
|
||||
this ILogger logger,
|
||||
AgentId agentId);
|
||||
ActorId agentId);
|
||||
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Trace,
|
||||
Message = "RESULT Sequential agent [{AgentId}]: {Message}")]
|
||||
public static partial void LogSequentialAgentResult(
|
||||
this ILogger logger,
|
||||
AgentId agentId,
|
||||
ActorId agentId,
|
||||
string? message);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ namespace Microsoft.Agents.Orchestration;
|
||||
/// <summary>
|
||||
/// Base abstractions for any actor that participates in an orchestration.
|
||||
/// </summary>
|
||||
public abstract class OrchestrationActor : BaseAgent
|
||||
public abstract class OrchestrationActor : RuntimeActor
|
||||
{
|
||||
/// <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)
|
||||
protected OrchestrationActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, string description, ILogger? logger = null)
|
||||
: base(id, runtime, description, logger)
|
||||
{
|
||||
this.Context = context;
|
||||
@@ -35,9 +35,9 @@ public abstract class OrchestrationActor : BaseAgent
|
||||
/// <returns>The agent identifier, if it exists.</returns>
|
||||
protected async ValueTask PublishMessageAsync(
|
||||
object message,
|
||||
AgentType agentType,
|
||||
ActorType agentType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.PublishMessageAsync(message, new TopicId(agentType), messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
await base.PublishMessageAsync(message, new TopicId(agentType.Name), messageId: null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,11 @@ public sealed class OrchestrationResult<TValue> : IDisposable
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
if (!this._isDisposed)
|
||||
{
|
||||
this._cancelSource.Dispose();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,13 +63,13 @@ public sealed class OrchestrationResult<TValue> : IDisposable
|
||||
/// <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 NET
|
||||
ObjectDisposedException.ThrowIf(this._isDisposed, this);
|
||||
#else
|
||||
if (this._isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(this.GetType().Name);
|
||||
}
|
||||
#else
|
||||
ObjectDisposedException.ThrowIf(this._isDisposed, this);
|
||||
#endif
|
||||
|
||||
this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic);
|
||||
@@ -96,30 +99,17 @@ public sealed class OrchestrationResult<TValue> : IDisposable
|
||||
/// </remarks>
|
||||
public void Cancel()
|
||||
{
|
||||
#if !NETCOREAPP
|
||||
#if NET
|
||||
ObjectDisposedException.ThrowIf(this._isDisposed, this);
|
||||
#else
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
@@ -12,12 +13,9 @@ 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>
|
||||
internal sealed class SequentialActor : AgentActor
|
||||
{
|
||||
private readonly AgentType _nextAgent;
|
||||
private readonly ActorType _nextAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SequentialActor"/> class.
|
||||
@@ -28,35 +26,32 @@ internal sealed class SequentialActor :
|
||||
/// <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)
|
||||
public SequentialActor(ActorId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ActorType nextAgent, ILogger<SequentialActor>? logger = null)
|
||||
: base(id, runtime, context, agent, logger)
|
||||
{
|
||||
logger?.LogInformation("ACTOR {ActorId} {NextAgent}", this.Id, nextAgent);
|
||||
this._nextAgent = nextAgent;
|
||||
|
||||
this.RegisterMessageHandler<SequentialMessages.Request>(this.HandleAsync);
|
||||
this.RegisterMessageHandler<SequentialMessages.Response>(this.HandleAsync);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext)
|
||||
{
|
||||
await this.InvokeAgentAsync(item.Messages, messageContext).ConfigureAwait(false);
|
||||
}
|
||||
public ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext, CancellationToken cancellationToken) =>
|
||||
this.InvokeAgentAsync(item.Messages, messageContext, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext)
|
||||
{
|
||||
await this.InvokeAgentAsync([item.Message], messageContext).ConfigureAwait(false);
|
||||
}
|
||||
public ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext, CancellationToken cancellationToken) =>
|
||||
this.InvokeAgentAsync([item.Message], messageContext, cancellationToken);
|
||||
|
||||
private async ValueTask InvokeAgentAsync(IList<ChatMessage> input, MessageContext messageContext)
|
||||
private async ValueTask InvokeAgentAsync(IList<ChatMessage> input, MessageContext messageContext, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Logger.LogInformation("INVOKE {ActorId} {NextAgent}", this.Id, this._nextAgent);
|
||||
|
||||
this.Logger.LogSequentialAgentInvoke(this.Id);
|
||||
|
||||
ChatMessage response = await this.InvokeAsync(input, messageContext.CancellationToken).ConfigureAwait(false);
|
||||
ChatMessage response = await this.InvokeAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.Logger.LogSequentialAgentResult(this.Id, response.Text);
|
||||
|
||||
await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent).ConfigureAwait(false);
|
||||
await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SequentialOrchestration<TInput, TOutput> : AgentOrchestration<TInpu
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
|
||||
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, ActorType? entryAgent)
|
||||
{
|
||||
if (!entryAgent.HasValue)
|
||||
{
|
||||
@@ -37,12 +37,12 @@ public class SequentialOrchestration<TInput, TOutput> : AgentOrchestration<TInpu
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
protected override async ValueTask<ActorType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
|
||||
{
|
||||
AgentType outputType = await registrar.RegisterResultTypeAsync<SequentialMessages.Response>(response => [response.Message]).ConfigureAwait(false);
|
||||
ActorType outputType = await registrar.RegisterResultTypeAsync<SequentialMessages.Response>(response => [response.Message]).ConfigureAwait(false);
|
||||
|
||||
// Each agent handsoff its result to the next agent.
|
||||
AgentType nextAgent = outputType;
|
||||
ActorType nextAgent = outputType;
|
||||
for (int index = this.Members.Count - 1; index >= 0; --index)
|
||||
{
|
||||
Agent agent = this.Members[index];
|
||||
@@ -53,20 +53,15 @@ public class SequentialOrchestration<TInput, TOutput> : AgentOrchestration<TInpu
|
||||
|
||||
return nextAgent;
|
||||
|
||||
ValueTask<AgentType> RegisterAgentAsync(Agent agent, int index, AgentType nextAgent) =>
|
||||
ValueTask<ActorType> RegisterAgentAsync(Agent agent, int index, ActorType nextAgent) =>
|
||||
runtime.RegisterOrchestrationAgentAsync(
|
||||
this.GetAgentType(context.Topic, index),
|
||||
(agentId, runtime) =>
|
||||
{
|
||||
SequentialActor actor = new(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger<SequentialActor>());
|
||||
|
||||
#if !NETCOREAPP
|
||||
return new ValueTask<IHostableAgent>(actor);
|
||||
#else
|
||||
return ValueTask.FromResult<IHostableAgent>(actor);
|
||||
#endif
|
||||
return new ValueTask<IRuntimeActor>(actor);
|
||||
});
|
||||
}
|
||||
|
||||
private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
|
||||
private ActorType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a unique identifier for an actor instance within an agent runtime,
|
||||
/// serving as the "address" of the actor instance for receiving messages.
|
||||
/// </summary>
|
||||
public readonly struct ActorId : IEquatable<ActorId>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActorId"/> struct from an <see cref="ActorType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type.</param>
|
||||
/// <param name="key">Actor instance identifier.</param>
|
||||
public ActorId(string type, string key) : this(new ActorType(type), key)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActorId"/> struct from an <see cref="ActorType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type.</param>
|
||||
/// <param name="key">Actor instance identifier.</param>
|
||||
public ActorId(ActorType type, string key)
|
||||
{
|
||||
if (!IsValidKey(key))
|
||||
{
|
||||
throw new ArgumentException($"Invalid {nameof(ActorId)} key.", nameof(key));
|
||||
}
|
||||
|
||||
this.Type = type;
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an identifier that associates an actor with a specific factory function.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
|
||||
/// </remarks>
|
||||
public ActorType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an actor instance identifier.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
|
||||
/// </remarks>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string of the format "type/key" into an <see cref="ActorId"/>.
|
||||
/// </summary>
|
||||
/// <param name="actorId">The actor ID string.</param>
|
||||
/// <returns>An instance of <see cref="ActorId"/>.</returns>
|
||||
public static ActorId Parse(string actorId)
|
||||
{
|
||||
if (!KeyValueParser.TryParse(actorId, out string? type, out string? key))
|
||||
{
|
||||
throw new FormatException($"Invalid actor ID: '{actorId}'. Expected format is 'type/key'.");
|
||||
}
|
||||
|
||||
return new ActorId(type, key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly string ToString() => $"{this.Type}/{this.Key}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj) =>
|
||||
obj is ActorId other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(ActorId other) =>
|
||||
this.Type == other.Type && this.Key == other.Key;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(this.Type, this.Key);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator ==(ActorId left, ActorId right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator !=(ActorId left, ActorId right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
/// <summary>Determines whether the specified key is valid.</summary>
|
||||
/// <remarks>It must be non-null, not be only whitespace, and only contain printable ASCII characters.</remarks>
|
||||
internal static bool IsValidKey(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#if NET
|
||||
return !key.AsSpan().ContainsAnyExceptInRange((char)32, (char)126);
|
||||
#else
|
||||
foreach (char c in key)
|
||||
{
|
||||
if ((int)c is < 32 or > 126)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata associated with an actor, including its type, unique key, and description.
|
||||
/// </summary>
|
||||
public readonly struct ActorMetadata : IEquatable<ActorMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActorMetadata"/> class with the specified type, key, and description.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the actor.</param>
|
||||
/// <param name="key">The unique key associated with the actor.</param>
|
||||
/// <param name="description">A brief description of the actor.</param>
|
||||
public ActorMetadata(ActorType type, string key, string description)
|
||||
{
|
||||
if (!ActorId.IsValidKey(key))
|
||||
{
|
||||
throw new ArgumentException("Invalid actor key.", nameof(key));
|
||||
}
|
||||
|
||||
if (description is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(description));
|
||||
}
|
||||
|
||||
this.Type = type;
|
||||
this.Key = key;
|
||||
this.Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an identifier that associates an actor with a specific factory function.
|
||||
/// </summary>
|
||||
public ActorType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A unique key identifying the actor instance.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A brief description of the actor's purpose or functionality.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is ActorMetadata actorMetadata && this.Equals(actorMetadata);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(ActorMetadata other) =>
|
||||
this.Type == other.Type &&
|
||||
this.Key == other.Key &&
|
||||
this.Description == other.Description;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(this.Type, this.Key, this.Description);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(ActorMetadata left, ActorMetadata right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(ActorMetadata left, ActorMetadata right) =>
|
||||
!(left == right);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the type of an actor.
|
||||
/// </summary>
|
||||
public readonly partial struct ActorType : IEquatable<ActorType>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActorId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type.</param>
|
||||
public ActorType(string type)
|
||||
{
|
||||
if (!IsValid(type))
|
||||
{
|
||||
throw new ArgumentException($"Invalid type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces.");
|
||||
}
|
||||
|
||||
this.Name = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The string representation of this actor type.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="ActorType"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/source".</returns>
|
||||
public override readonly string ToString() =>
|
||||
this.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is ActorType other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ActorType other) =>
|
||||
this.Name.Equals(other.Name, StringComparison.Ordinal);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
this.Name.GetHashCode();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(ActorType left, ActorType right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(ActorType left, ActorType right) =>
|
||||
!(left == right);
|
||||
|
||||
internal static bool IsValid(string type) =>
|
||||
type is not null && TypeRegex().IsMatch(type);
|
||||
|
||||
#if NET
|
||||
[GeneratedRegex("^[a-zA-Z_][a-zA-Z_0-9]*$")]
|
||||
private static partial Regex TypeRegex();
|
||||
#else
|
||||
private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z_0-9]*$", RegexOptions.Compiled);
|
||||
#endif
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Agent ID uniquely identifies an agent instance within an agent runtime, including a distributed runtime.
|
||||
/// It serves as the "address" of the agent instance for receiving messages.
|
||||
/// </summary>\
|
||||
/// <remarks>
|
||||
/// See the Python equivalent:
|
||||
/// <see href="https://github.com/microsoft/agent-runtime/blob/main/python/agent_runtime/core/agent_id.py">AgentId in AutoGen (Python)</see>.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")]
|
||||
public struct AgentId : IEquatable<AgentId>
|
||||
{
|
||||
/// <summary>
|
||||
/// The default source value used when no source is explicitly provided.
|
||||
/// </summary>
|
||||
public const string DefaultKey = "default";
|
||||
|
||||
private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126
|
||||
|
||||
/// <summary>
|
||||
/// An identifier that associates an agent with a specific factory function.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Agent instance identifier.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
internal static Regex KeyRegex1 => KeyRegex2;
|
||||
|
||||
internal static Regex KeyRegex2 => KeyRegex;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type.</param>
|
||||
/// <param name="key">Agent instance identifier.</param>
|
||||
public AgentId(string type, string key)
|
||||
{
|
||||
AgentType.Validate(type);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key) || !KeyRegex.IsMatch(key))
|
||||
{
|
||||
throw new ArgumentException($"Invalid AgentId key: '{key}'. Must only contain ASCII characters 32-126.");
|
||||
}
|
||||
|
||||
this.Type = type;
|
||||
this.Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct from a tuple.
|
||||
/// </summary>
|
||||
/// <param name="kvPair">A tuple containing the agent type and key.</param>
|
||||
public AgentId((string Type, string Key) kvPair)
|
||||
: this(kvPair.Type, kvPair.Key)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct from an <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type.</param>
|
||||
/// <param name="key">Agent instance identifier.</param>
|
||||
public AgentId(AgentType type, string key)
|
||||
: this(type.Name, key)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string of the format "type/key" into an <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="maybeAgentId">The agent ID string.</param>
|
||||
/// <returns>An instance of <see cref="AgentId"/>.</returns>
|
||||
public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key)));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/key".</returns>
|
||||
public override readonly string ToString() => $"{this.Type}/{this.Key}";
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current <see cref="AgentId"/>; otherwise, <c>false</c>.</returns>
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return (obj is AgentId other && this.Equals(other));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(AgentId other)
|
||||
{
|
||||
return this.Type == other.Type && this.Key == other.Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the current instance.</returns>
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Type, this.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts a string to an <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The string representation of an agent ID.</param>
|
||||
/// <returns>An instance of <see cref="AgentId"/>.</returns>
|
||||
public static explicit operator AgentId(string id) => FromStr(id);
|
||||
|
||||
/// <summary>
|
||||
/// Equality operator for <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
public static bool operator ==(AgentId left, AgentId right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Inequality operator for <see cref="AgentId"/>.
|
||||
/// </summary>
|
||||
public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata associated with an agent, including its type, unique key, and description.
|
||||
/// </summary>
|
||||
public readonly struct AgentMetadata(string type, string key, string description) : IEquatable<AgentMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// An identifier that associates an agent with a specific factory function.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Type { get; } = type;
|
||||
|
||||
/// <summary>
|
||||
/// A unique key identifying the agent instance.
|
||||
/// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_).
|
||||
/// </summary>
|
||||
public string Key { get; } = key;
|
||||
|
||||
/// <summary>
|
||||
/// A brief description of the agent's purpose or functionality.
|
||||
/// </summary>
|
||||
public string Description { get; } = description;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(AgentMetadata other)
|
||||
{
|
||||
return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Type, this.Key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(AgentMetadata left, AgentMetadata right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(AgentMetadata left, AgentMetadata right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// A proxy that allows you to use an <see cref="AgentId"/> in place of its associated <see cref="IAgent"/>.
|
||||
/// </summary>
|
||||
public class AgentProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// The runtime instance used to interact with agents.
|
||||
/// </summary>
|
||||
private readonly IAgentRuntime _runtime;
|
||||
private AgentMetadata? _metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentProxy"/> class.
|
||||
/// </summary>
|
||||
public AgentProxy(AgentId agentId, IAgentRuntime runtime)
|
||||
{
|
||||
this.Id = agentId;
|
||||
this._runtime = runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The target agent for this proxy.
|
||||
/// </summary>
|
||||
public AgentId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata of the agent.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An instance of <see cref="AgentMetadata"/> containing details about the agent.
|
||||
/// </value>
|
||||
public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap();
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the agent and processes the response.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send to the agent.</param>
|
||||
/// <param name="sender">The agent that is sending the message.</param>
|
||||
/// <param name="messageId">
|
||||
/// The message ID. If <c>null</c>, a new message ID will be generated.
|
||||
/// This message ID must be unique and is recommended to be a UUID.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">
|
||||
/// A token used to cancel an in-progress operation. Defaults to <c>null</c>.
|
||||
/// </param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the response from the agent.</returns>
|
||||
public ValueTask<object?> SendMessageAsync(object message, AgentId sender, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the state of the agent from a previously saved state.
|
||||
/// </summary>
|
||||
/// <param name="state">A dictionary representing the state of the agent. Must be JSON serializable.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public ValueTask LoadStateAsync(JsonElement state)
|
||||
{
|
||||
return this._runtime.LoadAgentStateAsync(this.Id, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the state of the agent. The result must be JSON serializable.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation, returning a dictionary containing the saved state.</returns>
|
||||
public ValueTask<JsonElement> SaveStateAsync()
|
||||
{
|
||||
return this._runtime.SaveAgentStateAsync(this.Id);
|
||||
}
|
||||
|
||||
private AgentMetadata QueryMetadataAndUnwrap()
|
||||
{
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the type of an agent as a string.
|
||||
/// This is a strongly-typed wrapper around a string, ensuring type safety when working with agent types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is immutable and provides implicit conversion to and from <see cref="string"/>.
|
||||
/// </remarks>
|
||||
public readonly partial struct AgentType : IEquatable<AgentType>
|
||||
{
|
||||
#if NET
|
||||
[GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]*$")]
|
||||
private static partial Regex TypeRegex();
|
||||
#else
|
||||
private static Regex TypeRegex() => new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
|
||||
#endif
|
||||
|
||||
internal static void Validate(string type)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(type) || !TypeRegex().IsMatch(type))
|
||||
{
|
||||
throw new ArgumentException($"Invalid AgentId type: '{type}'. Must be alphanumeric (a-z, 0-9, _) and cannot start with a number or contain spaces.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type.</param>
|
||||
public AgentType(string type)
|
||||
{
|
||||
Validate(type);
|
||||
this.Name = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The string representation of this agent type.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/source".</returns>
|
||||
public override readonly string ToString() => this.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts a <see cref="Type"/> to an <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The .NET <see cref="Type"/> to convert.</param>
|
||||
/// <returns>An <see cref="AgentType"/> instance with the name of the provided type.</returns>
|
||||
public static explicit operator AgentType(Type type) => new(type.Name);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a <see cref="string"/> to an <see cref="AgentType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The string representation of the agent type.</param>
|
||||
/// <returns>An <see cref="AgentType"/> instance with the given name.</returns>
|
||||
public static implicit operator AgentType(string type) => new(type);
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts an <see cref="AgentType"/> to a <see cref="string"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The <see cref="AgentType"/> instance.</param>
|
||||
/// <returns>The string representation of the agent type.</returns>
|
||||
public static implicit operator string(AgentType type) => type.ToString();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is AgentType other && this.Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(AgentType other)
|
||||
{
|
||||
return this.Name.Equals(other.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.Name.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(AgentType left, AgentType right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(AgentType left, AgentType right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the base class for an agent in the AutoGen system.
|
||||
/// </summary>
|
||||
public abstract class BaseAgent : IHostableAgent
|
||||
{
|
||||
private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement;
|
||||
|
||||
/// <summary>
|
||||
/// The activity source for tracing.
|
||||
/// </summary>
|
||||
public static readonly ActivitySource TraceSource = new($"{typeof(IAgent).Namespace}");
|
||||
|
||||
private readonly Dictionary<Type, HandlerInvoker> _handlerInvokers;
|
||||
private readonly IAgentRuntime _runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides logging capabilities used for diagnostic and operational information.
|
||||
/// </summary>
|
||||
protected internal ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the agent.
|
||||
/// </summary>
|
||||
protected string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the agent.
|
||||
/// </summary>
|
||||
public AgentId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata of the agent.
|
||||
/// </summary>
|
||||
public AgentMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the BaseAgent class with the specified identifier, runtime, description, and optional logger.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the agent.</param>
|
||||
/// <param name="runtime">The runtime environment in which the agent operates.</param>
|
||||
/// <param name="description">A brief description of the agent's purpose.</param>
|
||||
/// <param name="logger">An optional logger for recording diagnostic information.</param>
|
||||
protected BaseAgent(
|
||||
AgentId id,
|
||||
IAgentRuntime runtime,
|
||||
string description,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
this.Logger = logger ?? NullLogger.Instance;
|
||||
|
||||
this.Id = id;
|
||||
this.Description = description;
|
||||
this.Metadata = new AgentMetadata(this.Id.Type, this.Id.Key, this.Description);
|
||||
|
||||
this._runtime = runtime;
|
||||
this._handlerInvokers = HandlerInvoker.ReflectAgentHandlers(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message by determining its type and invoking the corresponding handler method if available.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to be handled.</param>
|
||||
/// <param name="messageContext">The context associated with the message.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, containing the response object or null.</returns>
|
||||
public async ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext)
|
||||
{
|
||||
// Determine type of message, then get handler method and invoke it
|
||||
Type messageType = message.GetType();
|
||||
if (this._handlerInvokers.TryGetValue(messageType, out HandlerInvoker? handlerInvoker))
|
||||
{
|
||||
return await handlerInvoker.InvokeAsync(message, messageContext).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask<JsonElement> SaveStateAsync()
|
||||
{
|
||||
return new ValueTask<JsonElement>(s_emptyElement);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask LoadStateAsync(JsonElement state) =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Closes this agent gracefully by releasing allocated resources and performing any necessary cleanup.
|
||||
/// </summary>
|
||||
public virtual ValueTask CloseAsync() =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient agent through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="agent">The requested agent's type.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected async ValueTask<AgentId?> GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._runtime.GetAgentAsync(agent, lazy: false).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient agent through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to send.</param>
|
||||
/// <param name="recipient">The recipient agent's identifier.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected ValueTask<object?> SendMessageAsync(object message, AgentId recipient, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all agents subscribed to a specific topic through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to publish.</param>
|
||||
/// <param name="topic">The topic identifier to which the message is published.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous publish operation.</returns>
|
||||
protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken);
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a handler cannot process the given message.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class CantHandleException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CantHandleException"/> class.
|
||||
/// </summary>
|
||||
public CantHandleException() : base("The handler cannot process the given message.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CantHandleException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public CantHandleException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CantHandleException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public CantHandleException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a message is dropped.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class MessageDroppedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class.
|
||||
/// </summary>
|
||||
public MessageDroppedException() : base("The message was dropped.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public MessageDroppedException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageDroppedException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public MessageDroppedException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an attempt is made to access an unavailable value, such as a remote resource.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NotAccessibleException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class.
|
||||
/// </summary>
|
||||
public NotAccessibleException() : base("The requested value is not accessible.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public NotAccessibleException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotAccessibleException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public NotAccessibleException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a message cannot be delivered.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class UndeliverableException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndeliverableException"/> class.
|
||||
/// </summary>
|
||||
public UndeliverableException() : base("The message cannot be delivered.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndeliverableException"/> class with a custom error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
public UndeliverableException(string message) : base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndeliverableException"/> class with a custom error message and an inner exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The custom error message.</param>
|
||||
/// <param name="innerException">The inner exception that caused this error.</param>
|
||||
public UndeliverableException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes handler methods asynchronously using reflection.
|
||||
/// The target methods must return either a ValueTask or a ValueTask{T}.
|
||||
/// This class wraps the reflection call and provides a unified asynchronous invocation interface.
|
||||
/// </summary>
|
||||
internal sealed class HandlerInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans the provided agent for implemented handler interfaces (IHandle<> and IHandle<,>) via reflection,
|
||||
/// creates a corresponding <see cref="HandlerInvoker"/> for each handler method, and returns a dictionary that maps
|
||||
/// the message type (first generic argument of the interface) to its invoker.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent instance whose handler interfaces will be reflected.</param>
|
||||
/// <returns>A dictionary mapping message types to their corresponding <see cref="HandlerInvoker"/> instances.</returns>
|
||||
public static Dictionary<Type, HandlerInvoker> ReflectAgentHandlers(BaseAgent agent)
|
||||
{
|
||||
Type realType = agent.GetType();
|
||||
|
||||
IEnumerable<Type> candidateInterfaces =
|
||||
realType.GetInterfaces()
|
||||
.Where(i => i.IsGenericType &&
|
||||
(i.GetGenericTypeDefinition() == typeof(IHandle<>) ||
|
||||
(i.GetGenericTypeDefinition() == typeof(IHandle<,>))));
|
||||
|
||||
Dictionary<Type, HandlerInvoker> invokers = [];
|
||||
foreach (Type interface_ in candidateInterfaces)
|
||||
{
|
||||
MethodInfo handleAsync =
|
||||
interface_.GetMethod(nameof(IHandle<object>.HandleAsync), BindingFlags.Instance | BindingFlags.Public) ??
|
||||
throw new InvalidOperationException($"No handler method found for interface {interface_.FullName}");
|
||||
|
||||
HandlerInvoker invoker = new(handleAsync, agent);
|
||||
invokers.Add(interface_.GetGenericArguments()[0], invoker);
|
||||
}
|
||||
|
||||
return invokers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the asynchronous invocation function.
|
||||
/// </summary>
|
||||
private Func<object?, MessageContext, ValueTask<object?>> Invocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandlerInvoker"/> class with the specified method information and target object.
|
||||
/// </summary>
|
||||
/// <param name="methodInfo">The MethodInfo representing the handler method to be invoked.</param>
|
||||
/// <param name="target">The target instance of the agent.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the target is missing for a non-static method or if the method's return type is not supported.</exception>
|
||||
private HandlerInvoker(MethodInfo methodInfo, BaseAgent target)
|
||||
{
|
||||
object? invocation(object? message, MessageContext messageContext) => methodInfo.Invoke(target, [message, messageContext]);
|
||||
|
||||
Func<object?, MessageContext, ValueTask<object?>> getResultAsync;
|
||||
// Check if the method returns a non-generic ValueTask
|
||||
if (methodInfo.ReturnType.IsAssignableFrom(typeof(ValueTask)))
|
||||
{
|
||||
getResultAsync = async (message, messageContext) =>
|
||||
{
|
||||
// Await the ValueTask and return null as there is no result value.
|
||||
await ((ValueTask)invocation(message, messageContext)!).ConfigureAwait(false);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
// Check if the method returns a generic ValueTask<T>
|
||||
else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))
|
||||
{
|
||||
// Obtain the generic type argument for ValueTask<T>
|
||||
MethodInfo typeEraseAwait = typeof(HandlerInvoker)
|
||||
.GetMethod(nameof(TypeEraseAwaitAsync), BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.MakeGenericMethod(methodInfo.ReturnType.GetGenericArguments()[0]);
|
||||
|
||||
getResultAsync = async (message, messageContext) =>
|
||||
{
|
||||
// Execute the invocation and then type-erase the ValueTask<T> to ValueTask<object?>
|
||||
object valueTask = invocation(message, messageContext)!;
|
||||
object? typelessValueTask = typeEraseAwait.Invoke(null, [valueTask]);
|
||||
|
||||
Debug.Assert(typelessValueTask is ValueTask<object?>, "Expected ValueTask<object?> after type erasure.");
|
||||
|
||||
return await ((ValueTask<object?>)typelessValueTask).ConfigureAwait(false);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Method {methodInfo.Name} must return a ValueTask or ValueTask<T>");
|
||||
}
|
||||
|
||||
this.Invocation = getResultAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the handler method asynchronously with the provided message and context.
|
||||
/// </summary>
|
||||
/// <param name="obj">The message to be passed as the first argument to the handler.</param>
|
||||
/// <param name="messageContext">The contextual information associated with the message.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation, which yields the handler's result.</returns>
|
||||
public async ValueTask<object?> InvokeAsync(object? obj, MessageContext messageContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.Invocation.Invoke(obj, messageContext).ConfigureAwait(false);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
// Unwrap the exception to get the original exception thrown by the handler method.
|
||||
Exception? innerException = ex.InnerException;
|
||||
if (innerException != null)
|
||||
{
|
||||
throw innerException;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Awaits a generic ValueTask and returns its result as an object.
|
||||
/// This method is used to convert a ValueTask{T} to ValueTask{object?}.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result contained in the ValueTask.</typeparam>
|
||||
/// <param name="vt">The ValueTask to be awaited.</param>
|
||||
/// <returns>A ValueTask containing the result as an object.</returns>
|
||||
private static async ValueTask<object?> TypeEraseAwaitAsync<T>(ValueTask<T> vt)
|
||||
{
|
||||
return await vt.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,22 @@ using System.Threading.Tasks;
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the runtime environment for agents, managing message sending, subscriptions, agent resolution, and state persistence.
|
||||
/// Defines the runtime environment for actors, managing message sending, subscriptions, actor resolution, and state persistence,
|
||||
/// all in support of agent-based architectures.
|
||||
/// </summary>
|
||||
public interface IAgentRuntime : ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a message to an agent and gets a response.
|
||||
/// This method should be used to communicate directly with an agent.
|
||||
/// Sends a message to an actor and gets a response.
|
||||
/// This method should be used to communicate directly with an actor.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="recipient">The agent to send the message to.</param>
|
||||
/// <param name="sender">The agent sending the message. Should be <c>null</c> if sent from an external source.</param>
|
||||
/// <param name="recipient">The actor to send the message to.</param>
|
||||
/// <param name="sender">The actor sending the message. Should be <c>null</c> if sent from an external source.</param>
|
||||
/// <param name="messageId">A unique identifier for the message. If <c>null</c>, a new ID will be generated.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the response from the agent.</returns>
|
||||
/// <exception cref="CantHandleException">Thrown if the recipient cannot handle the message.</exception>
|
||||
/// <exception cref="UndeliverableException">Thrown if the message cannot be delivered.</exception>
|
||||
ValueTask<object?> SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
/// <returns>A task representing the asynchronous operation, returning the response from the actor.</returns>
|
||||
ValueTask<object?> SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all agents subscribed to the given topic.
|
||||
@@ -33,90 +32,99 @@ public interface IAgentRuntime : ISaveState
|
||||
/// </summary>
|
||||
/// <param name="message">The message to publish.</param>
|
||||
/// <param name="topic">The topic to publish the message to.</param>
|
||||
/// <param name="sender">The agent sending the message. Defaults to <c>null</c>.</param>
|
||||
/// <param name="sender">The actor sending the message. Defaults to <c>null</c>.</param>
|
||||
/// <param name="messageId">A unique message ID. If <c>null</c>, a new one will be generated.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="UndeliverableException">Thrown if the message cannot be delivered.</exception>
|
||||
ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent by its unique identifier.
|
||||
/// Retrieves an actor by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The unique identifier of the agent.</param>
|
||||
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
|
||||
ValueTask<AgentId> GetAgentAsync(AgentId agentId, bool lazy = true/*, CancellationToken? = default*/);
|
||||
/// <param name="actorId">The unique identifier of the actor.</param>
|
||||
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
|
||||
ValueTask<ActorId> GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent by its type.
|
||||
/// Retrieves an actor by its type.
|
||||
/// </summary>
|
||||
/// <param name="agentType">The type of the agent.</param>
|
||||
/// <param name="key">An optional key to specify variations of the agent. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
|
||||
ValueTask<AgentId> GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/);
|
||||
/// <param name="actorType">The type of the actor.</param>
|
||||
/// <param name="key">An optional key to specify variations of the actor. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
|
||||
ValueTask<ActorId> GetActorAsync(ActorType actorType, string key = "default", bool lazy = true, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent by its string representation.
|
||||
/// Retrieves an actor by its string representation.
|
||||
/// </summary>
|
||||
/// <param name="agent">The string representation of the agent.</param>
|
||||
/// <param name="key">An optional key to specify variations of the agent. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the agent is fetched lazily.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's ID.</returns>
|
||||
ValueTask<AgentId> GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/);
|
||||
/// <param name="actor">The string representation of the actor.</param>
|
||||
/// <param name="key">An optional key to specify variations of the actor. Defaults to "default".</param>
|
||||
/// <param name="lazy">If <c>true</c>, the actor is fetched lazily.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's ID.</returns>
|
||||
ValueTask<ActorId> GetActorAsync(string actor, string key = "default", bool lazy = true, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the state of an agent.
|
||||
/// Saves the state of an actor.
|
||||
/// The result must be JSON serializable.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent whose state is being saved.</param>
|
||||
/// <param name="actorId">The ID of the actor whose state is being saved.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning a dictionary of the saved state.</returns>
|
||||
ValueTask<JsonElement> SaveAgentStateAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/);
|
||||
ValueTask<JsonElement> SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the saved state into an agent.
|
||||
/// Loads the saved state into an actor.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent whose state is being restored.</param>
|
||||
/// <param name="actorId">The ID of the actor whose state is being restored.</param>
|
||||
/// <param name="state">The state dictionary to restore.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state/*, CancellationToken? cancellationToken = default*/);
|
||||
ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves metadata for an agent.
|
||||
/// Retrieves metadata for an actor.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the agent's metadata.</returns>
|
||||
ValueTask<AgentMetadata> GetAgentMetadataAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/);
|
||||
/// <param name="actorId">The ID of the actor.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the actor's metadata.</returns>
|
||||
ValueTask<ActorMetadata> GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new subscription for the runtime to handle when processing published messages.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to add.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription/*, CancellationToken? cancellationToken = default*/);
|
||||
ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a subscription from the runtime.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The unique identifier of the subscription to remove.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if the subscription does not exist.</exception>
|
||||
ValueTask RemoveSubscriptionAsync(string subscriptionId/*, CancellationToken? cancellationToken = default*/);
|
||||
ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers an agent factory with the runtime, associating it with a specific agent type.
|
||||
/// Registers an actor factory with the runtime, associating it with a specific actor type.
|
||||
/// The type must be unique.
|
||||
/// </summary>
|
||||
/// <param name="type">The agent type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the agent instance.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered <see cref="AgentType"/>.</returns>
|
||||
ValueTask<AgentType> RegisterAgentFactoryAsync(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc);
|
||||
/// <param name="type">The actor type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the actor instance.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered <see cref="ActorType"/>.</returns>
|
||||
ValueTask<ActorType> RegisterActorFactoryAsync(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve an <see cref="AgentProxy"/> for the specified agent.
|
||||
/// Attempts to retrieve an <see cref="IdProxyActor"/> for the specified actor.
|
||||
/// </summary>
|
||||
/// <param name="agentId">The ID of the agent.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning an <see cref="AgentProxy"/> if successful.</returns>
|
||||
ValueTask<AgentProxy> TryGetAgentProxyAsync(AgentId agentId);
|
||||
/// <param name="actorId">The ID of the actor.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning an <see cref="IdProxyActor"/> if successful.</returns>
|
||||
ValueTask<IdProxyActor?> TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler interface for processing items of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item to be handled.</typeparam>
|
||||
public interface IHandle<in T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the specified item asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be handled.</param>
|
||||
/// <param name="messageContext">The context of the message being handled.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
ValueTask HandleAsync(T item, MessageContext messageContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler interface for processing items of type <typeparamref name="TIn"/> and <typeparamref name="TOut"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TIn">The input type</typeparam>
|
||||
/// <typeparam name="TOut">The output type</typeparam>
|
||||
public interface IHandle<in TIn, TOut>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the specified item asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to be handled.</param>
|
||||
/// <param name="messageContext">The context of the message being handled.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
ValueTask<TOut> HandleAsync(TIn item, MessageContext messageContext);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an agent that can be explicitly hosted and closed when the runtime shuts down.
|
||||
/// </summary>
|
||||
public interface IHostableAgent : IAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the runtime is closing.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask CloseAsync();
|
||||
}
|
||||
+11
-10
@@ -1,36 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an agent within the runtime that can process messages, maintain state, and be closed when no longer needed.
|
||||
/// Represents an actor within the runtime that can process messages, maintain state, and be closed when no longer needed.
|
||||
/// </summary>
|
||||
public interface IAgent : ISaveState
|
||||
public interface IRuntimeActor : ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the agent.
|
||||
/// Gets the unique identifier of the actor.
|
||||
/// </summary>
|
||||
AgentId Id { get; }
|
||||
ActorId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets metadata associated with the agent.
|
||||
/// Gets metadata associated with the actor.
|
||||
/// </summary>
|
||||
AgentMetadata Metadata { get; }
|
||||
ActorMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message for the agent.
|
||||
/// This should only be called by the runtime, not by other agents.
|
||||
/// Handles an incoming message for the actor.
|
||||
/// This should only be called by the runtime, not by other actors.
|
||||
/// </summary>
|
||||
/// <param name="message">The received message. The type should match one of the expected subscription types.</param>
|
||||
/// <param name="messageContext">The context of the message, providing additional metadata.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation, returning a response to the message.
|
||||
/// The response can be <c>null</c> if no reply is necessary.
|
||||
/// </returns>
|
||||
/// <exception cref="OperationCanceledException">Thrown if the message was cancelled.</exception>
|
||||
/// <exception cref="CantHandleException">Thrown if the agent cannot handle the message.</exception>
|
||||
ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET?
|
||||
ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,25 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
// TODO: Why is this interface needed? It's inherited by IAgentRuntime and IRuntimeActor.
|
||||
// Is the former needed (does IAgentRuntime need to not only persist every actor but do so via
|
||||
// this interface)? If not, these methods could be moved to IRuntimeActor.
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for saving and loading the state of an object.
|
||||
/// The state must be JSON serializable.
|
||||
/// Defines a contract for saving and loading the state of an object as JSON.
|
||||
/// </summary>
|
||||
public interface ISaveState
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the current state of the object.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation, returning a dictionary
|
||||
/// containing the saved state. The structure of the state is implementation-defined
|
||||
/// but must be JSON serializable.
|
||||
/// </returns>
|
||||
ValueTask<JsonElement> SaveStateAsync();
|
||||
ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a previously saved state into the object.
|
||||
@@ -28,6 +33,7 @@ public interface ISaveState
|
||||
/// A dictionary representing the saved state. The structure of the state
|
||||
/// is implementation-defined but must be JSON serializable.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
ValueTask LoadStateAsync(JsonElement state);
|
||||
ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a subscription that matches topics and maps them to agents.
|
||||
/// Defines a subscription that matches topics and maps them to actors.
|
||||
/// </summary>
|
||||
public interface ISubscriptionDefinition
|
||||
{
|
||||
@@ -42,10 +42,10 @@ public interface ISubscriptionDefinition
|
||||
bool Matches(TopicId topic);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="AgentId"/>.
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="ActorId"/>.
|
||||
/// Should only be called if <see cref="Matches"/> returns <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to map.</param>
|
||||
/// <returns>The <see cref="AgentId"/> that should handle the topic.</returns>
|
||||
AgentId MapToAgent(TopicId topic);
|
||||
/// <returns>The <see cref="ActorId"/> that should handle the topic.</returns>
|
||||
ActorId MapToActor(TopicId topic);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an actor proxy that allows you to use an <see cref="ActorId"/> in place of its associated <see cref="IRuntimeActor"/>.
|
||||
/// </summary>
|
||||
public sealed class IdProxyActor : IRuntimeActor
|
||||
{
|
||||
/// <summary>The runtime instance used to interact with actors.</summary>
|
||||
private readonly IAgentRuntime _runtime;
|
||||
/// <summary>The metadata for the actor, lazy-loaded.</summary>
|
||||
private ActorMetadata? _metadata;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IdProxyActor"/> class.
|
||||
/// </summary>
|
||||
public IdProxyActor(IAgentRuntime runtime, ActorId actorId)
|
||||
{
|
||||
this.Id = actorId;
|
||||
this._runtime = runtime;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ActorId Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ActorMetadata Metadata =>
|
||||
this._metadata ??=
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
this._runtime.GetActorMetadataAsync(this.Id).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<object?> SendMessageAsync(object message, ActorId sender, string? messageId = null, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.LoadActorStateAsync(this.Id, state, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default) =>
|
||||
this._runtime.SaveActorStateAsync(this.Id, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
ValueTask<object?> IRuntimeActor.OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken) =>
|
||||
new((object?)null);
|
||||
}
|
||||
+13
-34
@@ -1,52 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for parsing key-value string representations.
|
||||
/// </summary>
|
||||
internal static class KeyValueParserExtensions
|
||||
internal static class KeyValueParser
|
||||
{
|
||||
/// <summary>
|
||||
/// The regular expression pattern used to match key-value pairs in the format "key/value".
|
||||
/// </summary>
|
||||
private const string KVPairPattern = @"^(?<key>\w+)/(?<value>\w+)$";
|
||||
|
||||
/// <summary>
|
||||
/// The compiled regex used for extracting key-value pairs from a string.
|
||||
/// </summary>
|
||||
private static readonly Regex KVPairRegex = new(KVPairPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string in the format "key/value" into a tuple containing the key and value.
|
||||
/// </summary>
|
||||
/// <param name="inputPair">The input string containing a key-value pair.</param>
|
||||
/// <param name="keyName">The expected name of the key component.</param>
|
||||
/// <param name="valueName">The expected name of the value component.</param>
|
||||
/// <returns>A tuple containing the extracted key and value.</returns>
|
||||
/// <exception cref="FormatException">
|
||||
/// Thrown if the input string does not match the expected "key/value" format.
|
||||
/// </exception>
|
||||
/// <example>
|
||||
/// Example usage:
|
||||
/// <code>
|
||||
/// string input = "agent1/12345";
|
||||
/// var result = input.ToKVPair("Type", "Key");
|
||||
/// Console.WriteLine(result.Item1); // Outputs: agent1
|
||||
/// Console.WriteLine(result.Item2); // Outputs: 12345
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static (string, string) ToKeyValuePair(this string inputPair, string keyName, string valueName)
|
||||
public static bool TryParse(string input, [NotNullWhen(true)] out string? key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
Match match = KVPairRegex.Match(inputPair);
|
||||
if (match.Success)
|
||||
if (!string.IsNullOrEmpty(input))
|
||||
{
|
||||
return (match.Groups["key"].Value, match.Groups["value"].Value);
|
||||
int separatorIndex = input.IndexOf('/');
|
||||
if (separatorIndex >= 0)
|
||||
{
|
||||
key = input.Substring(0, separatorIndex);
|
||||
value = input.Substring(separatorIndex + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid key-value pair format: {inputPair}; expecting \"{{{keyName}}}/{{{valueName}}}\"");
|
||||
key = value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,32 +7,36 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the context of a message being sent within the agent runtime.
|
||||
/// This includes metadata such as the sender, topic, RPC status, and cancellation handling.
|
||||
/// </summary>
|
||||
public class MessageContext(string messageId, CancellationToken cancellationToken)
|
||||
/// <remarks>
|
||||
/// This includes metadata such as the sender, topic, ahd RPC status.
|
||||
/// </remarks>
|
||||
public sealed class MessageContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageContext"/> class.
|
||||
/// </summary>
|
||||
public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation)
|
||||
{ }
|
||||
private string? _messageId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this message.
|
||||
/// </summary>
|
||||
public string MessageId { get; } = messageId;
|
||||
public string MessageId
|
||||
{
|
||||
get => this._messageId ?? Interlocked.CompareExchange(ref this._messageId, Guid.NewGuid().ToString(), null) ?? this._messageId;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new ArgumentException("MessageId cannot be null or empty.", nameof(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cancellation token associated with this message.
|
||||
/// This can be used to cancel the operation if necessary.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; } = cancellationToken;
|
||||
this._messageId = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender of the message.
|
||||
/// If <c>null</c>, the sender is unspecified.
|
||||
/// </summary>
|
||||
public AgentId? Sender { get; set; }
|
||||
public ActorId? Sender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic associated with the message.
|
||||
|
||||
+5
-1
@@ -5,15 +5,19 @@
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);IDE1006;IDE0130</NoWarn>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<IsAotCompatible>false</IsAotCompatible> <!-- TODO: Fix this -->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectDiagnosticAttributesOnLegacy>true</InjectDiagnosticAttributesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="IRuntimeActor.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base implementation of <see cref="IRuntimeActor"/>.
|
||||
/// </summary>
|
||||
public abstract class RuntimeActor : IRuntimeActor
|
||||
{
|
||||
private static readonly JsonElement s_emptyElement = JsonDocument.Parse("{}").RootElement;
|
||||
|
||||
/// <summary>
|
||||
/// The activity source for tracing.
|
||||
/// </summary>
|
||||
public static readonly ActivitySource TraceSource = new($"{typeof(IRuntimeActor).Namespace}");
|
||||
|
||||
private readonly Dictionary<Type, HandlerInvoker> _handlerInvokers = [];
|
||||
private readonly IAgentRuntime _runtime;
|
||||
|
||||
private delegate ValueTask<object?> HandlerInvoker(object? message, MessageContext messageContext, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Provides logging capabilities used for diagnostic and operational information.
|
||||
/// </summary>
|
||||
protected internal ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the actor.
|
||||
/// </summary>
|
||||
public ActorId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata of the actor.
|
||||
/// </summary>
|
||||
public ActorMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RuntimeActor class with the specified identifier, runtime, description, and optional logger.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the actor.</param>
|
||||
/// <param name="runtime">The runtime environment in which the actor operates.</param>
|
||||
/// <param name="description">A brief description of the actor's purpose.</param>
|
||||
/// <param name="logger">An optional logger for recording diagnostic information.</param>
|
||||
protected RuntimeActor(
|
||||
ActorId id,
|
||||
IAgentRuntime runtime,
|
||||
string description,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
this.Id = id;
|
||||
this._runtime = runtime;
|
||||
this.Logger = logger ?? NullLogger.Instance;
|
||||
|
||||
this.Metadata = new ActorMetadata(this.Id.Type, this.Id.Key, description);
|
||||
}
|
||||
|
||||
/// <summary>Registers a handler for <typeparamref name="TInput"/>.</summary>
|
||||
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
|
||||
/// <param name="messageHandler">The handler function that processes the message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
|
||||
/// <remarks>
|
||||
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
|
||||
/// </remarks>
|
||||
protected void RegisterMessageHandler<TInput>(Func<TInput, MessageContext, CancellationToken, ValueTask> messageHandler)
|
||||
{
|
||||
if (messageHandler is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(messageHandler));
|
||||
}
|
||||
|
||||
if (this._handlerInvokers.ContainsKey(typeof(TInput)))
|
||||
{
|
||||
throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered.");
|
||||
}
|
||||
|
||||
this._handlerInvokers.Add(
|
||||
typeof(TInput),
|
||||
async (message, messageContext, cancellationToken) =>
|
||||
{
|
||||
await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false);
|
||||
return null; // No return value for void handlers
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Registers a handler for <typeparamref name="TInput"/> that produces a <typeparamref name="TOutput"/>.</summary>
|
||||
/// <typeparam name="TInput">The type of the input message for the handler.</typeparam>
|
||||
/// <typeparam name="TOutput">The type of the output message for the handler.</typeparam>
|
||||
/// <param name="messageHandler">The handler function that processes the message.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when a handler for the specified type is already registered.</exception>
|
||||
/// <remarks>
|
||||
/// The base implementation of <see cref="OnMessageAsync"/> will use these registered handlers to process incoming messages.
|
||||
/// </remarks>
|
||||
protected void RegisterMessageHandler<TInput, TOutput>(Func<TInput, MessageContext, CancellationToken, ValueTask<TOutput>> messageHandler)
|
||||
{
|
||||
if (messageHandler is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(messageHandler));
|
||||
}
|
||||
|
||||
if (this._handlerInvokers.ContainsKey(typeof(TInput)))
|
||||
{
|
||||
throw new InvalidOperationException($"A handler for type {typeof(TInput)} is already registered.");
|
||||
}
|
||||
|
||||
this._handlerInvokers.Add(
|
||||
typeof(TInput),
|
||||
async (message, messageContext, cancellationToken) =>
|
||||
{
|
||||
TOutput? result = await messageHandler((TInput)message!, messageContext, cancellationToken).ConfigureAwait(false);
|
||||
return (object?)result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an incoming message by determining its type and invoking the corresponding handler method if available.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to be handled.</param>
|
||||
/// <param name="messageContext">The context associated with the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, containing the response object or null.</returns>
|
||||
public ValueTask<object?> OnMessageAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the handler for the message type, and invoke it, if it exists.
|
||||
if (message is not null && this._handlerInvokers.TryGetValue(message.GetType(), out HandlerInvoker? handlerInvoker))
|
||||
{
|
||||
return handlerInvoker(message, messageContext, cancellationToken);
|
||||
}
|
||||
|
||||
return new((object?)null);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default) =>
|
||||
new(s_emptyElement);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default) =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient actor through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="actor">The requested actor's type.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected async ValueTask<ActorId?> GetActorAsync(ActorType actor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this._runtime.GetActorAsync(actor, lazy: false, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to a specified recipient actor through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to send.</param>
|
||||
/// <param name="recipient">The recipient actor's identifier.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation, returning the response object or null.</returns>
|
||||
protected ValueTask<object?> SendMessageAsync(object message, ActorId recipient, string? messageId = null, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a message to all actors subscribed to a specific topic through the runtime.
|
||||
/// </summary>
|
||||
/// <param name="message">The message object to publish.</param>
|
||||
/// <param name="topic">The topic identifier to which the message is published.</param>
|
||||
/// <param name="messageId">An optional identifier for the message.</param>
|
||||
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
|
||||
/// <returns>A ValueTask that represents the asynchronous publish operation.</returns>
|
||||
protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) =>
|
||||
this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken);
|
||||
}
|
||||
@@ -2,147 +2,111 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a topic identifier that defines the scope of a broadcast message.
|
||||
/// Provides a topic identifier that defines the scope of a broadcast message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The agent runtime implements a publish-subscribe model through its broadcast API,
|
||||
/// where messages must be published with a specific topic.
|
||||
///
|
||||
/// See the Python equivalent:
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type Specification</see>.
|
||||
/// </summary>
|
||||
public struct TopicId : IEquatable<TopicId>
|
||||
/// </remarks>
|
||||
public readonly partial struct TopicId : IEquatable<TopicId>
|
||||
{
|
||||
/// <summary>
|
||||
/// The default source value used when no source is explicitly provided.
|
||||
/// </summary>
|
||||
public const string DefaultSource = "default";
|
||||
private const string TypePattern = @"^[\w-.:=]+$";
|
||||
|
||||
/// <summary>
|
||||
/// The separator character for the string representation of the topic.
|
||||
/// </summary>
|
||||
public const string Separator = "/";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the event that this <see cref="TopicId"/> represents.
|
||||
/// This adheres to the CloudEvents specification.
|
||||
///
|
||||
/// Must match the pattern: <c>^[\w\-\.\:\=]+$</c>.
|
||||
///
|
||||
/// Learn more here:
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type</see>.
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source that identifies the context in which an event happened.
|
||||
/// This adheres to the CloudEvents specification.
|
||||
///
|
||||
/// Learn more here:
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#source-1">CloudEvents Source</see>.
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
#if NET
|
||||
[GeneratedRegex(TypePattern)]
|
||||
private static partial Regex TypeRegex();
|
||||
#else
|
||||
private static Regex TypeRegex() => s_typeRegex;
|
||||
private static readonly Regex s_typeRegex = new(TypePattern, RegexOptions.Compiled);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the topic.</param>
|
||||
/// <param name="source">The source of the event. Defaults to <see cref="DefaultSource"/> if not specified.</param>
|
||||
public TopicId(string type, string source = DefaultSource)
|
||||
/// <param name="type">The type of the topic. Must match the pattern: <c>^[\w-.:=]+$</c></param>
|
||||
/// <param name="source">The source of the event.</param>
|
||||
public TopicId(string type, string? source = null)
|
||||
{
|
||||
this.Type = type;
|
||||
this.Source = source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicId"/> struct from a tuple.
|
||||
/// </summary>
|
||||
/// <param name="kvPair">A tuple containing the topic type and source.</param>
|
||||
public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.Source)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string in the format "type/source" into a <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="maybeTopicId">The topic ID string.</param>
|
||||
/// <returns>An instance of <see cref="TopicId"/>.</returns>
|
||||
/// <exception cref="FormatException">Thrown when the string is not in the valid "type/source" format.</exception>
|
||||
public static TopicId FromStr(string maybeTopicId) => new(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source)));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "type/source".</returns>
|
||||
public override readonly string ToString() => $"{this.Type}{Separator}{this.Source}";
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current <see cref="TopicId"/>; otherwise, <c>false</c>.</returns>
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
if (obj is TopicId other)
|
||||
if (type is null)
|
||||
{
|
||||
return this.Type == other.Type && this.Source == other.Source;
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!TypeRegex().IsMatch(type))
|
||||
{
|
||||
throw new ArgumentException("Invalid type format.", nameof(type));
|
||||
}
|
||||
|
||||
// TODO: What validation should be performed on source? The cited cloudevents spec suggests it should be a URI reference.
|
||||
|
||||
this.Type = type;
|
||||
this.Source = source ?? "default";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="TopicId"/>.
|
||||
/// Gets the type of the event that this <see cref="TopicId"/> represents.
|
||||
/// </summary>
|
||||
/// <param name="other">The object to compare with the current instance.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current <see cref="TopicId"/>; otherwise, <c>false</c>.</returns>
|
||||
public readonly bool Equals([NotNullWhen(true)] TopicId other)
|
||||
{
|
||||
return this.Type == other.Type && this.Source == other.Source;
|
||||
}
|
||||
/// <remarks>
|
||||
/// This adheres to the CloudEvents specification.
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#type">CloudEvents Type</see>.
|
||||
/// </remarks>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this <see cref="TopicId"/>.
|
||||
/// Gets the source that identifies the context in which an event happened.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for the current instance.</returns>
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Type, this.Source);
|
||||
}
|
||||
/// <remarks>
|
||||
/// This adheres to the CloudEvents specification.
|
||||
/// <see href="https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#source-1">CloudEvents Source</see>.
|
||||
/// </remarks>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts a string to a <see cref="TopicId"/>.
|
||||
/// Convert a string of the format "type/key" into an <see cref="TopicId"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">The string representation of a topic ID.</param>
|
||||
/// <param name="TopicId">The actor ID string.</param>
|
||||
/// <returns>An instance of <see cref="TopicId"/>.</returns>
|
||||
public static explicit operator TopicId(string id) => FromStr(id);
|
||||
public static TopicId Parse(string TopicId)
|
||||
{
|
||||
if (!KeyValueParser.TryParse(TopicId, out string? type, out string? key))
|
||||
{
|
||||
throw new FormatException($"Invalid TopicId format: '{TopicId}'. Expected format is 'type/key'.");
|
||||
}
|
||||
|
||||
return new TopicId(type, key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly string ToString() => $"{this.Type}/{this.Source}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj) =>
|
||||
obj is TopicId other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly bool Equals(TopicId other) =>
|
||||
this.Type == other.Type && this.Source == other.Source;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(this.Type, this.Source);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator ==(TopicId left, TopicId right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator !=(TopicId left, TopicId right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
// TODO: Implement < for wildcard matching (type, *)
|
||||
// == => <
|
||||
// Type == other.Type => <
|
||||
/// <summary>
|
||||
/// Determines whether the given <see cref="TopicId"/> matches another topic.
|
||||
/// </summary>
|
||||
/// <param name="other">The topic ID to compare against.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the topic types are equal; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public readonly bool IsWildcardMatch(TopicId other)
|
||||
{
|
||||
return this.Type == other.Type;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator ==(TopicId left, TopicId right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator !=(TopicId left, TopicId right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
//public readonly bool IsWildcardMatch(TopicId other)
|
||||
//{
|
||||
// return this.Type == other.Type;
|
||||
//}
|
||||
}
|
||||
|
||||
+16
-16
@@ -6,8 +6,8 @@ using System.Diagnostics.CodeAnalysis;
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// This subscription matches on topics based on the exact type and maps to agents using the source of the topic as the agent key.
|
||||
/// This subscription causes each source to have its own agent instance.
|
||||
/// This subscription matches on topics based on the exact type and maps to actors using the source of the topic as the actor key.
|
||||
/// This subscription causes each source to have its own actor instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example:
|
||||
@@ -15,21 +15,21 @@ namespace Microsoft.Extensions.AI.Agents.Runtime;
|
||||
/// var subscription = new TypeSubscription("t1", "a1");
|
||||
/// </code>
|
||||
/// In this case:
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`.
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`.
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s1"` will be handled by an actor of type `"a1"` with key `"s1"`.
|
||||
/// - A <see cref="TopicId"/> with type `"t1"` and source `"s2"` will be handled by an actor of type `"a1"` with key `"s2"`.
|
||||
/// </remarks>
|
||||
public class TypeSubscription : ISubscriptionDefinition
|
||||
public sealed class TypeSubscription : ISubscriptionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypeSubscription"/> class.
|
||||
/// </summary>
|
||||
/// <param name="topicType">The exact topic type to match against.</param>
|
||||
/// <param name="agentType">Agent type to handle this subscription.</param>
|
||||
/// <param name="actorType">Actor type to handle this subscription.</param>
|
||||
/// <param name="id">Unique identifier for the subscription. If not provided, a new UUID will be generated.</param>
|
||||
public TypeSubscription(string topicType, AgentType agentType, string? id = null)
|
||||
public TypeSubscription(string topicType, ActorType actorType, string? id = null)
|
||||
{
|
||||
this.TopicType = topicType;
|
||||
this.AgentType = agentType;
|
||||
this.ActorType = actorType;
|
||||
this.Id = id ?? Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ public class TypeSubscription : ISubscriptionDefinition
|
||||
public string TopicType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agent type that handles this subscription.
|
||||
/// Gets the actor type that handles this subscription.
|
||||
/// </summary>
|
||||
public AgentType AgentType { get; }
|
||||
public ActorType ActorType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given <see cref="TopicId"/> matches the subscription based on an exact type match.
|
||||
@@ -59,19 +59,19 @@ public class TypeSubscription : ISubscriptionDefinition
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="AgentId"/>. Should only be called if <see cref="Matches"/> returns true.
|
||||
/// Maps a <see cref="TopicId"/> to an <see cref="ActorId"/>. Should only be called if <see cref="Matches"/> returns true.
|
||||
/// </summary>
|
||||
/// <param name="topic">The topic to map.</param>
|
||||
/// <returns>An <see cref="AgentId"/> representing the agent that should handle the topic.</returns>
|
||||
/// <returns>An <see cref="ActorId"/> representing the actor that should handle the topic.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the topic does not match the subscription.</exception>
|
||||
public AgentId MapToAgent(TopicId topic)
|
||||
public ActorId MapToActor(TopicId topic)
|
||||
{
|
||||
if (!this.Matches(topic))
|
||||
{
|
||||
throw new InvalidOperationException("TopicId does not match the subscription.");
|
||||
}
|
||||
|
||||
return new AgentId(this.AgentType, topic.Source);
|
||||
return new ActorId(this.ActorType, topic.Source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,7 +84,7 @@ public class TypeSubscription : ISubscriptionDefinition
|
||||
return
|
||||
obj is TypeSubscription other &&
|
||||
(this.Id == other.Id ||
|
||||
(this.AgentType == other.AgentType &&
|
||||
(this.ActorType == other.ActorType &&
|
||||
this.TopicType == other.TopicType));
|
||||
}
|
||||
|
||||
@@ -101,6 +101,6 @@ public class TypeSubscription : ISubscriptionDefinition
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Id, this.AgentType, this.TopicType);
|
||||
return HashCode.Combine(this.Id, this.ActorType, this.TopicType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,19 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-process/in-memory implementation of the agent runtime.
|
||||
/// </summary>
|
||||
public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
public sealed partial class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
{
|
||||
private readonly Dictionary<AgentType, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>>> _agentFactories = [];
|
||||
private readonly Dictionary<ActorType, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>>> _actorFactories = [];
|
||||
private readonly Dictionary<string, ISubscriptionDefinition> _subscriptions = [];
|
||||
private readonly ConcurrentQueue<MessageDelivery> _messageDeliveryQueue = new();
|
||||
|
||||
@@ -29,13 +27,13 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
private Func<bool> _shouldContinue = () => true;
|
||||
|
||||
// Exposed for testing purposes.
|
||||
internal int messageQueueCount;
|
||||
internal readonly Dictionary<AgentId, IHostableAgent> agentInstances = [];
|
||||
internal int _messageQueueCount;
|
||||
internal readonly Dictionary<ActorId, IRuntimeActor> _actorInstances = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether agents should receive messages they send themselves.
|
||||
/// Gets or sets a value indicating whether actors should receive messages they send themselves.
|
||||
/// </summary>
|
||||
public bool DeliverToSelf { get; set; } //= false;
|
||||
public bool DeliverToSelf { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -90,7 +88,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
/// <summary>
|
||||
/// This will run until the message queue is empty and then stop the runtime.
|
||||
/// </summary>
|
||||
public async Task RunUntilIdleAsync()
|
||||
public async Task RunUntilIdleAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Func<bool> oldShouldContinue = this._shouldContinue;
|
||||
this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty;
|
||||
@@ -102,7 +100,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
public ValueTask PublishMessageAsync(object message, TopicId topic, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.ExecuteTracedAsync(async () =>
|
||||
{
|
||||
@@ -112,14 +110,14 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
.ForPublish(topic, this.PublishMessageServicerAsync);
|
||||
|
||||
this._messageDeliveryQueue.Enqueue(delivery);
|
||||
Interlocked.Increment(ref this.messageQueueCount);
|
||||
Interlocked.Increment(ref this._messageQueueCount);
|
||||
|
||||
await delivery.ResultSink.Future.ConfigureAwait(false);
|
||||
await delivery.ResultTask.ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<object?> SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
public async ValueTask<object?> SendMessageAsync(object message, ActorId recipient, ActorId? sender = null, string? messageId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.ExecuteTracedAsync(async () =>
|
||||
{
|
||||
@@ -129,74 +127,67 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
.ForSend(recipient, this.SendMessageServicerAsync);
|
||||
|
||||
this._messageDeliveryQueue.Enqueue(delivery);
|
||||
Interlocked.Increment(ref this.messageQueueCount);
|
||||
Interlocked.Increment(ref this._messageQueueCount);
|
||||
|
||||
try
|
||||
{
|
||||
return await delivery.ResultSink.Future.ConfigureAwait(false);
|
||||
}
|
||||
catch (TargetInvocationException ex) when (ex.InnerException is OperationCanceledException innerOCEx)
|
||||
{
|
||||
throw new OperationCanceledException($"Delivery of message {messageId} was cancelled.", innerOCEx);
|
||||
}
|
||||
return await delivery.ResultTask.ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentId> GetAgentAsync(AgentId agentId, bool lazy = true)
|
||||
public async ValueTask<ActorId> GetActorAsync(ActorId actorId, bool lazy = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!lazy)
|
||||
{
|
||||
await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return agentId;
|
||||
return actorId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<AgentId> GetAgentAsync(AgentType agentType, string key = AgentId.DefaultKey, bool lazy = true)
|
||||
=> this.GetAgentAsync(new AgentId(agentType, key), lazy);
|
||||
public ValueTask<ActorId> GetActorAsync(ActorType actorType, string? key = null, bool lazy = true, CancellationToken cancellationToken = default)
|
||||
=> this.GetActorAsync(actorType.Name, key, lazy, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<AgentId> GetAgentAsync(string agent, string key = AgentId.DefaultKey, bool lazy = true)
|
||||
=> this.GetAgentAsync(new AgentId(agent, key), lazy);
|
||||
public ValueTask<ActorId> GetActorAsync(string actor, string? key = null, bool lazy = true, CancellationToken cancellationToken = default)
|
||||
=> this.GetActorAsync(new ActorId(actor, key ?? "default"), lazy, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentMetadata> GetAgentMetadataAsync(AgentId agentId)
|
||||
public async ValueTask<ActorMetadata> GetActorMetadataAsync(ActorId actorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
return agent.Metadata;
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
return actor.Metadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<TAgent> TryGetUnderlyingAgentInstanceAsync<TAgent>(AgentId agentId) where TAgent : IHostableAgent
|
||||
public async ValueTask<TActor> TryGetUnderlyingActorInstanceAsync<TActor>(ActorId actorId, CancellationToken cancellationToken = default) where TActor : IRuntimeActor
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (agent is not TAgent concreteAgent)
|
||||
if (actor is not TActor concreteActor)
|
||||
{
|
||||
throw new InvalidOperationException($"Agent with name {agentId.Type} is not of type {typeof(TAgent).Name}.");
|
||||
throw new InvalidOperationException($"Actor with name {actorId.Type} is not of type {typeof(TActor).Name}.");
|
||||
}
|
||||
|
||||
return concreteAgent;
|
||||
return concreteActor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state)
|
||||
public async ValueTask LoadActorStateAsync(ActorId actorId, JsonElement state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
await agent.LoadStateAsync(state).ConfigureAwait(false);
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
await actor.LoadStateAsync(state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<JsonElement> SaveAgentStateAsync(AgentId agentId)
|
||||
public async ValueTask<JsonElement> SaveActorStateAsync(ActorId actorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
return await agent.SaveStateAsync().ConfigureAwait(false);
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
return await actor.SaveStateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription)
|
||||
public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._subscriptions.ContainsKey(subscription.Id))
|
||||
{
|
||||
@@ -209,7 +200,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask RemoveSubscriptionAsync(string subscriptionId)
|
||||
public ValueTask RemoveSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!this._subscriptions.ContainsKey(subscriptionId))
|
||||
{
|
||||
@@ -222,61 +213,62 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LoadStateAsync(JsonElement state)
|
||||
public async ValueTask LoadStateAsync(JsonElement state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (JsonProperty agentIdStr in state.EnumerateObject())
|
||||
foreach (JsonProperty actorIdStr in state.EnumerateObject())
|
||||
{
|
||||
AgentId agentId = AgentId.FromStr(agentIdStr.Name);
|
||||
ActorId actorId = ActorId.Parse(actorIdStr.Name);
|
||||
|
||||
if (this._agentFactories.ContainsKey(agentId.Type))
|
||||
if (this._actorFactories.ContainsKey(actorId.Type))
|
||||
{
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
await agent.LoadStateAsync(agentIdStr.Value).ConfigureAwait(false);
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, cancellationToken).ConfigureAwait(false);
|
||||
await actor.LoadStateAsync(actorIdStr.Value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<JsonElement> SaveStateAsync()
|
||||
public async ValueTask<JsonElement> SaveStateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Dictionary<string, JsonElement> state = [];
|
||||
foreach (AgentId agentId in this.agentInstances.Keys)
|
||||
foreach (ActorId actorId in this._actorInstances.Keys)
|
||||
{
|
||||
JsonElement agentState = await this.agentInstances[agentId].SaveStateAsync().ConfigureAwait(false);
|
||||
state[agentId.ToString()] = agentState;
|
||||
JsonElement actorState = await this._actorInstances[actorId].SaveStateAsync(cancellationToken).ConfigureAwait(false);
|
||||
state[actorId.ToString()] = actorState;
|
||||
}
|
||||
return JsonSerializer.SerializeToElement(state);
|
||||
return JsonSerializer.SerializeToElement(state, InProcessRuntimeContext.Default.DictionaryStringJsonElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an agent factory with the runtime, associating it with a specific agent type.
|
||||
/// Registers an actor factory with the runtime, associating it with a specific actor type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgent">The type of agent created by the factory.</typeparam>
|
||||
/// <param name="type">The agent type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the agent instance.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered agent type.</returns>
|
||||
public ValueTask<AgentType> RegisterAgentFactoryAsync<TAgent>(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<TAgent>> factoryFunc) where TAgent : IHostableAgent
|
||||
// Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask<TAgent>'
|
||||
/// <typeparam name="TActor">The type of actor created by the factory.</typeparam>
|
||||
/// <param name="type">The actor type to associate with the factory.</param>
|
||||
/// <param name="factoryFunc">A function that asynchronously creates the actor instance.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation if needed.</param>
|
||||
/// <returns>A task representing the asynchronous operation, returning the registered actor type.</returns>
|
||||
public ValueTask<ActorType> RegisterActorFactoryAsync<TActor>(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<TActor>> factoryFunc, CancellationToken cancellationToken = default) where TActor : IRuntimeActor
|
||||
// Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask<TActor>'
|
||||
// and recurse into the same call, causing a stack overflow.
|
||||
=> this.RegisterAgentFactoryAsync(type, async ValueTask<IHostableAgent> (agentId, runtime) => await factoryFunc(agentId, runtime).ConfigureAwait(false));
|
||||
=> this.RegisterActorFactoryAsync(type, async ValueTask<IRuntimeActor> (actorId, runtime) => await factoryFunc(actorId, runtime).ConfigureAwait(false), cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentType> RegisterAgentFactoryAsync(AgentType type, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc)
|
||||
public async ValueTask<ActorType> RegisterActorFactoryAsync(ActorType type, Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>> factoryFunc, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._agentFactories.ContainsKey(type))
|
||||
if (this._actorFactories.ContainsKey(type))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent with type {type} already exists.");
|
||||
throw new InvalidOperationException($"Actor with type {type} already exists.");
|
||||
}
|
||||
|
||||
this._agentFactories.Add(type, factoryFunc);
|
||||
this._actorFactories.Add(type, factoryFunc);
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<AgentProxy> TryGetAgentProxyAsync(AgentId agentId)
|
||||
public async ValueTask<IdProxyActor?> TryGetActorProxyAsync(ActorId actorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
AgentProxy proxy = new(agentId, this);
|
||||
IdProxyActor proxy = new(this, actorId);
|
||||
|
||||
return proxy;
|
||||
}
|
||||
@@ -285,7 +277,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
{
|
||||
if (this._messageDeliveryQueue.TryDequeue(out MessageDelivery? delivery))
|
||||
{
|
||||
Interlocked.Decrement(ref this.messageQueueCount);
|
||||
Interlocked.Decrement(ref this._messageQueueCount);
|
||||
Debug.WriteLine($"Processing message {delivery.Message.MessageId}...");
|
||||
await delivery.InvokeAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
@@ -296,26 +288,18 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
ConcurrentDictionary<Guid, Task> pendingTasks = [];
|
||||
while (!cancellation.IsCancellationRequested && this._shouldContinue())
|
||||
{
|
||||
// Get a unique task id
|
||||
Guid taskId;
|
||||
do
|
||||
{
|
||||
taskId = Guid.NewGuid();
|
||||
} while (pendingTasks.ContainsKey(taskId));
|
||||
// Get a unique task id.
|
||||
Guid taskId = Guid.NewGuid();
|
||||
|
||||
// There is potentially a race condition here, but even if we leak a Task, we will
|
||||
// still catch it on the Finish() pass.
|
||||
ValueTask processTask = this.ProcessNextMessageAsync(cancellation);
|
||||
await Task.Yield();
|
||||
|
||||
// Check if the task is already completed
|
||||
if (processTask.IsCompleted)
|
||||
if (!processTask.IsCompleted)
|
||||
{
|
||||
continue;
|
||||
pendingTasks.TryAdd(taskId, processTask.AsTask().ContinueWith(t => pendingTasks.TryRemove(taskId, out _), TaskScheduler.Current));
|
||||
}
|
||||
|
||||
Task actualTask = processTask.AsTask();
|
||||
pendingTasks.TryAdd(taskId, actualTask.ContinueWith(t => pendingTasks.TryRemove(taskId, out _), TaskScheduler.Current));
|
||||
}
|
||||
|
||||
// The pending task dictionary may contain null values when a race condition is experienced during
|
||||
@@ -332,7 +316,7 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
throw new InvalidOperationException("Message must have a topic to be published.");
|
||||
}
|
||||
|
||||
List<Exception> exceptions = [];
|
||||
List<Exception>? exceptions = null;
|
||||
TopicId topic = envelope.Topic.Value;
|
||||
foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic)))
|
||||
{
|
||||
@@ -340,36 +324,36 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
{
|
||||
deliveryToken.ThrowIfCancellationRequested();
|
||||
|
||||
AgentId? sender = envelope.Sender;
|
||||
ActorId? sender = envelope.Sender;
|
||||
|
||||
using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken);
|
||||
MessageContext messageContext = new(envelope.MessageId, combinedSource.Token)
|
||||
|
||||
ActorId actorId = subscription.MapToActor(topic);
|
||||
if (!this.DeliverToSelf && sender.HasValue && sender == actorId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MessageContext messageContext = new()
|
||||
{
|
||||
MessageId = envelope.MessageId,
|
||||
Sender = sender,
|
||||
Topic = topic,
|
||||
IsRpc = false
|
||||
};
|
||||
|
||||
AgentId agentId = subscription.MapToAgent(topic);
|
||||
if (!this.DeliverToSelf && sender.HasValue && sender == agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(actorId, combinedSource.Token).ConfigureAwait(false);
|
||||
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
|
||||
|
||||
// TODO: Cancellation propagation!
|
||||
await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false);
|
||||
await actor.OnMessageAsync(envelope.Message, messageContext, combinedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
(exceptions ??= []).Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions.Count > 0)
|
||||
if (exceptions is not null)
|
||||
{
|
||||
// TODO: Unwrap TargetInvocationException?
|
||||
throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions);
|
||||
}
|
||||
}
|
||||
@@ -382,63 +366,72 @@ public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable
|
||||
}
|
||||
|
||||
using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken);
|
||||
MessageContext messageContext = new(envelope.MessageId, combinedSource.Token)
|
||||
MessageContext messageContext = new()
|
||||
{
|
||||
MessageId = envelope.MessageId,
|
||||
Sender = envelope.Sender,
|
||||
IsRpc = false
|
||||
};
|
||||
|
||||
AgentId receiver = envelope.Receiver.Value;
|
||||
IHostableAgent agent = await this.EnsureAgentAsync(receiver).ConfigureAwait(false);
|
||||
ActorId receiver = envelope.Receiver.Value;
|
||||
IRuntimeActor actor = await this.EnsureActorAsync(receiver, combinedSource.Token).ConfigureAwait(false);
|
||||
|
||||
return await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false);
|
||||
return await actor.OnMessageAsync(envelope.Message, messageContext, combinedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask<IHostableAgent> EnsureAgentAsync(AgentId agentId)
|
||||
private async ValueTask<IRuntimeActor> EnsureActorAsync(ActorId actorId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent))
|
||||
if (!this._actorInstances.TryGetValue(actorId, out IRuntimeActor? actor))
|
||||
{
|
||||
if (!this._agentFactories.TryGetValue(agentId.Type, out Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>>? factoryFunc))
|
||||
if (!this._actorFactories.TryGetValue(actorId.Type, out Func<ActorId, IAgentRuntime, ValueTask<IRuntimeActor>>? factoryFunc))
|
||||
{
|
||||
throw new InvalidOperationException($"Agent with name {agentId.Type} not found.");
|
||||
throw new InvalidOperationException($"Actor with name {actorId.Type} not found.");
|
||||
}
|
||||
|
||||
agent = await factoryFunc(agentId, this).ConfigureAwait(false);
|
||||
this.agentInstances.Add(agentId, agent);
|
||||
actor = await factoryFunc(actorId, this).ConfigureAwait(false);
|
||||
this._actorInstances.Add(actorId, actor);
|
||||
}
|
||||
|
||||
return this.agentInstances[agentId];
|
||||
return actor;
|
||||
}
|
||||
|
||||
private async Task FinishAsync(CancellationToken token)
|
||||
{
|
||||
foreach (IHostableAgent agent in this.agentInstances.Values)
|
||||
foreach (IRuntimeActor actor in this._actorInstances.Values)
|
||||
{
|
||||
if (!token.IsCancellationRequested)
|
||||
if (!token.IsCancellationRequested && actor is IAsyncDisposable closeableActor)
|
||||
{
|
||||
await agent.CloseAsync().ConfigureAwait(false);
|
||||
await closeableActor.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
this._shutdownSource?.Dispose();
|
||||
this._finishSource?.Dispose();
|
||||
this._finishSource = null;
|
||||
this._shutdownSource = null;
|
||||
if (this._shutdownSource is { } shutdownSource)
|
||||
{
|
||||
this._shutdownSource = null;
|
||||
shutdownSource.Dispose();
|
||||
}
|
||||
|
||||
if (this._finishSource is { } finishSource)
|
||||
{
|
||||
this._finishSource = null;
|
||||
finishSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
private ValueTask<T> ExecuteTracedAsync<T>(Func<ValueTask<T>> func)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
// TODO: Bind tracing
|
||||
return func();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
private ValueTask ExecuteTracedAsync(Func<ValueTask> func)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
// TODO: Bind tracing
|
||||
return func();
|
||||
}
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
|
||||
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
|
||||
private sealed partial class InProcessRuntimeContext : JsonSerializerContext;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,11 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess;
|
||||
|
||||
internal sealed class MessageDelivery(MessageEnvelope message, Func<MessageEnvelope, CancellationToken, ValueTask> servicer, IResultSink<object?> resultSink)
|
||||
internal sealed class MessageDelivery(MessageEnvelope message, Func<MessageEnvelope, CancellationToken, ValueTask> servicer, Task<object?> resultTask)
|
||||
{
|
||||
public MessageEnvelope Message { get; } = message;
|
||||
public Func<MessageEnvelope, CancellationToken, ValueTask> Servicer { get; } = servicer;
|
||||
public IResultSink<object?> ResultSink { get; } = resultSink;
|
||||
public Task<object?> ResultTask { get; } = resultTask;
|
||||
|
||||
public ValueTask InvokeAsync(CancellationToken cancellation)
|
||||
{
|
||||
return this.Servicer(this.Message, cancellation);
|
||||
}
|
||||
public ValueTask InvokeAsync(CancellationToken cancellation) => this.Servicer(this.Message, cancellation);
|
||||
}
|
||||
|
||||
@@ -6,73 +6,60 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess;
|
||||
|
||||
internal sealed class MessageEnvelope
|
||||
internal sealed class MessageEnvelope(object message, string? messageId = null, CancellationToken cancellation = default)
|
||||
{
|
||||
public object Message { get; }
|
||||
public string MessageId { get; }
|
||||
public object Message { get; } = message;
|
||||
public string MessageId { get; } = messageId ?? Guid.NewGuid().ToString();
|
||||
public TopicId? Topic { get; private set; }
|
||||
public AgentId? Sender { get; private set; }
|
||||
public AgentId? Receiver { get; private set; }
|
||||
public CancellationToken Cancellation { get; }
|
||||
public ActorId? Sender { get; private set; }
|
||||
public ActorId? Receiver { get; private set; }
|
||||
public CancellationToken Cancellation { get; } = cancellation;
|
||||
|
||||
public MessageEnvelope(object message, string? messageId = null, CancellationToken cancellation = default)
|
||||
{
|
||||
this.Message = message;
|
||||
this.MessageId = messageId ?? Guid.NewGuid().ToString();
|
||||
this.Cancellation = cancellation;
|
||||
}
|
||||
|
||||
public MessageEnvelope WithSender(AgentId? sender)
|
||||
public MessageEnvelope WithSender(ActorId? sender)
|
||||
{
|
||||
this.Sender = sender;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageDelivery ForSend(AgentId receiver, Func<MessageEnvelope, CancellationToken, ValueTask<object?>> servicer)
|
||||
public MessageDelivery ForSend(ActorId receiver, Func<MessageEnvelope, CancellationToken, ValueTask<object?>> servicer)
|
||||
{
|
||||
this.Receiver = receiver;
|
||||
|
||||
ResultSink<object?> resultSink = new();
|
||||
|
||||
return new MessageDelivery(this, BoundServicer, resultSink);
|
||||
|
||||
async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation)
|
||||
TaskCompletionSource<object?> tcs = new();
|
||||
return new MessageDelivery(this, async (MessageEnvelope envelope, CancellationToken cancellation) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
object? result = await servicer(envelope, cancellation).ConfigureAwait(false);
|
||||
resultSink.SetResult(result);
|
||||
tcs.SetResult(result);
|
||||
}
|
||||
catch (OperationCanceledException exception)
|
||||
{
|
||||
resultSink.SetCancelled(exception);
|
||||
tcs.TrySetCanceled(exception.CancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
resultSink.SetException(exception);
|
||||
tcs.SetException(exception);
|
||||
}
|
||||
}
|
||||
}, tcs.Task);
|
||||
}
|
||||
|
||||
public MessageDelivery ForPublish(TopicId topic, Func<MessageEnvelope, CancellationToken, ValueTask> servicer)
|
||||
{
|
||||
this.Topic = topic;
|
||||
|
||||
ResultSink<object?> waitForPublish = new();
|
||||
|
||||
async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation)
|
||||
TaskCompletionSource<object?> tcs = new();
|
||||
return new MessageDelivery(this, async (envelope, cancellation) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await servicer(envelope, cancellation).ConfigureAwait(false);
|
||||
waitForPublish.SetResult(null);
|
||||
tcs.SetResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
waitForPublish.SetException(ex);
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return new MessageDelivery(this, BoundServicer, waitForPublish);
|
||||
}, tcs.Task);
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -4,7 +4,6 @@
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<IsAotCompatible>false</IsAotCompatible> <!-- TODO: Fix this -->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Sources;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Runtime.InProcess;
|
||||
|
||||
internal interface IResultSink<TResult> : IValueTaskSource<TResult>
|
||||
{
|
||||
void SetResult(TResult result);
|
||||
void SetException(Exception exception);
|
||||
void SetCancelled(OperationCanceledException? exception = null);
|
||||
|
||||
ValueTask<TResult> Future { get; }
|
||||
}
|
||||
|
||||
internal sealed class ResultSink<TResult> : IResultSink<TResult>
|
||||
{
|
||||
private ManualResetValueTaskSourceCore<TResult> _core;
|
||||
|
||||
public bool IsCancelled { get; private set; }
|
||||
|
||||
public TResult GetResult(short token)
|
||||
{
|
||||
return this._core.GetResult(token);
|
||||
}
|
||||
|
||||
public ValueTaskSourceStatus GetStatus(short token)
|
||||
{
|
||||
return this._core.GetStatus(token);
|
||||
}
|
||||
|
||||
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
|
||||
{
|
||||
this._core.OnCompleted(continuation, state, token, flags);
|
||||
}
|
||||
|
||||
public void SetCancelled(OperationCanceledException? exception = null)
|
||||
{
|
||||
this.IsCancelled = true;
|
||||
this._core.SetException(exception ?? new OperationCanceledException());
|
||||
}
|
||||
|
||||
public void SetException(Exception exception)
|
||||
{
|
||||
this._core.SetException(exception);
|
||||
}
|
||||
|
||||
public void SetResult(TResult result)
|
||||
{
|
||||
this._core.SetResult(result);
|
||||
}
|
||||
|
||||
public ValueTask<TResult> Future => new(this, this._core.Version);
|
||||
}
|
||||
Reference in New Issue
Block a user