From 877dba17036a75a2d74aff7c7bd254f2ac89ea94 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 27 Mar 2025 09:28:18 -0700 Subject: [PATCH 01/57] Checkpoint --- dotnet/Directory.Packages.props | 14 +-- dotnet/SK-dotnet.sln | 9 ++ .../GettingStartedWithAgents.csproj | 8 +- .../Orchestration/Step01_Broadcast.cs | 59 ++++++++++ .../Orchestration/AgentOrchestration.cs | 94 ++++++++++++++++ dotnet/src/Agents/Orchestration/AgentProxy.cs | 31 ++++++ dotnet/src/Agents/Orchestration/AgentTeam.cs | 32 ++++++ .../Orchestration/Agents.Orchestration.csproj | 48 +++++++++ .../Broadcast/BroadcastMessages.cs | 45 ++++++++ .../Broadcast/BroadcastOrchestration.cs | 89 +++++++++++++++ .../Orchestration/Broadcast/BroadcastProxy.cs | 40 +++++++ .../Broadcast/BroadcastReciever.cs | 47 ++++++++ .../Orchestration/GroupChat/Messages.cs | 98 +++++++++++++++++ .../HandOff/HandoffOrchestration.cs | 44 ++++++++ .../src/Agents/Orchestration/ManagedAgent.cs | 72 +++++++++++++ .../src/Agents/Orchestration/ManagerAgent.cs | 99 +++++++++++++++++ .../Agents/Orchestration/Shim/RuntimeAgent.cs | 102 ++++++++++++++++++ .../Agents/Orchestration/Shim/Subscription.cs | 37 +++++++ 18 files changed, 960 insertions(+), 8 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs create mode 100644 dotnet/src/Agents/Orchestration/AgentOrchestration.cs create mode 100644 dotnet/src/Agents/Orchestration/AgentProxy.cs create mode 100644 dotnet/src/Agents/Orchestration/AgentTeam.cs create mode 100644 dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/Messages.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs create mode 100644 dotnet/src/Agents/Orchestration/ManagedAgent.cs create mode 100644 dotnet/src/Agents/Orchestration/ManagerAgent.cs create mode 100644 dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs create mode 100644 dotnet/src/Agents/Orchestration/Shim/Subscription.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 10b0b090f61c..60267f0ff0e8 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -29,18 +29,16 @@ - - - - - - + + + + @@ -56,7 +54,11 @@ + + + + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 92c45120ad12..ec415915da51 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -508,6 +508,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PineconeIntegrationTests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocolPlugin", "samples\Demos\ModelContextProtocolPlugin\ModelContextProtocolPlugin.csproj", "{801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.Orchestration", "src\Agents\Orchestration\Agents.Orchestration.csproj", "{D1A02387-FA60-22F8-C2ED-4676568B6CC3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1403,6 +1405,12 @@ Global {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Publish|Any CPU.Build.0 = Release|Any CPU {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Release|Any CPU.ActiveCfg = Release|Any CPU {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654}.Release|Any CPU.Build.0 = Release|Any CPU + {D1A02387-FA60-22F8-C2ED-4676568B6CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1A02387-FA60-22F8-C2ED-4676568B6CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1A02387-FA60-22F8-C2ED-4676568B6CC3}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {D1A02387-FA60-22F8-C2ED-4676568B6CC3}.Publish|Any CPU.Build.0 = Publish|Any CPU + {D1A02387-FA60-22F8-C2ED-4676568B6CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1A02387-FA60-22F8-C2ED-4676568B6CC3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1594,6 +1602,7 @@ Global {A5E6193C-8431-4C6E-B674-682CB41EAA0C} = {4F381919-F1BE-47D8-8558-3187ED04A84F} {E9A74E0C-BC02-4DDD-A487-89847EDF8026} = {4F381919-F1BE-47D8-8558-3187ED04A84F} {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {D1A02387-FA60-22F8-C2ED-4676568B6CC3} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index ffc4734e10d6..9f3b6c306104 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -8,8 +8,7 @@ false true - - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 + $(NoWarn);IDE0009;CS8618;CA1051;CA1050;CA1707;CA1054;CA2007;CA5394;VSTHRD111;CS1591;NU1605;RCS1110;RCS1243;SKEXP0001;SKEXP0010;SKEXP0020;SKEXP0040;SKEXP0050;SKEXP0060;SKEXP0070;SKEXP0101;SKEXP0110;OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 @@ -41,11 +40,16 @@ + + + + + diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs new file mode 100644 index 000000000000..51ae8204fa5a --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrate creation of with a , +/// and then eliciting its response to explicit user messages. +/// +public class Step01_Broadcast(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseBroadcastPatternAsync() + { + // Define the agents + ChatCompletionAgent agent1 = + new() + { + Instructions = "", + //Name = name, + Description = "Agent 1", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent2 = + new() + { + Instructions = "", + //Name = name, + Description = "Agent 2", + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Define the pattern + InProcessRuntime runtime = new(); + BroadcastOrchestration orchestration = new(runtime, BroadcastCompletedHandlerAsync, agent1, agent2); + + // Start the runtime + await runtime.StartAsync(); + await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "// %%%")); + await runtime.RunUntilIdleAsync(); + + //Console.WriteLine(orchestration.Result); + + ValueTask BroadcastCompletedHandlerAsync(BroadcastMessages.Result[] results) + { + Console.WriteLine("RESULT:"); + for (int index = 0; index < results.Length; ++index) + { + BroadcastMessages.Result result = results[index]; + Console.WriteLine($"#{index}: {result.Message}"); + } + return ValueTask.CompletedTask; + } + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs new file mode 100644 index 000000000000..ecd569293c55 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// %%% +/// +public abstract class AgentOrchestration +{ + private const int IsRegistered = 1; + private const int NotRegistered = 0; + private int _isRegistered = NotRegistered; + + /// + /// %%% + /// + /// + protected AgentOrchestration(IAgentRuntime runtime) + { + Verify.NotNull(runtime, nameof(runtime)); + + this.Runtime = runtime; + //this.Id = $"{this.GetType().Name}_{Guid.NewGuid():N}"; + this.Id = Guid.NewGuid().ToString("N"); + } + + /// + /// %%% + /// + public string Id { get; } + + /// + /// %%% + /// + protected IAgentRuntime Runtime { get; } + + /// + /// %%% + /// + /// + /// + public async ValueTask StartAsync(ChatMessageContent message) // %%% IS SUFFICIENTLY FLEXIBLE ??? + { + Verify.NotNull(message, nameof(message)); + + if (Interlocked.CompareExchange(ref this._isRegistered, NotRegistered, IsRegistered) == NotRegistered) + { + await this.RegisterAsync().ConfigureAwait(false); + } + + await this.MessageTaskAsync(message).ConfigureAwait(false); + } + + /// + /// %%% + /// + /// + /// + protected abstract ValueTask MessageTaskAsync(ChatMessageContent message); + + /// + /// %%% + /// + protected abstract ValueTask RegisterAsync(); + + /// + /// %%% + /// + /// + /// + /// + protected async Task RegisterTopicsAsync(string agentType, params TopicId[] topics) + { + for (int index = 0; index < topics.Length; ++index) + { + await this.Runtime.AddSubscriptionAsync(new Subscription(topics[index], agentType)).ConfigureAwait(false); + } + } + + /// + /// %%% + /// + /// + /// + protected string GetAgentId(Agent agent) + { + return (agent.Name ?? $"{agent.GetType().Name}_{agent.Id}").Replace("-", "_"); + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentProxy.cs b/dotnet/src/Agents/Orchestration/AgentProxy.cs new file mode 100644 index 000000000000..35efd8bd19e9 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/AgentProxy.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// A built around a . +/// +public abstract class AgentProxy : RuntimeAgent +{ + private AgentThread? _thread; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + protected AgentProxy(AgentId id, IAgentRuntime runtime, Agent agent) + : base(id, runtime, agent.Description ?? throw new ArgumentException($"The agent description must be defined (#{agent.Name ?? agent.Id}).")) // %%%: DESCRIPTION Contract + { + this.Agent = agent; + } + + /// + /// %%% + /// + protected Agent Agent { get; } +} diff --git a/dotnet/src/Agents/Orchestration/AgentTeam.cs b/dotnet/src/Agents/Orchestration/AgentTeam.cs new file mode 100644 index 000000000000..3c7d324ba7b9 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/AgentTeam.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// A for orchestrating a team of agents. +/// +public class AgentTeam : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT + +/// +/// Extensions for . +/// +public static class AgentTeamExtensions +{ + /// + /// Format the names of the agents in the team as a comma delimimted list. + /// + /// The agent team + /// A comma delimimted list of agent name. + public static string FormatNames(this AgentTeam team) => string.Join(",", team.Select(t => t.Key)); + + /// + /// Format the names and descriptions of the agents in the team as a markdown list. + /// + /// The agent team + /// A markdown list of agent names and descriptions. + public static string FormatList(this AgentTeam team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); +} diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj new file mode 100644 index 000000000000..27cbe9516b69 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -0,0 +1,48 @@ + + + + + Microsoft.SemanticKernel.Agents.Orchestration + Microsoft.SemanticKernel.Agents.Orchestration + net8.0 + + $(NoWarn);SKEXP0110;SKEXP0001 + false + preview + + + + + + + Semantic Kernel Agents - Orchestration + Defines Agent orchestration patterns. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs new file mode 100644 index 000000000000..a0c0df9ce625 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +/// +/// Common messages used by the . +/// +public static class BroadcastMessages +{ + /// + /// %%% COMMENT + /// + public sealed class Task + { + /// + /// %%% COMMENT + /// + public ChatMessageContent Message { get; init; } = new(); + } + + /// + /// %%% COMMENT + /// + public sealed class Result + { + /// + /// %%% COMMENT + /// + public ChatMessageContent Message { get; init; } = new(); + } + + /// + /// %%% + /// + /// + /// + public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; + + /// + /// %%% + /// + /// + /// + public static Task ToTask(this ChatMessageContent message) => new() { Message = message }; +} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs new file mode 100644 index 000000000000..1808d7733941 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +/// +/// %%% +/// +/// +public delegate ValueTask BroadcastCompletedHandlerAsync(BroadcastMessages.Result[] results); + +/// +/// %%% +/// +public class BroadcastOrchestration : AgentOrchestration +{ + internal sealed class Topics(string id) // %%% REVIEW + { + private const string Root = "BroadcastTopic"; + public TopicId Task = new($"{Root}_{nameof(Task)}_{id}", id); + //public TopicId Result = new($"{Root}_{nameof(Result)}_{id}", id); + } + + private readonly BroadcastCompletedHandlerAsync _completionHandler; + private readonly Agent[] _agents; + private readonly Topics _topics; + + /// + /// %%% + /// + /// + /// + /// + public BroadcastOrchestration(IAgentRuntime runtime, BroadcastCompletedHandlerAsync completionHandler, params Agent[] agents) + : base(runtime) + { + Verify.NotNull(completionHandler, nameof(completionHandler)); + //Verify.NotEmpty(agents, nameof(agents)); // %%% TODO: Utility + + this._agents = agents; + this._completionHandler = completionHandler; + this._topics = new Topics(this.Id); + } + + // ISCOMPLETE + // RESULTS + + /// + protected override async ValueTask MessageTaskAsync(ChatMessageContent message) + { + BroadcastMessages.Task task = new() { Message = message }; + await this.Runtime.PublishMessageAsync(task, this._topics.Task).ConfigureAwait(false); + } + + /// + protected override async ValueTask RegisterAsync() + { + AgentType receiverType = new($"{nameof(BroadcastReciever)}_{this.Id}"); + + // All agents respond to the same message. + foreach (Agent agent in this._agents) + { + await this.RegisterAgentAsync(agent, receiverType).ConfigureAwait(false); + } + + await this.Runtime.RegisterAgentFactoryAsync( + receiverType, + (agentId, runtime) => ValueTask.FromResult(new BroadcastReciever(agentId, runtime, this.HandleResult))).ConfigureAwait(false); + } + + private async ValueTask RegisterAgentAsync(Agent agent, AgentType receiverType) + { + string agentType = this.GetAgentId(agent); + await this.Runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => ValueTask.FromResult(new BroadcastProxy(agentId, runtime, agent, receiverType))).ConfigureAwait(false); + + await this.RegisterTopicsAsync(agentType, this._topics.Task).ConfigureAwait(false); + } + + private void HandleResult(BroadcastMessages.Result result) + { + // %%% TODO: ??? + Console.WriteLine(result); + } +} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs new file mode 100644 index 000000000000..4fd2303c2bc4 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// A built around a . +/// +internal sealed class BroadcastProxy : AgentProxy +{ + private readonly AgentType _recieverType; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + /// // %%% + public BroadcastProxy(AgentId id, IAgentRuntime runtime, Agent agent, AgentType recieverType) + : base(id, runtime, agent) + { + this.RegisterHandler(this.OnTaskAsync); + this._recieverType = recieverType; + } + + /// + private async ValueTask OnTaskAsync(BroadcastMessages.Task message, MessageContext context) + { + // %%% TODO: Input + AgentResponseItem[] responses = await this.Agent.InvokeAsync([]).ToArrayAsync().ConfigureAwait(false); + AgentResponseItem response = responses.First(); + await this.SendMessageAsync(response.Message, this._recieverType).ConfigureAwait(false); // %% CARDINALITY + await response.Thread.DeleteAsync().ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs new file mode 100644 index 000000000000..17d2697066bd --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// %%% +/// +/// +internal delegate void BroadcastResultHandler(BroadcastMessages.Result result); + +/// +/// A built around a . +/// +internal sealed class BroadcastReciever : RuntimeAgent +{ + private readonly BroadcastResultHandler _resultHandler; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// // %%% + public BroadcastReciever(AgentId id, IAgentRuntime runtime, BroadcastResultHandler resultHandler) + : base(id, runtime, "// %%% DESCRIPTION") + { + this.RegisterHandler(this.OnResultAsync); + this._resultHandler = resultHandler; + } + + /// + /// %%% + /// + public bool IsComplete => true; // %%% TODO + + /// + private ValueTask OnResultAsync(BroadcastMessages.Result message, MessageContext context) + { + this._resultHandler.Invoke(message); + + return ValueTask.CompletedTask; + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/Messages.cs b/dotnet/src/Agents/Orchestration/GroupChat/Messages.cs new file mode 100644 index 000000000000..78cf578cc152 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/Messages.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// Common messages used in the Magentic framework. +/// +public static class Messages +{ + /// + /// %%% COMMENT + /// + public sealed class Group + { + /// + /// %%% COMMENT + /// + public ChatMessageContent Message { get; init; } = new(); + } + + /// + /// %%% COMMENT + /// + public sealed class Result + { + /// + /// %%% COMMENT + /// + public ChatMessageContent Message { get; init; } = new(); + } + + /// + /// Reset/clear the conversation history. + /// + public sealed class Reset { } + + /// + /// Signal the agent to respond. + /// + public sealed class Speak { } + + /// + /// Report on internal task progress. + /// Include token usage for model interactions. + /// + public sealed class Progress + { + /// + /// Describes the type of progress. + /// + public string Label { get; init; } = string.Empty; + + /// + /// The total token count. + /// + public int? TotalTokens { get; init; } + + /// + /// The input token count. + /// + public int? InputTokens { get; init; } + + /// + /// The output token count. + /// + public int? OutputTokens { get; init; } + } + + /// + /// Defines the task to be performed. + /// + public sealed class Task + { + /// + /// A task that does not require any action. + /// + public static readonly Task None = new(); + + /// + /// The input that defines the task goal. + /// + public string Input { get; init; } = string.Empty; + } + + /// + /// %%% + /// + /// + /// + public static Group ToGroup(this ChatMessageContent message) => new() { Message = message }; + + /// + /// %%% + /// + /// + /// + public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs new file mode 100644 index 000000000000..2a7251fb4ced --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.HandOff; + +internal class HandoffOrchestration : AgentOrchestration +{ + private const string TopicType = nameof(HandoffOrchestration); // %%% NAME + private readonly Agent[] _agents; + private readonly ReadOnlyDictionary _topics; + + public HandoffOrchestration(IAgentRuntime runtime, params Agent[] agents) + : base(runtime) + { + //Verify.NotEmpty(agents, nameof(agents)); // %%% TODO + this._agents = agents; + this._topics = + agents + .ToDictionary( + agent => agent, + agent => new TopicId(TopicType, $"{agent.GetType().Name}{Guid.NewGuid()}")) + .AsReadOnly(); + } + + protected override async ValueTask MessageTaskAsync(ChatMessageContent message) + { + await Task.Delay(0).ConfigureAwait(false); + //await this.Runtime.PublishMessageAsync(message, this._topics[this._agents[0]]).ConfigureAwait(false); + } + + protected override async ValueTask RegisterAsync() + { + //foreach (Agent agent in this._agents) + //{ + // await this.Runtime.RegisterAgentAsync(agent, this._topics[agent]).ConfigureAwait(false); + //} + } +} diff --git a/dotnet/src/Agents/Orchestration/ManagedAgent.cs b/dotnet/src/Agents/Orchestration/ManagedAgent.cs new file mode 100644 index 000000000000..c25802236426 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/ManagedAgent.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// A that responds to a . +/// +public abstract class ManagedAgent : RuntimeAgent +{ + /// + /// The common topic for group-chat. + /// + public static readonly TopicId GroupChatTopic = new(nameof(GroupChatTopic)); + + /// + /// The common topic for hidden-chat. + /// + public static readonly TopicId InnerChatTopic = new(nameof(InnerChatTopic)); + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// The agent description. + protected ManagedAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + this.RegisterHandler(this.OnGroupMessageAsync); + this.RegisterHandler(this.OnResetMessageAsync); + this.RegisterHandler(this.OnSpeakMessageAsync); + } + + /// + /// %%% + /// + /// + protected abstract ValueTask ResetAsync(); + + /// + /// %%% + /// + /// + /// + protected abstract ValueTask RecieveMessageAsync(ChatMessageContent message); + + /// + /// %%% + /// + /// + protected abstract ValueTask SpeakAsync(); + + private ValueTask OnGroupMessageAsync(Messages.Group message, MessageContext context) + { + return this.RecieveMessageAsync(message.Message); + } + + private ValueTask OnResetMessageAsync(Messages.Reset message, MessageContext context) + { + return this.ResetAsync(); + } + + private async ValueTask OnSpeakMessageAsync(Messages.Speak message, MessageContext context) + { + ChatMessageContent response = await this.SpeakAsync().ConfigureAwait(false); + await this.PublishMessageAsync(response.ToGroup(), GroupChatTopic).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/ManagerAgent.cs b/dotnet/src/Agents/Orchestration/ManagerAgent.cs new file mode 100644 index 000000000000..2e3d45baea6c --- /dev/null +++ b/dotnet/src/Agents/Orchestration/ManagerAgent.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// A that orchestrates a team of agents. +/// +public abstract class ManagerAgent : RuntimeAgent +{ + /// + /// A common description for the orchestrator. + /// + public const string Description = "Orchestrates a team of agents to accomplish a defined task."; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// The team of agents being orchestrated + protected ManagerAgent(AgentId id, IAgentRuntime runtime, AgentTeam team) + : base(id, runtime, Description) + { + this.Chat = []; + this.Team = team; + this.RegisterHandler(this.OnTaskMessageAsync); + this.RegisterHandler(this.OnGroupMessageAsync); + } + + /// + /// The conversation history with the team. + /// + protected ChatHistory Chat { get; } + + /// + /// The input task. + /// + protected Messages.Task Task { get; private set; } = Messages.Task.None; + + /// + /// Metadata that describes team of agents being orchestrated. + /// + protected AgentTeam Team { get; } + + /// + /// Message a specific agent, by topic. + /// + protected Task RequestAgentResponseAsync(TopicId agentTopic) + { + return this.PublishMessageAsync(new Messages.Speak(), agentTopic); // %%% EXCEPTION: KeyNotFoundException/AggregateException + } + + /// + /// Defines one-time logic required to prepare to execute the given task. + /// + /// + /// The agent specific topic for first step in executing the task. + /// + /// + /// Returning a null TopicId indicates that the task will not be executed. + /// + protected abstract Task PrepareTaskAsync(); + + /// + /// Determines which agent's must respond. + /// + /// + /// The agent specific topic for first step in executing the task. + /// + /// + /// Returning a null TopicId indicates that the task will not be executed. + /// + protected abstract Task SelectAgentAsync(); + + private async ValueTask OnTaskMessageAsync(Messages.Task message, MessageContext context) + { + this.Task = message; + TopicId? agentTopic = await this.PrepareTaskAsync().ConfigureAwait(false); + if (agentTopic != null) + { + await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); + } + } + + private async ValueTask OnGroupMessageAsync(Messages.Group message, MessageContext context) + { + this.Chat.Add(message.Message); + TopicId? agentTopic = await this.SelectAgentAsync().ConfigureAwait(false); + if (agentTopic != null) + { + await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); + } + } +} diff --git a/dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs b/dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs new file mode 100644 index 000000000000..d451a3d25324 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Defines a signature for message processing. +/// +/// The messaging being processed. +/// The message context. +public delegate ValueTask MessageHandler(object message, MessageContext messageContext); + +/// +/// An base agent that can be hosted in a runtime (). +/// +public abstract class RuntimeAgent : IHostableAgent +{ + private readonly IAgentRuntime _runtime; + private readonly Dictionary _handlers; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// The agent description (exposed in ). + protected RuntimeAgent(AgentId id, IAgentRuntime runtime, string description) + { + this._handlers = []; + this._runtime = runtime; + this.Id = id; + this.Metadata = new(id.Type, id.Key, description); + } + + /// + public AgentId Id { get; } + + /// + public AgentMetadata Metadata { get; } + + /// + public virtual ValueTask CloseAsync() => ValueTask.CompletedTask; + + /// + public async ValueTask OnMessageAsync(object message, MessageContext messageContext) + { + // Match all handlers for the message type, including if the handler declares a base type of the message. + // Order for invoking handlers is entirely independant. + Task[] tasks = + [.. this._handlers.Keys + .Where(key => key.IsAssignableFrom(message.GetType())) + .Select(key => this._handlers[key].Invoke(message, messageContext).AsTask())]; + + Debug.WriteLine($"HANDLE MESSAGE - {message.GetType().Name}/{messageContext.Topic}: #{tasks.Length} "); + + await Task.WhenAll(tasks).ConfigureAwait(false); + + return null; + } + + /// + /// Register the handler for a given message type. + /// + /// The message type + /// The message handler + /// + /// The targeted message type may be the base type of the actual message. + /// + protected void RegisterHandler(Func messageHandler) + { + this._handlers[typeof(TMessage)] = (message, context) => messageHandler((TMessage)message, context); + } + + /// + /// Publishes a message to all agents subscribed to the given topic. + /// + /// The message type + /// The message to publish. + /// The topic to which to publish the message. + protected async Task PublishMessageAsync(TMessage message, TopicId topic) where TMessage : class + { + await this._runtime.PublishMessageAsync(message, topic, this.Id).ConfigureAwait(false); + } + + /// + /// %%% + /// + /// The message type + /// The message to publish. + /// %%% + protected async Task SendMessageAsync(TMessage message, AgentType agentType) where TMessage : class + { + AgentId agentId = await this._runtime.GetAgentAsync(agentType).ConfigureAwait(false); + await this._runtime.SendMessageAsync(message, agentId, this.Id).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/Shim/Subscription.cs b/dotnet/src/Agents/Orchestration/Shim/Subscription.cs new file mode 100644 index 000000000000..d91c97e5a6ec --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Shim/Subscription.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +internal sealed class Subscription(TopicId topic, string agentType, string? id = null) : ISubscriptionDefinition +{ + /// + public string Id { get; } = id ?? Guid.NewGuid().ToString(); + + /// + /// Gets the topic associated with the subscription. + /// + public TopicId Topic { get; } = topic; + + /// + public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; + + /// + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + public AgentId MapToAgent(TopicId topic) + { + if (!this.Matches(topic)) + { + throw new InvalidOperationException("Topic does not match the subscription."); + } + + return new AgentId(agentType, topic.Source); + } + + /// + public bool Matches(TopicId topic) => this.Topic.Type == topic.Type; +} From 6e04c1008ccad4c88b85684e7f4f6e6d36de445a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 27 Mar 2025 11:25:00 -0700 Subject: [PATCH 02/57] Checkpoint --- .../Orchestration/Step01_Broadcast.cs | 30 ++++--- .../Orchestration/Step02_Handoff.cs | 62 ++++++++++++++ .../Orchestration/Step03_GroupChat.cs | 62 ++++++++++++++ .../Orchestration/Step04_Custom.cs | 62 ++++++++++++++ .../Orchestration/Step05_Multiuse.cs | 78 ++++++++++++++++++ .../Orchestration/AgentOrchestration.cs | 4 + .../Orchestration/Agents.Orchestration.csproj | 1 - .../Broadcast/BroadcastMessages.cs | 2 +- .../Broadcast/BroadcastOrchestration.cs | 42 +++++----- .../Orchestration/Broadcast/BroadcastProxy.cs | 7 +- .../Broadcast/BroadcastReciever.cs | 10 +-- .../{Messages.cs => GroupChatMessages.cs} | 29 +------ .../{ => GroupChat}/ManagedAgent.cs | 15 ++-- .../{ => GroupChat}/ManagerAgent.cs | 15 ++-- .../Orchestration/HandOff/HandoffMessages.cs | 42 ++++++++++ .../HandOff/HandoffOrchestration.cs | 80 ++++++++++++++----- .../Orchestration/HandOff/HandoffProxy.cs | 39 +++++++++ .../Orchestration/HandOff/HandoffReciever.cs | 45 +++++++++++ 18 files changed, 516 insertions(+), 109 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs rename dotnet/src/Agents/Orchestration/GroupChat/{Messages.cs => GroupChatMessages.cs} (70%) rename dotnet/src/Agents/Orchestration/{ => GroupChat}/ManagedAgent.cs (75%) rename dotnet/src/Agents/Orchestration/{ => GroupChat}/ManagerAgent.cs (80%) create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs index 51ae8204fa5a..4a220f897188 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. + using Microsoft.AgentRuntime.InProcess; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; @@ -8,8 +9,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrate creation of with a , -/// and then eliciting its response to explicit user messages. +/// %%% /// public class Step01_Broadcast(ITestOutputHelper output) : BaseAgentsTest(output) { @@ -17,10 +17,11 @@ public class Step01_Broadcast(ITestOutputHelper output) : BaseAgentsTest(output) public async Task UseBroadcastPatternAsync() { // Define the agents + // %%% STRUCTURED OUTPUT ??? ChatCompletionAgent agent1 = new() { - Instructions = "", + Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", //Name = name, Description = "Agent 1", Kernel = this.CreateKernelWithChatCompletion(), @@ -28,30 +29,37 @@ public async Task UseBroadcastPatternAsync() ChatCompletionAgent agent2 = new() { - Instructions = "", + Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", //Name = name, Description = "Agent 2", Kernel = this.CreateKernelWithChatCompletion(), }; + ChatCompletionAgent agent3 = + new() + { + Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", + //Name = name, + Description = "Agent 3", + Kernel = this.CreateKernelWithChatCompletion(), + }; // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration orchestration = new(runtime, BroadcastCompletedHandlerAsync, agent1, agent2); + BroadcastOrchestration orchestration = new(runtime, BroadcastCompletedHandlerAsync, agent1, agent2, agent3); // Start the runtime await runtime.StartAsync(); - await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "// %%%")); + await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); await runtime.RunUntilIdleAsync(); + Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); - //Console.WriteLine(orchestration.Result); - - ValueTask BroadcastCompletedHandlerAsync(BroadcastMessages.Result[] results) + ValueTask BroadcastCompletedHandlerAsync(ChatMessageContent[] results) { Console.WriteLine("RESULT:"); for (int index = 0; index < results.Length; ++index) { - BroadcastMessages.Result result = results[index]; - Console.WriteLine($"#{index}: {result.Message}"); + ChatMessageContent result = results[index]; + Console.WriteLine($"#{index}: {result}"); } return ValueTask.CompletedTask; } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs new file mode 100644 index 000000000000..05f7ba8f4e0a --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// %%% +/// +public class Step02_Handoff(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseHandoffPatternAsync() + { + // Define the agents + // %%% STRUCTURED OUTPUT ??? + ChatCompletionAgent agent1 = + new() + { + Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", + //Name = name, + Description = "Agent 1", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent2 = + new() + { + Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", + //Name = name, + Description = "Agent 2", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent3 = + new() + { + Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", + //Name = name, + Description = "Agent 3", + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestration = new(runtime, HandoffCompletedHandlerAsync, agent1, agent2, agent3); + + // Start the runtime + await runtime.StartAsync(); + await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + await runtime.RunUntilIdleAsync(); + Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); + + ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) + { + Console.WriteLine($"RESULT: {result}"); + return ValueTask.CompletedTask; + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs new file mode 100644 index 000000000000..f4a33a6c3cc5 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// %%% +/// +public class Step03_GroupChat(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseGroupChatPatternAsync() + { + // Define the agents + // %%% STRUCTURED OUTPUT ??? + ChatCompletionAgent agent1 = + new() + { + Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", + //Name = name, + Description = "Agent 1", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent2 = + new() + { + Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", + //Name = name, + Description = "Agent 2", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent3 = + new() + { + Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", + //Name = name, + Description = "Agent 3", + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestration = new(runtime, HandoffCompletedHandlerAsync, agent1, agent2, agent3); + + // Start the runtime + await runtime.StartAsync(); + await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + await runtime.RunUntilIdleAsync(); + Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); + + ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) + { + Console.WriteLine($"RESULT: {result}"); + return ValueTask.CompletedTask; + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs new file mode 100644 index 000000000000..1871bab09e54 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// %%% +/// +public class Step04_Custom(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseCustomPatternAsync() + { + // Define the agents + // %%% STRUCTURED OUTPUT ??? + ChatCompletionAgent agent1 = + new() + { + Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", + //Name = name, + Description = "Agent 1", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent2 = + new() + { + Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", + //Name = name, + Description = "Agent 2", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent3 = + new() + { + Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", + //Name = name, + Description = "Agent 3", + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestration = new(runtime, HandoffCompletedHandlerAsync, agent1, agent2, agent3); + + // Start the runtime + await runtime.StartAsync(); + await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + await runtime.RunUntilIdleAsync(); + Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); + + ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) + { + Console.WriteLine($"RESULT: {result}"); + return ValueTask.CompletedTask; + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs new file mode 100644 index 000000000000..d459f8fe2a8f --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// %%% +/// +public class Step05_Multiuse(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseMultiplePatternsAsync() + { + // Define the agents + // %%% STRUCTURED OUTPUT ??? + ChatCompletionAgent agent1 = + new() + { + Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", + //Name = name, + Description = "Agent 1", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent2 = + new() + { + Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", + //Name = name, + Description = "Agent 2", + Kernel = this.CreateKernelWithChatCompletion(), + }; + ChatCompletionAgent agent3 = + new() + { + Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", + //Name = name, + Description = "Agent 3", + Kernel = this.CreateKernelWithChatCompletion(), + }; + + // Define the pattern + InProcessRuntime runtime = new(); + BroadcastOrchestration broadcast = new(runtime, BroadcastCompletedHandlerAsync, agent2, agent3); + HandoffOrchestration handoff = new(runtime, HandoffCompletedHandlerAsync, agent1); + + // Start the runtime + await runtime.StartAsync(); + await broadcast.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + await handoff.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + await runtime.RunUntilIdleAsync(); + + Console.WriteLine($"BROADCAST ISCOMPLETE = {broadcast.IsComplete}"); + Console.WriteLine($"HANDOFF ISCOMPLETE = {handoff.IsComplete}"); + + ValueTask BroadcastCompletedHandlerAsync(ChatMessageContent[] results) + { + Console.WriteLine("BROADCAST RESULT:"); + for (int index = 0; index < results.Length; ++index) + { + ChatMessageContent result = results[index]; + Console.WriteLine($"#{index}: {result}"); + } + return ValueTask.CompletedTask; + } + + ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) + { + Console.WriteLine($"HANDOFF RESULT: {result}"); + return ValueTask.CompletedTask; + } + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index ecd569293c55..b76d396e80de 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -28,6 +28,10 @@ protected AgentOrchestration(IAgentRuntime runtime) //this.Id = $"{this.GetType().Name}_{Guid.NewGuid():N}"; this.Id = Guid.NewGuid().ToString("N"); } + /// + /// %%% + /// + public abstract bool IsComplete { get; } /// /// %%% diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index 27cbe9516b69..5d5786e68fd0 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -42,7 +42,6 @@ - \ No newline at end of file diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs index a0c0df9ce625..65a224b25c5a 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs @@ -5,7 +5,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; /// /// Common messages used by the . /// -public static class BroadcastMessages +internal static class BroadcastMessages { /// /// %%% COMMENT diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs index 1808d7733941..4eafad38a23d 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System; +using System.Collections.Concurrent; +using System.Threading; using System.Threading.Tasks; using Microsoft.AgentRuntime; @@ -10,23 +11,18 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; /// %%% /// /// -public delegate ValueTask BroadcastCompletedHandlerAsync(BroadcastMessages.Result[] results); +public delegate ValueTask BroadcastCompletedHandlerAsync(ChatMessageContent[] results); /// /// %%% /// -public class BroadcastOrchestration : AgentOrchestration +public sealed class BroadcastOrchestration : AgentOrchestration { - internal sealed class Topics(string id) // %%% REVIEW - { - private const string Root = "BroadcastTopic"; - public TopicId Task = new($"{Root}_{nameof(Task)}_{id}", id); - //public TopicId Result = new($"{Root}_{nameof(Result)}_{id}", id); - } - private readonly BroadcastCompletedHandlerAsync _completionHandler; private readonly Agent[] _agents; - private readonly Topics _topics; + private readonly TopicId _topic; + private readonly ConcurrentQueue _results; + private int _resultCount; /// /// %%% @@ -42,17 +38,17 @@ public BroadcastOrchestration(IAgentRuntime runtime, BroadcastCompletedHandlerAs this._agents = agents; this._completionHandler = completionHandler; - this._topics = new Topics(this.Id); + this._topic = new($"BroadcastTopic_{nameof(Task)}_{this.Id}", this.Id); + this._results = []; } - // ISCOMPLETE - // RESULTS + /// + public override bool IsComplete => this._resultCount == this._agents.Length; /// protected override async ValueTask MessageTaskAsync(ChatMessageContent message) { - BroadcastMessages.Task task = new() { Message = message }; - await this.Runtime.PublishMessageAsync(task, this._topics.Task).ConfigureAwait(false); + await this.Runtime.PublishMessageAsync(message.ToTask(), this._topic).ConfigureAwait(false); } /// @@ -68,7 +64,7 @@ protected override async ValueTask RegisterAsync() await this.Runtime.RegisterAgentFactoryAsync( receiverType, - (agentId, runtime) => ValueTask.FromResult(new BroadcastReciever(agentId, runtime, this.HandleResult))).ConfigureAwait(false); + (agentId, runtime) => ValueTask.FromResult(new BroadcastReciever(agentId, runtime, this.HandleResultAsync))).ConfigureAwait(false); } private async ValueTask RegisterAgentAsync(Agent agent, AgentType receiverType) @@ -78,12 +74,16 @@ await this.Runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => ValueTask.FromResult(new BroadcastProxy(agentId, runtime, agent, receiverType))).ConfigureAwait(false); - await this.RegisterTopicsAsync(agentType, this._topics.Task).ConfigureAwait(false); + await this.RegisterTopicsAsync(agentType, this._topic).ConfigureAwait(false); } - private void HandleResult(BroadcastMessages.Result result) + private async ValueTask HandleResultAsync(BroadcastMessages.Result result) { - // %%% TODO: ??? - Console.WriteLine(result); + this._results.Enqueue(result.Message); + Interlocked.Increment(ref this._resultCount); + if (this.IsComplete) + { + await this._completionHandler.Invoke(this._results.ToArray()).ConfigureAwait(false); + } } } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs index 4fd2303c2bc4..f0fba8b3acaa 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs @@ -29,12 +29,11 @@ public BroadcastProxy(AgentId id, IAgentRuntime runtime, Agent agent, AgentType } /// - private async ValueTask OnTaskAsync(BroadcastMessages.Task message, MessageContext context) + private async ValueTask OnTaskAsync(BroadcastMessages.Task task, MessageContext context) { - // %%% TODO: Input - AgentResponseItem[] responses = await this.Agent.InvokeAsync([]).ToArrayAsync().ConfigureAwait(false); + AgentResponseItem[] responses = await this.Agent.InvokeAsync([task.Message]).ToArrayAsync().ConfigureAwait(false); AgentResponseItem response = responses.First(); - await this.SendMessageAsync(response.Message, this._recieverType).ConfigureAwait(false); // %% CARDINALITY + await this.SendMessageAsync(response.Message.ToResult(), this._recieverType).ConfigureAwait(false); // %% CARDINALITY await response.Thread.DeleteAsync().ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs index 17d2697066bd..68fd7fecfcc9 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs @@ -10,14 +10,14 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// %%% /// /// -internal delegate void BroadcastResultHandler(BroadcastMessages.Result result); +internal delegate ValueTask BroadcastResultHandlerAsync(BroadcastMessages.Result result); /// /// A built around a . /// internal sealed class BroadcastReciever : RuntimeAgent { - private readonly BroadcastResultHandler _resultHandler; + private readonly BroadcastResultHandlerAsync _resultHandler; /// /// Initializes a new instance of the class. @@ -25,7 +25,7 @@ internal sealed class BroadcastReciever : RuntimeAgent /// The unique identifier of the agent. /// The runtime associated with the agent. /// // %%% - public BroadcastReciever(AgentId id, IAgentRuntime runtime, BroadcastResultHandler resultHandler) + public BroadcastReciever(AgentId id, IAgentRuntime runtime, BroadcastResultHandlerAsync resultHandler) : base(id, runtime, "// %%% DESCRIPTION") { this.RegisterHandler(this.OnResultAsync); @@ -40,8 +40,6 @@ public BroadcastReciever(AgentId id, IAgentRuntime runtime, BroadcastResultHandl /// private ValueTask OnResultAsync(BroadcastMessages.Result message, MessageContext context) { - this._resultHandler.Invoke(message); - - return ValueTask.CompletedTask; + return this._resultHandler.Invoke(message); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/Messages.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs similarity index 70% rename from dotnet/src/Agents/Orchestration/GroupChat/Messages.cs rename to dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs index 78cf578cc152..e1e6d480d484 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/Messages.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs @@ -5,7 +5,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// Common messages used in the Magentic framework. /// -public static class Messages +public static class GroupChatMessages { /// /// %%% COMMENT @@ -39,33 +39,6 @@ public sealed class Reset { } /// public sealed class Speak { } - /// - /// Report on internal task progress. - /// Include token usage for model interactions. - /// - public sealed class Progress - { - /// - /// Describes the type of progress. - /// - public string Label { get; init; } = string.Empty; - - /// - /// The total token count. - /// - public int? TotalTokens { get; init; } - - /// - /// The input token count. - /// - public int? InputTokens { get; init; } - - /// - /// The output token count. - /// - public int? OutputTokens { get; init; } - } - /// /// Defines the task to be performed. /// diff --git a/dotnet/src/Agents/Orchestration/ManagedAgent.cs b/dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs similarity index 75% rename from dotnet/src/Agents/Orchestration/ManagedAgent.cs rename to dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs index c25802236426..8e622a0caf41 100644 --- a/dotnet/src/Agents/Orchestration/ManagedAgent.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs @@ -2,9 +2,8 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -namespace Microsoft.SemanticKernel.Agents.Orchestration; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// A that responds to a . @@ -30,9 +29,9 @@ public abstract class ManagedAgent : RuntimeAgent protected ManagedAgent(AgentId id, IAgentRuntime runtime, string description) : base(id, runtime, description) { - this.RegisterHandler(this.OnGroupMessageAsync); - this.RegisterHandler(this.OnResetMessageAsync); - this.RegisterHandler(this.OnSpeakMessageAsync); + this.RegisterHandler(this.OnGroupMessageAsync); + this.RegisterHandler(this.OnResetMessageAsync); + this.RegisterHandler(this.OnSpeakMessageAsync); } /// @@ -54,17 +53,17 @@ protected ManagedAgent(AgentId id, IAgentRuntime runtime, string description) /// protected abstract ValueTask SpeakAsync(); - private ValueTask OnGroupMessageAsync(Messages.Group message, MessageContext context) + private ValueTask OnGroupMessageAsync(GroupChatMessages.Group message, MessageContext context) { return this.RecieveMessageAsync(message.Message); } - private ValueTask OnResetMessageAsync(Messages.Reset message, MessageContext context) + private ValueTask OnResetMessageAsync(GroupChatMessages.Reset message, MessageContext context) { return this.ResetAsync(); } - private async ValueTask OnSpeakMessageAsync(Messages.Speak message, MessageContext context) + private async ValueTask OnSpeakMessageAsync(GroupChatMessages.Speak message, MessageContext context) { ChatMessageContent response = await this.SpeakAsync().ConfigureAwait(false); await this.PublishMessageAsync(response.ToGroup(), GroupChatTopic).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/ManagerAgent.cs b/dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs similarity index 80% rename from dotnet/src/Agents/Orchestration/ManagerAgent.cs rename to dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs index 2e3d45baea6c..9e62d8d33587 100644 --- a/dotnet/src/Agents/Orchestration/ManagerAgent.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs @@ -2,10 +2,9 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Microsoft.SemanticKernel.ChatCompletion; -namespace Microsoft.SemanticKernel.Agents.Orchestration; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// A that orchestrates a team of agents. @@ -28,8 +27,8 @@ protected ManagerAgent(AgentId id, IAgentRuntime runtime, AgentTeam team) { this.Chat = []; this.Team = team; - this.RegisterHandler(this.OnTaskMessageAsync); - this.RegisterHandler(this.OnGroupMessageAsync); + this.RegisterHandler(this.OnTaskMessageAsync); + this.RegisterHandler(this.OnGroupMessageAsync); } /// @@ -40,7 +39,7 @@ protected ManagerAgent(AgentId id, IAgentRuntime runtime, AgentTeam team) /// /// The input task. /// - protected Messages.Task Task { get; private set; } = Messages.Task.None; + protected GroupChatMessages.Task Task { get; private set; } = GroupChatMessages.Task.None; /// /// Metadata that describes team of agents being orchestrated. @@ -52,7 +51,7 @@ protected ManagerAgent(AgentId id, IAgentRuntime runtime, AgentTeam team) /// protected Task RequestAgentResponseAsync(TopicId agentTopic) { - return this.PublishMessageAsync(new Messages.Speak(), agentTopic); // %%% EXCEPTION: KeyNotFoundException/AggregateException + return this.PublishMessageAsync(new GroupChatMessages.Speak(), agentTopic); // %%% EXCEPTION: KeyNotFoundException/AggregateException } /// @@ -77,7 +76,7 @@ protected Task RequestAgentResponseAsync(TopicId agentTopic) /// protected abstract Task SelectAgentAsync(); - private async ValueTask OnTaskMessageAsync(Messages.Task message, MessageContext context) + private async ValueTask OnTaskMessageAsync(GroupChatMessages.Task message, MessageContext context) { this.Task = message; TopicId? agentTopic = await this.PrepareTaskAsync().ConfigureAwait(false); @@ -87,7 +86,7 @@ private async ValueTask OnTaskMessageAsync(Messages.Task message, MessageContext } } - private async ValueTask OnGroupMessageAsync(Messages.Group message, MessageContext context) + private async ValueTask OnGroupMessageAsync(GroupChatMessages.Group message, MessageContext context) { this.Chat.Add(message.Message); TopicId? agentTopic = await this.SelectAgentAsync().ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs new file mode 100644 index 000000000000..45d35a04cc0d --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// Common messages used by the . +/// +internal static class HandoffMessages +{ + /// + /// %%% COMMENT + /// + public sealed class Input // %%% NAME + { + /// + /// %%% COMMENT + /// + public ChatMessageContent Task { get; init; } = new(); + + /// + /// %%% COMMENT + /// + public List Results { get; init; } = []; + } + + /// + /// %%% + /// + /// + /// + public static Input ToInput(this ChatMessageContent task) => new() { Task = task }; + + /// + /// %%% + /// + /// + /// + /// + public static Input Forward(this Input source, ChatMessageContent result) => new() { Task = source.Task, Results = [.. source.Results, result] }; +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs index 2a7251fb4ced..f2d974bcb3b4 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs @@ -1,44 +1,82 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AgentRuntime; -namespace Microsoft.SemanticKernel.Agents.Orchestration.HandOff; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; -internal class HandoffOrchestration : AgentOrchestration +/// +/// %%% +/// +/// +public delegate ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result); + +/// +/// %%% +/// +public sealed class HandoffOrchestration : AgentOrchestration { - private const string TopicType = nameof(HandoffOrchestration); // %%% NAME + private readonly HandoffCompletedHandlerAsync _completionHandler; private readonly Agent[] _agents; - private readonly ReadOnlyDictionary _topics; + private readonly AgentType _firstAgent; + private ChatMessageContent? _result; - public HandoffOrchestration(IAgentRuntime runtime, params Agent[] agents) + /// + /// %%% + /// + /// + /// + /// + public HandoffOrchestration(IAgentRuntime runtime, HandoffCompletedHandlerAsync completionHandler, params Agent[] agents) : base(runtime) { - //Verify.NotEmpty(agents, nameof(agents)); // %%% TODO + Verify.NotNull(completionHandler, nameof(completionHandler)); + //Verify.NotEmpty(agents, nameof(agents)); // %%% TODO: Utility + + this._completionHandler = completionHandler; this._agents = agents; - this._topics = - agents - .ToDictionary( - agent => agent, - agent => new TopicId(TopicType, $"{agent.GetType().Name}{Guid.NewGuid()}")) - .AsReadOnly(); + this._firstAgent = this.GetAgentId(agents.First()); } + /// + public override bool IsComplete => this._result != null; + + /// protected override async ValueTask MessageTaskAsync(ChatMessageContent message) { - await Task.Delay(0).ConfigureAwait(false); - //await this.Runtime.PublishMessageAsync(message, this._topics[this._agents[0]]).ConfigureAwait(false); + AgentId agentId = await this.Runtime.GetAgentAsync(this._firstAgent).ConfigureAwait(false); // %%% COMMON PATTERN + await this.Runtime.SendMessageAsync(message.ToInput(), agentId).ConfigureAwait(false); } + /// protected override async ValueTask RegisterAsync() { - //foreach (Agent agent in this._agents) - //{ - // await this.Runtime.RegisterAgentAsync(agent, this._topics[agent]).ConfigureAwait(false); - //} + AgentType receiverType = new($"{nameof(HandoffReciever)}_{this.Id}"); + + // Each agent handsoff its result to the next agent. + for (int index = 0; index < this._agents.Length; ++index) + { + Agent agent = this._agents[index]; + AgentType nextAgent = index == this._agents.Length - 1 ? receiverType : this.GetAgentId(this._agents[index + 1]); + string agentType = this.GetAgentId(agent); + await this.Runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => ValueTask.FromResult(new HandoffProxy(agentId, runtime, agent, nextAgent))).ConfigureAwait(false); + } + + await this.Runtime.RegisterAgentFactoryAsync( + receiverType, + (agentId, runtime) => ValueTask.FromResult(new HandoffReciever(agentId, runtime, this.HandleResultAsync))).ConfigureAwait(false); + } + + private async ValueTask HandleResultAsync(HandoffMessages.Input result) + { + Interlocked.CompareExchange(ref this._result, result.Results.Last(), null); + if (this.IsComplete) + { + await this._completionHandler.Invoke(this._result).ConfigureAwait(false); + } } } diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs new file mode 100644 index 000000000000..43a0c13144f3 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// A built around a . +/// +internal sealed class HandoffProxy : AgentProxy +{ + private readonly AgentType _nextAgent; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + /// // %%% + public HandoffProxy(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) + : base(id, runtime, agent) + { + this.RegisterHandler(this.OnHandoffAsync); + this._nextAgent = nextAgent; + } + + /// + private async ValueTask OnHandoffAsync(HandoffMessages.Input message, MessageContext context) + { + AgentResponseItem[] responses = await this.Agent.InvokeAsync([message.Task]).ToArrayAsync().ConfigureAwait(false); + AgentResponseItem response = responses.First(); + await this.SendMessageAsync(message.Forward(response), this._nextAgent).ConfigureAwait(false); // %% CARDINALITY + await response.Thread.DeleteAsync().ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs new file mode 100644 index 000000000000..bd5f57a22165 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// %%% +/// +/// +internal delegate ValueTask HandoffResultHandlerAsync(HandoffMessages.Input result); + +/// +/// A built around a . +/// +internal sealed class HandoffReciever : RuntimeAgent +{ + private readonly HandoffResultHandlerAsync _resultHandler; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// // %%% + public HandoffReciever(AgentId id, IAgentRuntime runtime, HandoffResultHandlerAsync resultHandler) + : base(id, runtime, "// %%% DESCRIPTION") + { + this.RegisterHandler(this.OnHandoffAsync); + this._resultHandler = resultHandler; + } + + /// + /// %%% + /// + public bool IsComplete => true; // %%% TODO + + /// + private ValueTask OnHandoffAsync(HandoffMessages.Input message, MessageContext context) + { + return this._resultHandler.Invoke(message); + } +} From 1a6af45425398b314aaa03bf283554f3221d2b6e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sat, 12 Apr 2025 21:27:04 -0700 Subject: [PATCH 03/57] Checkpoint --- dotnet/Directory.Packages.props | 5 +- dotnet/nuget.config | 7 +- .../GettingStartedWithAgents.csproj | 3 +- .../Orchestration/Step01_Broadcast.cs | 137 +++++++++++----- .../Orchestration/Step02_Handoff.cs | 131 ++++++++++----- .../Orchestration/Step03_GroupChat.cs | 110 ++++++------- .../Orchestration/Step04_Custom.cs | 62 ------- .../Orchestration/Step04_Nested.cs | 78 +++++++++ .../Orchestration/Step05_Custom.cs | 15 ++ .../Orchestration/Step05_Multiuse.cs | 78 --------- dotnet/src/Agents/AzureAI/AzureAIAgent.cs | 2 +- dotnet/src/Agents/Orchestration/AgentActor.cs | 102 ++++++++++++ .../AgentOrchestration.RequestActor.cs | 60 +++++++ .../AgentOrchestration.ResultActor.cs | 72 ++++++++ .../Orchestration/AgentOrchestration.cs | 155 +++++++++++++----- dotnet/src/Agents/Orchestration/AgentProxy.cs | 31 ---- .../Orchestration/Agents.Orchestration.csproj | 19 ++- .../Orchestration/Broadcast/BroadcastActor.cs | 42 +++++ .../Broadcast/BroadcastMessages.cs | 32 ++-- .../BroadcastOrchestration.String.cs | 24 +++ .../Broadcast/BroadcastOrchestration.cs | 111 ++++++------- .../Orchestration/Broadcast/BroadcastProxy.cs | 39 ----- .../Broadcast/BroadcastReciever.cs | 45 ----- .../Broadcast/BroadcastResultActor.cs | 54 ++++++ .../Extensions/AgentExtensions.cs | 21 +++ .../Extensions/RuntimeExtensions.cs | 21 +++ .../Orchestration/GroupChat/ChatManager.cs | 124 ++++++++++++++ .../Orchestration/GroupChat/ChatMessages.cs | 77 +++++++++ .../{AgentTeam.cs => GroupChat/ChatTeam.cs} | 12 +- .../Orchestration/GroupChat/GroupChatActor.cs | 66 ++++++++ .../GroupChat/GroupChatManager.cs | 51 ++++++ .../GroupChat/GroupChatMessages.cs | 71 -------- .../GroupChat/GroupChatOrchestration.cs | 69 ++++++++ .../Orchestration/GroupChat/ManagedAgent.cs | 71 -------- .../Orchestration/GroupChat/ManagerAgent.cs | 98 ----------- .../Orchestration/HandOff/HandoffActor.cs | 42 +++++ .../Orchestration/HandOff/HandoffMessage.cs | 19 +++ .../Orchestration/HandOff/HandoffMessages.cs | 42 ----- .../HandoffOrchestration.ChatMessage.cs | 25 +++ .../HandOff/HandoffOrchestration.String.cs | 24 +++ .../HandOff/HandoffOrchestration.cs | 91 +++++----- .../Orchestration/HandOff/HandoffProxy.cs | 39 ----- .../Orchestration/HandOff/HandoffReciever.cs | 45 ----- .../Agents/Orchestration/Orchestratable.cs | 20 +++ .../Orchestration/OrchestrationResult.cs | 48 ++++++ .../Orchestration/OrchestrationTarget.cs | 108 ++++++++++++ .../Agents/Orchestration/Shim/RuntimeAgent.cs | 102 ------------ .../Agents/Orchestration/Shim/Subscription.cs | 37 ----- .../AgentUtilities/BaseOrchestrationTest.cs | 24 +++ .../samples/InternalUtilities/BaseTest.cs | 5 + 50 files changed, 1695 insertions(+), 1071 deletions(-) delete mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs delete mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs create mode 100644 dotnet/src/Agents/Orchestration/AgentActor.cs create mode 100644 dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs create mode 100644 dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs delete mode 100644 dotnet/src/Agents/Orchestration/AgentProxy.cs create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs delete mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs delete mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs create mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs create mode 100644 dotnet/src/Agents/Orchestration/Extensions/AgentExtensions.cs create mode 100644 dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs rename dotnet/src/Agents/Orchestration/{AgentTeam.cs => GroupChat/ChatTeam.cs} (57%) create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs delete mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs delete mode 100644 dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs delete mode 100644 dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs delete mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs create mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs delete mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs delete mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs create mode 100644 dotnet/src/Agents/Orchestration/Orchestratable.cs create mode 100644 dotnet/src/Agents/Orchestration/OrchestrationResult.cs create mode 100644 dotnet/src/Agents/Orchestration/OrchestrationTarget.cs delete mode 100644 dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs delete mode 100644 dotnet/src/Agents/Orchestration/Shim/Subscription.cs create mode 100644 dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 60267f0ff0e8..cf45697db515 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -34,8 +34,9 @@ - - + + + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 7159fcd04c36..25c36a4c8b8c 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -1,12 +1,17 @@ - + + + + + + diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index 9f3b6c306104..408c30112c3a 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -8,7 +8,8 @@ false true - $(NoWarn);IDE0009;CS8618;CA1051;CA1050;CA1707;CA1054;CA2007;CA5394;VSTHRD111;CS1591;NU1605;RCS1110;RCS1243;SKEXP0001;SKEXP0010;SKEXP0020;SKEXP0040;SKEXP0050;SKEXP0060;SKEXP0070;SKEXP0101;SKEXP0110;OPENAI001 + + $(NoWarn);MSB3277;IDE0009;CS8618;CA1051;CA1050;CA1707;CA1054;CA2007;CA5394;VSTHRD111;CS1591;NU1605;RCS1110;RCS1243;SKEXP0001;SKEXP0010;SKEXP0020;SKEXP0040;SKEXP0050;SKEXP0060;SKEXP0070;SKEXP0101;SKEXP0110;OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs index 4a220f897188..5ec91cc2c03f 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs @@ -1,67 +1,120 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.AgentRuntime.InProcess; -using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; -using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; /// -/// %%% +/// Demonstrates how to use the . /// -public class Step01_Broadcast(ITestOutputHelper output) : BaseAgentsTest(output) +public class Step01_Broadcast(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task UseBroadcastPatternAsync() + public async Task SimpleBroadcastAsync() { // Define the agents - // %%% STRUCTURED OUTPUT ??? - ChatCompletionAgent agent1 = - new() - { - Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", - //Name = name, - Description = "Agent 1", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent2 = - new() - { - Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", - //Name = name, - Description = "Agent 2", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent3 = - new() - { - Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", - //Name = name, - Description = "Agent 3", - Kernel = this.CreateKernelWithChatCompletion(), - }; + ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); + ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); + ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of onsonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration orchestration = new(runtime, BroadcastCompletedHandlerAsync, agent1, agent2, agent3); + BroadcastOrchestration orchestration = new(runtime, agent1, agent2, agent3); // Start the runtime await runtime.StartAsync(); - await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + string input = "The quick brown fox jumps over the lazy dog"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + + string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task NestedBroadcastAsync() + { + // Define the agents + ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); + ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); + ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); + + // Define the pattern + InProcessRuntime runtime = new(); + + BroadcastOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); + BroadcastOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); + BroadcastOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestrationMain.InvokeAsync(input); + + string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); + await runtime.RunUntilIdleAsync(); - Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); + } - ValueTask BroadcastCompletedHandlerAsync(ChatMessageContent[] results) + [Fact] + public async Task SingleActorAsync() + { + // Define the agents + ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + + // Define the pattern + InProcessRuntime runtime = new(); + BroadcastOrchestration orchestration = new(runtime, agent); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + + string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task SingleNestedActorAsync() + { + // Define the agents + ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + + // Define the pattern + InProcessRuntime runtime = new(); + BroadcastOrchestration orchestrationInner = CreateNested(runtime, agent); + BroadcastOrchestration orchestrationOuter = new(runtime, orchestrationInner); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestrationOuter.InvokeAsync(input); + + string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); + + await runtime.RunUntilIdleAsync(); + } + + private static BroadcastOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + { + return new(runtime, targets) { - Console.WriteLine("RESULT:"); - for (int index = 0; index < results.Length; ++index) - { - ChatMessageContent result = results[index]; - Console.WriteLine($"#{index}: {result}"); - } - return ValueTask.CompletedTask; - } + InputTransform = (BroadcastMessages.Task input) => input, + ResultTransform = (BroadcastMessages.Result[] results) => string.Join("\n", results.Select(result => $"{result.Message}")).ToBroadcastResult(), + }; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs index 05f7ba8f4e0a..afefcf814429 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs @@ -1,62 +1,119 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.AgentRuntime.InProcess; -using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; -using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; /// -/// %%% +/// Demonstrates how to use the . /// -public class Step02_Handoff(ITestOutputHelper output) : BaseAgentsTest(output) +public class Step02_Handoff(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task UseHandoffPatternAsync() + public async Task SimpleHandoffAsync() { // Define the agents - // %%% STRUCTURED OUTPUT ??? - ChatCompletionAgent agent1 = - new() - { - Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", - //Name = name, - Description = "Agent 1", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent2 = - new() - { - Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", - //Name = name, - Description = "Agent 2", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent3 = - new() - { - Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", - //Name = name, - Description = "Agent 3", - Kernel = this.CreateKernelWithChatCompletion(), - }; + ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); + ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); + ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of onsonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); // Define the pattern InProcessRuntime runtime = new(); - HandoffOrchestration orchestration = new(runtime, HandoffCompletedHandlerAsync, agent1, agent2, agent3); + HandoffOrchestration orchestration = new(runtime, agent1, agent2, agent3); // Start the runtime await runtime.StartAsync(); - await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); + string input = "The quick brown fox jumps over the lazy dog"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {text}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task NestedHandoffAsync() + { + // Define the agents + ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); + ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); + ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); + + // Define the pattern + InProcessRuntime runtime = new(); + + HandoffOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); + HandoffOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); + HandoffOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestrationMain.InvokeAsync(input); + + string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {output}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task SingleActorAsync() + { + // Define the agents + ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestration = new(runtime, agent); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + + string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {output}"); + await runtime.RunUntilIdleAsync(); - Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); + } + + [Fact] + public async Task SingleNestedActorAsync() + { + // Define the agents + ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestrationInner = CreateNested(runtime, agent); + HandoffOrchestration orchestrationOuter = new(runtime, orchestrationInner); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestrationOuter.InvokeAsync(input); + + string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {output}"); + + await runtime.RunUntilIdleAsync(); + } + + private static HandoffOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + { + return new(runtime, targets) { - Console.WriteLine($"RESULT: {result}"); - return ValueTask.CompletedTask; - } + InputTransform = (HandoffMessage input) => input, + ResultTransform = (HandoffMessage results) => results, + }; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index f4a33a6c3cc5..e00556d5c089 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -1,62 +1,60 @@ -// Copyright (c) Microsoft. All rights reserved. +//// Copyright (c) Microsoft. All rights reserved. -using Microsoft.AgentRuntime.InProcess; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; -using Microsoft.SemanticKernel.ChatCompletion; +//using Microsoft.AgentRuntime.InProcess; +//using Microsoft.SemanticKernel; +//using Microsoft.SemanticKernel.Agents; +//using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +//using Microsoft.SemanticKernel.ChatCompletion; +//using static Microsoft.SemanticKernel.Agents.Orchestration.GroupChat.ChatMessages; -namespace GettingStarted.Orchestration; +//namespace GettingStarted.Orchestration; -/// -/// %%% -/// -public class Step03_GroupChat(ITestOutputHelper output) : BaseAgentsTest(output) -{ - [Fact] - public async Task UseGroupChatPatternAsync() - { - // Define the agents - // %%% STRUCTURED OUTPUT ??? - ChatCompletionAgent agent1 = - new() - { - Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", - //Name = name, - Description = "Agent 1", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent2 = - new() - { - Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", - //Name = name, - Description = "Agent 2", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent3 = - new() - { - Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", - //Name = name, - Description = "Agent 3", - Kernel = this.CreateKernelWithChatCompletion(), - }; +///// +///// Demonstrates how to use the . +///// +//public class Step03_GroupChat(ITestOutputHelper output) : BaseAgentsTest(output) +//{ +// [Fact] +// public async Task UseGroupChatPatternAsync() +// { +// // Define the agents +// // %%% STRUCTURED OUTPUT ??? +// ChatCompletionAgent agent1 = +// new() +// { +// Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", +// //Name = name, +// Description = "Agent 1", +// Kernel = this.CreateKernelWithChatCompletion(), +// }; +// ChatCompletionAgent agent2 = +// new() +// { +// Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", +// //Name = name, +// Description = "Agent 2", +// Kernel = this.CreateKernelWithChatCompletion(), +// }; +// ChatCompletionAgent agent3 = +// new() +// { +// Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", +// //Name = name, +// Description = "Agent 3", +// Kernel = this.CreateKernelWithChatCompletion(), +// }; - // Define the pattern - InProcessRuntime runtime = new(); - HandoffOrchestration orchestration = new(runtime, HandoffCompletedHandlerAsync, agent1, agent2, agent3); +// // Define the pattern +// InProcessRuntime runtime = new(); +// GroupChatOrchestration orchestration = new(runtime, agent1, agent2, agent3); - // Start the runtime - await runtime.StartAsync(); - await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); - await runtime.RunUntilIdleAsync(); - Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); +// // Start the runtime +// await runtime.StartAsync(); +// await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); +// ChatMessageContent result = await orchestration.Future; +// Console.WriteLine("RESULT:"); +// this.WriteAgentChatMessage(result); - ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) - { - Console.WriteLine($"RESULT: {result}"); - return ValueTask.CompletedTask; - } - } -} +// await runtime.RunUntilIdleAsync(); +// } +//} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs deleted file mode 100644 index 1871bab09e54..000000000000 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Custom.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.AgentRuntime.InProcess; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace GettingStarted.Orchestration; - -/// -/// %%% -/// -public class Step04_Custom(ITestOutputHelper output) : BaseAgentsTest(output) -{ - [Fact] - public async Task UseCustomPatternAsync() - { - // Define the agents - // %%% STRUCTURED OUTPUT ??? - ChatCompletionAgent agent1 = - new() - { - Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", - //Name = name, - Description = "Agent 1", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent2 = - new() - { - Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", - //Name = name, - Description = "Agent 2", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent3 = - new() - { - Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", - //Name = name, - Description = "Agent 3", - Kernel = this.CreateKernelWithChatCompletion(), - }; - - // Define the pattern - InProcessRuntime runtime = new(); - HandoffOrchestration orchestration = new(runtime, HandoffCompletedHandlerAsync, agent1, agent2, agent3); - - // Start the runtime - await runtime.StartAsync(); - await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); - await runtime.RunUntilIdleAsync(); - Console.WriteLine($"ISCOMPLETE = {orchestration.IsComplete}"); - - ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) - { - Console.WriteLine($"RESULT: {result}"); - return ValueTask.CompletedTask; - } - } -} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs new file mode 100644 index 000000000000..ca16d347ba70 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use the . +/// +public class Step04_Nested(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task NestHandoffBroadcastAsync() + { + // Define the agents + ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); + ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); + ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); + + // Define the pattern + InProcessRuntime runtime = new(); + BroadcastOrchestration innerOrchestration = + new(runtime, agent3, agent4) + { + InputTransform = (HandoffMessage input) => new BroadcastMessages.Task { Message = input.Content }, + ResultTransform = (BroadcastMessages.Result[] output) => new HandoffMessage { Content = new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))) } // %%% FORMAT / CODE SMELL + }; + HandoffOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await outerOrchestration.InvokeAsync(input); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n> RESULT:\n{text}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task NestBroadcastHandoffAsync() + { + // Define the agents + ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); + ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); + ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration innerOrchestration = + new(runtime, agent3, agent4) + { + InputTransform = (BroadcastMessages.Task input) => new HandoffMessage { Content = input.Message }, + ResultTransform = (HandoffMessage result) => new BroadcastMessages.Result { Message = result.Content } + }; + BroadcastOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await outerOrchestration.InvokeAsync(input); + + string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n> RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); + + await runtime.RunUntilIdleAsync(); + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs new file mode 100644 index 000000000000..510aec0a112e --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace GettingStarted.Orchestration; + +/// +/// %%% COMMENT +/// +public class Step05_Custom(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public Task UseCustomPatternAsync() + { + return Task.CompletedTask; + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs deleted file mode 100644 index d459f8fe2a8f..000000000000 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Multiuse.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.AgentRuntime.InProcess; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace GettingStarted.Orchestration; - -/// -/// %%% -/// -public class Step05_Multiuse(ITestOutputHelper output) : BaseAgentsTest(output) -{ - [Fact] - public async Task UseMultiplePatternsAsync() - { - // Define the agents - // %%% STRUCTURED OUTPUT ??? - ChatCompletionAgent agent1 = - new() - { - Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", - //Name = name, - Description = "Agent 1", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent2 = - new() - { - Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", - //Name = name, - Description = "Agent 2", - Kernel = this.CreateKernelWithChatCompletion(), - }; - ChatCompletionAgent agent3 = - new() - { - Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", - //Name = name, - Description = "Agent 3", - Kernel = this.CreateKernelWithChatCompletion(), - }; - - // Define the pattern - InProcessRuntime runtime = new(); - BroadcastOrchestration broadcast = new(runtime, BroadcastCompletedHandlerAsync, agent2, agent3); - HandoffOrchestration handoff = new(runtime, HandoffCompletedHandlerAsync, agent1); - - // Start the runtime - await runtime.StartAsync(); - await broadcast.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); - await handoff.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); - await runtime.RunUntilIdleAsync(); - - Console.WriteLine($"BROADCAST ISCOMPLETE = {broadcast.IsComplete}"); - Console.WriteLine($"HANDOFF ISCOMPLETE = {handoff.IsComplete}"); - - ValueTask BroadcastCompletedHandlerAsync(ChatMessageContent[] results) - { - Console.WriteLine("BROADCAST RESULT:"); - for (int index = 0; index < results.Length; ++index) - { - ChatMessageContent result = results[index]; - Console.WriteLine($"#{index}: {result}"); - } - return ValueTask.CompletedTask; - } - - ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result) - { - Console.WriteLine($"HANDOFF RESULT: {result}"); - return ValueTask.CompletedTask; - } - } -} diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs index 1e9523f24f47..73ac8ef797ee 100644 --- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs +++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs @@ -93,7 +93,7 @@ public AzureAIAgent( } /// - /// %%% + /// The associated client. /// public AgentsClient Client { get; } diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs new file mode 100644 index 000000000000..263180a720f7 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// An actor that is represents an . +/// +public abstract class AgentActor : BaseAgent +{ + internal const string DefaultDescription = "A helpful agent"; // %%% TODO - CONSIDER + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent) + : base(id, runtime, agent.Description ?? DefaultDescription, GetLogger(agent)) + { + this.Agent = agent; + } + + /// + /// The associated agent + /// + protected Agent Agent { get; } + + /// + /// %%% COMMENT + /// + protected AgentThread? Thread { get; set; } + + /// + /// %%% COMMENT + /// + /// + /// + /// + protected ValueTask InvokeAsync(ChatMessageContent input, CancellationToken cancellationToken) + { + input.Role = AuthorRole.User; // %%% HACK + return this.InvokeAsync([input], cancellationToken); + } + + /// + /// %%% COMMENT + /// + /// + /// + /// + protected async ValueTask InvokeAsync(IList input, CancellationToken cancellationToken) + { + AgentResponseItem[] responses = + await this.Agent.InvokeAsync( + input, + this.Thread, + options: null, + cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); + + AgentResponseItem response = responses[0]; + this.Thread ??= response.Thread; + + return new ChatMessageContent(response.Message.Role, string.Join("\n\n", responses.Select(response => response.Message))) // %%% HACK + { + AuthorName = response.Message.AuthorName, + }; + } + + /// + /// %%% COMMENT + /// + /// + /// + /// + protected async IAsyncEnumerable InvokeStreamingAsync(ChatMessageContent input, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var responseStream = this.Agent.InvokeStreamingAsync([input], this.Thread, options: null, cancellationToken); + + await foreach (AgentResponseItem response in responseStream.ConfigureAwait(false)) + { + this.Thread ??= response.Thread; + yield return response.Message; + } + } + + private static ILogger GetLogger(Agent agent) + { + ILoggerFactory loggerFactory = agent.LoggerFactory ?? NullLoggerFactory.Instance; + return loggerFactory.CreateLogger(); + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs new file mode 100644 index 000000000000..0a09243627c2 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// An actor that represents the orchestration. +/// +public abstract partial class AgentOrchestration +{ + private sealed class RequestActor : BaseAgent, IHandle + { + private readonly Func _transform; + private readonly Func _action; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// // %%% COMMENT + /// // %%% COMMENT + public RequestActor( + AgentId id, + IAgentRuntime runtime, + Func transform, + Func action) + : base(id, runtime, $"{id.Type}_Actor") + { + this._transform = transform; + this._action = action; + } + + /// + /// %%% COMMENT + /// + /// + /// + /// + public async ValueTask HandleAsync(TInput item, MessageContext messageContext) + { + Trace.WriteLine($"> ORCHESTRATION ENTER: {this.Id.Type}"); + try + { + TSource source = this._transform.Invoke(item); + await this._action.Invoke(source).ConfigureAwait(false); + } + catch (Exception exception) + { + Trace.WriteLine($"ERROR: {exception.Message}"); + throw; // %%% EXCEPTION + } + } + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs new file mode 100644 index 000000000000..6d7935d78b5d --- /dev/null +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// An actor that represents the orchestration. +/// +public abstract partial class AgentOrchestration +{ + private sealed class ResultActor : BaseAgent, IHandle + { + private readonly TaskCompletionSource? _completionSource; + private readonly Func _transform; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// // %%% COMMENT + /// Signals completion. + public ResultActor( + AgentId id, + IAgentRuntime runtime, + Func transform, + TaskCompletionSource? completionSource = null) + : base(id, runtime, $"{id.Type}_Actor") + { + this._completionSource = completionSource; + this._transform = transform; + } + + /// + /// %%% COMMENT + /// + public AgentType? CompletionTarget { get; init; } + + /// + /// %%% COMMENT + /// + /// + /// + /// + public async ValueTask HandleAsync(TResult item, MessageContext messageContext) + { + Trace.WriteLine($"> ORCHESTRATION EXIT: {this.Id.Type}"); + + try + { + TOutput output = this._transform.Invoke(item); + + if (this.CompletionTarget != null) + { + await this.SendMessageAsync(output!, new AgentId(this.CompletionTarget, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID && NULL OVERRIDE + } + + this._completionSource?.SetResult(output); + } + catch (Exception exception) + { + Trace.WriteLine($"ERROR: {exception.Message}"); + throw; // %%% EXCEPTION + } + } + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index b76d396e80de..5c5e8dc34904 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -1,98 +1,173 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Threading; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// %%% +/// Base class for multi-agent orchestration patterns. /// -public abstract class AgentOrchestration +public abstract partial class AgentOrchestration : Orchestratable { - private const int IsRegistered = 1; - private const int NotRegistered = 0; - private int _isRegistered = NotRegistered; + private readonly string _orchestrationType; /// - /// %%% + /// Initializes a new instance of the class. /// - /// - protected AgentOrchestration(IAgentRuntime runtime) + /// The runtime associated with the orchestration. + /// // %%% COMMENT + protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) { Verify.NotNull(runtime, nameof(runtime)); this.Runtime = runtime; - //this.Id = $"{this.GetType().Name}_{Guid.NewGuid():N}"; - this.Id = Guid.NewGuid().ToString("N"); + this.Members = members; + this._orchestrationType = this.GetType().Name.Split('`').First(); } + + /// + /// %%% COMMENT + /// + public string Name { get; init; } = string.Empty; + + /// + /// %%% COMMENT + /// + public string Description { get; init; } = string.Empty; + + /// + /// %%% COMMENT + /// + public Func? InputTransform { get; init; } // %%% TODO: ASYNC + /// - /// %%% + /// %%% COMMENT /// - public abstract bool IsComplete { get; } + public Func? ResultTransform { get; init; } // %%% TODO: ASYNC /// - /// %%% + /// %%% COMMENT /// - public string Id { get; } + protected IReadOnlyList Members { get; } /// - /// %%% + /// Gets the runtime associated with the orchestration. /// protected IAgentRuntime Runtime { get; } /// - /// %%% + /// Initiate processing of the orchestration. /// - /// - /// - public async ValueTask StartAsync(ChatMessageContent message) // %%% IS SUFFICIENTLY FLEXIBLE ??? + /// The input message + /// // %%% COMMENT + public async ValueTask> InvokeAsync(TInput input, TimeSpan? timeout = null) { - Verify.NotNull(message, nameof(message)); + Verify.NotNull(input, nameof(input)); - if (Interlocked.CompareExchange(ref this._isRegistered, NotRegistered, IsRegistered) == NotRegistered) - { - await this.RegisterAsync().ConfigureAwait(false); - } + TopicId topic = new($"ID_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); + + TaskCompletionSource completion = new(); + + Trace.WriteLine($"!!! ORCHESTRATION REGISTER: {topic}\n"); + + AgentType orchestrationType = await this.RegisterAsync(topic, completion).ConfigureAwait(false); - await this.MessageTaskAsync(message).ConfigureAwait(false); + Trace.WriteLine($"\n!!! ORCHESTRATION INVOKE: {orchestrationType}\n"); + + //await this.Runtime.SendMessageAsync(input, new AgentId(orchestrationType, AgentId.DefaultKey)).ConfigureAwait(false); + Task task = this.Runtime.SendMessageAsync(input, new AgentId(orchestrationType, AgentId.DefaultKey)).AsTask(); // %%% TODO: REFINE + + Trace.WriteLine($"\n!!! ORCHESTRATION YIELD: {orchestrationType}"); + + return new OrchestrationResult(topic, completion); } /// - /// %%% + /// %%% COMMENT /// - /// + /// + /// /// - protected abstract ValueTask MessageTaskAsync(ChatMessageContent message); + protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{this._orchestrationType}_{suffix}"); + + /// + /// Initiate processing according to the orchestration pattern. + /// + /// // %%% COMMENT + /// The input message + /// // %%% COMMENT + protected abstract ValueTask StartAsync(TopicId topic, TSource input, AgentType? entryAgent); /// - /// %%% + /// %%% COMMENT /// - protected abstract ValueTask RegisterAsync(); + /// + /// + /// + protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType); /// - /// %%% + /// %%% COMMENT /// - /// - /// + /// + /// /// - protected async Task RegisterTopicsAsync(string agentType, params TopicId[] topics) + protected internal override ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor) + { + TopicId orchestrationTopic = new($"{externalTopic.Type}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); + + return this.RegisterAsync(orchestrationTopic, completion: null, targetActor); + } + + /// + /// %%% COMMENT + /// + protected async Task SubscribeAsync(string agentType, params TopicId[] topics) { for (int index = 0; index < topics.Length; ++index) { - await this.Runtime.AddSubscriptionAsync(new Subscription(topics[index], agentType)).ConfigureAwait(false); + await this.Runtime.AddSubscriptionAsync(new TypeSubscription(topics[index].Type, agentType)).ConfigureAwait(false); } } /// - /// %%% + /// %%% COMMENT /// - /// + /// + /// + /// /// - protected string GetAgentId(Agent agent) + private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor = null) { - return (agent.Name ?? $"{agent.GetType().Name}_{agent.Id}").Replace("-", "_"); + // Register actor for final result + AgentType orchestrationFinal = this.FormatAgentType(topic, "Root"); + await this.Runtime.RegisterAgentFactoryAsync( + orchestrationFinal, + (agentId, runtime) => + ValueTask.FromResult( + new ResultActor(agentId, runtime, this.ResultTransform!, completion) // %%% NULL OVERRIDE + { + CompletionTarget = targetActor, + })).ConfigureAwait(false); + + // Register orchestration members + AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal).ConfigureAwait(false); + + // Register actor for orchestration entry-point + AgentType orchestrationEntry = this.FormatAgentType(topic, "Boot"); + await this.Runtime.RegisterAgentFactoryAsync( + orchestrationEntry, + (agentId, runtime) => + ValueTask.FromResult( + new RequestActor(agentId, runtime, this.InputTransform!, async (TSource source) => await this.StartAsync(topic, source, entryAgent).ConfigureAwait(false))) // %%% NULL OVERRIDE + ).ConfigureAwait(false); + + return orchestrationEntry; } } diff --git a/dotnet/src/Agents/Orchestration/AgentProxy.cs b/dotnet/src/Agents/Orchestration/AgentProxy.cs deleted file mode 100644 index 35efd8bd19e9..000000000000 --- a/dotnet/src/Agents/Orchestration/AgentProxy.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.AgentRuntime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// A built around a . -/// -public abstract class AgentProxy : RuntimeAgent -{ - private AgentThread? _thread; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// An . - protected AgentProxy(AgentId id, IAgentRuntime runtime, Agent agent) - : base(id, runtime, agent.Description ?? throw new ArgumentException($"The agent description must be defined (#{agent.Name ?? agent.Id}).")) // %%%: DESCRIPTION Contract - { - this.Agent = agent; - } - - /// - /// %%% - /// - protected Agent Agent { get; } -} diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index 5d5786e68fd0..2b5825ea6428 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -5,7 +5,7 @@ Microsoft.SemanticKernel.Agents.Orchestration Microsoft.SemanticKernel.Agents.Orchestration net8.0 - + $(NoWarn);SKEXP0110;SKEXP0001 false preview @@ -28,10 +28,12 @@ + + @@ -40,8 +42,19 @@ - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs new file mode 100644 index 000000000000..a2680b70b79a --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +/// +/// An used with the . +/// +internal sealed class BroadcastActor : AgentActor, IHandle +{ + private readonly AgentType _orchestrationType; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + /// Identifies the orchestration agent. + public BroadcastActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType) : + base(id, runtime, agent) + { + this._orchestrationType = orchestrationType; + } + + /// + public async ValueTask HandleAsync(BroadcastMessages.Task item, MessageContext messageContext) + { + Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} INPUT - {item.Message}"); + + ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); + + Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} OUTPUT - {response}"); + + await this.SendMessageAsync(response.ToBroadcastResult(), new AgentId(this._orchestrationType, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID + //await this.Thread?.DeleteAsync().ConfigureAwait(false); // %%% OPTIONAL ??? + } +} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs index 65a224b25c5a..856812e69545 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs @@ -1,14 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel.ChatCompletion; + namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; /// -/// Common messages used by the . +/// Common messages used by the . /// -internal static class BroadcastMessages +public static class BroadcastMessages { /// - /// %%% COMMENT + /// The input task for a . /// public sealed class Task { @@ -19,7 +21,7 @@ public sealed class Task } /// - /// %%% COMMENT + /// A result from a . /// public sealed class Result { @@ -30,16 +32,22 @@ public sealed class Result } /// - /// %%% + /// Extension method to convert a to a . + /// + public static Result ToBroadcastResult(this string text, AuthorRole? role = null) => new() { Message = new ChatMessageContent(role ?? AuthorRole.Assistant, text) }; + + /// + /// Extension method to convert a to a . + /// + public static Result ToBroadcastResult(this ChatMessageContent message) => new() { Message = message }; + + /// + /// Extension method to convert a to a . /// - /// - /// - public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; + public static Task ToBroadcastTask(this string text, AuthorRole? role = null) => new() { Message = new ChatMessageContent(role ?? AuthorRole.User, text) }; /// - /// %%% + /// Extension method to convert a to a . /// - /// - /// - public static Task ToTask(this ChatMessageContent message) => new() { Message = message }; + public static Task ToBroadcastTask(this ChatMessageContent message) => new() { Message = message }; } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs new file mode 100644 index 000000000000..b1f7c7ddb2be --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +/// +/// An orchestration that broadcasts the input message to each agent. +/// +public sealed class BroadcastOrchestration : BroadcastOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents to be orchestrated. + public BroadcastOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + : base(runtime, members) + { + this.InputTransform = (string input) => input.ToBroadcastTask(); + this.ResultTransform = (BroadcastMessages.Result[] result) => [.. result.Select(r => r.Message.Content ?? string.Empty)]; + } +} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs index 4eafad38a23d..6e3c1e5f0361 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs @@ -1,89 +1,84 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Concurrent; -using System.Threading; +using System; +using System.Diagnostics; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AgentRuntime; namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; /// -/// %%% +/// An orchestration that broadcasts the input message to each agent. /// -/// -public delegate ValueTask BroadcastCompletedHandlerAsync(ChatMessageContent[] results); - -/// -/// %%% -/// -public sealed class BroadcastOrchestration : AgentOrchestration +public class BroadcastOrchestration + : AgentOrchestration { - private readonly BroadcastCompletedHandlerAsync _completionHandler; - private readonly Agent[] _agents; - private readonly TopicId _topic; - private readonly ConcurrentQueue _results; - private int _resultCount; - /// - /// %%% + /// Initializes a new instance of the class. /// - /// - /// - /// - public BroadcastOrchestration(IAgentRuntime runtime, BroadcastCompletedHandlerAsync completionHandler, params Agent[] agents) - : base(runtime) + /// The runtime associated with the orchestration. + /// The agents participating in the orchestration. + public BroadcastOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + : base(runtime, agents) { - Verify.NotNull(completionHandler, nameof(completionHandler)); - //Verify.NotEmpty(agents, nameof(agents)); // %%% TODO: Utility - - this._agents = agents; - this._completionHandler = completionHandler; - this._topic = new($"BroadcastTopic_{nameof(Task)}_{this.Id}", this.Id); - this._results = []; } /// - public override bool IsComplete => this._resultCount == this._agents.Length; - - /// - protected override async ValueTask MessageTaskAsync(ChatMessageContent message) + protected override ValueTask StartAsync(TopicId topic, BroadcastMessages.Task input, AgentType? entryAgent) { - await this.Runtime.PublishMessageAsync(message.ToTask(), this._topic).ConfigureAwait(false); + Trace.WriteLine($"> BROADCAST START: {topic}"); + return this.Runtime.PublishMessageAsync(input, topic); } /// - protected override async ValueTask RegisterAsync() + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType) { - AgentType receiverType = new($"{nameof(BroadcastReciever)}_{this.Id}"); + // Register result actor + AgentType resultType = this.FormatAgentType(topic, "Results"); + await this.Runtime.RegisterAgentFactoryAsync( + resultType, + (agentId, runtime) => + ValueTask.FromResult( + new BroadcastResultActor(agentId, runtime, orchestrationType, this.Members.Count))).ConfigureAwait(false); + Trace.WriteLine($"> BROADCAST RESULTS: {resultType}"); - // All agents respond to the same message. - foreach (Agent agent in this._agents) + // Register member actors - All agents respond to the same message. + int agentCount = 0; + foreach (OrchestrationTarget member in this.Members) { - await this.RegisterAgentAsync(agent, receiverType).ConfigureAwait(false); - } + ++agentCount; - await this.Runtime.RegisterAgentFactoryAsync( - receiverType, - (agentId, runtime) => ValueTask.FromResult(new BroadcastReciever(agentId, runtime, this.HandleResultAsync))).ConfigureAwait(false); - } + AgentType memberType; - private async ValueTask RegisterAgentAsync(Agent agent, AgentType receiverType) - { - string agentType = this.GetAgentId(agent); - await this.Runtime.RegisterAgentFactoryAsync( - agentType, - (agentId, runtime) => ValueTask.FromResult(new BroadcastProxy(agentId, runtime, agent, receiverType))).ConfigureAwait(false); + switch (member.TargetType) + { + case OrchestrationTargetType.Agent: + memberType = await RegisterAgentAsync(member.Agent!).ConfigureAwait(false); + break; + case OrchestrationTargetType.Orchestratable: + memberType = await member.Orchestration!.RegisterAsync(topic, resultType).ConfigureAwait(false); // %%% NULL OVERIDE + break; + default: + throw new InvalidOperationException($"Unsupported target type: {member.TargetType}"); // %%% EXCEPTION TYPE + } - await this.RegisterTopicsAsync(agentType, this._topic).ConfigureAwait(false); - } + Trace.WriteLine($"> BROADCAST MEMBER #{agentCount}: {memberType}"); - private async ValueTask HandleResultAsync(BroadcastMessages.Result result) - { - this._results.Enqueue(result.Message); - Interlocked.Increment(ref this._resultCount); - if (this.IsComplete) + await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); + } + + return null; + + async ValueTask RegisterAgentAsync(Agent agent) { - await this._completionHandler.Invoke(this._results.ToArray()).ConfigureAwait(false); + AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); + await this.Runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => + ValueTask.FromResult(new BroadcastActor(agentId, runtime, agent, resultType))).ConfigureAwait(false); + + return agentType; } } } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs deleted file mode 100644 index f0fba8b3acaa..000000000000 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastProxy.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// A built around a . -/// -internal sealed class BroadcastProxy : AgentProxy -{ - private readonly AgentType _recieverType; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// An . - /// // %%% - public BroadcastProxy(AgentId id, IAgentRuntime runtime, Agent agent, AgentType recieverType) - : base(id, runtime, agent) - { - this.RegisterHandler(this.OnTaskAsync); - this._recieverType = recieverType; - } - - /// - private async ValueTask OnTaskAsync(BroadcastMessages.Task task, MessageContext context) - { - AgentResponseItem[] responses = await this.Agent.InvokeAsync([task.Message]).ToArrayAsync().ConfigureAwait(false); - AgentResponseItem response = responses.First(); - await this.SendMessageAsync(response.Message.ToResult(), this._recieverType).ConfigureAwait(false); // %% CARDINALITY - await response.Thread.DeleteAsync().ConfigureAwait(false); - } -} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs deleted file mode 100644 index 68fd7fecfcc9..000000000000 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastReciever.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// %%% -/// -/// -internal delegate ValueTask BroadcastResultHandlerAsync(BroadcastMessages.Result result); - -/// -/// A built around a . -/// -internal sealed class BroadcastReciever : RuntimeAgent -{ - private readonly BroadcastResultHandlerAsync _resultHandler; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// // %%% - public BroadcastReciever(AgentId id, IAgentRuntime runtime, BroadcastResultHandlerAsync resultHandler) - : base(id, runtime, "// %%% DESCRIPTION") - { - this.RegisterHandler(this.OnResultAsync); - this._resultHandler = resultHandler; - } - - /// - /// %%% - /// - public bool IsComplete => true; // %%% TODO - - /// - private ValueTask OnResultAsync(BroadcastMessages.Result message, MessageContext context) - { - return this._resultHandler.Invoke(message); - } -} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs new file mode 100644 index 000000000000..5898257c2a64 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; + +/// +/// %%% COMMENT +/// +internal sealed class BroadcastResultActor : BaseAgent, + IHandle +{ + private readonly ConcurrentQueue _results; + private readonly AgentType _orchestrationType; + private readonly int _expectedCount; + private int _resultCount; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// Identifies the orchestration agent. + /// The expected number of messages to be recieved. + public BroadcastResultActor( + AgentId id, + IAgentRuntime runtime, + AgentType orchestrationType, + int expectedCount) + : base(id, runtime, "Captures the results of the BroadcastOrchestration") + { + this._orchestrationType = orchestrationType; + this._expectedCount = expectedCount; + this._results = []; + } + + /// + public async ValueTask HandleAsync(BroadcastMessages.Result item, MessageContext messageContext) + { + Trace.WriteLine($"> BROADCAST RESULT: {this.Id.Type} (#{this._resultCount + 1})"); + + this._results.Enqueue(item); + + if (Interlocked.Increment(ref this._resultCount) == this._expectedCount) + { + await this.SendMessageAsync(this._results.ToArray(), new AgentId(this._orchestrationType, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID + } + } +} diff --git a/dotnet/src/Agents/Orchestration/Extensions/AgentExtensions.cs b/dotnet/src/Agents/Orchestration/Extensions/AgentExtensions.cs new file mode 100644 index 000000000000..ce7d0f4c2ba3 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Extensions/AgentExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +//using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Extensions; + +/// +/// Extension methods for . +/// +internal static class AgentExtensions +{ + ///// + ///// Provides a properly formatted unique identifier to an . + ///// + ///// The specified agent. + ///// The parent orchestration + //public static string GetAgentType(this Agent agent, AgentOrchestration orchestration) + //{ + // return $"{agent.Name ?? agent.GetType().Name}_{agent.Id}_{orchestration.Id}".Replace("-", "_"); + //} +} diff --git a/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs b/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs new file mode 100644 index 000000000000..70f039d23cf0 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs @@ -0,0 +1,21 @@ +//// Copyright (c) Microsoft. All rights reserved. + +//using System.Threading.Tasks; +//using Microsoft.AgentRuntime; + +//namespace Microsoft.SemanticKernel.Agents.Orchestration.Extensions; + +///// +///// Extension methods for . +///// +//internal static class RuntimeExtensions +//{ +// /// +// /// Sends a message to the specified agent. +// /// +// public static async ValueTask SendMessageAsync(this IAgentRuntime runtime, object message, AgentType agentType) +// { +// AgentId agentId = await runtime.GetAgentAsync(agentType).ConfigureAwait(false); +// await runtime.SendMessageAsync(message, agentId).ConfigureAwait(false); +// } +//} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs new file mode 100644 index 000000000000..6b16784774ce --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs @@ -0,0 +1,124 @@ +//// Copyright (c) Microsoft. All rights reserved. + +//using System.Diagnostics; +//using System.Threading.Tasks; +//using Microsoft.AgentRuntime; +//using Microsoft.SemanticKernel.ChatCompletion; + +//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +///// +///// A that orchestrates a team of agents. +///// +//public abstract class ChatManager : RuntimeAgent +//{ +// /// +// /// A common description for the orchestrator. +// /// +// public const string Description = "Orchestrates a team of agents to accomplish a defined task."; +// private readonly TaskCompletionSource _completionSource; + +// /// +// /// Initializes a new instance of the class. +// /// +// /// The unique identifier of the agent. +// /// The runtime associated with the agent. +// /// The team of agents being orchestrated +// /// Signals completion. +// protected ChatManager(AgentId id, IAgentRuntime runtime, ChatTeam team, TaskCompletionSource completionSource) +// : base(id, runtime, Description) +// { +// this.Chat = []; +// this.Team = team; +// this._completionSource = completionSource; +// Debug.WriteLine($">>> NAMES: {this.Team.FormatNames()}"); +// Debug.WriteLine($">>> TEAM:\n{this.Team.FormatList()}"); + +// this.RegisterHandler(this.OnTaskMessageAsync); +// this.RegisterHandler(this.OnGroupMessageAsync); +// this.RegisterHandler(this.OnResultMessageAsync); +// } + +// /// +// /// The conversation history with the team. +// /// +// protected ChatHistory Chat { get; } + +// /// +// /// The input task. +// /// +// protected ChatMessages.InputTask Task { get; private set; } = ChatMessages.InputTask.None; // %%% TYPE CONFLICT IN NAME + +// /// +// /// Metadata that describes team of agents being orchestrated. +// /// +// protected ChatTeam Team { get; } + +// /// +// /// Message a specific agent, by topic. +// /// +// protected Task RequestAgentResponseAsync(TopicId agentTopic) +// { +// return this.PublishMessageAsync(new ChatMessages.Speak(), agentTopic); +// } + +// /// +// /// Defines one-time logic required to prepare to execute the given task. +// /// +// /// +// /// The agent specific topic for first step in executing the task. +// /// +// /// +// /// Returning a null TopicId indicates that the task will not be executed. +// /// +// protected abstract Task PrepareTaskAsync(); + +// ///// +// ///// %%% TODO +// ///// +// // %%% TODO protected abstract Task RequestResultAsync(); + +// /// +// /// Determines which agent's must respond. +// /// +// /// +// /// The agent specific topic for first step in executing the task. +// /// +// /// +// /// Returning a null TopicId indicates that the task will not be executed. +// /// +// protected abstract Task SelectAgentAsync(); + +// private async ValueTask OnTaskMessageAsync(ChatMessages.InputTask message, MessageContext context) +// { +// Debug.WriteLine($">>> TASK: {message.Message}"); +// this.Task = message; +// TopicId? agentTopic = await this.PrepareTaskAsync().ConfigureAwait(false); +// if (agentTopic != null) +// { +// await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); +// } +// } + +// private async ValueTask OnGroupMessageAsync(ChatMessages.Group message, MessageContext context) +// { +// Debug.WriteLine($">>> CHAT: {message.Message}"); +// this.Chat.Add(message.Message); +// TopicId? agentTopic = await this.SelectAgentAsync().ConfigureAwait(false); +// if (agentTopic != null) +// { +// await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); +// } +// else +// { +// //await this.RequestResultAsync().ConfigureAwait(false); // %%% TODO - GROUP CHAT +// } +// } + +// private ValueTask OnResultMessageAsync(ChatMessages.Result result, MessageContext context) +// { +// Debug.WriteLine($">>> RESULT: {result.Message}"); +// this._completionSource.SetResult(result.Message); +// return ValueTask.CompletedTask; +// } +//} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs new file mode 100644 index 000000000000..b6498da4b981 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs @@ -0,0 +1,77 @@ +//// Copyright (c) Microsoft. All rights reserved. + +//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +///// +///// Common messages used for agent chat patterns. +///// +//public static class ChatMessages +//{ +// /// +// /// %%% COMMENT +// /// +// internal static readonly ChatMessageContent Empty = new(); + +// /// +// /// Broadcast a message to all . +// /// +// public sealed class Group +// { +// /// +// /// The chat message being broadcast. +// /// +// public ChatMessageContent Message { get; init; } = Empty; +// } + +// /// +// /// Reset/clear the conversation history for all . +// /// +// public sealed class Reset { } + +// /// +// /// The final result. +// /// +// public sealed class Result +// { +// /// +// /// The chat message captures the final result. +// /// +// public ChatMessageContent Message { get; init; } = Empty; +// } + +// /// +// /// Signal a to respond. +// /// +// public sealed class Speak { } + +// /// +// /// The input task for a . +// /// +// public sealed class InputTask +// { +// /// +// /// A task that does not require any action. +// /// +// public static readonly InputTask None = new(); + +// /// +// /// The input that defines the task goal. +// /// +// public ChatMessageContent Message { get; init; } = Empty; +// } + +// /// +// /// Extension method to convert a to a . +// /// +// public static Group ToGroup(this ChatMessageContent message) => new() { Message = message }; + +// /// +// /// Extension method to convert a to a . +// /// +// public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; + +// /// +// /// Extension method to convert a to a . +// /// +// public static InputTask ToTask(this ChatMessageContent message) => new() { Message = message }; +//} diff --git a/dotnet/src/Agents/Orchestration/AgentTeam.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatTeam.cs similarity index 57% rename from dotnet/src/Agents/Orchestration/AgentTeam.cs rename to dotnet/src/Agents/Orchestration/GroupChat/ChatTeam.cs index 3c7d324ba7b9..5d1089969aed 100644 --- a/dotnet/src/Agents/Orchestration/AgentTeam.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatTeam.cs @@ -4,15 +4,15 @@ using System.Linq; using Microsoft.AgentRuntime; -namespace Microsoft.SemanticKernel.Agents.Orchestration; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// -/// A for orchestrating a team of agents. +/// %%% COMMENT /// -public class AgentTeam : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT +public class ChatTeam : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT /// -/// Extensions for . +/// Extensions for . /// public static class AgentTeamExtensions { @@ -21,12 +21,12 @@ public static class AgentTeamExtensions /// /// The agent team /// A comma delimimted list of agent name. - public static string FormatNames(this AgentTeam team) => string.Join(",", team.Select(t => t.Key)); + public static string FormatNames(this ChatTeam team) => string.Join(",", team.Select(t => t.Key)); /// /// Format the names and descriptions of the agents in the team as a markdown list. /// /// The agent team /// A markdown list of agent names and descriptions. - public static string FormatList(this AgentTeam team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); + public static string FormatList(this ChatTeam team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs new file mode 100644 index 000000000000..da26a72600d9 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs @@ -0,0 +1,66 @@ +//// Copyright (c) Microsoft. All rights reserved. + +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading.Tasks; +//using Microsoft.AgentRuntime; + +//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +///// +///// %%% COMMENT +///// +//internal sealed class GroupChatActor : AgentActor +//{ +// private readonly List _cache; +// private readonly TopicId _chatTopic; +// private AgentThread? _thread; + +// /// +// /// Initializes a new instance of the class. +// /// +// /// The unique identifier of the agent. +// /// The runtime associated with the agent. +// /// An . +// /// The unique topic used to broadcast to the entire chat. +// public GroupChatActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId chatTopic) +// : base(id, runtime, agent) +// { +// this._cache = []; +// this._chatTopic = chatTopic; + +// this.RegisterHandler(this.OnGroupMessageAsync); +// this.RegisterHandler(this.OnResetMessageAsync); +// this.RegisterHandler(this.OnSpeakMessageAsync); +// } + +// private ValueTask OnGroupMessageAsync(ChatMessages.Group message, MessageContext context) +// { +// this._cache.Add(message.Message); + +// return ValueTask.CompletedTask; +// } + +// private async ValueTask OnResetMessageAsync(ChatMessages.Reset message, MessageContext context) +// { +// if (this._thread is not null) +// { +// await this._thread.DeleteAsync().ConfigureAwait(false); +// this._thread = null; +// } +// } + +// private async ValueTask OnSpeakMessageAsync(ChatMessages.Speak message, MessageContext context) +// { +// AgentResponseItem[] responses = await this.Agent.InvokeAsync(this._cache, this._thread).ToArrayAsync().ConfigureAwait(false); +// AgentResponseItem response = responses.First(); +// this._thread ??= response.Thread; +// this._cache.Clear(); +// ChatMessageContent output = +// new(response.Message.Role, string.Join("\n\n", responses.Select(response => response.Message))) +// { +// AuthorName = response.Message.AuthorName, +// }; +// await this.PublishMessageAsync(output.ToGroup(), this._chatTopic).ConfigureAwait(false); +// } +//} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs new file mode 100644 index 000000000000..af9b3110bf18 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -0,0 +1,51 @@ +//// Copyright (c) Microsoft. All rights reserved. + +//using System; +//using System.Linq; +//using System.Threading.Tasks; +//using Microsoft.AgentRuntime; + +//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +///// +///// A that orchestrates a team of agents. +///// +//internal sealed class GroupChatManager : ChatManager +//{ +// private readonly TaskCompletionSource _completionSource; + +// /// +// /// Initializes a new instance of the class. +// /// +// /// The unique identifier of the agent. +// /// The runtime associated with the agent. +// /// The team of agents being orchestrated +// /// Signals completion. +// public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatTeam team, TaskCompletionSource completionSource) +// : base(id, runtime, team, completionSource) +// { +// this._completionSource = completionSource; +// } + +// /// +// protected override Task PrepareTaskAsync() +// { +// return this.SelectAgentAsync(); +// } + +// /// +// protected override Task SelectAgentAsync() +// { +// // %%% PLACEHOLDER +//#pragma warning disable CA5394 // Do not use insecure randomness +// int index = Random.Shared.Next(this.Team.Count + 1); +//#pragma warning restore CA5394 // Do not use insecure randomness +// var topics = this.Team.Values.Select(value => value.Topic).ToArray(); +// TopicId? topic = null; +// if (index < this.Team.Count) +// { +// topic = topics[index]; +// } +// return System.Threading.Tasks.Task.FromResult(topic); +// } +//} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs deleted file mode 100644 index e1e6d480d484..000000000000 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; - -/// -/// Common messages used in the Magentic framework. -/// -public static class GroupChatMessages -{ - /// - /// %%% COMMENT - /// - public sealed class Group - { - /// - /// %%% COMMENT - /// - public ChatMessageContent Message { get; init; } = new(); - } - - /// - /// %%% COMMENT - /// - public sealed class Result - { - /// - /// %%% COMMENT - /// - public ChatMessageContent Message { get; init; } = new(); - } - - /// - /// Reset/clear the conversation history. - /// - public sealed class Reset { } - - /// - /// Signal the agent to respond. - /// - public sealed class Speak { } - - /// - /// Defines the task to be performed. - /// - public sealed class Task - { - /// - /// A task that does not require any action. - /// - public static readonly Task None = new(); - - /// - /// The input that defines the task goal. - /// - public string Input { get; init; } = string.Empty; - } - - /// - /// %%% - /// - /// - /// - public static Group ToGroup(this ChatMessageContent message) => new() { Message = message }; - - /// - /// %%% - /// - /// - /// - public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; -} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs new file mode 100644 index 000000000000..003b503f1d01 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -0,0 +1,69 @@ +//// Copyright (c) Microsoft. All rights reserved. + +//using System.Threading.Tasks; +//using Microsoft.AgentRuntime; +//using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; + +//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +///// +///// An orchestration that coordinates a multi-agent conversation. +///// +//public sealed class GroupChatOrchestration : AgentOrchestration +//{ +// private readonly TaskCompletionSource _completionSource; +// private readonly Agent[] _agents; + +// /// +// /// Initializes a new instance of the class. +// /// +// /// The runtime associated with the orchestration. +// /// The agents participating in the orchestration. +// public GroupChatOrchestration(IAgentRuntime runtime, params Agent[] agents) +// : base(runtime) +// { +// Verify.NotNullOrEmpty(agents, nameof(agents)); + +// this._completionSource = new TaskCompletionSource(); +// this._agents = agents; +// } + +// /// +// /// %%% COMMENT +// /// +// public Task Future => this._completionSource.Task; + +// /// +// protected override async ValueTask MessageTaskAsync(ChatMessageContent message) +// { +// AgentType managerType = new($"{nameof(GroupChatManager)}_{this.Id}"); // %%% COMMON +// await this.Runtime.SendMessageAsync(message.ToTask(), managerType).ConfigureAwait(false); +// } + +// /// +// protected override async ValueTask PrepareAsync() +// { +// AgentType managerType = new($"{nameof(GroupChatManager)}_{this.Id}"); // %%% COMMON +// TopicId chatTopic = new($"GroupChatTopic_{this.Id}"); // %%% OTHER TOPICS: RESET ??? + +// ChatTeam team = []; +// foreach (Agent agent in this._agents) +// { +// AgentType agentType = agent.GetAgentType(this); +// await this.Runtime.RegisterAgentFactoryAsync( +// agentType, +// (agentId, runtime) => ValueTask.FromResult(new GroupChatActor(agentId, runtime, agent, chatTopic))).ConfigureAwait(false); +// TopicId agentTopic = new($"AgentTopic_{agent.Id}_{this.Id}".Replace("-", "_")); // %%% EXTENSION ??? +// team[agent.Name ?? agent.Id] = (agentTopic, agent.Description); + +// await this.RegisterTopicsAsync(agentType, chatTopic).ConfigureAwait(false); +// await this.RegisterTopicsAsync(agentType, agentTopic).ConfigureAwait(false); +// } + +// await this.Runtime.RegisterAgentFactoryAsync( +// managerType, +// (agentId, runtime) => ValueTask.FromResult(new GroupChatManager(agentId, runtime, team, this._completionSource))).ConfigureAwait(false); + +// await this.RegisterTopicsAsync(managerType, chatTopic).ConfigureAwait(false); +// } +//} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs b/dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs deleted file mode 100644 index 8e622a0caf41..000000000000 --- a/dotnet/src/Agents/Orchestration/GroupChat/ManagedAgent.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.AgentRuntime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; - -/// -/// A that responds to a . -/// -public abstract class ManagedAgent : RuntimeAgent -{ - /// - /// The common topic for group-chat. - /// - public static readonly TopicId GroupChatTopic = new(nameof(GroupChatTopic)); - - /// - /// The common topic for hidden-chat. - /// - public static readonly TopicId InnerChatTopic = new(nameof(InnerChatTopic)); - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// The agent description. - protected ManagedAgent(AgentId id, IAgentRuntime runtime, string description) - : base(id, runtime, description) - { - this.RegisterHandler(this.OnGroupMessageAsync); - this.RegisterHandler(this.OnResetMessageAsync); - this.RegisterHandler(this.OnSpeakMessageAsync); - } - - /// - /// %%% - /// - /// - protected abstract ValueTask ResetAsync(); - - /// - /// %%% - /// - /// - /// - protected abstract ValueTask RecieveMessageAsync(ChatMessageContent message); - - /// - /// %%% - /// - /// - protected abstract ValueTask SpeakAsync(); - - private ValueTask OnGroupMessageAsync(GroupChatMessages.Group message, MessageContext context) - { - return this.RecieveMessageAsync(message.Message); - } - - private ValueTask OnResetMessageAsync(GroupChatMessages.Reset message, MessageContext context) - { - return this.ResetAsync(); - } - - private async ValueTask OnSpeakMessageAsync(GroupChatMessages.Speak message, MessageContext context) - { - ChatMessageContent response = await this.SpeakAsync().ConfigureAwait(false); - await this.PublishMessageAsync(response.ToGroup(), GroupChatTopic).ConfigureAwait(false); - } -} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs b/dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs deleted file mode 100644 index 9e62d8d33587..000000000000 --- a/dotnet/src/Agents/Orchestration/GroupChat/ManagerAgent.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; - -/// -/// A that orchestrates a team of agents. -/// -public abstract class ManagerAgent : RuntimeAgent -{ - /// - /// A common description for the orchestrator. - /// - public const string Description = "Orchestrates a team of agents to accomplish a defined task."; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// The team of agents being orchestrated - protected ManagerAgent(AgentId id, IAgentRuntime runtime, AgentTeam team) - : base(id, runtime, Description) - { - this.Chat = []; - this.Team = team; - this.RegisterHandler(this.OnTaskMessageAsync); - this.RegisterHandler(this.OnGroupMessageAsync); - } - - /// - /// The conversation history with the team. - /// - protected ChatHistory Chat { get; } - - /// - /// The input task. - /// - protected GroupChatMessages.Task Task { get; private set; } = GroupChatMessages.Task.None; - - /// - /// Metadata that describes team of agents being orchestrated. - /// - protected AgentTeam Team { get; } - - /// - /// Message a specific agent, by topic. - /// - protected Task RequestAgentResponseAsync(TopicId agentTopic) - { - return this.PublishMessageAsync(new GroupChatMessages.Speak(), agentTopic); // %%% EXCEPTION: KeyNotFoundException/AggregateException - } - - /// - /// Defines one-time logic required to prepare to execute the given task. - /// - /// - /// The agent specific topic for first step in executing the task. - /// - /// - /// Returning a null TopicId indicates that the task will not be executed. - /// - protected abstract Task PrepareTaskAsync(); - - /// - /// Determines which agent's must respond. - /// - /// - /// The agent specific topic for first step in executing the task. - /// - /// - /// Returning a null TopicId indicates that the task will not be executed. - /// - protected abstract Task SelectAgentAsync(); - - private async ValueTask OnTaskMessageAsync(GroupChatMessages.Task message, MessageContext context) - { - this.Task = message; - TopicId? agentTopic = await this.PrepareTaskAsync().ConfigureAwait(false); - if (agentTopic != null) - { - await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); - } - } - - private async ValueTask OnGroupMessageAsync(GroupChatMessages.Group message, MessageContext context) - { - this.Chat.Add(message.Message); - TopicId? agentTopic = await this.SelectAgentAsync().ConfigureAwait(false); - if (agentTopic != null) - { - await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); - } - } -} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs new file mode 100644 index 000000000000..bf2ef7b5301d --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs @@ -0,0 +1,42 @@ +//// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// An actor used with the . +/// +internal sealed class HandoffActor : AgentActor, IHandle +{ + private readonly AgentType _nextAgent; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + /// The indentifier of the next agent for which to handoff the result + public HandoffActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) + : base(id, runtime, agent) + { + this._nextAgent = nextAgent; + } + + /// + public async ValueTask HandleAsync(HandoffMessage item, MessageContext messageContext) + { + Trace.WriteLine($"> HANDOFF ACTOR: {this.Id.Type} INPUT - {item.Content}"); + + ChatMessageContent response = await this.InvokeAsync(item.Content, messageContext.CancellationToken).ConfigureAwait(false); + + Trace.WriteLine($"> HANDOFF ACTOR: {this.Id.Type} OUTPUT - {response}"); + + await this.SendMessageAsync(HandoffMessage.FromChat(response), new AgentId(this._nextAgent, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID + //await response.Thread.DeleteAsync().ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs new file mode 100644 index 000000000000..4993f1e7af1e --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// A message that describes the input task and captures results for a . +/// +public sealed class HandoffMessage // %%% SIMPLIFY +{ + /// + /// The input task. + /// + public ChatMessageContent Content { get; init; } = new(); + + /// + /// Extension method to convert a to a . + /// + public static HandoffMessage FromChat(ChatMessageContent content) => new() { Content = content }; +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs deleted file mode 100644 index 45d35a04cc0d..000000000000 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessages.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; - -/// -/// Common messages used by the . -/// -internal static class HandoffMessages -{ - /// - /// %%% COMMENT - /// - public sealed class Input // %%% NAME - { - /// - /// %%% COMMENT - /// - public ChatMessageContent Task { get; init; } = new(); - - /// - /// %%% COMMENT - /// - public List Results { get; init; } = []; - } - - /// - /// %%% - /// - /// - /// - public static Input ToInput(this ChatMessageContent task) => new() { Task = task }; - - /// - /// %%% - /// - /// - /// - /// - public static Input Forward(this Input source, ChatMessageContent result) => new() { Task = source.Task, Results = [.. source.Results, result] }; -} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs new file mode 100644 index 000000000000..67739d486b3a --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// An orchestration that broadcasts the input message to each agent. +/// +public sealed partial class HandoffOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents to be orchestrated. + public static HandoffOrchestration ForMessage(IAgentRuntime runtime, params OrchestrationTarget[] members) // %%% CONSIDER + { + return new HandoffOrchestration(runtime, members) + { + InputTransform = HandoffMessage.FromChat, + ResultTransform = (HandoffMessage result) => result.Content, + }; + } +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs new file mode 100644 index 000000000000..322f06a57594 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// An orchestration that broadcasts the input message to each agent. +/// +public sealed partial class HandoffOrchestration : HandoffOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents to be orchestrated. + public HandoffOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + : base(runtime, members) + { + this.InputTransform = (string input) => HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.User, input)); + this.ResultTransform = (HandoffMessage result) => result.Content.ToString(); + } +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs index f2d974bcb3b4..66bad76bb53e 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs @@ -1,82 +1,69 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Linq; -using System.Threading; +using System; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// -/// %%% +/// An orchestration that provides the input message to the first agent +/// and sequentially passes each agent result to the next agent. /// -/// -public delegate ValueTask HandoffCompletedHandlerAsync(ChatMessageContent result); - -/// -/// %%% -/// -public sealed class HandoffOrchestration : AgentOrchestration +public class HandoffOrchestration : AgentOrchestration { - private readonly HandoffCompletedHandlerAsync _completionHandler; - private readonly Agent[] _agents; - private readonly AgentType _firstAgent; - private ChatMessageContent? _result; - /// - /// %%% + /// Initializes a new instance of the class. /// - /// - /// - /// - public HandoffOrchestration(IAgentRuntime runtime, HandoffCompletedHandlerAsync completionHandler, params Agent[] agents) - : base(runtime) + /// The runtime associated with the orchestration. + /// The agents participating in the orchestration. + public HandoffOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + : base(runtime, agents) { - Verify.NotNull(completionHandler, nameof(completionHandler)); - //Verify.NotEmpty(agents, nameof(agents)); // %%% TODO: Utility - - this._completionHandler = completionHandler; - this._agents = agents; - this._firstAgent = this.GetAgentId(agents.First()); } /// - public override bool IsComplete => this._result != null; - - /// - protected override async ValueTask MessageTaskAsync(ChatMessageContent message) + protected override async ValueTask StartAsync(TopicId topic, HandoffMessage input, AgentType? entryAgent) { - AgentId agentId = await this.Runtime.GetAgentAsync(this._firstAgent).ConfigureAwait(false); // %%% COMMON PATTERN - await this.Runtime.SendMessageAsync(message.ToInput(), agentId).ConfigureAwait(false); + Trace.WriteLine($"> HANDOFF START: {topic} [{entryAgent}]"); + + await this.Runtime.SendMessageAsync(input, new AgentId(entryAgent!, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID & NULL OVERRIDE } /// - protected override async ValueTask RegisterAsync() + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType) { - AgentType receiverType = new($"{nameof(HandoffReciever)}_{this.Id}"); - // Each agent handsoff its result to the next agent. - for (int index = 0; index < this._agents.Length; ++index) + AgentType nextAgent = orchestrationType; + for (int index = this.Members.Count - 1; index >= 0; --index) { - Agent agent = this._agents[index]; - AgentType nextAgent = index == this._agents.Length - 1 ? receiverType : this.GetAgentId(this._agents[index + 1]); - string agentType = this.GetAgentId(agent); - await this.Runtime.RegisterAgentFactoryAsync( - agentType, - (agentId, runtime) => ValueTask.FromResult(new HandoffProxy(agentId, runtime, agent, nextAgent))).ConfigureAwait(false); + Trace.WriteLine($"> HANDOFF NEXT #{index}: {nextAgent}"); + OrchestrationTarget member = this.Members[index]; + switch (member.TargetType) + { + case OrchestrationTargetType.Agent: + nextAgent = await RegisterAgentAsync(topic, nextAgent, index, member).ConfigureAwait(false); + break; + case OrchestrationTargetType.Orchestratable: + nextAgent = await member.Orchestration!.RegisterAsync(topic, nextAgent).ConfigureAwait(false); // %%% NULL OVERIDE + break; + default: + throw new InvalidOperationException($"Unsupported target type: {member.TargetType}"); // %%% EXCEPTION TYPE + } + Trace.WriteLine($"> HANDOFF MEMBER #{index}: {nextAgent}"); } - await this.Runtime.RegisterAgentFactoryAsync( - receiverType, - (agentId, runtime) => ValueTask.FromResult(new HandoffReciever(agentId, runtime, this.HandleResultAsync))).ConfigureAwait(false); - } + return nextAgent; - private async ValueTask HandleResultAsync(HandoffMessages.Input result) - { - Interlocked.CompareExchange(ref this._result, result.Results.Last(), null); - if (this.IsComplete) + async Task RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, OrchestrationTarget member) { - await this._completionHandler.Invoke(this._result).ConfigureAwait(false); + AgentType agentType = this.GetAgentType(topic, index); + return await this.Runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, member.Agent!, nextAgent))).ConfigureAwait(false); // %%% NULL OVERRIDE } } + + private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); } diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs deleted file mode 100644 index 43a0c13144f3..000000000000 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffProxy.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// A built around a . -/// -internal sealed class HandoffProxy : AgentProxy -{ - private readonly AgentType _nextAgent; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// An . - /// // %%% - public HandoffProxy(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) - : base(id, runtime, agent) - { - this.RegisterHandler(this.OnHandoffAsync); - this._nextAgent = nextAgent; - } - - /// - private async ValueTask OnHandoffAsync(HandoffMessages.Input message, MessageContext context) - { - AgentResponseItem[] responses = await this.Agent.InvokeAsync([message.Task]).ToArrayAsync().ConfigureAwait(false); - AgentResponseItem response = responses.First(); - await this.SendMessageAsync(message.Forward(response), this._nextAgent).ConfigureAwait(false); // %% CARDINALITY - await response.Thread.DeleteAsync().ConfigureAwait(false); - } -} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs deleted file mode 100644 index bd5f57a22165..000000000000 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffReciever.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// %%% -/// -/// -internal delegate ValueTask HandoffResultHandlerAsync(HandoffMessages.Input result); - -/// -/// A built around a . -/// -internal sealed class HandoffReciever : RuntimeAgent -{ - private readonly HandoffResultHandlerAsync _resultHandler; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// // %%% - public HandoffReciever(AgentId id, IAgentRuntime runtime, HandoffResultHandlerAsync resultHandler) - : base(id, runtime, "// %%% DESCRIPTION") - { - this.RegisterHandler(this.OnHandoffAsync); - this._resultHandler = resultHandler; - } - - /// - /// %%% - /// - public bool IsComplete => true; // %%% TODO - - /// - private ValueTask OnHandoffAsync(HandoffMessages.Input message, MessageContext context) - { - return this._resultHandler.Invoke(message); - } -} diff --git a/dotnet/src/Agents/Orchestration/Orchestratable.cs b/dotnet/src/Agents/Orchestration/Orchestratable.cs new file mode 100644 index 000000000000..5fe5729b12fd --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Orchestratable.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// %%% COMMENT +/// +public abstract class Orchestratable +{ + /// + /// %%% COMMENT + /// + /// + /// + /// + protected internal abstract ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor); +} diff --git a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs new file mode 100644 index 000000000000..862ecda1248c --- /dev/null +++ b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// %%% COMMENT +/// +/// +public sealed class OrchestrationResult +{ + private readonly TaskCompletionSource _completion; + + internal OrchestrationResult(TopicId topic, TaskCompletionSource completion) + { + this.Topic = topic; + this._completion = completion; + } + + /// + /// %%% COMMENT + /// + public TopicId Topic { get; } + + /// + /// %%% COMMENT + /// + /// + public async ValueTask GetValueAsync(TimeSpan? timeout = null) // %%% TODO: TryGetValueAsync ??? + { + Trace.WriteLine($"\n!!! ORCHESTRATION AWAIT: {this.Topic}\n"); + + if (timeout.HasValue) + { + Task[] tasks = [this._completion.Task]; + if (!Task.WaitAll(tasks, timeout.Value)) + { + throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); // %%% EXCEPTION TYPE + } + } + + return await this._completion.Task.ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs b/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs new file mode 100644 index 000000000000..55ea358f76ed --- /dev/null +++ b/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// %%% COMMENT +/// +public enum OrchestrationTargetType +{ + /// + /// %%% COMMENT + /// + Agent, + + /// + /// %%% COMMENT + /// + Orchestratable, +} + +/// +/// %%% COMMENT +/// +public readonly struct OrchestrationTarget : IEquatable +{ + /// + /// %%% COMMENT + /// + public static implicit operator OrchestrationTarget(Agent target) => new(target); + + /// + /// %%% COMMENT + /// + public static implicit operator OrchestrationTarget(Orchestratable target) => new(target); + + internal OrchestrationTarget(Agent agent) + { + this.Agent = agent; + this.TargetType = OrchestrationTargetType.Agent; + } + + internal OrchestrationTarget(Orchestratable orchestration) + { + this.Orchestration = orchestration; + this.TargetType = OrchestrationTargetType.Orchestratable; + } + + /// + /// %%% COMMENT + /// + public Agent? Agent { get; } + + /// + /// %%% COMMENT + /// + public Orchestratable? Orchestration { get; } + + /// + /// %%% COMMENT + /// + public OrchestrationTargetType TargetType { get; } + + /// + public override readonly bool Equals(object? obj) + { + return obj != null && this.Equals(obj is OrchestrationTarget); + } + + /// + /// %%% COMMENT + /// + /// + /// + public readonly bool Equals(OrchestrationTarget other) + { + return this.Agent == other.Agent && this.Orchestration == other.Orchestration; + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Agent?.GetHashCode() ?? 0, this.Orchestration?.GetHashCode() ?? 0); + } + + /// + /// %%% COMMENT + /// + /// + /// + /// + public static bool operator ==(OrchestrationTarget left, OrchestrationTarget right) + { + return left.Equals(right); + } + + /// + /// %%% COMMENT + /// + /// + /// + /// + public static bool operator !=(OrchestrationTarget left, OrchestrationTarget right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs b/dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs deleted file mode 100644 index d451a3d25324..000000000000 --- a/dotnet/src/Agents/Orchestration/Shim/RuntimeAgent.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AgentRuntime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// Defines a signature for message processing. -/// -/// The messaging being processed. -/// The message context. -public delegate ValueTask MessageHandler(object message, MessageContext messageContext); - -/// -/// An base agent that can be hosted in a runtime (). -/// -public abstract class RuntimeAgent : IHostableAgent -{ - private readonly IAgentRuntime _runtime; - private readonly Dictionary _handlers; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// The agent description (exposed in ). - protected RuntimeAgent(AgentId id, IAgentRuntime runtime, string description) - { - this._handlers = []; - this._runtime = runtime; - this.Id = id; - this.Metadata = new(id.Type, id.Key, description); - } - - /// - public AgentId Id { get; } - - /// - public AgentMetadata Metadata { get; } - - /// - public virtual ValueTask CloseAsync() => ValueTask.CompletedTask; - - /// - public async ValueTask OnMessageAsync(object message, MessageContext messageContext) - { - // Match all handlers for the message type, including if the handler declares a base type of the message. - // Order for invoking handlers is entirely independant. - Task[] tasks = - [.. this._handlers.Keys - .Where(key => key.IsAssignableFrom(message.GetType())) - .Select(key => this._handlers[key].Invoke(message, messageContext).AsTask())]; - - Debug.WriteLine($"HANDLE MESSAGE - {message.GetType().Name}/{messageContext.Topic}: #{tasks.Length} "); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - return null; - } - - /// - /// Register the handler for a given message type. - /// - /// The message type - /// The message handler - /// - /// The targeted message type may be the base type of the actual message. - /// - protected void RegisterHandler(Func messageHandler) - { - this._handlers[typeof(TMessage)] = (message, context) => messageHandler((TMessage)message, context); - } - - /// - /// Publishes a message to all agents subscribed to the given topic. - /// - /// The message type - /// The message to publish. - /// The topic to which to publish the message. - protected async Task PublishMessageAsync(TMessage message, TopicId topic) where TMessage : class - { - await this._runtime.PublishMessageAsync(message, topic, this.Id).ConfigureAwait(false); - } - - /// - /// %%% - /// - /// The message type - /// The message to publish. - /// %%% - protected async Task SendMessageAsync(TMessage message, AgentType agentType) where TMessage : class - { - AgentId agentId = await this._runtime.GetAgentAsync(agentType).ConfigureAwait(false); - await this._runtime.SendMessageAsync(message, agentId, this.Id).ConfigureAwait(false); - } -} diff --git a/dotnet/src/Agents/Orchestration/Shim/Subscription.cs b/dotnet/src/Agents/Orchestration/Shim/Subscription.cs deleted file mode 100644 index d91c97e5a6ec..000000000000 --- a/dotnet/src/Agents/Orchestration/Shim/Subscription.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.AgentRuntime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -internal sealed class Subscription(TopicId topic, string agentType, string? id = null) : ISubscriptionDefinition -{ - /// - public string Id { get; } = id ?? Guid.NewGuid().ToString(); - - /// - /// Gets the topic associated with the subscription. - /// - public TopicId Topic { get; } = topic; - - /// - public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; - - /// - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - public AgentId MapToAgent(TopicId topic) - { - if (!this.Matches(topic)) - { - throw new InvalidOperationException("Topic does not match the subscription."); - } - - return new AgentId(agentType, topic.Source); - } - - /// - public bool Matches(TopicId topic) => this.Topic.Type == topic.Type; -} diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs new file mode 100644 index 000000000000..353c5b18de87 --- /dev/null +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Agents; + +/// +/// Base class for samples that demonstrate the usage of host agents +/// based on API's such as Open AI Assistants or Azure AI Agents. +/// +public abstract class BaseOrchestrationTest(ITestOutputHelper output) : BaseAgentsTest(output) +{ + protected const int ResultTimeoutInSeconds = 10; + + protected ChatCompletionAgent CreateAgent(string instructions, string? name = null, string? description = null) + { + return + new ChatCompletionAgent + { + Instructions = instructions, + Name = name, + Description = description, + Kernel = this.CreateKernelWithChatCompletion(), + }; + } +} diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 78816c97e2e2..4582c7e83440 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics; using System.Reflection; using System.Text; using System.Text.Json; @@ -89,6 +90,10 @@ protected BaseTest(ITestOutputHelper output, bool redirectSystemConsoleOutput = .AddUserSecrets(Assembly.GetExecutingAssembly()) .Build(); + TextWriterTraceListener traceListener = new(this); + Trace.Listeners.Clear(); + Trace.Listeners.Add(traceListener); + TestConfiguration.Initialize(configRoot); // Redirect System.Console output to the test output if requested From 1afef9a70d2d6ca509642778f2864d814cf05a99 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sat, 12 Apr 2025 22:52:35 -0700 Subject: [PATCH 04/57] Checkpoint --- dotnet/Directory.Packages.props | 6 +- .../Orchestration/Step03_GroupChat.cs | 1 - .../Orchestration/Step04_Nested.cs | 2 +- .../Orchestration/Step05_Custom.cs | 6 +- dotnet/src/Agents/Orchestration/AgentActor.cs | 81 ++++++++++++----- .../AgentOrchestration.RequestActor.cs | 25 +++--- .../AgentOrchestration.ResultActor.cs | 33 +++---- .../Orchestration/AgentOrchestration.cs | 70 +++++++-------- .../Orchestration/Broadcast/BroadcastActor.cs | 5 +- .../Broadcast/BroadcastMessages.cs | 4 +- .../Broadcast/BroadcastOrchestration.cs | 16 ++-- .../Broadcast/BroadcastResultActor.cs | 8 +- .../Extensions/RuntimeExtensions.cs | 40 +++++---- .../Orchestration/HandOff/HandoffActor.cs | 5 +- .../Orchestration/HandOff/HandoffMessage.cs | 2 +- .../HandoffOrchestration.ChatMessage.cs | 25 ------ .../HandOff/HandoffOrchestration.cs | 24 +++-- .../Agents/Orchestration/Orchestratable.cs | 11 +-- .../Orchestration/OrchestrationResult.cs | 21 +++-- .../Orchestration/OrchestrationTarget.cs | 87 ++++++++++++++----- .../src/Agents/Orchestration/PatternActor.cs | 41 +++++++++ .../AgentUtilities/BaseOrchestrationTest.cs | 2 +- 22 files changed, 313 insertions(+), 202 deletions(-) delete mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs create mode 100644 dotnet/src/Agents/Orchestration/PatternActor.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index cf45697db515..cbef79520a86 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -34,9 +34,9 @@ - - - + + + diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index e00556d5c089..589b57df9bc0 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -18,7 +18,6 @@ // public async Task UseGroupChatPatternAsync() // { // // Define the agents -// // %%% STRUCTURED OUTPUT ??? // ChatCompletionAgent agent1 = // new() // { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs index ca16d347ba70..ae934fbabea8 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -30,7 +30,7 @@ public async Task NestHandoffBroadcastAsync() new(runtime, agent3, agent4) { InputTransform = (HandoffMessage input) => new BroadcastMessages.Task { Message = input.Content }, - ResultTransform = (BroadcastMessages.Result[] output) => new HandoffMessage { Content = new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))) } // %%% FORMAT / CODE SMELL + ResultTransform = (BroadcastMessages.Result[] output) => HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content)))) }; HandoffOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs index 510aec0a112e..14e922917953 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs @@ -1,14 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel.Agents.Orchestration; + namespace GettingStarted.Orchestration; /// -/// %%% COMMENT +/// Demonstrates how to build a custom . /// public class Step05_Custom(ITestOutputHelper output) : BaseAgentsTest(output) { [Fact] - public Task UseCustomPatternAsync() + public Task UseCustomPatternAsync() // %%% TODO { return Task.CompletedTask; } diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index 263180a720f7..6d18f956c921 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AgentRuntime; -using Microsoft.AgentRuntime.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.ChatCompletion; @@ -14,51 +14,77 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// An actor that is represents an . +/// An actor that represents an . /// -public abstract class AgentActor : BaseAgent +public abstract class AgentActor : PatternActor { - internal const string DefaultDescription = "A helpful agent"; // %%% TODO - CONSIDER /// /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// An . - protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent) - : base(id, runtime, agent.Description ?? DefaultDescription, GetLogger(agent)) + /// Option to automatically clean-up agent thread + protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThread = false) + : base( + id, + runtime, + VerifyDescripion(agent), + GetLogger(agent)) { this.Agent = agent; + this.NoThread = noThread; } /// - /// The associated agent + /// Gets the associated agent. /// protected Agent Agent { get; } /// - /// %%% COMMENT + /// Gets a value indicating whether the agent thread should be removed after use. + /// + protected bool NoThread { get; } + + /// + /// Gets or sets the current conversation thread used during agent communication. /// protected AgentThread? Thread { get; set; } /// - /// %%% COMMENT + /// Deletes the agent thread. /// - /// /// /// + protected async ValueTask DeleteThreadAsync(CancellationToken cancellationToken) + { + if (this.Thread != null) + { + await this.Thread.DeleteAsync(cancellationToken).ConfigureAwait(false); + this.Thread = null; + } + } + + /// + /// Invokes the agent with a single chat message. + /// This method sets the message role to and delegates to the overload accepting multiple messages. + /// + /// The chat message content to send. + /// A cancellation token that can be used to cancel the operation. + /// A task that returns the response . protected ValueTask InvokeAsync(ChatMessageContent input, CancellationToken cancellationToken) { input.Role = AuthorRole.User; // %%% HACK - return this.InvokeAsync([input], cancellationToken); + return this.InvokeAsync(new[] { input }, cancellationToken); } /// - /// %%% COMMENT + /// Invokes the agent with multiple chat messages. + /// Processes the response items and consolidates the messages into a single . /// - /// - /// - /// + /// The list of chat messages to send. + /// A cancellation token that can be used to cancel the operation. + /// A task that returns the response . protected async ValueTask InvokeAsync(IList input, CancellationToken cancellationToken) { AgentResponseItem[] responses = @@ -78,22 +104,35 @@ await this.Agent.InvokeAsync( } /// - /// %%% COMMENT + /// Invokes the agent and streams chat message responses asynchronously. + /// Yields each streaming message as it becomes available. /// - /// - /// - /// + /// The chat message content to send. + /// A cancellation token that can be used to cancel the stream. + /// An asynchronous stream of responses. protected async IAsyncEnumerable InvokeStreamingAsync(ChatMessageContent input, [EnumeratorCancellation] CancellationToken cancellationToken) { - var responseStream = this.Agent.InvokeStreamingAsync([input], this.Thread, options: null, cancellationToken); + var responseStream = this.Agent.InvokeStreamingAsync(new[] { input }, this.Thread, options: null, cancellationToken); await foreach (AgentResponseItem response in responseStream.ConfigureAwait(false)) { - this.Thread ??= response.Thread; + if (this.NoThread) + { + // Do not block on thread clean-up + Task task = this.DeleteThreadAsync(cancellationToken).AsTask(); + } + { + this.Thread ??= response.Thread; + } yield return response.Message; } } + private static string VerifyDescripion(Agent agent) + { + return agent.Description ?? throw new ArgumentException($"Missing agent description: {agent.Name ?? agent.Id}", nameof(agent)); + } + private static ILogger GetLogger(Agent agent) { ILoggerFactory loggerFactory = agent.LoggerFactory ?? NullLoggerFactory.Instance; diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index 0a09243627c2..a6d3f701e716 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -8,23 +8,23 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; -/// -/// An actor that represents the orchestration. -/// public abstract partial class AgentOrchestration { - private sealed class RequestActor : BaseAgent, IHandle + /// + /// Actor responsible for receiving final message and transforming it into the output type. + /// + private sealed class RequestActor : PatternActor, IHandle { private readonly Func _transform; private readonly Func _action; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. - /// // %%% COMMENT - /// // %%% COMMENT + /// A function that transforms an input of type TInput into a source type TSource. + /// An asynchronous function that processes the resulting source. public RequestActor( AgentId id, IAgentRuntime runtime, @@ -37,11 +37,11 @@ public RequestActor( } /// - /// %%% COMMENT + /// Handles the incoming message by transforming the input and executing the corresponding action asynchronously. /// - /// - /// - /// + /// The input message of type TInput. + /// The context of the message, providing additional details. + /// A ValueTask representing the asynchronous operation. public async ValueTask HandleAsync(TInput item, MessageContext messageContext) { Trace.WriteLine($"> ORCHESTRATION ENTER: {this.Id.Type}"); @@ -53,7 +53,8 @@ public async ValueTask HandleAsync(TInput item, MessageContext messageContext) catch (Exception exception) { Trace.WriteLine($"ERROR: {exception.Message}"); - throw; // %%% EXCEPTION + // Log exception details and allow orchestration to fail + throw; } } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index 6d7935d78b5d..b23bdf621e31 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -8,23 +8,23 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; -/// -/// An actor that represents the orchestration. -/// public abstract partial class AgentOrchestration { - private sealed class ResultActor : BaseAgent, IHandle + /// + /// Actor responsible for receiving the resultant message, transforming it, and handling further orchestration. + /// + private sealed class ResultActor : PatternActor, IHandle { private readonly TaskCompletionSource? _completionSource; private readonly Func _transform; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. - /// // %%% COMMENT - /// Signals completion. + /// A delegate that transforms a TResult instance into a TOutput instance. + /// Optional TaskCompletionSource to signal orchestration completion. public ResultActor( AgentId id, IAgentRuntime runtime, @@ -37,16 +37,18 @@ public ResultActor( } /// - /// %%% COMMENT + /// Gets or sets the optional target agent type to which the output message is forwarded. /// public AgentType? CompletionTarget { get; init; } /// - /// %%% COMMENT + /// Processes the received TResult message by transforming it into a TOutput message. + /// If a CompletionTarget is defined, it sends the transformed message to the corresponding agent. + /// Additionally, it signals completion via the provided TaskCompletionSource if available. /// - /// - /// - /// + /// The result item to process. + /// The context associated with the message. + /// A ValueTask representing asynchronous operation. public async ValueTask HandleAsync(TResult item, MessageContext messageContext) { Trace.WriteLine($"> ORCHESTRATION EXIT: {this.Id.Type}"); @@ -55,9 +57,9 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) { TOutput output = this._transform.Invoke(item); - if (this.CompletionTarget != null) + if (this.CompletionTarget.HasValue) { - await this.SendMessageAsync(output!, new AgentId(this.CompletionTarget, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID && NULL OVERRIDE + await this.SendMessageAsync(output!, this.CompletionTarget.Value, messageContext.CancellationToken).ConfigureAwait(false); } this._completionSource?.SetResult(output); @@ -65,7 +67,8 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) catch (Exception exception) { Trace.WriteLine($"ERROR: {exception.Message}"); - throw; // %%% EXCEPTION + // Log exception details and fail orchestration as per design. + throw; } } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 5c5e8dc34904..67ed5aa7b01b 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -7,11 +7,12 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// Base class for multi-agent orchestration patterns. +/// Base class for multi-agent agent orchestration patterns. /// public abstract partial class AgentOrchestration : Orchestratable { @@ -21,7 +22,7 @@ public abstract partial class AgentOrchestration class. /// /// The runtime associated with the orchestration. - /// // %%% COMMENT + /// Specifies the member agents or orchestrations participating in this orchestration. protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) { Verify.NotNull(runtime, nameof(runtime)); @@ -32,27 +33,27 @@ protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] } /// - /// %%% COMMENT + /// Gets the name of the orchestration. /// public string Name { get; init; } = string.Empty; /// - /// %%% COMMENT + /// Gets the description of the orchestration. /// public string Description { get; init; } = string.Empty; /// - /// %%% COMMENT + /// Transforms the orchestration input into a source input suitable for processing. /// public Func? InputTransform { get; init; } // %%% TODO: ASYNC /// - /// %%% COMMENT + /// Transforms the processed result into the final output form. /// public Func? ResultTransform { get; init; } // %%% TODO: ASYNC /// - /// %%% COMMENT + /// Gets the list of member targets involved in the orchestration. /// protected IReadOnlyList Members { get; } @@ -62,10 +63,10 @@ protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] protected IAgentRuntime Runtime { get; } /// - /// Initiate processing of the orchestration. + /// Initiates processing of the orchestration. /// - /// The input message - /// // %%% COMMENT + /// The input message. + /// Optional timeout for the orchestration process. public async ValueTask> InvokeAsync(TInput input, TimeSpan? timeout = null) { Verify.NotNull(input, nameof(input)); @@ -80,8 +81,7 @@ public async ValueTask> InvokeAsync(TInput input, T Trace.WriteLine($"\n!!! ORCHESTRATION INVOKE: {orchestrationType}\n"); - //await this.Runtime.SendMessageAsync(input, new AgentId(orchestrationType, AgentId.DefaultKey)).ConfigureAwait(false); - Task task = this.Runtime.SendMessageAsync(input, new AgentId(orchestrationType, AgentId.DefaultKey)).AsTask(); // %%% TODO: REFINE + Task task = this.Runtime.SendMessageAsync(input, orchestrationType).AsTask(); // %%% TODO: REFINE Trace.WriteLine($"\n!!! ORCHESTRATION YIELD: {orchestrationType}"); @@ -89,35 +89,35 @@ public async ValueTask> InvokeAsync(TInput input, T } /// - /// %%% COMMENT + /// Formats and returns a unique AgentType based on the provided topic and suffix. /// - /// - /// - /// + /// The topic identifier used in formatting the agent type. + /// A suffix to differentiate the agent type. + /// A formatted AgentType object. protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{this._orchestrationType}_{suffix}"); /// - /// Initiate processing according to the orchestration pattern. + /// Initiates processing according to the orchestration pattern. /// - /// // %%% COMMENT - /// The input message - /// // %%% COMMENT + /// The unique identifier for the orchestration session. + /// The input message to be transformed and processed. + /// The initial agent type used for starting the orchestration. protected abstract ValueTask StartAsync(TopicId topic, TSource input, AgentType? entryAgent); /// - /// %%% COMMENT + /// Registers additional orchestration members and returns the entry agent if available. /// - /// - /// - /// + /// The topic identifier for the orchestration session. + /// The orchestration type used in registration. + /// The entry AgentType for the orchestration, if any. protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType); /// - /// %%% COMMENT + /// Registers the orchestration with the runtime using an external topic and an optional target actor. /// - /// - /// - /// + /// The external topic identifier to register with. + /// An optional target actor that may influence registration behavior. + /// A ValueTask containing the AgentType that indicates the registered agent. protected internal override ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor) { TopicId orchestrationTopic = new($"{externalTopic.Type}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); @@ -126,8 +126,10 @@ protected internal override ValueTask RegisterAsync(TopicId externalT } /// - /// %%% COMMENT + /// Subscribes the specified agent type to the provided topics. /// + /// The agent type to subscribe. + /// A variable list of topics for subscription. protected async Task SubscribeAsync(string agentType, params TopicId[] topics) { for (int index = 0; index < topics.Length; ++index) @@ -137,12 +139,12 @@ protected async Task SubscribeAsync(string agentType, params TopicId[] topics) } /// - /// %%% COMMENT + /// Registers the orchestration's root and boot agents, setting up completion and target routing. /// - /// - /// - /// - /// + /// The unique topic for the orchestration session. + /// A TaskCompletionSource for the final result output, if applicable. + /// An optional target actor for routing results. + /// The AgentType representing the orchestration entry point. private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor = null) { // Register actor for final result diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs index a2680b70b79a..9d12fae2e61e 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs @@ -22,7 +22,7 @@ internal sealed class BroadcastActor : AgentActor, IHandleAn . /// Identifies the orchestration agent. public BroadcastActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType) : - base(id, runtime, agent) + base(id, runtime, agent, noThread: true) { this._orchestrationType = orchestrationType; } @@ -36,7 +36,6 @@ public async ValueTask HandleAsync(BroadcastMessages.Task item, MessageContext m Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} OUTPUT - {response}"); - await this.SendMessageAsync(response.ToBroadcastResult(), new AgentId(this._orchestrationType, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID - //await this.Thread?.DeleteAsync().ConfigureAwait(false); // %%% OPTIONAL ??? + await this.SendMessageAsync(response.ToBroadcastResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs index 856812e69545..3d7d214cff0d 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs @@ -15,7 +15,7 @@ public static class BroadcastMessages public sealed class Task { /// - /// %%% COMMENT + /// The input message. /// public ChatMessageContent Message { get; init; } = new(); } @@ -26,7 +26,7 @@ public sealed class Task public sealed class Result { /// - /// %%% COMMENT + /// The result message. /// public ChatMessageContent Message { get; init; } = new(); } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs index 6e3c1e5f0361..c6961462ca98 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Buffers; using System.Diagnostics; using System.Reflection; using System.Threading.Tasks; @@ -51,16 +52,13 @@ await this.Runtime.RegisterAgentFactoryAsync( AgentType memberType; - switch (member.TargetType) + if (member.IsAgent(out Agent? agent)) { - case OrchestrationTargetType.Agent: - memberType = await RegisterAgentAsync(member.Agent!).ConfigureAwait(false); - break; - case OrchestrationTargetType.Orchestratable: - memberType = await member.Orchestration!.RegisterAsync(topic, resultType).ConfigureAwait(false); // %%% NULL OVERIDE - break; - default: - throw new InvalidOperationException($"Unsupported target type: {member.TargetType}"); // %%% EXCEPTION TYPE + memberType = await RegisterAgentAsync(agent).ConfigureAwait(false); + } + else if (member.IsOrchestration(out Orchestratable? orchestration)) + { + memberType = await orchestration.RegisterAsync(topic, resultType).ConfigureAwait(false); } Trace.WriteLine($"> BROADCAST MEMBER #{agentCount}: {memberType}"); diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs index 5898257c2a64..0137463dee45 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs @@ -10,9 +10,9 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; /// -/// %%% COMMENT +/// Actor for capturing each message. /// -internal sealed class BroadcastResultActor : BaseAgent, +internal sealed class BroadcastResultActor : PatternActor, IHandle { private readonly ConcurrentQueue _results; @@ -42,13 +42,13 @@ public BroadcastResultActor( /// public async ValueTask HandleAsync(BroadcastMessages.Result item, MessageContext messageContext) { - Trace.WriteLine($"> BROADCAST RESULT: {this.Id.Type} (#{this._resultCount + 1})"); + Trace.WriteLine($"> BROADCAST RESULT: {this.Id.Type} (#{this._resultCount + 1}/{this._expectedCount})"); this._results.Enqueue(item); if (Interlocked.Increment(ref this._resultCount) == this._expectedCount) { - await this.SendMessageAsync(this._results.ToArray(), new AgentId(this._orchestrationType, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID + await this.SendMessageAsync(this._results.ToArray(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); } } } diff --git a/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs b/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs index 70f039d23cf0..9b5bfeeeda40 100644 --- a/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs +++ b/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs @@ -1,21 +1,25 @@ -//// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -//using System.Threading.Tasks; -//using Microsoft.AgentRuntime; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; -//namespace Microsoft.SemanticKernel.Agents.Orchestration.Extensions; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Extensions; -///// -///// Extension methods for . -///// -//internal static class RuntimeExtensions -//{ -// /// -// /// Sends a message to the specified agent. -// /// -// public static async ValueTask SendMessageAsync(this IAgentRuntime runtime, object message, AgentType agentType) -// { -// AgentId agentId = await runtime.GetAgentAsync(agentType).ConfigureAwait(false); -// await runtime.SendMessageAsync(message, agentId).ConfigureAwait(false); -// } -//} +/// +/// Extension methods for . +/// +internal static class RuntimeExtensions +{ + /// + /// Sends a message to the specified agent. + /// + public static async ValueTask SendMessageAsync(this IAgentRuntime runtime, object message, AgentType agentType, CancellationToken cancellationToken = default) + { + AgentId? agentId = await runtime.GetAgentAsync(agentType, lazy: false).ConfigureAwait(false); + if (agentId.HasValue) + { + await runtime.SendMessageAsync(message, agentId.Value, sender: null, messageId: null, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs index bf2ef7b5301d..9e54c75a7bcf 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs @@ -22,7 +22,7 @@ internal sealed class HandoffActor : AgentActor, IHandle /// An . /// The indentifier of the next agent for which to handoff the result public HandoffActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) - : base(id, runtime, agent) + : base(id, runtime, agent, noThread: true) { this._nextAgent = nextAgent; } @@ -36,7 +36,6 @@ public async ValueTask HandleAsync(HandoffMessage item, MessageContext messageCo Trace.WriteLine($"> HANDOFF ACTOR: {this.Id.Type} OUTPUT - {response}"); - await this.SendMessageAsync(HandoffMessage.FromChat(response), new AgentId(this._nextAgent, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID - //await response.Thread.DeleteAsync().ConfigureAwait(false); + await this.SendMessageAsync(HandoffMessage.FromChat(response), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs index 4993f1e7af1e..db5a176ecd61 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs @@ -5,7 +5,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// /// A message that describes the input task and captures results for a . /// -public sealed class HandoffMessage // %%% SIMPLIFY +public sealed class HandoffMessage { /// /// The input task. diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs deleted file mode 100644 index 67739d486b3a..000000000000 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.ChatMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.AgentRuntime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; - -/// -/// An orchestration that broadcasts the input message to each agent. -/// -public sealed partial class HandoffOrchestration -{ - /// - /// Initializes a new instance of the class. - /// - /// The runtime associated with the orchestration. - /// The agents to be orchestrated. - public static HandoffOrchestration ForMessage(IAgentRuntime runtime, params OrchestrationTarget[] members) // %%% CONSIDER - { - return new HandoffOrchestration(runtime, members) - { - InputTransform = HandoffMessage.FromChat, - ResultTransform = (HandoffMessage result) => result.Content, - }; - } -} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs index 66bad76bb53e..d6c72252c2c8 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; @@ -28,7 +28,7 @@ protected override async ValueTask StartAsync(TopicId topic, HandoffMessage inpu { Trace.WriteLine($"> HANDOFF START: {topic} [{entryAgent}]"); - await this.Runtime.SendMessageAsync(input, new AgentId(entryAgent!, AgentId.DefaultKey)).ConfigureAwait(false); // %%% AGENTID & NULL OVERRIDE + await this.Runtime.SendMessageAsync(input, entryAgent!.Value).ConfigureAwait(false); // NULL OVERRIDE } /// @@ -40,28 +40,26 @@ protected override async ValueTask StartAsync(TopicId topic, HandoffMessage inpu { Trace.WriteLine($"> HANDOFF NEXT #{index}: {nextAgent}"); OrchestrationTarget member = this.Members[index]; - switch (member.TargetType) + + if (member.IsAgent(out Agent? agent)) + { + nextAgent = await RegisterAgentAsync(topic, nextAgent, index, agent).ConfigureAwait(false); + } + else if (member.IsOrchestration(out Orchestratable? orchestration)) { - case OrchestrationTargetType.Agent: - nextAgent = await RegisterAgentAsync(topic, nextAgent, index, member).ConfigureAwait(false); - break; - case OrchestrationTargetType.Orchestratable: - nextAgent = await member.Orchestration!.RegisterAsync(topic, nextAgent).ConfigureAwait(false); // %%% NULL OVERIDE - break; - default: - throw new InvalidOperationException($"Unsupported target type: {member.TargetType}"); // %%% EXCEPTION TYPE + nextAgent = await orchestration.RegisterAsync(topic, nextAgent).ConfigureAwait(false); } Trace.WriteLine($"> HANDOFF MEMBER #{index}: {nextAgent}"); } return nextAgent; - async Task RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, OrchestrationTarget member) + async Task RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, Agent agent) { AgentType agentType = this.GetAgentType(topic, index); return await this.Runtime.RegisterAgentFactoryAsync( agentType, - (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, member.Agent!, nextAgent))).ConfigureAwait(false); // %%% NULL OVERRIDE + (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, agent, nextAgent))).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Orchestratable.cs b/dotnet/src/Agents/Orchestration/Orchestratable.cs index 5fe5729b12fd..7600e8397e62 100644 --- a/dotnet/src/Agents/Orchestration/Orchestratable.cs +++ b/dotnet/src/Agents/Orchestration/Orchestratable.cs @@ -6,15 +6,16 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// %%% COMMENT +/// Common protocol for so it +/// can be utlized by an another orchestration. /// public abstract class Orchestratable { /// - /// %%% COMMENT + /// Registers the orchestratable component with the external system using a specified topic and an optional target actor. /// - /// - /// - /// + /// The topic identifier to be used for registration. + /// An optional target actor type, if applicable, that may influence registration behavior. + /// A ValueTask containing the AgentType that indicates the registered agent. protected internal abstract ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor); } diff --git a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs index 862ecda1248c..25920b5109b3 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs @@ -8,9 +8,10 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// %%% COMMENT +/// Represents the result of an orchestration operation that yields a value of type . +/// This class encapsulates the asynchronous completion of an orchestration process. /// -/// +/// The type of the value produced by the orchestration. public sealed class OrchestrationResult { private readonly TaskCompletionSource _completion; @@ -22,24 +23,28 @@ internal OrchestrationResult(TopicId topic, TaskCompletionSource complet } /// - /// %%% COMMENT + /// Gets the topic identifier associated with this orchestration result. /// public TopicId Topic { get; } /// - /// %%% COMMENT + /// Asynchronously retrieves the orchestration result value. + /// If a timeout is specified, the method will throw a + /// if the orchestration does not complete within the allotted time. /// - /// - public async ValueTask GetValueAsync(TimeSpan? timeout = null) // %%% TODO: TryGetValueAsync ??? + /// An optional representing the maximum wait duration. + /// A representing the result of the orchestration. + /// Thrown if the orchestration does not complete within the specified timeout period. + public async ValueTask GetValueAsync(TimeSpan? timeout = null) { Trace.WriteLine($"\n!!! ORCHESTRATION AWAIT: {this.Topic}\n"); if (timeout.HasValue) { - Task[] tasks = [this._completion.Task]; + Task[] tasks = { this._completion.Task }; if (!Task.WaitAll(tasks, timeout.Value)) { - throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); // %%% EXCEPTION TYPE + throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); } } diff --git a/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs b/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs index 55ea358f76ed..f9b549944c81 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs @@ -1,46 +1,57 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// %%% COMMENT +/// Represents a target for orchestration operations. This target can be either an Agent or an Orchestratable object. /// public enum OrchestrationTargetType { /// - /// %%% COMMENT + /// Target is an . /// Agent, /// - /// %%% COMMENT + /// Target is an object. /// Orchestratable, } /// -/// %%% COMMENT +/// Encapsulates the target entity for orchestration, which may be an Agent or an Orchestratable object. /// public readonly struct OrchestrationTarget : IEquatable { /// - /// %%% COMMENT + /// Creates an orchestration target from the specified . /// + /// The agent to convert to an orchestration target. public static implicit operator OrchestrationTarget(Agent target) => new(target); /// - /// %%% COMMENT + /// Creates an orchestration target from the specified object. /// + /// The orchestratable object to convert to an orchestration target. public static implicit operator OrchestrationTarget(Orchestratable target) => new(target); + /// + /// Initializes a new instance of the struct with an . + /// + /// A target agent. internal OrchestrationTarget(Agent agent) { this.Agent = agent; this.TargetType = OrchestrationTargetType.Agent; } + /// + /// Initializes a new instance of the struct with an object. + /// + /// A target orchestratable object. internal OrchestrationTarget(Orchestratable orchestration) { this.Orchestration = orchestration; @@ -48,31 +59,65 @@ internal OrchestrationTarget(Orchestratable orchestration) } /// - /// %%% COMMENT + /// Gets the associated if this target represents an agent; otherwise, null. /// public Agent? Agent { get; } /// - /// %%% COMMENT + /// Gets the associated object if this target represents an orchestratable entity; otherwise, null. /// public Orchestratable? Orchestration { get; } /// - /// %%% COMMENT + /// Gets the type of the orchestration target, indicating whether it is an agent or an orchestratable object. /// public OrchestrationTargetType TargetType { get; } + /// + /// Determines whether the target is an and retrieves it if available. + /// + /// The agent reference + /// True if agent + public bool IsAgent([NotNullWhen(true)] out Agent? orchestration) + { + if (this.TargetType == OrchestrationTargetType.Agent) + { + orchestration = this.Agent!; + return true; + } + + orchestration = null; + return false; + } + + /// + /// Determines whether the target is an and retrieves it if available. + /// + /// The orchestration reference + /// True if orchestration + public bool IsOrchestration([NotNullWhen(true)] out Orchestratable? orchestration) + { + if (this.TargetType == OrchestrationTargetType.Orchestratable) + { + orchestration = this.Orchestration!; + return true; + } + + orchestration = null; + return false; + } + /// public override readonly bool Equals(object? obj) { - return obj != null && this.Equals(obj is OrchestrationTarget); + return obj != null && obj is OrchestrationTarget target && this.Equals(target); } /// - /// %%% COMMENT + /// Determines whether the specified is equal to the current instance. /// - /// - /// + /// The other orchestration target to compare. + /// true if the targets are equal; otherwise, false. public readonly bool Equals(OrchestrationTarget other) { return this.Agent == other.Agent && this.Orchestration == other.Orchestration; @@ -85,22 +130,22 @@ public override readonly int GetHashCode() } /// - /// %%% COMMENT + /// Determines whether two instances are equal. /// - /// - /// - /// + /// The first orchestration target. + /// The second orchestration target. + /// true if the targets are equal; otherwise, false. public static bool operator ==(OrchestrationTarget left, OrchestrationTarget right) { return left.Equals(right); } /// - /// %%% COMMENT + /// Determines whether two instances are not equal. /// - /// - /// - /// + /// The first orchestration target. + /// The second orchestration target. + /// true if the targets are not equal; otherwise, false. public static bool operator !=(OrchestrationTarget left, OrchestrationTarget right) { return !(left == right); diff --git a/dotnet/src/Agents/Orchestration/PatternActor.cs b/dotnet/src/Agents/Orchestration/PatternActor.cs new file mode 100644 index 000000000000..3ddf0037e5ed --- /dev/null +++ b/dotnet/src/Agents/Orchestration/PatternActor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// An actor that represents an . +/// +public abstract class PatternActor : BaseAgent +{ + /// + /// Initializes a new instance of the class. + /// + protected PatternActor(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) + : base(id, runtime, description, logger) + { + } + + /// + /// Sends a message to a specified recipient agent-type through the runtime. + /// + /// The message object to send. + /// The recipient agent's type. + /// A token used to cancel the operation if needed. + protected async ValueTask SendMessageAsync( + object message, + AgentType agentType, + CancellationToken cancellationToken) + { + AgentId? agentId = await this.GetAgentAsync(agentType, cancellationToken).ConfigureAwait(false); + if (agentId.HasValue) + { + await this.SendMessageAsync(message, agentId.Value, messageId: null, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs index 353c5b18de87..114ee31c22b3 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs @@ -17,7 +17,7 @@ protected ChatCompletionAgent CreateAgent(string instructions, string? name = nu { Instructions = instructions, Name = name, - Description = description, + Description = "test agent", Kernel = this.CreateKernelWithChatCompletion(), }; } From c9b3a6f122fac215dad62db7a1fe5b601a2a79de Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 13 Apr 2025 00:59:11 -0700 Subject: [PATCH 05/57] Group Chat --- .../GroupChat/GroupChatOrchestration.cs | 152 ++++++++++-------- 1 file changed, 83 insertions(+), 69 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 003b503f1d01..34df89f3b433 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -1,69 +1,83 @@ -//// Copyright (c) Microsoft. All rights reserved. - -//using System.Threading.Tasks; -//using Microsoft.AgentRuntime; -//using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; - -//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; - -///// -///// An orchestration that coordinates a multi-agent conversation. -///// -//public sealed class GroupChatOrchestration : AgentOrchestration -//{ -// private readonly TaskCompletionSource _completionSource; -// private readonly Agent[] _agents; - -// /// -// /// Initializes a new instance of the class. -// /// -// /// The runtime associated with the orchestration. -// /// The agents participating in the orchestration. -// public GroupChatOrchestration(IAgentRuntime runtime, params Agent[] agents) -// : base(runtime) -// { -// Verify.NotNullOrEmpty(agents, nameof(agents)); - -// this._completionSource = new TaskCompletionSource(); -// this._agents = agents; -// } - -// /// -// /// %%% COMMENT -// /// -// public Task Future => this._completionSource.Task; - -// /// -// protected override async ValueTask MessageTaskAsync(ChatMessageContent message) -// { -// AgentType managerType = new($"{nameof(GroupChatManager)}_{this.Id}"); // %%% COMMON -// await this.Runtime.SendMessageAsync(message.ToTask(), managerType).ConfigureAwait(false); -// } - -// /// -// protected override async ValueTask PrepareAsync() -// { -// AgentType managerType = new($"{nameof(GroupChatManager)}_{this.Id}"); // %%% COMMON -// TopicId chatTopic = new($"GroupChatTopic_{this.Id}"); // %%% OTHER TOPICS: RESET ??? - -// ChatTeam team = []; -// foreach (Agent agent in this._agents) -// { -// AgentType agentType = agent.GetAgentType(this); -// await this.Runtime.RegisterAgentFactoryAsync( -// agentType, -// (agentId, runtime) => ValueTask.FromResult(new GroupChatActor(agentId, runtime, agent, chatTopic))).ConfigureAwait(false); -// TopicId agentTopic = new($"AgentTopic_{agent.Id}_{this.Id}".Replace("-", "_")); // %%% EXTENSION ??? -// team[agent.Name ?? agent.Id] = (agentTopic, agent.Description); - -// await this.RegisterTopicsAsync(agentType, chatTopic).ConfigureAwait(false); -// await this.RegisterTopicsAsync(agentType, agentTopic).ConfigureAwait(false); -// } - -// await this.Runtime.RegisterAgentFactoryAsync( -// managerType, -// (agentId, runtime) => ValueTask.FromResult(new GroupChatManager(agentId, runtime, team, this._completionSource))).ConfigureAwait(false); - -// await this.RegisterTopicsAsync(managerType, chatTopic).ConfigureAwait(false); -// } -//} +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// An orchestration that coordinates a group-chat. +/// +public class GroupChatOrchestration : + AgentOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents participating in the orchestration. + public GroupChatOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + : base(runtime, agents) + { + } + + /// + protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask input, AgentType? entryAgent) + { + Trace.WriteLine($"> GROUPCHAT START: {topic} [{entryAgent}]"); + + return this.Runtime.SendMessageAsync(input, entryAgent!.Value); + } + + /// + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType) + { + AgentType managerType = this.FormatAgentType(topic, "Manager"); + + int agentCount = 0; + ChatTeam team = []; + foreach (OrchestrationTarget member in this.Members) + { + AgentType memberType = default; + + if (member.IsAgent(out Agent? agent)) + { + memberType = await RegisterAgentAsync(agent).ConfigureAwait(false); + } + else if (member.IsOrchestration(out Orchestratable? orchestration)) + { + memberType = await orchestration.RegisterAsync(topic, managerType).ConfigureAwait(false); + } + + Trace.WriteLine($"> GROUPCHAT MEMBER #{agentCount}: {memberType}"); + + await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); + } + + await this.Runtime.RegisterAgentFactoryAsync( + managerType, + (agentId, runtime) => + ValueTask.FromResult( + new GroupChatManager(agentId, runtime, team))).ConfigureAwait(false); + + await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); + + return null; + + async ValueTask RegisterAgentAsync(Agent agent) + { + AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); + await this.Runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => + ValueTask.FromResult(new GroupChatActor(agentId, runtime, agent, topic))).ConfigureAwait(false); + + await this.SubscribeAsync(agentType, topic).ConfigureAwait(false); + //await this.RegisterTopicsAsync(agentType, agentTopic).ConfigureAwait(false); // %%% CRITICAL + + return agentType; + } + } +} From 33c4e41ab8143779725f94f5762f528dd2db02c0 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 13 Apr 2025 01:01:20 -0700 Subject: [PATCH 06/57] Group Chat --- .../Orchestration/Step01_Broadcast.cs | 4 +- .../Orchestration/Step02_Handoff.cs | 4 +- .../Orchestration/Step03_GroupChat.cs | 99 ++++---- .../Orchestration/Step04_Nested.cs | 8 +- dotnet/src/Agents/Orchestration/AgentActor.cs | 4 +- .../AgentOrchestration.RequestActor.cs | 6 +- .../AgentOrchestration.ResultActor.cs | 6 +- .../Orchestration/AgentOrchestration.cs | 20 +- .../Orchestration/Agents.Orchestration.csproj | 1 - .../BroadcastOrchestration.String.cs | 5 +- .../Broadcast/BroadcastOrchestration.cs | 5 +- .../GroupChat/{ChatTeam.cs => ChatGroup.cs} | 12 +- .../Orchestration/GroupChat/ChatManager.cs | 222 +++++++++--------- .../Orchestration/GroupChat/ChatMessages.cs | 132 +++++------ .../Orchestration/GroupChat/GroupChatActor.cs | 123 +++++----- .../GroupChat/GroupChatManager.cs | 89 ++++--- .../GroupChatOrchestration.String.cs | 25 ++ .../GroupChat/GroupChatOrchestration.cs | 12 +- .../HandOff/HandoffOrchestration.String.cs | 5 +- .../src/Agents/Orchestration/PatternActor.cs | 2 +- 20 files changed, 395 insertions(+), 389 deletions(-) rename dotnet/src/Agents/Orchestration/GroupChat/{ChatTeam.cs => ChatGroup.cs} (59%) create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs index 5ec91cc2c03f..54c33500695c 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs @@ -113,8 +113,8 @@ public async Task SingleNestedActorAsync() { return new(runtime, targets) { - InputTransform = (BroadcastMessages.Task input) => input, - ResultTransform = (BroadcastMessages.Result[] results) => string.Join("\n", results.Select(result => $"{result.Message}")).ToBroadcastResult(), + InputTransform = (BroadcastMessages.Task input) => ValueTask.FromResult(input), + ResultTransform = (BroadcastMessages.Result[] results) => ValueTask.FromResult(string.Join("\n", results.Select(result => $"{result.Message}")).ToBroadcastResult()), }; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs index afefcf814429..04242007f42d 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs @@ -112,8 +112,8 @@ private static HandoffOrchestration CreateNested { return new(runtime, targets) { - InputTransform = (HandoffMessage input) => input, - ResultTransform = (HandoffMessage results) => results, + InputTransform = (HandoffMessage input) => ValueTask.FromResult(input), + ResultTransform = (HandoffMessage results) => ValueTask.FromResult(results), }; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 589b57df9bc0..04e41c40b846 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -1,59 +1,40 @@ -//// Copyright (c) Microsoft. All rights reserved. - -//using Microsoft.AgentRuntime.InProcess; -//using Microsoft.SemanticKernel; -//using Microsoft.SemanticKernel.Agents; -//using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -//using Microsoft.SemanticKernel.ChatCompletion; -//using static Microsoft.SemanticKernel.Agents.Orchestration.GroupChat.ChatMessages; - -//namespace GettingStarted.Orchestration; - -///// -///// Demonstrates how to use the . -///// -//public class Step03_GroupChat(ITestOutputHelper output) : BaseAgentsTest(output) -//{ -// [Fact] -// public async Task UseGroupChatPatternAsync() -// { -// // Define the agents -// ChatCompletionAgent agent1 = -// new() -// { -// Instructions = "Count the number of words in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nWords: ", -// //Name = name, -// Description = "Agent 1", -// Kernel = this.CreateKernelWithChatCompletion(), -// }; -// ChatCompletionAgent agent2 = -// new() -// { -// Instructions = "Count the number of vowels in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nVowels: ", -// //Name = name, -// Description = "Agent 2", -// Kernel = this.CreateKernelWithChatCompletion(), -// }; -// ChatCompletionAgent agent3 = -// new() -// { -// Instructions = "Count the number of consonants in the most recent user input without repeating the input. ALWAYS report the count as a number using the format:\nConsonants: ", -// //Name = name, -// Description = "Agent 3", -// Kernel = this.CreateKernelWithChatCompletion(), -// }; - -// // Define the pattern -// InProcessRuntime runtime = new(); -// GroupChatOrchestration orchestration = new(runtime, agent1, agent2, agent3); - -// // Start the runtime -// await runtime.StartAsync(); -// await orchestration.StartAsync(new ChatMessageContent(AuthorRole.User, "The quick brown fox jumps over the lazy dog")); -// ChatMessageContent result = await orchestration.Future; -// Console.WriteLine("RESULT:"); -// this.WriteAgentChatMessage(result); - -// await runtime.RunUntilIdleAsync(); -// } -//} +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use the . +/// +public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task UseGroupChatPatternAsync() + { + // Define the agents + ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); + ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); + ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of onsonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + + // Define the pattern + InProcessRuntime runtime = new(); + GroupChatOrchestration orchestration = new(runtime, agent1, agent2, agent3); + + // Start the runtime + await runtime.StartAsync(); + + string input = "The quick brown fox jumps over the lazy dog"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {text}"); + + await runtime.RunUntilIdleAsync(); + } + + // %%% MORE SAMPLES - GROUPCHAT +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs index ae934fbabea8..f6ca88ee64a5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -29,8 +29,8 @@ public async Task NestHandoffBroadcastAsync() BroadcastOrchestration innerOrchestration = new(runtime, agent3, agent4) { - InputTransform = (HandoffMessage input) => new BroadcastMessages.Task { Message = input.Content }, - ResultTransform = (BroadcastMessages.Result[] output) => HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content)))) + InputTransform = (HandoffMessage input) => ValueTask.FromResult(new BroadcastMessages.Task { Message = input.Content }), + ResultTransform = (BroadcastMessages.Result[] output) => ValueTask.FromResult(HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))))) }; HandoffOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); @@ -59,8 +59,8 @@ public async Task NestBroadcastHandoffAsync() HandoffOrchestration innerOrchestration = new(runtime, agent3, agent4) { - InputTransform = (BroadcastMessages.Task input) => new HandoffMessage { Content = input.Message }, - ResultTransform = (HandoffMessage result) => new BroadcastMessages.Result { Message = result.Content } + InputTransform = (BroadcastMessages.Task input) => ValueTask.FromResult(new HandoffMessage { Content = input.Message }), + ResultTransform = (HandoffMessage result) => ValueTask.FromResult(new BroadcastMessages.Result { Message = result.Content }) }; BroadcastOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index 6d18f956c921..a7282e09d859 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -74,7 +74,6 @@ protected async ValueTask DeleteThreadAsync(CancellationToken cancellationToken) /// A task that returns the response . protected ValueTask InvokeAsync(ChatMessageContent input, CancellationToken cancellationToken) { - input.Role = AuthorRole.User; // %%% HACK return this.InvokeAsync(new[] { input }, cancellationToken); } @@ -97,7 +96,8 @@ await this.Agent.InvokeAsync( AgentResponseItem response = responses[0]; this.Thread ??= response.Thread; - return new ChatMessageContent(response.Message.Role, string.Join("\n\n", responses.Select(response => response.Message))) // %%% HACK + // The vast majority of responses will be a single message. Responses with multiple messages will have their content merged. + return new ChatMessageContent(response.Message.Role, string.Join("\n\n", responses.Select(response => response.Message))) { AuthorName = response.Message.AuthorName, }; diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index a6d3f701e716..15c9084d896c 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -15,7 +15,7 @@ public abstract partial class AgentOrchestration private sealed class RequestActor : PatternActor, IHandle { - private readonly Func _transform; + private readonly Func> _transform; private readonly Func _action; /// @@ -28,7 +28,7 @@ private sealed class RequestActor : PatternActor, IHandle public RequestActor( AgentId id, IAgentRuntime runtime, - Func transform, + Func> transform, Func action) : base(id, runtime, $"{id.Type}_Actor") { @@ -47,7 +47,7 @@ public async ValueTask HandleAsync(TInput item, MessageContext messageContext) Trace.WriteLine($"> ORCHESTRATION ENTER: {this.Id.Type}"); try { - TSource source = this._transform.Invoke(item); + TSource source = await this._transform.Invoke(item).ConfigureAwait(false); await this._action.Invoke(source).ConfigureAwait(false); } catch (Exception exception) diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index b23bdf621e31..c40199792f8c 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -16,7 +16,7 @@ public abstract partial class AgentOrchestration { private readonly TaskCompletionSource? _completionSource; - private readonly Func _transform; + private readonly Func> _transform; /// /// Initializes a new instance of the class. @@ -28,7 +28,7 @@ private sealed class ResultActor : PatternActor, IHandle public ResultActor( AgentId id, IAgentRuntime runtime, - Func transform, + Func> transform, TaskCompletionSource? completionSource = null) : base(id, runtime, $"{id.Type}_Actor") { @@ -55,7 +55,7 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) try { - TOutput output = this._transform.Invoke(item); + TOutput output = await this._transform.Invoke(item).ConfigureAwait(false); if (this.CompletionTarget.HasValue) { diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 67ed5aa7b01b..1f6ce05737db 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -45,12 +45,12 @@ protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] /// /// Transforms the orchestration input into a source input suitable for processing. /// - public Func? InputTransform { get; init; } // %%% TODO: ASYNC + public Func>? InputTransform { get; init; } /// /// Transforms the processed result into the final output form. /// - public Func? ResultTransform { get; init; } // %%% TODO: ASYNC + public Func>? ResultTransform { get; init; } /// /// Gets the list of member targets involved in the orchestration. @@ -81,7 +81,7 @@ public async ValueTask> InvokeAsync(TInput input, T Trace.WriteLine($"\n!!! ORCHESTRATION INVOKE: {orchestrationType}\n"); - Task task = this.Runtime.SendMessageAsync(input, orchestrationType).AsTask(); // %%% TODO: REFINE + Task task = this.Runtime.SendMessageAsync(input, orchestrationType).AsTask(); Trace.WriteLine($"\n!!! ORCHESTRATION YIELD: {orchestrationType}"); @@ -147,13 +147,23 @@ protected async Task SubscribeAsync(string agentType, params TopicId[] topics) /// The AgentType representing the orchestration entry point. private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor = null) { + // %%% REQUIRED + if (this.InputTransform == null) + { + throw new InvalidOperationException("InputTransform must be set before invoking the orchestration."); + } + if (this.ResultTransform == null) + { + throw new InvalidOperationException("ResultTransform must be set before invoking the orchestration."); + } + // Register actor for final result AgentType orchestrationFinal = this.FormatAgentType(topic, "Root"); await this.Runtime.RegisterAgentFactoryAsync( orchestrationFinal, (agentId, runtime) => ValueTask.FromResult( - new ResultActor(agentId, runtime, this.ResultTransform!, completion) // %%% NULL OVERRIDE + new ResultActor(agentId, runtime, this.ResultTransform, completion) { CompletionTarget = targetActor, })).ConfigureAwait(false); @@ -167,7 +177,7 @@ await this.Runtime.RegisterAgentFactoryAsync( orchestrationEntry, (agentId, runtime) => ValueTask.FromResult( - new RequestActor(agentId, runtime, this.InputTransform!, async (TSource source) => await this.StartAsync(topic, source, entryAgent).ConfigureAwait(false))) // %%% NULL OVERRIDE + new RequestActor(agentId, runtime, this.InputTransform, async (TSource source) => await this.StartAsync(topic, source, entryAgent).ConfigureAwait(false))) ).ConfigureAwait(false); return orchestrationEntry; diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index 2b5825ea6428..cd7f8ff4ccf8 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -33,7 +33,6 @@ - diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs index b1f7c7ddb2be..7291d1699f1c 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Linq; +using System.Threading.Tasks; using Microsoft.AgentRuntime; namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; @@ -18,7 +19,7 @@ public sealed class BroadcastOrchestration : BroadcastOrchestration input.ToBroadcastTask(); - this.ResultTransform = (BroadcastMessages.Result[] result) => [.. result.Select(r => r.Message.Content ?? string.Empty)]; + this.InputTransform = (string input) => ValueTask.FromResult(input.ToBroadcastTask()); + this.ResultTransform = (BroadcastMessages.Result[] result) => ValueTask.FromResult([.. result.Select(r => r.Message.Content ?? string.Empty)]); } } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs index c6961462ca98..491700e799f2 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Buffers; using System.Diagnostics; -using System.Reflection; using System.Threading.Tasks; using Microsoft.AgentRuntime; @@ -50,7 +47,7 @@ await this.Runtime.RegisterAgentFactoryAsync( { ++agentCount; - AgentType memberType; + AgentType memberType = default; if (member.IsAgent(out Agent? agent)) { diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatTeam.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs similarity index 59% rename from dotnet/src/Agents/Orchestration/GroupChat/ChatTeam.cs rename to dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs index 5d1089969aed..5e073a5ae1e1 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatTeam.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs @@ -7,26 +7,26 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// -/// %%% COMMENT +/// Descibes a team of agents participating in a group chat. /// -public class ChatTeam : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT +public class ChatGroup : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT /// -/// Extensions for . +/// Extensions for . /// -public static class AgentTeamExtensions +public static class ChatGroupExtensions { /// /// Format the names of the agents in the team as a comma delimimted list. /// /// The agent team /// A comma delimimted list of agent name. - public static string FormatNames(this ChatTeam team) => string.Join(",", team.Select(t => t.Key)); + public static string FormatNames(this ChatGroup team) => string.Join(",", team.Select(t => t.Key)); /// /// Format the names and descriptions of the agents in the team as a markdown list. /// /// The agent team /// A markdown list of agent names and descriptions. - public static string FormatList(this ChatTeam team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); + public static string FormatList(this ChatGroup team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs index 6b16784774ce..85d9aecd3d24 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs @@ -1,124 +1,126 @@ -//// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -//using System.Diagnostics; -//using System.Threading.Tasks; -//using Microsoft.AgentRuntime; -//using Microsoft.SemanticKernel.ChatCompletion; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; +using Microsoft.SemanticKernel.ChatCompletion; -//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -///// -///// A that orchestrates a team of agents. -///// -//public abstract class ChatManager : RuntimeAgent -//{ -// /// -// /// A common description for the orchestrator. -// /// -// public const string Description = "Orchestrates a team of agents to accomplish a defined task."; -// private readonly TaskCompletionSource _completionSource; +/// +/// An used to manage a . +/// +public abstract class ChatManager : + PatternActor, + IHandle, + IHandle, + IHandle +{ + /// + /// A common description for the manager. + /// + public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task."; -// /// -// /// Initializes a new instance of the class. -// /// -// /// The unique identifier of the agent. -// /// The runtime associated with the agent. -// /// The team of agents being orchestrated -// /// Signals completion. -// protected ChatManager(AgentId id, IAgentRuntime runtime, ChatTeam team, TaskCompletionSource completionSource) -// : base(id, runtime, Description) -// { -// this.Chat = []; -// this.Team = team; -// this._completionSource = completionSource; -// Debug.WriteLine($">>> NAMES: {this.Team.FormatNames()}"); -// Debug.WriteLine($">>> TEAM:\n{this.Team.FormatList()}"); + private readonly AgentType _orchestrationType; -// this.RegisterHandler(this.OnTaskMessageAsync); -// this.RegisterHandler(this.OnGroupMessageAsync); -// this.RegisterHandler(this.OnResultMessageAsync); -// } + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// The team of agents being orchestrated + /// Identifies the orchestration agent. + protected ChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType) + : base(id, runtime, DefaultDescription) + { + this.Chat = []; + this.Team = team; + this._orchestrationType = orchestrationType; + Trace.WriteLine($">>> MANAGER NAMES: {this.Team.FormatNames()}"); + Trace.WriteLine($">>> MANAGER TEAM:\n{this.Team.FormatList()}"); + } -// /// -// /// The conversation history with the team. -// /// -// protected ChatHistory Chat { get; } + /// + /// The conversation history with the team. + /// + protected ChatHistory Chat { get; } -// /// -// /// The input task. -// /// -// protected ChatMessages.InputTask Task { get; private set; } = ChatMessages.InputTask.None; // %%% TYPE CONFLICT IN NAME + /// + /// The input task. + /// + protected ChatMessages.InputTask InputTask { get; private set; } = ChatMessages.InputTask.None; -// /// -// /// Metadata that describes team of agents being orchestrated. -// /// -// protected ChatTeam Team { get; } + /// + /// Metadata that describes team of agents being orchestrated. + /// + protected ChatGroup Team { get; } -// /// -// /// Message a specific agent, by topic. -// /// -// protected Task RequestAgentResponseAsync(TopicId agentTopic) -// { -// return this.PublishMessageAsync(new ChatMessages.Speak(), agentTopic); -// } + /// + /// Message a specific agent, by topic. + /// + protected ValueTask RequestAgentResponseAsync(AgentType agentType, CancellationToken cancellationToken) + { + Trace.WriteLine($">>> MANAGER NEXT: {agentType}"); + return this.SendMessageAsync(new ChatMessages.Speak(), agentType, cancellationToken); + } -// /// -// /// Defines one-time logic required to prepare to execute the given task. -// /// -// /// -// /// The agent specific topic for first step in executing the task. -// /// -// /// -// /// Returning a null TopicId indicates that the task will not be executed. -// /// -// protected abstract Task PrepareTaskAsync(); + /// + /// Defines one-time logic required to prepare to execute the given task. + /// + /// + /// The agent specific topic for first step in executing the task. + /// + /// + /// Returning a null TopicId indicates that the task will not be executed. + /// + protected abstract Task PrepareTaskAsync(); -// ///// -// ///// %%% TODO -// ///// -// // %%% TODO protected abstract Task RequestResultAsync(); + /// + /// Determines which agent's must respond. + /// + /// + /// The agent specific topic for first step in executing the task. + /// + /// + /// Returning a null TopicId indicates that the task will not be executed. + /// + protected abstract Task SelectAgentAsync(); -// /// -// /// Determines which agent's must respond. -// /// -// /// -// /// The agent specific topic for first step in executing the task. -// /// -// /// -// /// Returning a null TopicId indicates that the task will not be executed. -// /// -// protected abstract Task SelectAgentAsync(); + /// + public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext messageContext) + { + Trace.WriteLine($">>> MANAGER TASK: {item.Message}"); + this.InputTask = item; + AgentType? agentType = await this.PrepareTaskAsync().ConfigureAwait(false); + if (agentType != null) + { + await this.RequestAgentResponseAsync(agentType.Value, messageContext.CancellationToken).ConfigureAwait(false); + } + } -// private async ValueTask OnTaskMessageAsync(ChatMessages.InputTask message, MessageContext context) -// { -// Debug.WriteLine($">>> TASK: {message.Message}"); -// this.Task = message; -// TopicId? agentTopic = await this.PrepareTaskAsync().ConfigureAwait(false); -// if (agentTopic != null) -// { -// await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); -// } -// } + /// + public async ValueTask HandleAsync(ChatMessages.Group item, MessageContext messageContext) + { + Trace.WriteLine($">>> MANAGER CHAT: {item.Message}"); + this.Chat.Add(item.Message); + AgentType? agentType = await this.SelectAgentAsync().ConfigureAwait(false); + if (agentType != null) + { + await this.RequestAgentResponseAsync(agentType.Value, messageContext.CancellationToken).ConfigureAwait(false); + } + else + { + Trace.WriteLine(">>> MANAGER NO AGENT"); + await this.SendMessageAsync(item.Message.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); // %%% PLACEHOLDER - FINAL MESSAGE + } + } -// private async ValueTask OnGroupMessageAsync(ChatMessages.Group message, MessageContext context) -// { -// Debug.WriteLine($">>> CHAT: {message.Message}"); -// this.Chat.Add(message.Message); -// TopicId? agentTopic = await this.SelectAgentAsync().ConfigureAwait(false); -// if (agentTopic != null) -// { -// await this.RequestAgentResponseAsync(agentTopic.Value).ConfigureAwait(false); -// } -// else -// { -// //await this.RequestResultAsync().ConfigureAwait(false); // %%% TODO - GROUP CHAT -// } -// } - -// private ValueTask OnResultMessageAsync(ChatMessages.Result result, MessageContext context) -// { -// Debug.WriteLine($">>> RESULT: {result.Message}"); -// this._completionSource.SetResult(result.Message); -// return ValueTask.CompletedTask; -// } -//} + /// + public ValueTask HandleAsync(ChatMessages.Result item, MessageContext messageContext) + { + Trace.WriteLine($">>> MANAGER RESULT: {item.Message}"); + return ValueTask.CompletedTask; + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs index b6498da4b981..6423f2649580 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs @@ -1,77 +1,77 @@ -//// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -///// -///// Common messages used for agent chat patterns. -///// -//public static class ChatMessages -//{ -// /// -// /// %%% COMMENT -// /// -// internal static readonly ChatMessageContent Empty = new(); +/// +/// Common messages used for agent chat patterns. +/// +public static class ChatMessages +{ + /// + /// An empty message instance as a default. + /// + internal static readonly ChatMessageContent Empty = new(); -// /// -// /// Broadcast a message to all . -// /// -// public sealed class Group -// { -// /// -// /// The chat message being broadcast. -// /// -// public ChatMessageContent Message { get; init; } = Empty; -// } + /// + /// Broadcast a message to all . + /// + public sealed class Group + { + /// + /// The chat message being broadcast. + /// + public ChatMessageContent Message { get; init; } = Empty; + } -// /// -// /// Reset/clear the conversation history for all . -// /// -// public sealed class Reset { } + /// + /// Reset/clear the conversation history for all . + /// + public sealed class Reset { } -// /// -// /// The final result. -// /// -// public sealed class Result -// { -// /// -// /// The chat message captures the final result. -// /// -// public ChatMessageContent Message { get; init; } = Empty; -// } + /// + /// The final result. + /// + public sealed class Result + { + /// + /// The chat response message. + /// + public ChatMessageContent Message { get; init; } = Empty; + } -// /// -// /// Signal a to respond. -// /// -// public sealed class Speak { } + /// + /// Signal a to respond. + /// + public sealed class Speak { } -// /// -// /// The input task for a . -// /// -// public sealed class InputTask -// { -// /// -// /// A task that does not require any action. -// /// -// public static readonly InputTask None = new(); + /// + /// The input task. + /// + public sealed class InputTask + { + /// + /// A task that does not require any action. + /// + public static readonly InputTask None = new(); -// /// -// /// The input that defines the task goal. -// /// -// public ChatMessageContent Message { get; init; } = Empty; -// } + /// + /// The input that defines the task goal. + /// + public ChatMessageContent Message { get; init; } = Empty; + } -// /// -// /// Extension method to convert a to a . -// /// -// public static Group ToGroup(this ChatMessageContent message) => new() { Message = message }; + /// + /// Extension method to convert a to a . + /// + public static Group ToGroup(this ChatMessageContent message) => new() { Message = message }; -// /// -// /// Extension method to convert a to a . -// /// -// public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; + /// + /// Extension method to convert a to a . + /// + public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; -// /// -// /// Extension method to convert a to a . -// /// -// public static InputTask ToTask(this ChatMessageContent message) => new() { Message = message }; -//} + /// + /// Extension method to convert a to a . + /// + public static InputTask ToInputTask(this ChatMessageContent message) => new() { Message = message }; +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs index da26a72600d9..4950400c96d1 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs @@ -1,66 +1,57 @@ -//// Copyright (c) Microsoft. All rights reserved. - -//using System.Collections.Generic; -//using System.Linq; -//using System.Threading.Tasks; -//using Microsoft.AgentRuntime; - -//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; - -///// -///// %%% COMMENT -///// -//internal sealed class GroupChatActor : AgentActor -//{ -// private readonly List _cache; -// private readonly TopicId _chatTopic; -// private AgentThread? _thread; - -// /// -// /// Initializes a new instance of the class. -// /// -// /// The unique identifier of the agent. -// /// The runtime associated with the agent. -// /// An . -// /// The unique topic used to broadcast to the entire chat. -// public GroupChatActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId chatTopic) -// : base(id, runtime, agent) -// { -// this._cache = []; -// this._chatTopic = chatTopic; - -// this.RegisterHandler(this.OnGroupMessageAsync); -// this.RegisterHandler(this.OnResetMessageAsync); -// this.RegisterHandler(this.OnSpeakMessageAsync); -// } - -// private ValueTask OnGroupMessageAsync(ChatMessages.Group message, MessageContext context) -// { -// this._cache.Add(message.Message); - -// return ValueTask.CompletedTask; -// } - -// private async ValueTask OnResetMessageAsync(ChatMessages.Reset message, MessageContext context) -// { -// if (this._thread is not null) -// { -// await this._thread.DeleteAsync().ConfigureAwait(false); -// this._thread = null; -// } -// } - -// private async ValueTask OnSpeakMessageAsync(ChatMessages.Speak message, MessageContext context) -// { -// AgentResponseItem[] responses = await this.Agent.InvokeAsync(this._cache, this._thread).ToArrayAsync().ConfigureAwait(false); -// AgentResponseItem response = responses.First(); -// this._thread ??= response.Thread; -// this._cache.Clear(); -// ChatMessageContent output = -// new(response.Message.Role, string.Join("\n\n", responses.Select(response => response.Message))) -// { -// AuthorName = response.Message.AuthorName, -// }; -// await this.PublishMessageAsync(output.ToGroup(), this._chatTopic).ConfigureAwait(false); -// } -//} +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// An used with the . +/// +internal sealed class GroupChatActor : + AgentActor, + IHandle, + IHandle, + IHandle +{ + private readonly List _cache; + private readonly TopicId _groupTopic; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + /// The unique topic used to broadcast to the entire chat. + public GroupChatActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic) + : base(id, runtime, agent) + { + this._cache = []; + this._groupTopic = groupTopic; + } + + /// + public ValueTask HandleAsync(ChatMessages.Group item, MessageContext messageContext) + { + this._cache.Add(item.Message); + + return ValueTask.CompletedTask; + } + + /// + public async ValueTask HandleAsync(ChatMessages.Reset item, MessageContext messageContext) + { + await this.DeleteThreadAsync(messageContext.CancellationToken).ConfigureAwait(false); + } + + /// + public async ValueTask HandleAsync(ChatMessages.Speak item, MessageContext messageContext) + { + ChatMessageContent response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); + this._cache.Clear(); + await this.PublishMessageAsync(response.ToGroup(), this._groupTopic).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index af9b3110bf18..19d627108c31 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -1,51 +1,48 @@ -//// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -//using System; -//using System.Linq; -//using System.Threading.Tasks; -//using Microsoft.AgentRuntime; +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; -//namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -///// -///// A that orchestrates a team of agents. -///// -//internal sealed class GroupChatManager : ChatManager -//{ -// private readonly TaskCompletionSource _completionSource; +/// +/// An used to manage a . +/// +internal sealed class GroupChatManager : ChatManager +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// The team of agents being orchestrated + /// Identifies the orchestration agent. + public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType) + : base(id, runtime, team, orchestrationType) + { + } -// /// -// /// Initializes a new instance of the class. -// /// -// /// The unique identifier of the agent. -// /// The runtime associated with the agent. -// /// The team of agents being orchestrated -// /// Signals completion. -// public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatTeam team, TaskCompletionSource completionSource) -// : base(id, runtime, team, completionSource) -// { -// this._completionSource = completionSource; -// } + /// + protected override Task PrepareTaskAsync() + { + return this.SelectAgentAsync(); + } -// /// -// protected override Task PrepareTaskAsync() -// { -// return this.SelectAgentAsync(); -// } - -// /// -// protected override Task SelectAgentAsync() -// { -// // %%% PLACEHOLDER -//#pragma warning disable CA5394 // Do not use insecure randomness -// int index = Random.Shared.Next(this.Team.Count + 1); -//#pragma warning restore CA5394 // Do not use insecure randomness -// var topics = this.Team.Values.Select(value => value.Topic).ToArray(); -// TopicId? topic = null; -// if (index < this.Team.Count) -// { -// topic = topics[index]; -// } -// return System.Threading.Tasks.Task.FromResult(topic); -// } -//} + /// + protected override Task SelectAgentAsync() + { + // %%% PLACEHOLDER SELECTION LOGIC +#pragma warning disable CA5394 // Do not use insecure randomness + int index = Random.Shared.Next(this.Team.Count + 1); +#pragma warning restore CA5394 // Do not use insecure randomness + AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; + AgentType? agentType = null; + if (index < this.Team.Count) + { + agentType = agentTypes[index]; + } + return Task.FromResult(agentType); + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs new file mode 100644 index 000000000000..ea0ebb444d5f --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// An orchestration that broadcasts the input message to each agent. +/// +public sealed partial class GroupChatOrchestration : GroupChatOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents to be orchestrated. + public GroupChatOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + : base(runtime, members) + { + this.InputTransform = (string input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input).ToInputTask()); + this.ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToString()); + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 34df89f3b433..1c0fcb87fa8b 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -37,11 +37,12 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in AgentType managerType = this.FormatAgentType(topic, "Manager"); int agentCount = 0; - ChatTeam team = []; + ChatGroup team = []; foreach (OrchestrationTarget member in this.Members) { - AgentType memberType = default; + ++agentCount; + AgentType memberType = default; if (member.IsAgent(out Agent? agent)) { memberType = await RegisterAgentAsync(agent).ConfigureAwait(false); @@ -51,6 +52,8 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in memberType = await orchestration.RegisterAsync(topic, managerType).ConfigureAwait(false); } + team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID + Trace.WriteLine($"> GROUPCHAT MEMBER #{agentCount}: {memberType}"); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); @@ -60,11 +63,11 @@ await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManager(agentId, runtime, team))).ConfigureAwait(false); + new GroupChatManager(agentId, runtime, team, orchestrationType))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); - return null; + return managerType; async ValueTask RegisterAgentAsync(Agent agent) { @@ -75,7 +78,6 @@ await this.Runtime.RegisterAgentFactoryAsync( ValueTask.FromResult(new GroupChatActor(agentId, runtime, agent, topic))).ConfigureAwait(false); await this.SubscribeAsync(agentType, topic).ConfigureAwait(false); - //await this.RegisterTopicsAsync(agentType, agentTopic).ConfigureAwait(false); // %%% CRITICAL return agentType; } diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs index 322f06a57594..921f7ae24a21 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.SemanticKernel.ChatCompletion; @@ -18,7 +19,7 @@ public sealed partial class HandoffOrchestration : HandoffOrchestration HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.User, input)); - this.ResultTransform = (HandoffMessage result) => result.Content.ToString(); + this.InputTransform = (string input) => ValueTask.FromResult(HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.User, input))); + this.ResultTransform = (HandoffMessage result) => ValueTask.FromResult(result.Content.ToString()); } } diff --git a/dotnet/src/Agents/Orchestration/PatternActor.cs b/dotnet/src/Agents/Orchestration/PatternActor.cs index 3ddf0037e5ed..b75893b60b19 100644 --- a/dotnet/src/Agents/Orchestration/PatternActor.cs +++ b/dotnet/src/Agents/Orchestration/PatternActor.cs @@ -30,7 +30,7 @@ protected PatternActor(AgentId id, IAgentRuntime runtime, string description, IL protected async ValueTask SendMessageAsync( object message, AgentType agentType, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { AgentId? agentId = await this.GetAgentAsync(agentType, cancellationToken).ConfigureAwait(false); if (agentId.HasValue) From d2c5db9f3ca95b23f8b6d2edd378d09785dfa243 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 13 Apr 2025 01:45:43 -0700 Subject: [PATCH 07/57] More --- .../Orchestration/Step03_GroupChat.cs | 53 ++++++++++++++++++- .../Orchestration/Agents.Orchestration.csproj | 3 +- .../Orchestration/GroupChat/ChatManager.cs | 10 +++- .../Orchestration/GroupChat/GroupChatActor.cs | 6 +++ .../GroupChat/GroupChatManager.cs | 4 +- .../GroupChat/GroupChatOrchestration.cs | 2 +- 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 04e41c40b846..ad2044844d02 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.AgentRuntime.InProcess; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; @@ -13,7 +15,7 @@ namespace GettingStarted.Orchestration; public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task UseGroupChatPatternAsync() + public async Task SimpleGroupChatAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); @@ -37,4 +39,53 @@ public async Task UseGroupChatPatternAsync() } // %%% MORE SAMPLES - GROUPCHAT + + [Fact] + public async Task SingleActorAsync() + { + // Define the agents + ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + + // Define the pattern + InProcessRuntime runtime = new(); + GroupChatOrchestration orchestration = new(runtime, agent); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + + string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {output}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task SingleNestedActorAsync() + { + // Define the agents + ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + + // Define the pattern + InProcessRuntime runtime = new(); + GroupChatOrchestration orchestrationInner = new(runtime, agent) + { + InputTransform = (ChatMessages.InputTask input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input.Message.ToString()).ToInputTask()), + ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToResult()) + }; + GroupChatOrchestration orchestrationOuter = new(runtime, orchestrationInner); + + // Start the runtime + await runtime.StartAsync(); + string input = "1"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestrationOuter.InvokeAsync(input); + + string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {output}"); + + await runtime.RunUntilIdleAsync(); + } } diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index cd7f8ff4ccf8..f3cb84a7f74e 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -4,8 +4,9 @@ Microsoft.SemanticKernel.Agents.Orchestration Microsoft.SemanticKernel.Agents.Orchestration + net8.0 - + $(NoWarn);SKEXP0110;SKEXP0001 false preview diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs index 85d9aecd3d24..77ea56a19288 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs @@ -24,6 +24,7 @@ public abstract class ChatManager : public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task."; private readonly AgentType _orchestrationType; + private readonly TopicId _groupTopic; /// /// Initializes a new instance of the class. @@ -32,7 +33,7 @@ public abstract class ChatManager : /// The runtime associated with the agent. /// The team of agents being orchestrated /// Identifies the orchestration agent. - protected ChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType) + protected ChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) : base(id, runtime, DefaultDescription) { this.Chat = []; @@ -40,6 +41,7 @@ protected ChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentTy this._orchestrationType = orchestrationType; Trace.WriteLine($">>> MANAGER NAMES: {this.Team.FormatNames()}"); Trace.WriteLine($">>> MANAGER TEAM:\n{this.Team.FormatList()}"); + this._groupTopic = groupTopic; } /// @@ -97,6 +99,12 @@ public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext m if (agentType != null) { await this.RequestAgentResponseAsync(agentType.Value, messageContext.CancellationToken).ConfigureAwait(false); + await this.PublishMessageAsync(item.Message.ToGroup(), this._groupTopic).ConfigureAwait(false); + } + else + { + Trace.WriteLine(">>> MANAGER NO AGENT"); + await this.SendMessageAsync(item.Message.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); // %%% PLACEHOLDER - FINAL MESSAGE } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs index 4950400c96d1..34627dcdf363 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; @@ -50,7 +51,12 @@ public async ValueTask HandleAsync(ChatMessages.Reset item, MessageContext messa /// public async ValueTask HandleAsync(ChatMessages.Speak item, MessageContext messageContext) { + Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} SPEAK"); + ChatMessageContent response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); + + Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} OUTPUT - {response}"); + this._cache.Clear(); await this.PublishMessageAsync(response.ToGroup(), this._groupTopic).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index 19d627108c31..144c7695dd8e 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -19,8 +19,8 @@ internal sealed class GroupChatManager : ChatManager /// The runtime associated with the agent. /// The team of agents being orchestrated /// Identifies the orchestration agent. - public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType) - : base(id, runtime, team, orchestrationType) + public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) + : base(id, runtime, team, orchestrationType, groupTopic) { } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 1c0fcb87fa8b..37bc67a63fdc 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -63,7 +63,7 @@ await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManager(agentId, runtime, team, orchestrationType))).ConfigureAwait(false); + new GroupChatManager(agentId, runtime, team, orchestrationType, topic))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); From e9b2dcd052722f55b4901ddba099deeea1c7d428 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 13 Apr 2025 01:51:39 -0700 Subject: [PATCH 08/57] Warnings --- dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs | 1 + dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs index 77ea56a19288..a012ac6d6b3a 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs @@ -33,6 +33,7 @@ public abstract class ChatManager : /// The runtime associated with the agent. /// The team of agents being orchestrated /// Identifies the orchestration agent. + /// The unique topic used to broadcast to the entire chat. protected ChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) : base(id, runtime, DefaultDescription) { diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index 144c7695dd8e..9c63ff6dbc3f 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -19,6 +19,7 @@ internal sealed class GroupChatManager : ChatManager /// The runtime associated with the agent. /// The team of agents being orchestrated /// Identifies the orchestration agent. + /// The unique topic used to broadcast to the entire chat. public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) : base(id, runtime, team, orchestrationType, groupTopic) { From 27b7fcbee471b3faa85fa68c4fc5d947f0867087 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 14 Apr 2025 08:58:18 -0700 Subject: [PATCH 09/57] Typos --- dotnet/src/Agents/Orchestration/AgentActor.cs | 4 ++-- .../Agents/Orchestration/Broadcast/BroadcastResultActor.cs | 2 +- dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs | 2 +- dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs | 4 ++-- dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index a7282e09d859..b44ced295b67 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -29,7 +29,7 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre : base( id, runtime, - VerifyDescripion(agent), + VerifyDescription(agent), GetLogger(agent)) { this.Agent = agent; @@ -128,7 +128,7 @@ protected async IAsyncEnumerable InvokeStreamingAsy } } - private static string VerifyDescripion(Agent agent) + private static string VerifyDescription(Agent agent) { return agent.Description ?? throw new ArgumentException($"Missing agent description: {agent.Name ?? agent.Id}", nameof(agent)); } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs index 0137463dee45..0c914f297843 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs +++ b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs @@ -26,7 +26,7 @@ internal sealed class BroadcastResultActor : PatternActor, /// The unique identifier of the agent. /// The runtime associated with the agent. /// Identifies the orchestration agent. - /// The expected number of messages to be recieved. + /// The expected number of messages to be received. public BroadcastResultActor( AgentId id, IAgentRuntime runtime, diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs index 5e073a5ae1e1..bbfac7114f6e 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs @@ -7,7 +7,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// -/// Descibes a team of agents participating in a group chat. +/// Describes a team of agents participating in a group chat. /// public class ChatGroup : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs index 9e54c75a7bcf..0c08e0176a19 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// -/// An actor used with the . +/// An actor used with the . /// internal sealed class HandoffActor : AgentActor, IHandle { @@ -20,7 +20,7 @@ internal sealed class HandoffActor : AgentActor, IHandle /// The unique identifier of the agent. /// The runtime associated with the agent. /// An . - /// The indentifier of the next agent for which to handoff the result + /// The identifier of the next agent for which to handoff the result public HandoffActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) : base(id, runtime, agent, noThread: true) { diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs index db5a176ecd61..345c1bf65f22 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs +++ b/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs @@ -3,7 +3,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// -/// A message that describes the input task and captures results for a . +/// A message that describes the input task and captures results for a . /// public sealed class HandoffMessage { From 4ff842958d1a448d34e1954d925b6729b7c6ac95 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 14 Apr 2025 09:58:23 -0700 Subject: [PATCH 10/57] Update term --- dotnet/src/Agents/Orchestration/AgentOrchestration.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 1f6ce05737db..ee14267da1b6 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -16,7 +16,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// public abstract partial class AgentOrchestration : Orchestratable { - private readonly string _orchestrationType; + private readonly string _orchestrationRoot; /// /// Initializes a new instance of the class. @@ -29,7 +29,7 @@ protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] this.Runtime = runtime; this.Members = members; - this._orchestrationType = this.GetType().Name.Split('`').First(); + this._orchestrationRoot = this.GetType().Name.Split('`').First(); } /// @@ -94,7 +94,7 @@ public async ValueTask> InvokeAsync(TInput input, T /// The topic identifier used in formatting the agent type. /// A suffix to differentiate the agent type. /// A formatted AgentType object. - protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{this._orchestrationType}_{suffix}"); + protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{this._orchestrationRoot}_{suffix}"); /// /// Initiates processing according to the orchestration pattern. From eca2c629d3cb406d8e3961139a0f8ba1c327844d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 14 Apr 2025 13:47:39 -0700 Subject: [PATCH 11/57] Rename orchestrations --- ...ep01_Broadcast.cs => Step01_Concurrent.cs} | 30 +++++------ ...Step02_Handoff.cs => Step02_Sequential.cs} | 30 +++++------ .../Orchestration/Step04_Nested.cs | 26 ++++----- .../Broadcast/BroadcastMessages.cs | 53 ------------------- .../ConcurrentActor.cs} | 18 +++---- .../Concurrent/ConcurrentMessages.cs | 53 +++++++++++++++++++ .../ConcurrentOrchestration.String.cs} | 12 ++--- .../ConcurrentOrchestration.cs} | 22 ++++---- .../ConcurrentResultActor.cs} | 20 +++---- .../{GroupChatActor.cs => ChatAgentActor.cs} | 10 ++-- .../{ChatManager.cs => ChatManagerActor.cs} | 6 +-- .../Orchestration/GroupChat/ChatMessages.cs | 6 +-- ...hatManager.cs => GroupChatManagerActor.cs} | 8 +-- .../GroupChat/GroupChatOrchestration.cs | 6 +-- .../HandOff/HandoffOrchestration.String.cs | 25 --------- .../SequentialActor.cs} | 18 +++---- .../SequentialMessage.cs} | 10 ++-- .../SequentialOrchestration.String.cs | 26 +++++++++ .../SequentialOrchestration.cs} | 18 +++---- 19 files changed, 198 insertions(+), 199 deletions(-) rename dotnet/samples/GettingStartedWithAgents/Orchestration/{Step01_Broadcast.cs => Step01_Concurrent.cs} (73%) rename dotnet/samples/GettingStartedWithAgents/Orchestration/{Step02_Handoff.cs => Step02_Sequential.cs} (74%) delete mode 100644 dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs rename dotnet/src/Agents/Orchestration/{Broadcast/BroadcastActor.cs => Concurrent/ConcurrentActor.cs} (50%) create mode 100644 dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs rename dotnet/src/Agents/Orchestration/{Broadcast/BroadcastOrchestration.String.cs => Concurrent/ConcurrentOrchestration.String.cs} (50%) rename dotnet/src/Agents/Orchestration/{Broadcast/BroadcastOrchestration.cs => Concurrent/ConcurrentOrchestration.cs} (67%) rename dotnet/src/Agents/Orchestration/{Broadcast/BroadcastResultActor.cs => Concurrent/ConcurrentResultActor.cs} (64%) rename dotnet/src/Agents/Orchestration/GroupChat/{GroupChatActor.cs => ChatAgentActor.cs} (86%) rename dotnet/src/Agents/Orchestration/GroupChat/{ChatManager.cs => ChatManagerActor.cs} (96%) rename dotnet/src/Agents/Orchestration/GroupChat/{GroupChatManager.cs => GroupChatManagerActor.cs} (82%) delete mode 100644 dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs rename dotnet/src/Agents/Orchestration/{HandOff/HandoffActor.cs => Sequential/SequentialActor.cs} (51%) rename dotnet/src/Agents/Orchestration/{HandOff/HandoffMessage.cs => Sequential/SequentialMessage.cs} (54%) create mode 100644 dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs rename dotnet/src/Agents/Orchestration/{HandOff/HandoffOrchestration.cs => Sequential/SequentialOrchestration.cs} (73%) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs similarity index 73% rename from dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs rename to dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index 54c33500695c..a9e111ab57e0 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Broadcast.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -3,17 +3,17 @@ using Microsoft.AgentRuntime.InProcess; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; -using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// -public class Step01_Broadcast(ITestOutputHelper output) : BaseOrchestrationTest(output) +public class Step01_Concurrent(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleBroadcastAsync() + public async Task SimpleConcurrentAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); @@ -22,7 +22,7 @@ public async Task SimpleBroadcastAsync() // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration orchestration = new(runtime, agent1, agent2, agent3); + ConcurrentOrchestration orchestration = new(runtime, agent1, agent2, agent3); // Start the runtime await runtime.StartAsync(); @@ -37,7 +37,7 @@ public async Task SimpleBroadcastAsync() } [Fact] - public async Task NestedBroadcastAsync() + public async Task NestedConcurrentAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); @@ -48,9 +48,9 @@ public async Task NestedBroadcastAsync() // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); - BroadcastOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); - BroadcastOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); + ConcurrentOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); + ConcurrentOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); + ConcurrentOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); // Start the runtime await runtime.StartAsync(); @@ -72,7 +72,7 @@ public async Task SingleActorAsync() // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration orchestration = new(runtime, agent); + ConcurrentOrchestration orchestration = new(runtime, agent); // Start the runtime await runtime.StartAsync(); @@ -94,8 +94,8 @@ public async Task SingleNestedActorAsync() // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration orchestrationInner = CreateNested(runtime, agent); - BroadcastOrchestration orchestrationOuter = new(runtime, orchestrationInner); + ConcurrentOrchestration orchestrationInner = CreateNested(runtime, agent); + ConcurrentOrchestration orchestrationOuter = new(runtime, orchestrationInner); // Start the runtime await runtime.StartAsync(); @@ -109,12 +109,12 @@ public async Task SingleNestedActorAsync() await runtime.RunUntilIdleAsync(); } - private static BroadcastOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + private static ConcurrentOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) { return new(runtime, targets) { - InputTransform = (BroadcastMessages.Task input) => ValueTask.FromResult(input), - ResultTransform = (BroadcastMessages.Result[] results) => ValueTask.FromResult(string.Join("\n", results.Select(result => $"{result.Message}")).ToBroadcastResult()), + InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(input), + ResultTransform = (ConcurrentMessages.Result[] results) => ValueTask.FromResult(string.Join("\n", results.Select(result => $"{result.Message}")).ToResult()), }; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs similarity index 74% rename from dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs rename to dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index 04242007f42d..3fada2b7b09e 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -3,17 +3,17 @@ using Microsoft.AgentRuntime.InProcess; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// -public class Step02_Handoff(ITestOutputHelper output) : BaseOrchestrationTest(output) +public class Step02_Sequentail(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleHandoffAsync() + public async Task SimpleSequentailAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); @@ -22,7 +22,7 @@ public async Task SimpleHandoffAsync() // Define the pattern InProcessRuntime runtime = new(); - HandoffOrchestration orchestration = new(runtime, agent1, agent2, agent3); + SequentialOrchestration orchestration = new(runtime, agent1, agent2, agent3); // Start the runtime await runtime.StartAsync(); @@ -36,7 +36,7 @@ public async Task SimpleHandoffAsync() } [Fact] - public async Task NestedHandoffAsync() + public async Task NestedSequentailAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); @@ -47,9 +47,9 @@ public async Task NestedHandoffAsync() // Define the pattern InProcessRuntime runtime = new(); - HandoffOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); - HandoffOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); - HandoffOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); + SequentialOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); + SequentialOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); + SequentialOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); // Start the runtime await runtime.StartAsync(); @@ -71,7 +71,7 @@ public async Task SingleActorAsync() // Define the pattern InProcessRuntime runtime = new(); - HandoffOrchestration orchestration = new(runtime, agent); + SequentialOrchestration orchestration = new(runtime, agent); // Start the runtime await runtime.StartAsync(); @@ -93,8 +93,8 @@ public async Task SingleNestedActorAsync() // Define the pattern InProcessRuntime runtime = new(); - HandoffOrchestration orchestrationInner = CreateNested(runtime, agent); - HandoffOrchestration orchestrationOuter = new(runtime, orchestrationInner); + SequentialOrchestration orchestrationInner = CreateNested(runtime, agent); + SequentialOrchestration orchestrationOuter = new(runtime, orchestrationInner); // Start the runtime await runtime.StartAsync(); @@ -108,12 +108,12 @@ public async Task SingleNestedActorAsync() await runtime.RunUntilIdleAsync(); } - private static HandoffOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + private static SequentialOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) { return new(runtime, targets) { - InputTransform = (HandoffMessage input) => ValueTask.FromResult(input), - ResultTransform = (HandoffMessage results) => ValueTask.FromResult(results), + InputTransform = (SequentialMessage input) => ValueTask.FromResult(input), + ResultTransform = (SequentialMessage results) => ValueTask.FromResult(results), }; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs index f6ca88ee64a5..85e3fc2de3c9 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -4,19 +4,19 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; -using Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; -using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step04_Nested(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task NestHandoffBroadcastAsync() + public async Task NestSequentialGroupsAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); @@ -26,13 +26,13 @@ public async Task NestHandoffBroadcastAsync() // Define the pattern InProcessRuntime runtime = new(); - BroadcastOrchestration innerOrchestration = + ConcurrentOrchestration innerOrchestration = new(runtime, agent3, agent4) { - InputTransform = (HandoffMessage input) => ValueTask.FromResult(new BroadcastMessages.Task { Message = input.Content }), - ResultTransform = (BroadcastMessages.Result[] output) => ValueTask.FromResult(HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))))) + InputTransform = (SequentialMessage input) => ValueTask.FromResult(new ConcurrentMessages.Request { Message = input.Content }), + ResultTransform = (ConcurrentMessages.Result[] output) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))))) }; - HandoffOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); + SequentialOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); // Start the runtime await runtime.StartAsync(); @@ -46,7 +46,7 @@ public async Task NestHandoffBroadcastAsync() } [Fact] - public async Task NestBroadcastHandoffAsync() + public async Task NestConcurrentGroupsAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); @@ -56,13 +56,13 @@ public async Task NestBroadcastHandoffAsync() // Define the pattern InProcessRuntime runtime = new(); - HandoffOrchestration innerOrchestration = + SequentialOrchestration innerOrchestration = new(runtime, agent3, agent4) { - InputTransform = (BroadcastMessages.Task input) => ValueTask.FromResult(new HandoffMessage { Content = input.Message }), - ResultTransform = (HandoffMessage result) => ValueTask.FromResult(new BroadcastMessages.Result { Message = result.Content }) + InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(new SequentialMessage { Content = input.Message }), + ResultTransform = (SequentialMessage result) => ValueTask.FromResult(new ConcurrentMessages.Result { Message = result.Content }) }; - BroadcastOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); + ConcurrentOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); // Start the runtime await runtime.StartAsync(); diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs b/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs deleted file mode 100644 index 3d7d214cff0d..000000000000 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastMessages.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; - -/// -/// Common messages used by the . -/// -public static class BroadcastMessages -{ - /// - /// The input task for a . - /// - public sealed class Task - { - /// - /// The input message. - /// - public ChatMessageContent Message { get; init; } = new(); - } - - /// - /// A result from a . - /// - public sealed class Result - { - /// - /// The result message. - /// - public ChatMessageContent Message { get; init; } = new(); - } - - /// - /// Extension method to convert a to a . - /// - public static Result ToBroadcastResult(this string text, AuthorRole? role = null) => new() { Message = new ChatMessageContent(role ?? AuthorRole.Assistant, text) }; - - /// - /// Extension method to convert a to a . - /// - public static Result ToBroadcastResult(this ChatMessageContent message) => new() { Message = message }; - - /// - /// Extension method to convert a to a . - /// - public static Task ToBroadcastTask(this string text, AuthorRole? role = null) => new() { Message = new ChatMessageContent(role ?? AuthorRole.User, text) }; - - /// - /// Extension method to convert a to a . - /// - public static Task ToBroadcastTask(this ChatMessageContent message) => new() { Message = message }; -} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs similarity index 50% rename from dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs rename to dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index 9d12fae2e61e..8e0fc6343846 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -5,37 +5,37 @@ using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// -/// An used with the . +/// An used with the . /// -internal sealed class BroadcastActor : AgentActor, IHandle +internal sealed class ConcurrentActor : AgentActor, IHandle { private readonly AgentType _orchestrationType; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// An . /// Identifies the orchestration agent. - public BroadcastActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType) : + public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType) : base(id, runtime, agent, noThread: true) { this._orchestrationType = orchestrationType; } /// - public async ValueTask HandleAsync(BroadcastMessages.Task item, MessageContext messageContext) + public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext) { - Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} INPUT - {item.Message}"); + Trace.WriteLine($"> CONCURRENT ACTOR: {this.Id.Type} INPUT - {item.Message}"); ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); - Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} OUTPUT - {response}"); + Trace.WriteLine($"> CONCURRENT ACTOR: {this.Id.Type} OUTPUT - {response}"); - await this.SendMessageAsync(response.ToBroadcastResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(response.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs new file mode 100644 index 000000000000..1846081dca5b --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; + +/// +/// Common messages used by the . +/// +public static class ConcurrentMessages +{ + /// + /// The input task for a . + /// + public sealed class Request + { + /// + /// The request message. + /// + public ChatMessageContent Message { get; init; } = new(); + } + + /// + /// A result from a . + /// + public sealed class Result + { + /// + /// The result message. + /// + public ChatMessageContent Message { get; init; } = new(); + } + + /// + /// Extension method to convert a to a . + /// + public static Result ToResult(this string text, AuthorRole? role = null) => new() { Message = new ChatMessageContent(role ?? AuthorRole.Assistant, text) }; + + /// + /// Extension method to convert a to a . + /// + public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; + + /// + /// Extension method to convert a to a . + /// + public static Request ToRequest(this string text, AuthorRole? role = null) => new() { Message = new ChatMessageContent(role ?? AuthorRole.User, text) }; + + /// + /// Extension method to convert a to a . + /// + public static Request ToInput(this ChatMessageContent message) => new() { Message = message }; +} diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs similarity index 50% rename from dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs rename to dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs index 7291d1699f1c..f9d024298c4e 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -4,22 +4,22 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// /// An orchestration that broadcasts the input message to each agent. /// -public sealed class BroadcastOrchestration : BroadcastOrchestration +public sealed class ConcurrentOrchestration : ConcurrentOrchestration { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents to be orchestrated. - public BroadcastOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + public ConcurrentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) : base(runtime, members) { - this.InputTransform = (string input) => ValueTask.FromResult(input.ToBroadcastTask()); - this.ResultTransform = (BroadcastMessages.Result[] result) => ValueTask.FromResult([.. result.Select(r => r.Message.Content ?? string.Empty)]); + this.InputTransform = (string input) => ValueTask.FromResult(input.ToRequest()); + this.ResultTransform = (ConcurrentMessages.Result[] result) => ValueTask.FromResult([.. result.Select(r => r.Message.Content ?? string.Empty)]); } } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs similarity index 67% rename from dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs rename to dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 491700e799f2..656dc0de45b5 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -4,28 +4,28 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// /// An orchestration that broadcasts the input message to each agent. /// -public class BroadcastOrchestration - : AgentOrchestration +public class ConcurrentOrchestration + : AgentOrchestration { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents participating in the orchestration. - public BroadcastOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + public ConcurrentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) : base(runtime, agents) { } /// - protected override ValueTask StartAsync(TopicId topic, BroadcastMessages.Task input, AgentType? entryAgent) + protected override ValueTask StartAsync(TopicId topic, ConcurrentMessages.Request input, AgentType? entryAgent) { - Trace.WriteLine($"> BROADCAST START: {topic}"); + Trace.WriteLine($"> CONCURRENT START: {topic}"); return this.Runtime.PublishMessageAsync(input, topic); } @@ -38,8 +38,8 @@ await this.Runtime.RegisterAgentFactoryAsync( resultType, (agentId, runtime) => ValueTask.FromResult( - new BroadcastResultActor(agentId, runtime, orchestrationType, this.Members.Count))).ConfigureAwait(false); - Trace.WriteLine($"> BROADCAST RESULTS: {resultType}"); + new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count))).ConfigureAwait(false); + Trace.WriteLine($"> CONCURRENT RESULTS: {resultType}"); // Register member actors - All agents respond to the same message. int agentCount = 0; @@ -58,7 +58,7 @@ await this.Runtime.RegisterAgentFactoryAsync( memberType = await orchestration.RegisterAsync(topic, resultType).ConfigureAwait(false); } - Trace.WriteLine($"> BROADCAST MEMBER #{agentCount}: {memberType}"); + Trace.WriteLine($"> CONCURRENT MEMBER #{agentCount}: {memberType}"); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } @@ -71,7 +71,7 @@ async ValueTask RegisterAgentAsync(Agent agent) await this.Runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => - ValueTask.FromResult(new BroadcastActor(agentId, runtime, agent, resultType))).ConfigureAwait(false); + ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType))).ConfigureAwait(false); return agentType; } diff --git a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs similarity index 64% rename from dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs rename to dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs index 0c914f297843..a9611884f112 100644 --- a/dotnet/src/Agents/Orchestration/Broadcast/BroadcastResultActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs @@ -7,32 +7,32 @@ using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Broadcast; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// -/// Actor for capturing each message. +/// Actor for capturing each message. /// -internal sealed class BroadcastResultActor : PatternActor, - IHandle +internal sealed class ConcurrentResultActor : PatternActor, + IHandle { - private readonly ConcurrentQueue _results; + private readonly ConcurrentQueue _results; private readonly AgentType _orchestrationType; private readonly int _expectedCount; private int _resultCount; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// Identifies the orchestration agent. /// The expected number of messages to be received. - public BroadcastResultActor( + public ConcurrentResultActor( AgentId id, IAgentRuntime runtime, AgentType orchestrationType, int expectedCount) - : base(id, runtime, "Captures the results of the BroadcastOrchestration") + : base(id, runtime, "Captures the results of the ConcurrentOrchestration") { this._orchestrationType = orchestrationType; this._expectedCount = expectedCount; @@ -40,9 +40,9 @@ public BroadcastResultActor( } /// - public async ValueTask HandleAsync(BroadcastMessages.Result item, MessageContext messageContext) + public async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext) { - Trace.WriteLine($"> BROADCAST RESULT: {this.Id.Type} (#{this._resultCount + 1}/{this._expectedCount})"); + Trace.WriteLine($"> CONCURRENT RESULT: {this.Id.Type} (#{this._resultCount + 1}/{this._expectedCount})"); this._results.Enqueue(item); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatAgentActor.cs similarity index 86% rename from dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs rename to dotnet/src/Agents/Orchestration/GroupChat/ChatAgentActor.cs index 34627dcdf363..2c2a1efb0263 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatAgentActor.cs @@ -11,7 +11,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An used with the . /// -internal sealed class GroupChatActor : +internal sealed class ChatAgentActor : AgentActor, IHandle, IHandle, @@ -21,13 +21,13 @@ internal sealed class GroupChatActor : private readonly TopicId _groupTopic; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// An . /// The unique topic used to broadcast to the entire chat. - public GroupChatActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic) + public ChatAgentActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic) : base(id, runtime, agent) { this._cache = []; @@ -51,11 +51,11 @@ public async ValueTask HandleAsync(ChatMessages.Reset item, MessageContext messa /// public async ValueTask HandleAsync(ChatMessages.Speak item, MessageContext messageContext) { - Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} SPEAK"); + Trace.WriteLine($"> GROUPCHAT ACTOR: {this.Id.Type} SPEAK"); ChatMessageContent response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); - Trace.WriteLine($"> BROADCAST ACTOR: {this.Id.Type} OUTPUT - {response}"); + Trace.WriteLine($"> GROUPCHAT ACTOR: {this.Id.Type} OUTPUT - {response}"); this._cache.Clear(); await this.PublishMessageAsync(response.ToGroup(), this._groupTopic).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatManagerActor.cs similarity index 96% rename from dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs rename to dotnet/src/Agents/Orchestration/GroupChat/ChatManagerActor.cs index a012ac6d6b3a..31414e0c4d87 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatManagerActor.cs @@ -12,7 +12,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An used to manage a . /// -public abstract class ChatManager : +public abstract class ChatManagerActor : PatternActor, IHandle, IHandle, @@ -27,14 +27,14 @@ public abstract class ChatManager : private readonly TopicId _groupTopic; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. - protected ChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) + protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) : base(id, runtime, DefaultDescription) { this.Chat = []; diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs b/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs index 6423f2649580..2c5b362eaeb1 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs @@ -13,7 +13,7 @@ public static class ChatMessages internal static readonly ChatMessageContent Empty = new(); /// - /// Broadcast a message to all . + /// Broadcast a message to all . /// public sealed class Group { @@ -24,7 +24,7 @@ public sealed class Group } /// - /// Reset/clear the conversation history for all . + /// Reset/clear the conversation history for all . /// public sealed class Reset { } @@ -40,7 +40,7 @@ public sealed class Result } /// - /// Signal a to respond. + /// Signal a to respond. /// public sealed class Speak { } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs similarity index 82% rename from dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs rename to dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index 9c63ff6dbc3f..012fc7d5a872 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -8,19 +8,19 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// -/// An used to manage a . +/// An used to manage a . /// -internal sealed class GroupChatManager : ChatManager +internal sealed class GroupChatManagerActor : ChatManagerActor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. - public GroupChatManager(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) + public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) : base(id, runtime, team, orchestrationType, groupTopic) { } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 37bc67a63fdc..7348b6d3f097 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -63,7 +63,7 @@ await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManager(agentId, runtime, team, orchestrationType, topic))).ConfigureAwait(false); + new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); @@ -75,9 +75,7 @@ async ValueTask RegisterAgentAsync(Agent agent) await this.Runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => - ValueTask.FromResult(new GroupChatActor(agentId, runtime, agent, topic))).ConfigureAwait(false); - - await this.SubscribeAsync(agentType, topic).ConfigureAwait(false); + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic))).ConfigureAwait(false); return agentType; } diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs b/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs deleted file mode 100644 index 921f7ae24a21..000000000000 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.String.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.AgentRuntime; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; - -/// -/// An orchestration that broadcasts the input message to each agent. -/// -public sealed partial class HandoffOrchestration : HandoffOrchestration -{ - /// - /// Initializes a new instance of the class. - /// - /// The runtime associated with the orchestration. - /// The agents to be orchestrated. - public HandoffOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) - : base(runtime, members) - { - this.InputTransform = (string input) => ValueTask.FromResult(HandoffMessage.FromChat(new ChatMessageContent(AuthorRole.User, input))); - this.ResultTransform = (HandoffMessage result) => ValueTask.FromResult(result.Content.ToString()); - } -} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs similarity index 51% rename from dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs rename to dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index 0c08e0176a19..0fbf51e88c05 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -5,37 +5,37 @@ using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// -/// An actor used with the . +/// An actor used with the . /// -internal sealed class HandoffActor : AgentActor, IHandle +internal sealed class SequentialActor : AgentActor, IHandle { private readonly AgentType _nextAgent; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. /// An . /// The identifier of the next agent for which to handoff the result - public HandoffActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) + public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) : base(id, runtime, agent, noThread: true) { this._nextAgent = nextAgent; } /// - public async ValueTask HandleAsync(HandoffMessage item, MessageContext messageContext) + public async ValueTask HandleAsync(SequentialMessage item, MessageContext messageContext) { - Trace.WriteLine($"> HANDOFF ACTOR: {this.Id.Type} INPUT - {item.Content}"); + Trace.WriteLine($"> SEQUENTIAL ACTOR: {this.Id.Type} INPUT - {item.Content}"); ChatMessageContent response = await this.InvokeAsync(item.Content, messageContext.CancellationToken).ConfigureAwait(false); - Trace.WriteLine($"> HANDOFF ACTOR: {this.Id.Type} OUTPUT - {response}"); + Trace.WriteLine($"> SEQUENTIAL ACTOR: {this.Id.Type} OUTPUT - {response}"); - await this.SendMessageAsync(HandoffMessage.FromChat(response), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(SequentialMessage.FromChat(response), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs similarity index 54% rename from dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs rename to dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs index 345c1bf65f22..512c163cf5e9 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffMessage.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// -/// A message that describes the input task and captures results for a . +/// A message that describes the input task and captures results for a . /// -public sealed class HandoffMessage +public sealed class SequentialMessage { /// /// The input task. @@ -13,7 +13,7 @@ public sealed class HandoffMessage public ChatMessageContent Content { get; init; } = new(); /// - /// Extension method to convert a to a . + /// Extension method to convert a to a . /// - public static HandoffMessage FromChat(ChatMessageContent content) => new() { Content = content }; + public static SequentialMessage FromChat(ChatMessageContent content) => new() { Content = content }; } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs new file mode 100644 index 000000000000..bb84c150f472 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; + +/// +/// An orchestration that passes the input message to the first agent, and +/// then the subsequent result to the next agent, etc... +/// +public sealed partial class SequentialOrchestration : SequentialOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents to be orchestrated. + public SequentialOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + : base(runtime, members) + { + this.InputTransform = (string input) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.User, input))); + this.ResultTransform = (SequentialMessage result) => ValueTask.FromResult(result.Content.ToString()); + } +} diff --git a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs similarity index 73% rename from dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs rename to dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index d6c72252c2c8..417874c46569 100644 --- a/dotnet/src/Agents/Orchestration/HandOff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -5,28 +5,28 @@ using Microsoft.AgentRuntime; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// /// An orchestration that provides the input message to the first agent /// and sequentially passes each agent result to the next agent. /// -public class HandoffOrchestration : AgentOrchestration +public class SequentialOrchestration : AgentOrchestration { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents participating in the orchestration. - public HandoffOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + public SequentialOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) : base(runtime, agents) { } /// - protected override async ValueTask StartAsync(TopicId topic, HandoffMessage input, AgentType? entryAgent) + protected override async ValueTask StartAsync(TopicId topic, SequentialMessage input, AgentType? entryAgent) { - Trace.WriteLine($"> HANDOFF START: {topic} [{entryAgent}]"); + Trace.WriteLine($"> SEQUENTIAL START: {topic} [{entryAgent}]"); await this.Runtime.SendMessageAsync(input, entryAgent!.Value).ConfigureAwait(false); // NULL OVERRIDE } @@ -38,7 +38,7 @@ protected override async ValueTask StartAsync(TopicId topic, HandoffMessage inpu AgentType nextAgent = orchestrationType; for (int index = this.Members.Count - 1; index >= 0; --index) { - Trace.WriteLine($"> HANDOFF NEXT #{index}: {nextAgent}"); + Trace.WriteLine($"> SEQUENTIAL NEXT #{index}: {nextAgent}"); OrchestrationTarget member = this.Members[index]; if (member.IsAgent(out Agent? agent)) @@ -49,7 +49,7 @@ protected override async ValueTask StartAsync(TopicId topic, HandoffMessage inpu { nextAgent = await orchestration.RegisterAsync(topic, nextAgent).ConfigureAwait(false); } - Trace.WriteLine($"> HANDOFF MEMBER #{index}: {nextAgent}"); + Trace.WriteLine($"> SEQUENTIAL MEMBER #{index}: {nextAgent}"); } return nextAgent; @@ -59,7 +59,7 @@ async Task RegisterAgentAsync(TopicId topic, AgentType nextAgent, int AgentType agentType = this.GetAgentType(topic, index); return await this.Runtime.RegisterAgentFactoryAsync( agentType, - (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, agent, nextAgent))).ConfigureAwait(false); + (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent))).ConfigureAwait(false); } } From 65abcf0a4a81dabaf63492beba5733c75549e0b6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Apr 2025 08:29:43 -0700 Subject: [PATCH 12/57] Replace `Trace` with `ILogger` --- dotnet/Directory.Packages.props | 14 +-- dotnet/SK-dotnet.sln | 4 + .../GettingStartedWithAgents.csproj | 9 +- .../Orchestration/Step01_Concurrent.cs | 2 +- .../Orchestration/Step03_GroupChat.cs | 1 + .../Orchestration/Step04_Nested.cs | 6 +- .../AgentOrchestration.RequestActor.cs | 13 +- .../AgentOrchestration.ResultActor.cs | 12 +- .../Orchestration/AgentOrchestration.cs | 33 ++++-- .../{GroupChat => Chat}/ChatAgentActor.cs | 7 +- .../{GroupChat => Chat}/ChatGroup.cs | 2 +- .../{GroupChat => Chat}/ChatManagerActor.cs | 19 ++- .../{GroupChat => Chat}/ChatMessages.cs | 2 +- .../Concurrent/ConcurrentActor.cs | 5 +- .../Concurrent/ConcurrentOrchestration.cs | 13 +- .../Concurrent/ConcurrentResultActor.cs | 10 +- .../GroupChat/GroupChatManagerActor.cs | 1 + .../GroupChatOrchestration.String.cs | 1 + .../GroupChat/GroupChatOrchestration.cs | 11 +- .../Logging/AgentOrchestrationLogMessages.cs | 112 ++++++++++++++++++ .../Logging/ChatOrchestrationLogMessages.cs | 77 ++++++++++++ .../ConcurrentOrchestrationLogMessages.cs | 75 ++++++++++++ .../GroupChatOrchestrationLogMessages.cs | 44 +++++++ .../Logging/OrchestrationResultLogMessages.cs | 51 ++++++++ .../SequentialOrchestrationLogMessages.cs | 62 ++++++++++ .../Magentic/MagenticManagerActor.cs | 50 ++++++++ .../Magentic/MagenticOrchestration.String.cs | 26 ++++ .../Magentic/MagenticOrchestration.cs | 82 +++++++++++++ .../Agents/Orchestration/Orchestratable.cs | 4 +- .../Orchestration/OrchestrationResult.cs | 11 +- .../Sequential/SequentialActor.cs | 7 +- .../Sequential/SequentialMessage.cs | 4 +- .../SequentialOrchestration.String.cs | 2 +- .../Sequential/SequentialOrchestration.cs | 11 +- .../samples/InternalUtilities/BaseTest.cs | 4 - 35 files changed, 693 insertions(+), 94 deletions(-) rename dotnet/src/Agents/Orchestration/{GroupChat => Chat}/ChatAgentActor.cs (89%) rename dotnet/src/Agents/Orchestration/{GroupChat => Chat}/ChatGroup.cs (94%) rename dotnet/src/Agents/Orchestration/{GroupChat => Chat}/ChatManagerActor.cs (88%) rename dotnet/src/Agents/Orchestration/{GroupChat => Chat}/ChatMessages.cs (97%) create mode 100644 dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs create mode 100644 dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs create mode 100644 dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 33dbda999f2b..1a2eabc9c9d3 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -6,14 +6,14 @@ + + + - - - @@ -42,9 +42,9 @@ - - - + + + @@ -56,8 +56,8 @@ - + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 8ca5bfea1725..c9ed0b99ab66 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -1656,6 +1656,10 @@ Global {A5E6193C-8431-4C6E-B674-682CB41EAA0C} = {4F381919-F1BE-47D8-8558-3187ED04A84F} {E9A74E0C-BC02-4DDD-A487-89847EDF8026} = {4F381919-F1BE-47D8-8558-3187ED04A84F} {801C9CE4-53AF-D2DB-E0D6-9A6BB47E9654} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {12C7E0C7-A7DF-3BC3-0D4B-1A706BCE6981} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {B06770D5-2F3E-4271-9F6B-3AA9E716176F} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {08D84994-794A-760F-95FD-4EFA8998A16D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {D1A02387-FA60-22F8-C2ED-4676568B6CC3} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index f9655d1bf6de..d50025b705ce 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -9,8 +9,7 @@ true - - $(NoWarn);MSB3277;IDE1006;IDE0009;CS8618;CA1051;CA1050;CA1707;CA1054;CA2007;CA5394;VSTHRD111;CS1591;NU1605;RCS1110;RCS1243;SKEXP0001;SKEXP0010;SKEXP0020;SKEXP0040;SKEXP0050;SKEXP0060;SKEXP0070;SKEXP0101;SKEXP0110;OPENAI001 + $(NoWarn);IDE1006;IDE0009;CS8618;CA1051;CA1050;CA1707;CA1054;CA2007;CA5394;VSTHRD111;CS1591;NU1605;RCS1110;RCS1243;SKEXP0001;SKEXP0010;SKEXP0020;SKEXP0040;SKEXP0050;SKEXP0060;SKEXP0070;SKEXP0101;SKEXP0110;OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 @@ -26,9 +25,9 @@ - - - + + + diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index a9e111ab57e0..cfe225c8a1ad 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -22,7 +22,7 @@ public async Task SimpleConcurrentAsync() // Define the pattern InProcessRuntime runtime = new(); - ConcurrentOrchestration orchestration = new(runtime, agent1, agent2, agent3); + ConcurrentOrchestration orchestration = new(runtime, agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index ad2044844d02..16058dfec8c9 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -4,6 +4,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs index 85e3fc2de3c9..594a68d84c4f 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -29,7 +29,7 @@ public async Task NestSequentialGroupsAsync() ConcurrentOrchestration innerOrchestration = new(runtime, agent3, agent4) { - InputTransform = (SequentialMessage input) => ValueTask.FromResult(new ConcurrentMessages.Request { Message = input.Content }), + InputTransform = (SequentialMessage input) => ValueTask.FromResult(new ConcurrentMessages.Request { Message = input.Message }), ResultTransform = (ConcurrentMessages.Result[] output) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))))) }; SequentialOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); @@ -59,8 +59,8 @@ public async Task NestConcurrentGroupsAsync() SequentialOrchestration innerOrchestration = new(runtime, agent3, agent4) { - InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(new SequentialMessage { Content = input.Message }), - ResultTransform = (SequentialMessage result) => ValueTask.FromResult(new ConcurrentMessages.Result { Message = result.Content }) + InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(new SequentialMessage { Message = input.Message }), + ResultTransform = (SequentialMessage result) => ValueTask.FromResult(new ConcurrentMessages.Result { Message = result.Message }) }; ConcurrentOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index 15c9084d896c..6dd73abdb79c 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration; @@ -25,12 +25,14 @@ private sealed class RequestActor : PatternActor, IHandle /// The runtime associated with the agent. /// A function that transforms an input of type TInput into a source type TSource. /// An asynchronous function that processes the resulting source. + /// The logger to use for the actor public RequestActor( AgentId id, IAgentRuntime runtime, Func> transform, - Func action) - : base(id, runtime, $"{id.Type}_Actor") + Func action, + ILogger? logger = null) + : base(id, runtime, $"{id.Type}_Actor", logger) { this._transform = transform; this._action = action; @@ -44,16 +46,17 @@ public RequestActor( /// A ValueTask representing the asynchronous operation. public async ValueTask HandleAsync(TInput item, MessageContext messageContext) { - Trace.WriteLine($"> ORCHESTRATION ENTER: {this.Id.Type}"); + this.Logger.LogOrchestrationRequestInvoke(this.Id); try { TSource source = await this._transform.Invoke(item).ConfigureAwait(false); await this._action.Invoke(source).ConfigureAwait(false); + Logger.LogOrchestrationStart(this.Id); } catch (Exception exception) { - Trace.WriteLine($"ERROR: {exception.Message}"); // Log exception details and allow orchestration to fail + this.Logger.LogOrchestrationRequestFailure(this.Id, exception); throw; } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index c40199792f8c..9f19d0bf9e84 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration; @@ -25,12 +25,14 @@ private sealed class ResultActor : PatternActor, IHandle /// The runtime associated with the agent. /// A delegate that transforms a TResult instance into a TOutput instance. /// Optional TaskCompletionSource to signal orchestration completion. + /// The logger to use for the actor public ResultActor( AgentId id, IAgentRuntime runtime, Func> transform, - TaskCompletionSource? completionSource = null) - : base(id, runtime, $"{id.Type}_Actor") + TaskCompletionSource? completionSource = null, + ILogger? logger = null) + : base(id, runtime, $"{id.Type}_Actor", logger) { this._completionSource = completionSource; this._transform = transform; @@ -51,7 +53,7 @@ public ResultActor( /// A ValueTask representing asynchronous operation. public async ValueTask HandleAsync(TResult item, MessageContext messageContext) { - Trace.WriteLine($"> ORCHESTRATION EXIT: {this.Id.Type}"); + this.Logger.LogOrchestrationResultInvoke(this.Id); try { @@ -66,8 +68,8 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) } catch (Exception exception) { - Trace.WriteLine($"ERROR: {exception.Message}"); // Log exception details and fail orchestration as per design. + this.Logger.LogOrchestrationResultFailure(this.Id, exception); throw; } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index ee14267da1b6..091bc18f0da6 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Agents.Orchestration; @@ -42,6 +43,11 @@ protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] /// public string Description { get; init; } = string.Empty; + /// + /// Gets the associated logger. + /// + public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; + /// /// Transforms the orchestration input into a source input suitable for processing. /// @@ -69,23 +75,25 @@ protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] /// Optional timeout for the orchestration process. public async ValueTask> InvokeAsync(TInput input, TimeSpan? timeout = null) { + ILogger logger = this.LoggerFactory.CreateLogger(this.GetType()); + Verify.NotNull(input, nameof(input)); TopicId topic = new($"ID_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); TaskCompletionSource completion = new(); - Trace.WriteLine($"!!! ORCHESTRATION REGISTER: {topic}\n"); + logger.LogOrchestrationRegistration(this._orchestrationRoot, topic); - AgentType orchestrationType = await this.RegisterAsync(topic, completion).ConfigureAwait(false); + AgentType orchestrationType = await this.RegisterAsync(topic, completion, targetActor: null, logger).ConfigureAwait(false); - Trace.WriteLine($"\n!!! ORCHESTRATION INVOKE: {orchestrationType}\n"); + logger.LogOrchestrationInvoke(this._orchestrationRoot, topic); Task task = this.Runtime.SendMessageAsync(input, orchestrationType).AsTask(); - Trace.WriteLine($"\n!!! ORCHESTRATION YIELD: {orchestrationType}"); + logger.LogOrchestrationYield(this._orchestrationRoot, topic); - return new OrchestrationResult(topic, completion); + return new OrchestrationResult(topic, completion, logger); } /// @@ -110,7 +118,8 @@ public async ValueTask> InvokeAsync(TInput input, T /// The topic identifier for the orchestration session. /// The orchestration type used in registration. /// The entry AgentType for the orchestration, if any. - protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType); + /// The logger to use during registration + protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger); /// /// Registers the orchestration with the runtime using an external topic and an optional target actor. @@ -118,11 +127,12 @@ public async ValueTask> InvokeAsync(TInput input, T /// The external topic identifier to register with. /// An optional target actor that may influence registration behavior. /// A ValueTask containing the AgentType that indicates the registered agent. - protected internal override ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor) + /// The logger to use during registration + protected internal override ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor, ILogger logger) { TopicId orchestrationTopic = new($"{externalTopic.Type}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); - return this.RegisterAsync(orchestrationTopic, completion: null, targetActor); + return this.RegisterAsync(orchestrationTopic, completion: null, targetActor, logger); } /// @@ -144,8 +154,9 @@ protected async Task SubscribeAsync(string agentType, params TopicId[] topics) /// The unique topic for the orchestration session. /// A TaskCompletionSource for the final result output, if applicable. /// An optional target actor for routing results. + /// The logger to use during registration /// The AgentType representing the orchestration entry point. - private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor = null) + private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor, ILogger logger) { // %%% REQUIRED if (this.InputTransform == null) @@ -169,7 +180,7 @@ await this.Runtime.RegisterAgentFactoryAsync( })).ConfigureAwait(false); // Register orchestration members - AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal).ConfigureAwait(false); + AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal, logger).ConfigureAwait(false); // Register actor for orchestration entry-point AgentType orchestrationEntry = this.FormatAgentType(topic, "Boot"); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatAgentActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs similarity index 89% rename from dotnet/src/Agents/Orchestration/GroupChat/ChatAgentActor.cs rename to dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs index 2c2a1efb0263..ef51ffd5c4cd 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatAgentActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs @@ -5,8 +5,9 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; /// /// An used with the . @@ -51,11 +52,11 @@ public async ValueTask HandleAsync(ChatMessages.Reset item, MessageContext messa /// public async ValueTask HandleAsync(ChatMessages.Speak item, MessageContext messageContext) { - Trace.WriteLine($"> GROUPCHAT ACTOR: {this.Id.Type} SPEAK"); + this.Logger.LogChatAgentInvoke(this.Id); ChatMessageContent response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); - Trace.WriteLine($"> GROUPCHAT ACTOR: {this.Id.Type} OUTPUT - {response}"); + this.Logger.LogChatAgentResult(this.Id, response.Content); this._cache.Clear(); await this.PublishMessageAsync(response.ToGroup(), this._groupTopic).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs b/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs similarity index 94% rename from dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs rename to dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs index bbfac7114f6e..44213abc202a 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatGroup.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs @@ -4,7 +4,7 @@ using System.Linq; using Microsoft.AgentRuntime; -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; /// /// Describes a team of agents participating in a group chat. diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatManagerActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs similarity index 88% rename from dotnet/src/Agents/Orchestration/GroupChat/ChatManagerActor.cs rename to dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs index 31414e0c4d87..1a11c23b7882 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Microsoft.SemanticKernel.ChatCompletion; -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; /// /// An used to manage a . @@ -40,8 +40,6 @@ protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, Ag this.Chat = []; this.Team = team; this._orchestrationType = orchestrationType; - Trace.WriteLine($">>> MANAGER NAMES: {this.Team.FormatNames()}"); - Trace.WriteLine($">>> MANAGER TEAM:\n{this.Team.FormatList()}"); this._groupTopic = groupTopic; } @@ -65,7 +63,7 @@ protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, Ag /// protected ValueTask RequestAgentResponseAsync(AgentType agentType, CancellationToken cancellationToken) { - Trace.WriteLine($">>> MANAGER NEXT: {agentType}"); + this.Logger.LogChatManagerSelect(this.Id, agentType); return this.SendMessageAsync(new ChatMessages.Speak(), agentType, cancellationToken); } @@ -94,7 +92,7 @@ protected ValueTask RequestAgentResponseAsync(AgentType agentType, CancellationT /// public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext messageContext) { - Trace.WriteLine($">>> MANAGER TASK: {item.Message}"); + this.Logger.LogChatManagerInit(this.Id); this.InputTask = item; AgentType? agentType = await this.PrepareTaskAsync().ConfigureAwait(false); if (agentType != null) @@ -104,7 +102,7 @@ public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext m } else { - Trace.WriteLine(">>> MANAGER NO AGENT"); + this.Logger.LogChatManagerTerminate(this.Id); await this.SendMessageAsync(item.Message.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); // %%% PLACEHOLDER - FINAL MESSAGE } } @@ -112,7 +110,8 @@ public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext m /// public async ValueTask HandleAsync(ChatMessages.Group item, MessageContext messageContext) { - Trace.WriteLine($">>> MANAGER CHAT: {item.Message}"); + this.Logger.LogChatManagerInvoke(this.Id); + this.Chat.Add(item.Message); AgentType? agentType = await this.SelectAgentAsync().ConfigureAwait(false); if (agentType != null) @@ -121,7 +120,7 @@ public async ValueTask HandleAsync(ChatMessages.Group item, MessageContext messa } else { - Trace.WriteLine(">>> MANAGER NO AGENT"); + this.Logger.LogChatManagerTerminate(this.Id); await this.SendMessageAsync(item.Message.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); // %%% PLACEHOLDER - FINAL MESSAGE } } @@ -129,7 +128,7 @@ public async ValueTask HandleAsync(ChatMessages.Group item, MessageContext messa /// public ValueTask HandleAsync(ChatMessages.Result item, MessageContext messageContext) { - Trace.WriteLine($">>> MANAGER RESULT: {item.Message}"); + this.Logger.LogChatManagerResult(this.Id); return ValueTask.CompletedTask; } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs b/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs similarity index 97% rename from dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs rename to dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs index 2c5b362eaeb1..ed778ed2bb0a 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/ChatMessages.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; /// /// Common messages used for agent chat patterns. diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index 8e0fc6343846..52b572697190 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; @@ -30,11 +29,11 @@ public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType /// public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext) { - Trace.WriteLine($"> CONCURRENT ACTOR: {this.Id.Type} INPUT - {item.Message}"); + this.Logger.LogConcurrentAgentInvoke(this.Id, item.Message.Content); ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); - Trace.WriteLine($"> CONCURRENT ACTOR: {this.Id.Type} OUTPUT - {response}"); + this.Logger.LogConcurrentAgentResult(this.Id, response.Content); await this.SendMessageAsync(response.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 656dc0de45b5..561c68c04f49 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -25,12 +25,11 @@ public ConcurrentOrchestration(IAgentRuntime runtime, params OrchestrationTarget /// protected override ValueTask StartAsync(TopicId topic, ConcurrentMessages.Request input, AgentType? entryAgent) { - Trace.WriteLine($"> CONCURRENT START: {topic}"); return this.Runtime.PublishMessageAsync(input, topic); } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) { // Register result actor AgentType resultType = this.FormatAgentType(topic, "Results"); @@ -38,8 +37,8 @@ await this.Runtime.RegisterAgentFactoryAsync( resultType, (agentId, runtime) => ValueTask.FromResult( - new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count))).ConfigureAwait(false); - Trace.WriteLine($"> CONCURRENT RESULTS: {resultType}"); + new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + logger.LogConcurrentRegistration(resultType, "RESULTS"); // Register member actors - All agents respond to the same message. int agentCount = 0; @@ -55,10 +54,10 @@ await this.Runtime.RegisterAgentFactoryAsync( } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - memberType = await orchestration.RegisterAsync(topic, resultType).ConfigureAwait(false); + memberType = await orchestration.RegisterAsync(topic, resultType, logger).ConfigureAwait(false); } - Trace.WriteLine($"> CONCURRENT MEMBER #{agentCount}: {memberType}"); + logger.LogConcurrentRegistration(memberType, "MEMBER", agentCount); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs index a9611884f112..7b824ed36352 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Concurrent; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -27,12 +27,14 @@ internal sealed class ConcurrentResultActor : PatternActor, /// The runtime associated with the agent. /// Identifies the orchestration agent. /// The expected number of messages to be received. + /// The logger to use for the actor public ConcurrentResultActor( AgentId id, IAgentRuntime runtime, AgentType orchestrationType, - int expectedCount) - : base(id, runtime, "Captures the results of the ConcurrentOrchestration") + int expectedCount, + ILogger logger) + : base(id, runtime, "Captures the results of the ConcurrentOrchestration", logger) { this._orchestrationType = orchestrationType; this._expectedCount = expectedCount; @@ -42,7 +44,7 @@ public ConcurrentResultActor( /// public async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext) { - Trace.WriteLine($"> CONCURRENT RESULT: {this.Id.Type} (#{this._resultCount + 1}/{this._expectedCount})"); + this.Logger.LogConcurrentResultCapture(this.Id, this._resultCount + 1, this._expectedCount); this._results.Enqueue(item); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index 012fc7d5a872..e8eed5001d1f 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs index ea0ebb444d5f..540e8f2d7b5f 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 7348b6d3f097..c7ca6ff37a98 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -26,13 +27,11 @@ public GroupChatOrchestration(IAgentRuntime runtime, params OrchestrationTarget[ /// protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask input, AgentType? entryAgent) { - Trace.WriteLine($"> GROUPCHAT START: {topic} [{entryAgent}]"); - return this.Runtime.SendMessageAsync(input, entryAgent!.Value); } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) { AgentType managerType = this.FormatAgentType(topic, "Manager"); @@ -49,12 +48,12 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - memberType = await orchestration.RegisterAsync(topic, managerType).ConfigureAwait(false); + memberType = await orchestration.RegisterAsync(topic, managerType, logger).ConfigureAwait(false); } team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID - Trace.WriteLine($"> GROUPCHAT MEMBER #{agentCount}: {memberType}"); + logger.LogGroupChatRegistration(memberType, "MEMBER", agentCount); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs new file mode 100644 index 000000000000..a62efc82bc75 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class AgentOrchestrationLogMessages +{ + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Registering orchestration {Orchestration} for topic: {Topic}")] + public static partial void LogOrchestrationRegistration( + this ILogger logger, + string orchestration, + TopicId topic); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Invoking orchestration {Orchestration} for topic: {Topic}")] + public static partial void LogOrchestrationInvoke( + this ILogger logger, + string orchestration, + TopicId topic); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Yielding orchestration {Orchestration} for topic: {Topic}")] + public static partial void LogOrchestrationYield( + this ILogger logger, + string orchestration, + TopicId topic); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Orchestration started: {AgentId}")] + public static partial void LogOrchestrationStart( + this ILogger logger, + AgentId agentId); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Orchestration request actor initiating pattern: {AgentId}")] + public static partial void LogOrchestrationRequestInvoke( + this ILogger logger, + AgentId agentId); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Orchestration request actor failed: {AgentId}")] + public static partial void LogOrchestrationRequestFailure( + this ILogger logger, + AgentId agentId, + Exception exception); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Orchestration result actor finalizing pattern: {AgentId}")] + public static partial void LogOrchestrationResultInvoke( + this ILogger logger, + AgentId agentId); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Orchestration result actor failed: {AgentId}")] + public static partial void LogOrchestrationResultFailure( + this ILogger logger, + AgentId agentId, + Exception exception); +} diff --git a/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs new file mode 100644 index 000000000000..b34ee229414f --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class ChatOrchestrationLogMessages +{ + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat agent invoked [{AgentId}]")] + public static partial void LogChatAgentInvoke( + this ILogger logger, + AgentId agentId); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat agent result [{AgentId}]: {Message}")] + public static partial void LogChatAgentResult( + this ILogger logger, + AgentId agentId, + string? message); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat manager initialized [{AgentId}]")] + public static partial void LogChatManagerInit( + this ILogger logger, + AgentId agentId); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat manager invoked [{AgentId}]")] + public static partial void LogChatManagerInvoke( + this ILogger logger, + AgentId agentId); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat manager terminating [{AgentId}]")] + public static partial void LogChatManagerTerminate( + this ILogger logger, + AgentId agentId); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat manager final result [{AgentId}]")] + public static partial void LogChatManagerResult( + this ILogger logger, + AgentId agentId); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Chat manager selected agent [{AgentId}]: {NextAgent}")] + public static partial void LogChatManagerSelect( + this ILogger logger, + AgentId agentId, + AgentType nextAgent); +} diff --git a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs new file mode 100644 index 000000000000..2c761c3a4107 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class ConcurrentOrchestrationLogMessages +{ + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Concurrent agent invoked [{AgentId}]: {Message}")] + public static partial void LogConcurrentAgentInvoke( + this ILogger logger, + AgentId agentId, + string? message); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Concurrent agent result [{AgentId}]: {Message}")] + public static partial void LogConcurrentAgentResult( + this ILogger logger, + AgentId agentId, + string? message); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Concurrent actor registered [{AgentType}]: {label}")] + public static partial void LogConcurrentRegistration( + this ILogger logger, + AgentType agentType, + string label); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Concurrent actor registered [{AgentType}]: {label} #{Count}")] + public static partial void LogConcurrentRegistration( + this ILogger logger, + AgentType agentType, + string label, + int count); + + /// + /// Logs result capture. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Concurrent result captured [{AgentId}]: ({ResultCount} / {ExpectedCount})")] + public static partial void LogConcurrentResultCapture( + this ILogger logger, + AgentId agentId, + int resultCount, + int expectedCount); +} diff --git a/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs new file mode 100644 index 000000000000..7ba6af8b7eab --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class GroupChatOrchestrationLogMessages +{ + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "GroupChat actor registered [{AgentType}]: {label}")] + public static partial void LogGroupChatRegistration( + this ILogger logger, + AgentType agentType, + string label); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "GroupChat actor registered [{AgentType}]: {label} #{Count}")] + public static partial void LogGroupChatRegistration( + this ILogger logger, + AgentType agentType, + string label, + int count); +} diff --git a/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs new file mode 100644 index 000000000000..90194a135adf --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class OrchestrationResultLogMessages +{ + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Awaiting orchestration result for topic: {Topic}")] + public static partial void LogOrchestrationResultAwait( + this ILogger logger, + TopicId topic); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Orchestration result timeout for topic: {Topic}")] + public static partial void LogOrchestrationResultTimeout( + this ILogger logger, + TopicId topic); + + /// + /// Logs awaiting the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Orchestration result completed for topic: {Topic}")] + public static partial void LogOrchestrationResultComplete( + this ILogger logger, + TopicId topic); +} diff --git a/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs new file mode 100644 index 000000000000..ed6725677dbc --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class SequentialOrchestrationLogMessages +{ + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Sequential agent invoked [{AgentId}]: {Message}")] + public static partial void LogSequentialAgentInvoke( + this ILogger logger, + AgentId agentId, + string? message); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "Sequential agent result [{AgentId}]: {Message}")] + public static partial void LogSequentialAgentResult( + this ILogger logger, + AgentId agentId, + string? message); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Sequential actor registered [{AgentType}]: {label}")] + public static partial void LogSequentialRegistration( + this ILogger logger, + AgentType agentType, + string label); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Sequential actor registered [{AgentType}]: {label} #{Count}")] + public static partial void LogSequentialRegistration( + this ILogger logger, + AgentType agentType, + string label, + int count); +} diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs new file mode 100644 index 000000000000..96a9fd9109c7 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; + +/// +/// An used to manage a . +/// +internal sealed class MagenticManagerActor : ChatManagerActor +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// The team of agents being orchestrated + /// Identifies the orchestration agent. + /// The unique topic used to broadcast to the entire chat. + public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) + : base(id, runtime, team, orchestrationType, groupTopic) + { + } + + /// + protected override Task PrepareTaskAsync() + { + return this.SelectAgentAsync(); + } + + /// + protected override Task SelectAgentAsync() + { + // %%% PLACEHOLDER SELECTION LOGIC +#pragma warning disable CA5394 // Do not use insecure randomness + int index = Random.Shared.Next(this.Team.Count + 1); +#pragma warning restore CA5394 // Do not use insecure randomness + AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; + AgentType? agentType = null; + if (index < this.Team.Count) + { + agentType = agentTypes[index]; + } + return Task.FromResult(agentType); + } +} diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs new file mode 100644 index 000000000000..4e04b5ff2089 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; + +/// +/// An orchestration that broadcasts the input message to each agent. +/// +public sealed partial class MagenticOrchestration : MagenticOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents to be orchestrated. + public MagenticOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + : base(runtime, members) + { + this.InputTransform = (string input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input).ToInputTask()); + this.ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToString()); + } +} diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs new file mode 100644 index 000000000000..eac923c5578e --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; + +/// +/// An orchestration that coordinates a group-chat. +/// +public class MagenticOrchestration : + AgentOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// The agents participating in the orchestration. + public MagenticOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + : base(runtime, agents) + { + } + + /// + protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask input, AgentType? entryAgent) + { + return this.Runtime.SendMessageAsync(input, entryAgent!.Value); + } + + /// + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) + { + AgentType managerType = this.FormatAgentType(topic, "Manager"); + + int agentCount = 0; + ChatGroup team = []; + foreach (OrchestrationTarget member in this.Members) + { + ++agentCount; + + AgentType memberType = default; + if (member.IsAgent(out Agent? agent)) + { + memberType = await RegisterAgentAsync(agent).ConfigureAwait(false); + } + else if (member.IsOrchestration(out Orchestratable? orchestration)) + { + memberType = await orchestration.RegisterAsync(topic, managerType, logger).ConfigureAwait(false); + } + + team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID + + logger.LogGroupChatRegistration(memberType, "MEMBER", agentCount); + + await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); + } + + await this.Runtime.RegisterAgentFactoryAsync( + managerType, + (agentId, runtime) => + ValueTask.FromResult( + new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic))).ConfigureAwait(false); + + await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); + + return managerType; + + async ValueTask RegisterAgentAsync(Agent agent) + { + AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); + await this.Runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic))).ConfigureAwait(false); + + return agentType; + } + } +} diff --git a/dotnet/src/Agents/Orchestration/Orchestratable.cs b/dotnet/src/Agents/Orchestration/Orchestratable.cs index 7600e8397e62..f8d82f809f57 100644 --- a/dotnet/src/Agents/Orchestration/Orchestratable.cs +++ b/dotnet/src/Agents/Orchestration/Orchestratable.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration; @@ -16,6 +17,7 @@ public abstract class Orchestratable /// /// The topic identifier to be used for registration. /// An optional target actor type, if applicable, that may influence registration behavior. + /// The logger to use during registration /// A ValueTask containing the AgentType that indicates the registered agent. - protected internal abstract ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor); + protected internal abstract ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor, ILogger logger); } diff --git a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs index 25920b5109b3..929c08c1c368 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration; @@ -15,11 +15,13 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; public sealed class OrchestrationResult { private readonly TaskCompletionSource _completion; + private readonly ILogger _logger; - internal OrchestrationResult(TopicId topic, TaskCompletionSource completion) + internal OrchestrationResult(TopicId topic, TaskCompletionSource completion, ILogger logger) { this.Topic = topic; this._completion = completion; + this._logger = logger; } /// @@ -37,17 +39,20 @@ internal OrchestrationResult(TopicId topic, TaskCompletionSource complet /// Thrown if the orchestration does not complete within the specified timeout period. public async ValueTask GetValueAsync(TimeSpan? timeout = null) { - Trace.WriteLine($"\n!!! ORCHESTRATION AWAIT: {this.Topic}\n"); + this._logger.LogOrchestrationResultAwait(this.Topic); if (timeout.HasValue) { Task[] tasks = { this._completion.Task }; if (!Task.WaitAll(tasks, timeout.Value)) { + this._logger.LogOrchestrationResultTimeout(this.Topic); throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); } } + this._logger.LogOrchestrationResultComplete(this.Topic); + return await this._completion.Task.ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index 0fbf51e88c05..4718c162fad2 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -1,6 +1,5 @@ //// Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; @@ -30,11 +29,11 @@ public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType /// public async ValueTask HandleAsync(SequentialMessage item, MessageContext messageContext) { - Trace.WriteLine($"> SEQUENTIAL ACTOR: {this.Id.Type} INPUT - {item.Content}"); + this.Logger.LogSequentialAgentInvoke(this.Id, item.Message.Content); - ChatMessageContent response = await this.InvokeAsync(item.Content, messageContext.CancellationToken).ConfigureAwait(false); + ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); - Trace.WriteLine($"> SEQUENTIAL ACTOR: {this.Id.Type} OUTPUT - {response}"); + this.Logger.LogSequentialAgentResult(this.Id, item.Message.Content); await this.SendMessageAsync(SequentialMessage.FromChat(response), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs index 512c163cf5e9..fcce86311843 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs @@ -10,10 +10,10 @@ public sealed class SequentialMessage /// /// The input task. /// - public ChatMessageContent Content { get; init; } = new(); + public ChatMessageContent Message { get; init; } = new(); /// /// Extension method to convert a to a . /// - public static SequentialMessage FromChat(ChatMessageContent content) => new() { Content = content }; + public static SequentialMessage FromChat(ChatMessageContent content) => new() { Message = content }; } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs index bb84c150f472..f637765c4b4a 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs @@ -21,6 +21,6 @@ public SequentialOrchestration(IAgentRuntime runtime, params OrchestrationTarget : base(runtime, members) { this.InputTransform = (string input) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.User, input))); - this.ResultTransform = (SequentialMessage result) => ValueTask.FromResult(result.Content.ToString()); + this.ResultTransform = (SequentialMessage result) => ValueTask.FromResult(result.Message.ToString()); } } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 417874c46569..1b43d4f767a8 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; @@ -26,19 +26,16 @@ public SequentialOrchestration(IAgentRuntime runtime, params OrchestrationTarget /// protected override async ValueTask StartAsync(TopicId topic, SequentialMessage input, AgentType? entryAgent) { - Trace.WriteLine($"> SEQUENTIAL START: {topic} [{entryAgent}]"); - await this.Runtime.SendMessageAsync(input, entryAgent!.Value).ConfigureAwait(false); // NULL OVERRIDE } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) { // Each agent handsoff its result to the next agent. AgentType nextAgent = orchestrationType; for (int index = this.Members.Count - 1; index >= 0; --index) { - Trace.WriteLine($"> SEQUENTIAL NEXT #{index}: {nextAgent}"); OrchestrationTarget member = this.Members[index]; if (member.IsAgent(out Agent? agent)) @@ -47,9 +44,9 @@ protected override async ValueTask StartAsync(TopicId topic, SequentialMessage i } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - nextAgent = await orchestration.RegisterAsync(topic, nextAgent).ConfigureAwait(false); + nextAgent = await orchestration.RegisterAsync(topic, nextAgent, logger).ConfigureAwait(false); } - Trace.WriteLine($"> SEQUENTIAL MEMBER #{index}: {nextAgent}"); + logger.LogConcurrentRegistration(nextAgent, "MEMBER", index); } return nextAgent; diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 4582c7e83440..c30bdb430e64 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -90,10 +90,6 @@ protected BaseTest(ITestOutputHelper output, bool redirectSystemConsoleOutput = .AddUserSecrets(Assembly.GetExecutingAssembly()) .Build(); - TextWriterTraceListener traceListener = new(this); - Trace.Listeners.Clear(); - Trace.Listeners.Add(traceListener); - TestConfiguration.Initialize(configRoot); // Redirect System.Console output to the test output if requested From 7aba6bb8fab7cf64654b2053362382346a6dd4c2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Apr 2025 13:44:56 -0700 Subject: [PATCH 13/57] Log tuning --- .../Orchestration/Step01_Concurrent.cs | 6 +- .../Orchestration/Step02_Sequential.cs | 8 +- .../Orchestration/Step03_GroupChat.cs | 6 +- .../Orchestration/Step04_Nested.cs | 4 +- dotnet/src/Agents/Orchestration/AgentActor.cs | 5 +- .../AgentOrchestration.RequestActor.cs | 17 +++-- .../AgentOrchestration.ResultActor.cs | 10 ++- .../Orchestration/AgentOrchestration.cs | 20 ++--- .../Orchestration/Chat/ChatAgentActor.cs | 7 +- .../Orchestration/Chat/ChatManagerActor.cs | 6 +- .../Concurrent/ConcurrentActor.cs | 6 +- .../Concurrent/ConcurrentOrchestration.cs | 13 +++- .../GroupChat/GroupChatManagerActor.cs | 6 +- .../GroupChat/GroupChatOrchestration.cs | 14 +++- .../Logging/AgentOrchestrationLogMessages.cs | 75 +++++++++++++++---- .../Logging/ChatOrchestrationLogMessages.cs | 14 ++-- .../ConcurrentOrchestrationLogMessages.cs | 31 +------- .../GroupChatOrchestrationLogMessages.cs | 44 ----------- .../Logging/OrchestrationResultLogMessages.cs | 9 ++- .../SequentialOrchestrationLogMessages.cs | 29 +------ .../Magentic/MagenticManagerActor.cs | 6 +- .../Magentic/MagenticOrchestration.cs | 15 +++- .../Orchestration/OrchestrationResult.cs | 10 ++- .../Sequential/SequentialActor.cs | 8 +- .../Sequential/SequentialOrchestration.cs | 10 ++- 25 files changed, 190 insertions(+), 189 deletions(-) delete mode 100644 dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index cfe225c8a1ad..fbc6cb917587 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -50,7 +50,7 @@ public async Task NestedConcurrentAsync() ConcurrentOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); ConcurrentOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); - ConcurrentOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); + ConcurrentOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -72,7 +72,7 @@ public async Task SingleActorAsync() // Define the pattern InProcessRuntime runtime = new(); - ConcurrentOrchestration orchestration = new(runtime, agent); + ConcurrentOrchestration orchestration = new(runtime, agent) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -95,7 +95,7 @@ public async Task SingleNestedActorAsync() // Define the pattern InProcessRuntime runtime = new(); ConcurrentOrchestration orchestrationInner = CreateNested(runtime, agent); - ConcurrentOrchestration orchestrationOuter = new(runtime, orchestrationInner); + ConcurrentOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index 3fada2b7b09e..f4dcfe0b4c65 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -22,7 +22,7 @@ public async Task SimpleSequentailAsync() // Define the pattern InProcessRuntime runtime = new(); - SequentialOrchestration orchestration = new(runtime, agent1, agent2, agent3); + SequentialOrchestration orchestration = new(runtime, agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -49,7 +49,7 @@ public async Task NestedSequentailAsync() SequentialOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); SequentialOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); - SequentialOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight); + SequentialOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -71,7 +71,7 @@ public async Task SingleActorAsync() // Define the pattern InProcessRuntime runtime = new(); - SequentialOrchestration orchestration = new(runtime, agent); + SequentialOrchestration orchestration = new(runtime, agent) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -94,7 +94,7 @@ public async Task SingleNestedActorAsync() // Define the pattern InProcessRuntime runtime = new(); SequentialOrchestration orchestrationInner = CreateNested(runtime, agent); - SequentialOrchestration orchestrationOuter = new(runtime, orchestrationInner); + SequentialOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 16058dfec8c9..c114362ef047 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -25,7 +25,7 @@ public async Task SimpleGroupChatAsync() // Define the pattern InProcessRuntime runtime = new(); - GroupChatOrchestration orchestration = new(runtime, agent1, agent2, agent3); + GroupChatOrchestration orchestration = new(runtime, agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -49,7 +49,7 @@ public async Task SingleActorAsync() // Define the pattern InProcessRuntime runtime = new(); - GroupChatOrchestration orchestration = new(runtime, agent); + GroupChatOrchestration orchestration = new(runtime, agent) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -76,7 +76,7 @@ public async Task SingleNestedActorAsync() InputTransform = (ChatMessages.InputTask input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input.Message.ToString()).ToInputTask()), ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToResult()) }; - GroupChatOrchestration orchestrationOuter = new(runtime, orchestrationInner); + GroupChatOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs index 594a68d84c4f..4e70ff94a9ef 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -32,7 +32,7 @@ public async Task NestSequentialGroupsAsync() InputTransform = (SequentialMessage input) => ValueTask.FromResult(new ConcurrentMessages.Request { Message = input.Message }), ResultTransform = (ConcurrentMessages.Result[] output) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))))) }; - SequentialOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); + SequentialOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -62,7 +62,7 @@ public async Task NestConcurrentGroupsAsync() InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(new SequentialMessage { Message = input.Message }), ResultTransform = (SequentialMessage result) => ValueTask.FromResult(new ConcurrentMessages.Result { Message = result.Message }) }; - ConcurrentOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2); + ConcurrentOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index b44ced295b67..517abe881096 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -25,12 +25,13 @@ public abstract class AgentActor : PatternActor /// The runtime associated with the agent. /// An . /// Option to automatically clean-up agent thread - protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThread = false) + /// The logger to use for the actor + protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThread = false, ILogger? logger = null) : base( id, runtime, VerifyDescription(agent), - GetLogger(agent)) + logger ?? GetLogger(agent)) { this.Agent = agent; this.NoThread = noThread; diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index 6dd73abdb79c..17f427a6c52d 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -15,25 +15,29 @@ public abstract partial class AgentOrchestration private sealed class RequestActor : PatternActor, IHandle { + private readonly string _orchestrationRoot; private readonly Func> _transform; - private readonly Func _action; + private readonly Func _action; /// /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// // %%% COMMENT /// A function that transforms an input of type TInput into a source type TSource. /// An asynchronous function that processes the resulting source. /// The logger to use for the actor public RequestActor( AgentId id, IAgentRuntime runtime, + string orchestrationRoot, Func> transform, - Func action, + Func action, ILogger? logger = null) : base(id, runtime, $"{id.Type}_Actor", logger) { + this._orchestrationRoot = orchestrationRoot; this._transform = transform; this._action = action; } @@ -46,17 +50,18 @@ public RequestActor( /// A ValueTask representing the asynchronous operation. public async ValueTask HandleAsync(TInput item, MessageContext messageContext) { - this.Logger.LogOrchestrationRequestInvoke(this.Id); + this.Logger.LogOrchestrationRequestInvoke(this._orchestrationRoot, this.Id); try { TSource source = await this._transform.Invoke(item).ConfigureAwait(false); - await this._action.Invoke(source).ConfigureAwait(false); - Logger.LogOrchestrationStart(this.Id); + Task task = this._action.Invoke(source).AsTask(); + this.Logger.LogOrchestrationStart(this._orchestrationRoot, this.Id); + await task.ConfigureAwait(false); } catch (Exception exception) { // Log exception details and allow orchestration to fail - this.Logger.LogOrchestrationRequestFailure(this.Id, exception); + this.Logger.LogOrchestrationRequestFailure(this._orchestrationRoot, this.Id, exception); throw; } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index 9f19d0bf9e84..3a2c18e12719 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -16,6 +16,7 @@ public abstract partial class AgentOrchestration { private readonly TaskCompletionSource? _completionSource; + private readonly string _orchestrationRoot; private readonly Func> _transform; /// @@ -23,18 +24,21 @@ private sealed class ResultActor : PatternActor, IHandle /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// // %%% COMMENT /// A delegate that transforms a TResult instance into a TOutput instance. /// Optional TaskCompletionSource to signal orchestration completion. /// The logger to use for the actor public ResultActor( AgentId id, IAgentRuntime runtime, + string orchestrationRoot, Func> transform, TaskCompletionSource? completionSource = null, - ILogger? logger = null) + ILogger? logger = null) : base(id, runtime, $"{id.Type}_Actor", logger) { this._completionSource = completionSource; + this._orchestrationRoot = orchestrationRoot; this._transform = transform; } @@ -53,7 +57,7 @@ public ResultActor( /// A ValueTask representing asynchronous operation. public async ValueTask HandleAsync(TResult item, MessageContext messageContext) { - this.Logger.LogOrchestrationResultInvoke(this.Id); + this.Logger.LogOrchestrationResultInvoke(this._orchestrationRoot, this.Id); try { @@ -69,7 +73,7 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) catch (Exception exception) { // Log exception details and fail orchestration as per design. - this.Logger.LogOrchestrationResultFailure(this.Id, exception); + this.Logger.LogOrchestrationResultFailure(this._orchestrationRoot, this.Id, exception); throw; } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 091bc18f0da6..d18a7109feed 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -22,15 +22,16 @@ public abstract partial class AgentOrchestration /// Initializes a new instance of the class. /// + /// // %%% COMMENT /// The runtime associated with the orchestration. /// Specifies the member agents or orchestrations participating in this orchestration. - protected AgentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) + protected AgentOrchestration(string name, IAgentRuntime runtime, params OrchestrationTarget[] members) { Verify.NotNull(runtime, nameof(runtime)); this.Runtime = runtime; this.Members = members; - this._orchestrationRoot = this.GetType().Name.Split('`').First(); + this._orchestrationRoot = name; } /// @@ -83,8 +84,6 @@ public async ValueTask> InvokeAsync(TInput input, T TaskCompletionSource completion = new(); - logger.LogOrchestrationRegistration(this._orchestrationRoot, topic); - AgentType orchestrationType = await this.RegisterAsync(topic, completion, targetActor: null, logger).ConfigureAwait(false); logger.LogOrchestrationInvoke(this._orchestrationRoot, topic); @@ -93,7 +92,7 @@ public async ValueTask> InvokeAsync(TInput input, T logger.LogOrchestrationYield(this._orchestrationRoot, topic); - return new OrchestrationResult(topic, completion, logger); + return new OrchestrationResult(this._orchestrationRoot, topic, completion, logger); } /// @@ -154,11 +153,12 @@ protected async Task SubscribeAsync(string agentType, params TopicId[] topics) /// The unique topic for the orchestration session. /// A TaskCompletionSource for the final result output, if applicable. /// An optional target actor for routing results. - /// The logger to use during registration + /// The orchestration logger (for use during registration) /// The AgentType representing the orchestration entry point. private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor, ILogger logger) { - // %%% REQUIRED + logger.LogOrchestrationRegistrationStart(this._orchestrationRoot, topic); + if (this.InputTransform == null) { throw new InvalidOperationException("InputTransform must be set before invoking the orchestration."); @@ -174,7 +174,7 @@ await this.Runtime.RegisterAgentFactoryAsync( orchestrationFinal, (agentId, runtime) => ValueTask.FromResult( - new ResultActor(agentId, runtime, this.ResultTransform, completion) + new ResultActor(agentId, runtime, this._orchestrationRoot, this.ResultTransform, completion, this.LoggerFactory.CreateLogger()) { CompletionTarget = targetActor, })).ConfigureAwait(false); @@ -188,9 +188,11 @@ await this.Runtime.RegisterAgentFactoryAsync( orchestrationEntry, (agentId, runtime) => ValueTask.FromResult( - new RequestActor(agentId, runtime, this.InputTransform, async (TSource source) => await this.StartAsync(topic, source, entryAgent).ConfigureAwait(false))) + new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), this.LoggerFactory.CreateLogger())) ).ConfigureAwait(false); + logger.LogOrchestrationRegistrationDone(this._orchestrationRoot, topic); + return orchestrationEntry; } } diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs index ef51ffd5c4cd..f3a71fc2b673 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; @@ -28,8 +28,9 @@ internal sealed class ChatAgentActor : /// The runtime associated with the agent. /// An . /// The unique topic used to broadcast to the entire chat. - public ChatAgentActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic) - : base(id, runtime, agent) + /// The logger to use for the actor + public ChatAgentActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic, ILogger? logger = null) + : base(id, runtime, agent, noThread: false, logger) { this._cache = []; this._groupTopic = groupTopic; diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs index 1a11c23b7882..dd9ecb622c7b 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Microsoft.SemanticKernel.ChatCompletion; @@ -34,8 +35,9 @@ public abstract class ChatManagerActor : /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. - protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) - : base(id, runtime, DefaultDescription) + /// The logger to use for the actor + protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) + : base(id, runtime, DefaultDescription, logger) { this.Chat = []; this.Team = team; diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index 52b572697190..e78438bdf9c4 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -20,8 +21,9 @@ internal sealed class ConcurrentActor : AgentActor, IHandleThe runtime associated with the agent. /// An . /// Identifies the orchestration agent. - public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType) : - base(id, runtime, agent, noThread: true) + /// The logger to use for the actor + public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType, ILogger? logger = null) : + base(id, runtime, agent, noThread: true, logger) { this._orchestrationType = orchestrationType; } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 561c68c04f49..8cfc590faafa 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -12,13 +14,15 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; public class ConcurrentOrchestration : AgentOrchestration { + internal static readonly string OrchestrationName = typeof(ConcurrentOrchestration<,>).Name.Split('`').First(); + /// /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents participating in the orchestration. public ConcurrentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) - : base(runtime, agents) + : base(OrchestrationName, runtime, agents) { } @@ -38,7 +42,7 @@ await this.Runtime.RegisterAgentFactoryAsync( (agentId, runtime) => ValueTask.FromResult( new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); - logger.LogConcurrentRegistration(resultType, "RESULTS"); + logger.LogRegisterActor(OrchestrationName, resultType, "RESULTS"); // Register member actors - All agents respond to the same message. int agentCount = 0; @@ -57,7 +61,7 @@ await this.Runtime.RegisterAgentFactoryAsync( memberType = await orchestration.RegisterAsync(topic, resultType, logger).ConfigureAwait(false); } - logger.LogConcurrentRegistration(memberType, "MEMBER", agentCount); + logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } @@ -67,10 +71,11 @@ await this.Runtime.RegisterAgentFactoryAsync( async ValueTask RegisterAgentAsync(Agent agent) { AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); + ILogger loggerActor = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => - ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType))).ConfigureAwait(false); + ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType, loggerActor))).ConfigureAwait(false); return agentType; } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index e8eed5001d1f..cf4d2e0f80db 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -21,8 +22,9 @@ internal sealed class GroupChatManagerActor : ChatManagerActor /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. - public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) - : base(id, runtime, team, orchestrationType, groupTopic) + /// The logger to use for the actor + public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) + : base(id, runtime, team, orchestrationType, groupTopic, logger) { } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index c7ca6ff37a98..de6459954f28 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -14,13 +16,15 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; public class GroupChatOrchestration : AgentOrchestration { + internal static readonly string OrchestrationName = typeof(ConcurrentOrchestration<,>).Name.Split('`').First(); + /// /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents participating in the orchestration. public GroupChatOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) - : base(runtime, agents) + : base(OrchestrationName, runtime, agents) { } @@ -53,16 +57,17 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID - logger.LogGroupChatRegistration(memberType, "MEMBER", agentCount); + logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } + ILogger loggerManager = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic))).ConfigureAwait(false); + new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, loggerManager))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); @@ -71,10 +76,11 @@ await this.Runtime.RegisterAgentFactoryAsync( async ValueTask RegisterAgentAsync(Agent agent) { AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); + ILogger loggerActor = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic))).ConfigureAwait(false); + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerActor))).ConfigureAwait(false); return agentType; } diff --git a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs index a62efc82bc75..23efa5f35990 100644 --- a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs @@ -23,90 +23,135 @@ internal static partial class AgentOrchestrationLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Registering orchestration {Orchestration} for topic: {Topic}")] - public static partial void LogOrchestrationRegistration( + Message = "REGISTER {Orchestration} Start: {Topic}")] + public static partial void LogOrchestrationRegistrationStart( this ILogger logger, string orchestration, TopicId topic); + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "REGISTER ACTOR {Orchestration} {label}: {AgentType}")] + public static partial void LogRegisterActor( + this ILogger logger, + string orchestration, + AgentType agentType, + string label); + + /// + /// Logs actor registration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "REGISTER ACTOR {Orchestration} {label} #{Count}: {AgentType}")] + public static partial void LogRegisterActor( + this ILogger logger, + string orchestration, + AgentType agentType, + string label, + int count); + /// /// Logs awaiting the orchestration. /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "REGISTER {Orchestration} Complete: {Topic}")] + public static partial void LogOrchestrationRegistrationDone( + this ILogger logger, + string orchestration, + TopicId topic); + + /// + /// Logs orchestration invocation. + /// [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "Invoking orchestration {Orchestration} for topic: {Topic}")] + Message = "INVOKE {Orchestration}: {Topic}")] public static partial void LogOrchestrationInvoke( this ILogger logger, string orchestration, TopicId topic); /// - /// Logs awaiting the orchestration. + /// Logs that the orchestration + /// has started successfully and yielded control back to the caller. /// [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Yielding orchestration {Orchestration} for topic: {Topic}")] + Message = "YIELD {Orchestration}: {Topic}")] public static partial void LogOrchestrationYield( this ILogger logger, string orchestration, TopicId topic); /// - /// Logs actor registration. + /// Logs the start of the outer orchestration. /// [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "Orchestration started: {AgentId}")] + Message = "START {Orchestration}: {AgentId}")] public static partial void LogOrchestrationStart( this ILogger logger, + string orchestration, AgentId agentId); /// - /// Logs awaiting the orchestration. + /// %%% COMMENT /// [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "Orchestration request actor initiating pattern: {AgentId}")] + Message = "INIT {Orchestration}: {AgentId}")] public static partial void LogOrchestrationRequestInvoke( this ILogger logger, + string orchestration, AgentId agentId); /// - /// Logs awaiting the orchestration. + /// %%% COMMENT /// [LoggerMessage( EventId = 0, Level = LogLevel.Error, - Message = "Orchestration request actor failed: {AgentId}")] + Message = "{Orchestration} request failed: {AgentId}")] public static partial void LogOrchestrationRequestFailure( this ILogger logger, + string orchestration, AgentId agentId, Exception exception); /// - /// Logs awaiting the orchestration. + /// %%% COMMENT /// [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "Orchestration result actor finalizing pattern: {AgentId}")] + Message = "EXIT {Orchestration}: {AgentId}")] public static partial void LogOrchestrationResultInvoke( this ILogger logger, + string orchestration, AgentId agentId); /// - /// Logs awaiting the orchestration. + /// %%% COMMENT /// [LoggerMessage( EventId = 0, Level = LogLevel.Error, - Message = "Orchestration result actor failed: {AgentId}")] + Message = "{Orchestration} result failed: {AgentId}")] public static partial void LogOrchestrationResultFailure( this ILogger logger, + string orchestration, AgentId agentId, Exception exception); } diff --git a/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs index b34ee229414f..ba2f7a7f7294 100644 --- a/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs @@ -20,7 +20,7 @@ internal static partial class ChatOrchestrationLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat agent invoked [{AgentId}]")] + Message = "CHAT AGENT invoked [{AgentId}]")] public static partial void LogChatAgentInvoke( this ILogger logger, AgentId agentId); @@ -28,7 +28,7 @@ public static partial void LogChatAgentInvoke( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat agent result [{AgentId}]: {Message}")] + Message = "CHAT AGENT result [{AgentId}]: {Message}")] public static partial void LogChatAgentResult( this ILogger logger, AgentId agentId, @@ -37,7 +37,7 @@ public static partial void LogChatAgentResult( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat manager initialized [{AgentId}]")] + Message = "CHAT MANAGER initialized [{AgentId}]")] public static partial void LogChatManagerInit( this ILogger logger, AgentId agentId); @@ -45,7 +45,7 @@ public static partial void LogChatManagerInit( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat manager invoked [{AgentId}]")] + Message = "CHAT MANAGER invoked [{AgentId}]")] public static partial void LogChatManagerInvoke( this ILogger logger, AgentId agentId); @@ -53,7 +53,7 @@ public static partial void LogChatManagerInvoke( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat manager terminating [{AgentId}]")] + Message = "CHAT MANAGER terminating [{AgentId}]")] public static partial void LogChatManagerTerminate( this ILogger logger, AgentId agentId); @@ -61,7 +61,7 @@ public static partial void LogChatManagerTerminate( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat manager final result [{AgentId}]")] + Message = "CHAT MANAGER answer [{AgentId}]")] public static partial void LogChatManagerResult( this ILogger logger, AgentId agentId); @@ -69,7 +69,7 @@ public static partial void LogChatManagerResult( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Chat manager selected agent [{AgentId}]: {NextAgent}")] + Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")] public static partial void LogChatManagerSelect( this ILogger logger, AgentId agentId, diff --git a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs index 2c761c3a4107..6faae339a0fc 100644 --- a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs @@ -20,7 +20,7 @@ internal static partial class ConcurrentOrchestrationLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Concurrent agent invoked [{AgentId}]: {Message}")] + Message = "REQUEST Concurrent agent [{AgentId}]: {Message}")] public static partial void LogConcurrentAgentInvoke( this ILogger logger, AgentId agentId, @@ -29,44 +29,19 @@ public static partial void LogConcurrentAgentInvoke( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Concurrent agent result [{AgentId}]: {Message}")] + Message = "RESULT Concurrent agent [{AgentId}]: {Message}")] public static partial void LogConcurrentAgentResult( this ILogger logger, AgentId agentId, string? message); - /// - /// Logs actor registration. - /// - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "Concurrent actor registered [{AgentType}]: {label}")] - public static partial void LogConcurrentRegistration( - this ILogger logger, - AgentType agentType, - string label); - - /// - /// Logs actor registration. - /// - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "Concurrent actor registered [{AgentType}]: {label} #{Count}")] - public static partial void LogConcurrentRegistration( - this ILogger logger, - AgentType agentType, - string label, - int count); - /// /// Logs result capture. /// [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "Concurrent result captured [{AgentId}]: ({ResultCount} / {ExpectedCount})")] + Message = "COLLECT Concurrent result [{AgentId}]: ({ResultCount} / {ExpectedCount})")] public static partial void LogConcurrentResultCapture( this ILogger logger, AgentId agentId, diff --git a/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs deleted file mode 100644 index 7ba6af8b7eab..000000000000 --- a/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.AgentRuntime; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// Extensions for logging . -/// -/// -/// This extension uses the to -/// generate logging code at compile time to achieve optimized code. -/// -[ExcludeFromCodeCoverage] -internal static partial class GroupChatOrchestrationLogMessages -{ - /// - /// Logs actor registration. - /// - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "GroupChat actor registered [{AgentType}]: {label}")] - public static partial void LogGroupChatRegistration( - this ILogger logger, - AgentType agentType, - string label); - - /// - /// Logs actor registration. - /// - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "GroupChat actor registered [{AgentType}]: {label} #{Count}")] - public static partial void LogGroupChatRegistration( - this ILogger logger, - AgentType agentType, - string label, - int count); -} diff --git a/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs index 90194a135adf..4f5b94eb3fe3 100644 --- a/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs @@ -22,9 +22,10 @@ internal static partial class OrchestrationResultLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Awaiting orchestration result for topic: {Topic}")] + Message = "AWAIT {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultAwait( this ILogger logger, + string orchestration, TopicId topic); /// @@ -33,9 +34,10 @@ public static partial void LogOrchestrationResultAwait( [LoggerMessage( EventId = 0, Level = LogLevel.Error, - Message = "Orchestration result timeout for topic: {Topic}")] + Message = "TIMEOUT {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultTimeout( this ILogger logger, + string orchestration, TopicId topic); /// @@ -44,8 +46,9 @@ public static partial void LogOrchestrationResultTimeout( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Orchestration result completed for topic: {Topic}")] + Message = "COMPLETE {Orchestration}: {Topic}")] public static partial void LogOrchestrationResultComplete( this ILogger logger, + string orchestration, TopicId topic); } diff --git a/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs index ed6725677dbc..dbae0f46aee0 100644 --- a/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs @@ -20,7 +20,7 @@ internal static partial class SequentialOrchestrationLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Sequential agent invoked [{AgentId}]: {Message}")] + Message = "REQUEST Sequential agent [{AgentId}]: {Message}")] public static partial void LogSequentialAgentInvoke( this ILogger logger, AgentId agentId, @@ -29,34 +29,9 @@ public static partial void LogSequentialAgentInvoke( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "Sequential agent result [{AgentId}]: {Message}")] + Message = "RESULT Sequential agent [{AgentId}]: {Message}")] public static partial void LogSequentialAgentResult( this ILogger logger, AgentId agentId, string? message); - - /// - /// Logs actor registration. - /// - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "Sequential actor registered [{AgentType}]: {label}")] - public static partial void LogSequentialRegistration( - this ILogger logger, - AgentType agentType, - string label); - - /// - /// Logs actor registration. - /// - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "Sequential actor registered [{AgentType}]: {label} #{Count}")] - public static partial void LogSequentialRegistration( - this ILogger logger, - AgentType agentType, - string label, - int count); } diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs index 96a9fd9109c7..9aafeb1bfd57 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; @@ -21,8 +22,9 @@ internal sealed class MagenticManagerActor : ChatManagerActor /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. - public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic) - : base(id, runtime, team, orchestrationType, groupTopic) + /// The logger to use for the actor + public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) + : base(id, runtime, team, orchestrationType, groupTopic, logger) { } diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs index eac923c5578e..1b6c1c6f1627 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; @@ -14,13 +17,15 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; public class MagenticOrchestration : AgentOrchestration { + internal static readonly string OrchestrationName = typeof(ConcurrentOrchestration<,>).Name.Split('`').First(); + /// /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents participating in the orchestration. public MagenticOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) - : base(runtime, agents) + : base(OrchestrationName, runtime, agents) { } @@ -53,16 +58,17 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID - logger.LogGroupChatRegistration(memberType, "MEMBER", agentCount); + logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } + ILogger loggerManager = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic))).ConfigureAwait(false); + new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic, loggerManager))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); @@ -71,10 +77,11 @@ await this.Runtime.RegisterAgentFactoryAsync( async ValueTask RegisterAgentAsync(Agent agent) { AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); + ILogger loggerActor = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic))).ConfigureAwait(false); + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerActor))).ConfigureAwait(false); return agentType; } diff --git a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs index 929c08c1c368..934f15be68c4 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs @@ -14,11 +14,13 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// The type of the value produced by the orchestration. public sealed class OrchestrationResult { + private readonly string _orchestration; private readonly TaskCompletionSource _completion; private readonly ILogger _logger; - internal OrchestrationResult(TopicId topic, TaskCompletionSource completion, ILogger logger) + internal OrchestrationResult(string orchestration, TopicId topic, TaskCompletionSource completion, ILogger logger) { + this._orchestration = orchestration; this.Topic = topic; this._completion = completion; this._logger = logger; @@ -39,19 +41,19 @@ internal OrchestrationResult(TopicId topic, TaskCompletionSource complet /// Thrown if the orchestration does not complete within the specified timeout period. public async ValueTask GetValueAsync(TimeSpan? timeout = null) { - this._logger.LogOrchestrationResultAwait(this.Topic); + this._logger.LogOrchestrationResultAwait(this._orchestration, this.Topic); if (timeout.HasValue) { Task[] tasks = { this._completion.Task }; if (!Task.WaitAll(tasks, timeout.Value)) { - this._logger.LogOrchestrationResultTimeout(this.Topic); + this._logger.LogOrchestrationResultTimeout(this._orchestration, this.Topic); throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); } } - this._logger.LogOrchestrationResultComplete(this.Topic); + this._logger.LogOrchestrationResultComplete(this._orchestration, this.Topic); return await this._completion.Task.ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index 4718c162fad2..7d5d0d99224d 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; +using Microsoft.Extensions.Logging; namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; @@ -20,8 +21,9 @@ internal sealed class SequentialActor : AgentActor, IHandle /// The runtime associated with the agent. /// An . /// The identifier of the next agent for which to handoff the result - public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent) - : base(id, runtime, agent, noThread: true) + /// The logger to use for the actor + public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent, ILogger? logger = null) + : base(id, runtime, agent, noThread: true, logger) { this._nextAgent = nextAgent; } @@ -33,7 +35,7 @@ public async ValueTask HandleAsync(SequentialMessage item, MessageContext messag ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); - this.Logger.LogSequentialAgentResult(this.Id, item.Message.Content); + this.Logger.LogSequentialAgentResult(this.Id, response.Content); await this.SendMessageAsync(SequentialMessage.FromChat(response), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 1b43d4f767a8..08e8efdb69a4 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.Extensions.Logging; @@ -13,13 +14,15 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// public class SequentialOrchestration : AgentOrchestration { + internal static readonly string OrchestrationName = typeof(SequentialOrchestration<,>).Name.Split('`').First(); + /// /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. /// The agents participating in the orchestration. public SequentialOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) - : base(runtime, agents) + : base(OrchestrationName, runtime, agents) { } @@ -46,7 +49,7 @@ protected override async ValueTask StartAsync(TopicId topic, SequentialMessage i { nextAgent = await orchestration.RegisterAsync(topic, nextAgent, logger).ConfigureAwait(false); } - logger.LogConcurrentRegistration(nextAgent, "MEMBER", index); + logger.LogRegisterActor(OrchestrationName, nextAgent, "MEMBER", index + 1); } return nextAgent; @@ -54,9 +57,10 @@ protected override async ValueTask StartAsync(TopicId topic, SequentialMessage i async Task RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, Agent agent) { AgentType agentType = this.GetAgentType(topic, index); + ILogger loggerActor = this.LoggerFactory.CreateLogger(); return await this.Runtime.RegisterAgentFactoryAsync( agentType, - (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent))).ConfigureAwait(false); + (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent, loggerActor))).ConfigureAwait(false); } } From 3a5d2f4c71075d31d83250e6a26ea4841d3deaef Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Apr 2025 13:46:54 -0700 Subject: [PATCH 14/57] Namespace clean-up --- dotnet/src/Agents/Orchestration/AgentOrchestration.cs | 1 - .../Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs | 1 - .../src/Agents/Orchestration/Magentic/MagenticOrchestration.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index d18a7109feed..d718a57c47c4 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 8cfc590faafa..a2781a9cb952 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.AgentRuntime; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs index 1b6c1c6f1627..b124e4cbd4bd 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs @@ -7,7 +7,6 @@ using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; -using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; From 274f951ea3be50dc209821509758c779af9a901a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 16 Apr 2025 15:42:00 -0700 Subject: [PATCH 15/57] Clean-up patterns --- .../Orchestration/AgentOrchestration.cs | 32 +++++++++---------- .../Orchestration/Chat/ChatAgentActor.cs | 2 +- .../Concurrent/ConcurrentActor.cs | 2 +- .../Concurrent/ConcurrentOrchestration.cs | 15 ++++----- .../GroupChat/GroupChatManagerActor.cs | 4 +-- .../GroupChat/GroupChatOrchestration.cs | 18 ++++------- .../Magentic/MagenticManagerActor.cs | 2 +- .../Magentic/MagenticOrchestration.cs | 18 ++++------- .../Sequential/SequentialActor.cs | 2 +- .../Sequential/SequentialOrchestration.cs | 11 +++---- 10 files changed, 47 insertions(+), 59 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index d718a57c47c4..842df90bd6df 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -168,27 +168,27 @@ private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSo } // Register actor for final result - AgentType orchestrationFinal = this.FormatAgentType(topic, "Root"); - await this.Runtime.RegisterAgentFactoryAsync( - orchestrationFinal, - (agentId, runtime) => - ValueTask.FromResult( - new ResultActor(agentId, runtime, this._orchestrationRoot, this.ResultTransform, completion, this.LoggerFactory.CreateLogger()) - { - CompletionTarget = targetActor, - })).ConfigureAwait(false); + AgentType orchestrationFinal = + await this.Runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(topic, "Root"), + (agentId, runtime) => + ValueTask.FromResult( + new ResultActor(agentId, runtime, this._orchestrationRoot, this.ResultTransform, completion, this.LoggerFactory.CreateLogger()) + { + CompletionTarget = targetActor, + })).ConfigureAwait(false); // Register orchestration members AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal, logger).ConfigureAwait(false); // Register actor for orchestration entry-point - AgentType orchestrationEntry = this.FormatAgentType(topic, "Boot"); - await this.Runtime.RegisterAgentFactoryAsync( - orchestrationEntry, - (agentId, runtime) => - ValueTask.FromResult( - new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), this.LoggerFactory.CreateLogger())) - ).ConfigureAwait(false); + AgentType orchestrationEntry = + await this.Runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(topic, "Boot"), + (agentId, runtime) => + ValueTask.FromResult( + new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), this.LoggerFactory.CreateLogger())) + ).ConfigureAwait(false); logger.LogOrchestrationRegistrationDone(this._orchestrationRoot, topic); diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs index f3a71fc2b673..089971d723d9 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs @@ -29,7 +29,7 @@ internal sealed class ChatAgentActor : /// An . /// The unique topic used to broadcast to the entire chat. /// The logger to use for the actor - public ChatAgentActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic, ILogger? logger = null) + public ChatAgentActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic, ILogger? logger = null) : base(id, runtime, agent, noThread: false, logger) { this._cache = []; diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index e78438bdf9c4..af61ecf0cc0e 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -22,7 +22,7 @@ internal sealed class ConcurrentActor : AgentActor, IHandleAn . /// Identifies the orchestration agent. /// The logger to use for the actor - public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType, ILogger? logger = null) : + public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType, ILogger? logger = null) : base(id, runtime, agent, noThread: true, logger) { this._orchestrationType = orchestrationType; diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index a2781a9cb952..af464ca768ac 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -67,16 +67,13 @@ await this.Runtime.RegisterAgentFactoryAsync( return null; - async ValueTask RegisterAgentAsync(Agent agent) + ValueTask RegisterAgentAsync(Agent agent) { - AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); - ILogger loggerActor = this.LoggerFactory.CreateLogger(); - await this.Runtime.RegisterAgentFactoryAsync( - agentType, - (agentId, runtime) => - ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType, loggerActor))).ConfigureAwait(false); - - return agentType; + return + this.Runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(topic, $"Agent_{agentCount}"), + (agentId, runtime) => + ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType, this.LoggerFactory.CreateLogger()))); } } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index cf4d2e0f80db..4b76924c6f98 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -12,7 +12,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An used to manage a . /// -internal sealed class GroupChatManagerActor : ChatManagerActor +internal sealed class GroupChatManagerActor : ChatManagerActor // %%% ABSTRACT { /// /// Initializes a new instance of the class. @@ -23,7 +23,7 @@ internal sealed class GroupChatManagerActor : ChatManagerActor /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. /// The logger to use for the actor - public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) + public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) : base(id, runtime, team, orchestrationType, groupTopic, logger) { } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index de6459954f28..53c59195a444 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -62,27 +62,23 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } - ILogger loggerManager = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, loggerManager))).ConfigureAwait(false); + new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); return managerType; - async ValueTask RegisterAgentAsync(Agent agent) + ValueTask RegisterAgentAsync(Agent agent) { - AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); - ILogger loggerActor = this.LoggerFactory.CreateLogger(); - await this.Runtime.RegisterAgentFactoryAsync( - agentType, - (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerActor))).ConfigureAwait(false); - - return agentType; + return + this.Runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(topic, $"Agent_{agentCount}"), + (agentId, runtime) => + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, this.LoggerFactory.CreateLogger()))); } } } diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs index 9aafeb1bfd57..c6994d1ab542 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs @@ -23,7 +23,7 @@ internal sealed class MagenticManagerActor : ChatManagerActor /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. /// The logger to use for the actor - public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) + public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) : base(id, runtime, team, orchestrationType, groupTopic, logger) { } diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs index b124e4cbd4bd..ac6b5fcd5a63 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs @@ -62,27 +62,23 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); } - ILogger loggerManager = this.LoggerFactory.CreateLogger(); await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic, loggerManager))).ConfigureAwait(false); + new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); return managerType; - async ValueTask RegisterAgentAsync(Agent agent) + ValueTask RegisterAgentAsync(Agent agent) { - AgentType agentType = this.FormatAgentType(topic, $"Agent_{agentCount}"); - ILogger loggerActor = this.LoggerFactory.CreateLogger(); - await this.Runtime.RegisterAgentFactoryAsync( - agentType, - (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerActor))).ConfigureAwait(false); - - return agentType; + return + this.Runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(topic, $"Agent_{agentCount}"), + (agentId, runtime) => + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, this.LoggerFactory.CreateLogger()))); } } } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index 7d5d0d99224d..0ae9f135cd92 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -22,7 +22,7 @@ internal sealed class SequentialActor : AgentActor, IHandle /// An . /// The identifier of the next agent for which to handoff the result /// The logger to use for the actor - public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent, ILogger? logger = null) + public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent, ILogger? logger = null) : base(id, runtime, agent, noThread: true, logger) { this._nextAgent = nextAgent; diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 08e8efdb69a4..f9546a6a72de 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -54,13 +54,12 @@ protected override async ValueTask StartAsync(TopicId topic, SequentialMessage i return nextAgent; - async Task RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, Agent agent) + ValueTask RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, Agent agent) { - AgentType agentType = this.GetAgentType(topic, index); - ILogger loggerActor = this.LoggerFactory.CreateLogger(); - return await this.Runtime.RegisterAgentFactoryAsync( - agentType, - (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent, loggerActor))).ConfigureAwait(false); + return + this.Runtime.RegisterAgentFactoryAsync( + this.GetAgentType(topic, index), + (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent, this.LoggerFactory.CreateLogger()))); } } From 872a761c088607d6f179418d4ecec939c39fb994 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:08:21 -0700 Subject: [PATCH 16/57] Stable? --- dotnet/Directory.Packages.props | 19 +- dotnet/SK-dotnet.sln | 57 +++ .../Abstractions.Tests/AgentIdTests.cs | 121 +++++ .../Abstractions.Tests/AgentMetaDataTests.cs | 22 + .../Abstractions.Tests/AgentProxyTests.cs | 90 ++++ .../Abstractions.Tests/AgentTypeTests.cs | 64 +++ .../Abstractions.Tests/MessageContextTests.cs | 81 +++ .../Runtime.Abstractions.Tests.csproj | 32 ++ .../Abstractions.Tests/TopicIdTests.cs | 184 +++++++ .../Agents/Runtime/Abstractions/AgentId.cs | 136 ++++++ .../Runtime/Abstractions/AgentMetadata.cs | 57 +++ .../Agents/Runtime/Abstractions/AgentProxy.cs | 84 ++++ .../Agents/Runtime/Abstractions/AgentType.cs | 97 ++++ .../Exceptions/CantHandleException.cs | 31 ++ .../Exceptions/MessageDroppedException.cs | 31 ++ .../Exceptions/NotAccessibleException.cs | 31 ++ .../Exceptions/UndeliverableException.cs | 31 ++ .../src/Agents/Runtime/Abstractions/IAgent.cs | 34 ++ .../Runtime/Abstractions/IAgentRuntime.cs | 120 +++++ .../Runtime/Abstractions/IHostableAgent.cs | 16 + .../Agents/Runtime/Abstractions/ISaveState.cs | 33 ++ .../Abstractions/ISubscriptionDefinition.cs | 52 ++ .../Internal/KeyValueParserExtensions.cs | 52 ++ .../Runtime/Abstractions/MessageContext.cs | 45 ++ .../Abstractions/Runtime.Abstractions.csproj | 42 ++ .../Agents/Runtime/Abstractions/TopicId.cs | 149 ++++++ .../Core.Tests/AgentRuntimeExtensionsTests.cs | 153 ++++++ .../Core.Tests/AgentsAppBuilderTests.cs | 167 +++++++ .../Runtime/Core.Tests/AgentsAppTests.cs | 276 +++++++++++ .../Runtime/Core.Tests/BaseAgentTests.cs | 352 ++++++++++++++ .../Core.Tests/Runtime.Core.Tests.csproj | 33 ++ .../TypePrefixSubscriptionAttributeTests.cs | 54 ++ .../Core.Tests/TypePrefixSubscriptionTests.cs | 233 +++++++++ .../TypeSubscriptionAttributeTests.cs | 54 ++ .../Core.Tests/TypeSubscriptionTests.cs | 190 ++++++++ .../Runtime/Core/AgentRuntimeExtensions.cs | 132 +++++ dotnet/src/Agents/Runtime/Core/AgentsApp.cs | 102 ++++ .../Agents/Runtime/Core/AgentsAppBuilder.cs | 135 +++++ dotnet/src/Agents/Runtime/Core/BaseAgent.cs | 162 ++++++ dotnet/src/Agents/Runtime/Core/IHandle.cs | 35 ++ .../Runtime/Core/Internal/HandlerInvoker.cs | 135 +++++ .../Agents/Runtime/Core/Runtime.Core.csproj | 43 ++ .../Runtime/Core/TypePrefixSubscription.cs | 107 ++++ .../Core/TypePrefixSubscriptionAttribute.cs | 27 + .../Agents/Runtime/Core/TypeSubscription.cs | 106 ++++ .../Runtime/Core/TypeSubscriptionAttribute.cs | 27 + .../InProcess.Tests/InProcessRuntimeTests.cs | 342 +++++++++++++ .../InProcess.Tests/MessageDeliveryTests.cs | 77 +++ .../InProcess.Tests/MessageEnvelopeTests.cs | 101 ++++ .../InProcess.Tests/MessagingTestFixture.cs | 200 ++++++++ .../InProcess.Tests/PublishMessageTests.cs | 125 +++++ .../InProcess.Tests/ResultSinkTests.cs | 104 ++++ .../Runtime.InProcess.Tests.csproj | 33 ++ .../InProcess.Tests/SendMessageTests.cs | 94 ++++ .../Runtime/InProcess.Tests/TestAgents.cs | 45 ++ .../InProcess.Tests/TestSubscription.cs | 34 ++ .../Runtime/InProcess/InProcessRuntime.cs | 460 ++++++++++++++++++ .../Runtime/InProcess/MessageDelivery.cs | 16 + .../Runtime/InProcess/MessageEnvelope.cs | 75 +++ .../Agents/Runtime/InProcess/ResultSink.cs | 55 +++ .../InProcess/Runtime.InProcess.csproj | 36 ++ .../src/System/ValueTaskExtensions.cs | 45 ++ 62 files changed, 6068 insertions(+), 8 deletions(-) create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj create mode 100644 dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/AgentId.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/AgentType.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/IAgent.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs create mode 100644 dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj create mode 100644 dotnet/src/Agents/Runtime/Abstractions/TopicId.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs create mode 100644 dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs create mode 100644 dotnet/src/Agents/Runtime/Core/AgentsApp.cs create mode 100644 dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs create mode 100644 dotnet/src/Agents/Runtime/Core/BaseAgent.cs create mode 100644 dotnet/src/Agents/Runtime/Core/IHandle.cs create mode 100644 dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs create mode 100644 dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj create mode 100644 dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs create mode 100644 dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs create mode 100644 dotnet/src/Agents/Runtime/Core/TypeSubscription.cs create mode 100644 dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess/ResultSink.cs create mode 100644 dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj create mode 100644 dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index faa6bae454a1..9950fcc9affd 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,15 +5,16 @@ true + + + + - - - @@ -57,8 +58,8 @@ - + @@ -89,6 +90,7 @@ + @@ -96,18 +98,18 @@ - + - + + + @@ -123,6 +125,7 @@ + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index b7202e21a1b1..acf61a114486 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -533,6 +533,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents.Proc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents.Grpc", "samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.Grpc\ProcessWithCloudEvents.Grpc.csproj", "{08D84994-794A-760F-95FD-4EFA8998A16D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Runtime", "Runtime", "{A70ED5A7-F8E1-4A57-9455-3C05989542DA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Abstractions", "src\Agents\Runtime\Abstractions\Runtime.Abstractions.csproj", "{B9C86C5D-EB4C-8A16-E567-27025AC59A28}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Abstractions.Tests", "src\Agents\Runtime\Abstractions.Tests\Runtime.Abstractions.Tests.csproj", "{BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Core", "src\Agents\Runtime\Core\Runtime.Core.csproj", "{19DC60E6-AD08-4BCB-A4DF-B80E0941B458}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.Core.Tests", "src\Agents\Runtime\Core.Tests\Runtime.Core.Tests.csproj", "{A4F05541-7D23-A5A9-033D-382F1E13D0FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.InProcess", "src\Agents\Runtime\InProcess\Runtime.InProcess.csproj", "{CCC909E4-5269-A31E-0BFD-4863B4B29BBB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.InProcess.Tests", "src\Agents\Runtime\InProcess.Tests\Runtime.InProcess.Tests.csproj", "{DA6B4ED4-ED0B-D25C-889C-9F940E714891}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1458,6 +1472,42 @@ Global {08D84994-794A-760F-95FD-4EFA8998A16D}.Publish|Any CPU.Build.0 = Release|Any CPU {08D84994-794A-760F-95FD-4EFA8998A16D}.Release|Any CPU.ActiveCfg = Release|Any CPU {08D84994-794A-760F-95FD-4EFA8998A16D}.Release|Any CPU.Build.0 = Release|Any CPU + {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Publish|Any CPU.Build.0 = Release|Any CPU + {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9C86C5D-EB4C-8A16-E567-27025AC59A28}.Release|Any CPU.Build.0 = Release|Any CPU + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Publish|Any CPU.Build.0 = Release|Any CPU + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4}.Release|Any CPU.Build.0 = Release|Any CPU + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Publish|Any CPU.Build.0 = Release|Any CPU + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458}.Release|Any CPU.Build.0 = Release|Any CPU + {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Publish|Any CPU.Build.0 = Release|Any CPU + {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4F05541-7D23-A5A9-033D-382F1E13D0FE}.Release|Any CPU.Build.0 = Release|Any CPU + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Publish|Any CPU.Build.0 = Release|Any CPU + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB}.Release|Any CPU.Build.0 = Release|Any CPU + {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Publish|Any CPU.ActiveCfg = Release|Any CPU + {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Publish|Any CPU.Build.0 = Release|Any CPU + {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA6B4ED4-ED0B-D25C-889C-9F940E714891}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1656,6 +1706,13 @@ Global {7C092DD9-9985-4D18-A817-15317D984149} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {7C092DD9-9985-4D18-A817-15317D984149} {08D84994-794A-760F-95FD-4EFA8998A16D} = {7C092DD9-9985-4D18-A817-15317D984149} + {A70ED5A7-F8E1-4A57-9455-3C05989542DA} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} + {B9C86C5D-EB4C-8A16-E567-27025AC59A28} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} + {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} + {19DC60E6-AD08-4BCB-A4DF-B80E0941B458} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} + {A4F05541-7D23-A5A9-033D-382F1E13D0FE} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} + {CCC909E4-5269-A31E-0BFD-4863B4B29BBB} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} + {DA6B4ED4-ED0B-D25C-889C-9F940E714891} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs new file mode 100644 index 000000000000..f10da7c9a3f8 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentIdTests.cs +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.Abstractions.Tests; + +[Trait("Category", "Unit")] +public class AgentIdTests() +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("invalid\u007Fkey")] // DEL character (127) is outside ASCII 32-126 range + [InlineData("invalid\u0000key")] // NULL character is outside ASCII 32-126 range + [InlineData("invalid\u0010key")] // Control character is outside ASCII 32-126 range + [InlineData("InvalidKey💀")] // Control character is outside ASCII 32-126 range + public void AgentIdShouldThrowArgumentExceptionWithInvalidKey(string? invalidKey) + { + // Act & Assert + ArgumentException exception = Assert.Throws(() => new AgentId("validType", invalidKey!)); + Assert.Contains("Invalid AgentId key", exception.Message); + } + + [Fact] + public void AgentIdShouldInitializeCorrectlyTest() + { + AgentId agentId = new("TestType", "TestKey"); + + agentId.Type.Should().Be("TestType"); + agentId.Key.Should().Be("TestKey"); + } + + [Fact] + public void AgentIdShouldConvertFromTupleTest() + { + (string, string) agentTuple = ("TupleType", "TupleKey"); + AgentId agentId = new(agentTuple); + + agentId.Type.Should().Be("TupleType"); + agentId.Key.Should().Be("TupleKey"); + } + + [Fact] + public void AgentIdShouldConvertFromAgentType() + { + AgentType agentType = "TestType"; + AgentId agentId = new(agentType, "TestKey"); + + agentId.Type.Should().Be("TestType"); + agentId.Key.Should().Be("TestKey"); + } + + [Fact] + public void AgentIdShouldParseFromStringTest() + { + AgentId agentId = AgentId.FromStr("ParsedType/ParsedKey"); + + agentId.Type.Should().Be("ParsedType"); + agentId.Key.Should().Be("ParsedKey"); + } + + [Fact] + public void AgentIdShouldCompareEqualityCorrectlyTest() + { + AgentId agentId1 = new("SameType", "SameKey"); + AgentId agentId2 = new("SameType", "SameKey"); + AgentId agentId3 = new("DifferentType", "DifferentKey"); + + agentId1.Should().Be(agentId2); + agentId1.Should().NotBe(agentId3); + (agentId1 == agentId2).Should().BeTrue(); + (agentId1 != agentId3).Should().BeTrue(); + } + + [Fact] + public void AgentIdShouldGenerateCorrectHashCodeTest() + { + AgentId agentId1 = new("HashType", "HashKey"); + AgentId agentId2 = new("HashType", "HashKey"); + AgentId agentId3 = new("DifferentType", "DifferentKey"); + + agentId1.GetHashCode().Should().Be(agentId2.GetHashCode()); + agentId1.GetHashCode().Should().NotBe(agentId3.GetHashCode()); + } + + [Fact] + public void AgentIdShouldConvertExplicitlyFromStringTest() + { + AgentId agentId = (AgentId)"ConvertedType/ConvertedKey"; + + agentId.Type.Should().Be("ConvertedType"); + agentId.Key.Should().Be("ConvertedKey"); + } + + [Fact] + public void AgentIdShouldReturnCorrectToStringTest() + { + AgentId agentId = new("ToStringType", "ToStringKey"); + + agentId.ToString().Should().Be("ToStringType/ToStringKey"); + } + + [Fact] + public void AgentIdShouldCompareInequalityForWrongTypeTest() + { + AgentId agentId1 = new("Type1", "Key1"); + + (!agentId1.Equals(Guid.NewGuid())).Should().BeTrue(); + } + + [Fact] + public void AgentIdShouldCompareInequalityCorrectlyTest() + { + AgentId agentId1 = new("Type1", "Key1"); + AgentId agentId2 = new("Type2", "Key2"); + + (agentId1 != agentId2).Should().BeTrue(); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs new file mode 100644 index 000000000000..4a22551c43d3 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentMetaDataTests.cs +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.Abstractions.Tests; + +[Trait("Category", "Unit")] +public class AgentMetadataTests() +{ + [Fact] + public void AgentMetadataShouldInitializeCorrectlyTest() + { + // Arrange & Act + AgentMetadata metadata = new("TestType", "TestKey", "TestDescription"); + + // Assert + metadata.Type.Should().Be("TestType"); + metadata.Key.Should().Be("TestKey"); + metadata.Description.Should().Be("TestDescription"); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs new file mode 100644 index 000000000000..21fd2b686add --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentProxyTests.cs + +using System.Text.Json; +using Moq; +using Xunit; + +namespace Microsoft.AgentRuntime.Abstractions.Tests; + +[Trait("Category", "Unit")] +public class AgentProxyTests +{ + private readonly Mock mockRuntime; + private readonly AgentId agentId; + private readonly AgentProxy agentProxy; + + public AgentProxyTests() + { + this.mockRuntime = new Mock(); + this.agentId = new AgentId("testType", "testKey"); + this.agentProxy = new AgentProxy(this.agentId, this.mockRuntime.Object); + } + + [Fact] + public void IdMatchesAgentIdTest() + { + // Assert + Assert.Equal(this.agentId, this.agentProxy.Id); + } + + [Fact] + public void MetadataShouldMatchAgentTest() + { + AgentMetadata expectedMetadata = new("testType", "testKey", "testDescription"); + this.mockRuntime.Setup(r => r.GetAgentMetadataAsync(this.agentId)) + .ReturnsAsync(expectedMetadata); + + Assert.Equal(expectedMetadata, this.agentProxy.Metadata); + } + + [Fact] + public async Task SendMessageResponseTest() + { + // Arrange + object message = new { Content = "Hello" }; + AgentId sender = new("senderType", "senderKey"); + object response = new { Content = "Response" }; + + this.mockRuntime.Setup(r => r.SendMessageAsync(message, this.agentId, sender, null, It.IsAny())) + .ReturnsAsync(response); + + // Act + object? result = await this.agentProxy.SendMessageAsync(message, sender); + + // Assert + Assert.Equal(response, result); + } + + [Fact] + public async Task LoadStateTest() + { + // Arrange + JsonElement state = JsonDocument.Parse("{\"key\":\"value\"}").RootElement; + + this.mockRuntime.Setup(r => r.LoadAgentStateAsync(this.agentId, state)) + .Returns(ValueTask.CompletedTask); + + // Act + await this.agentProxy.LoadStateAsync(state); + + // Assert + this.mockRuntime.Verify(r => r.LoadAgentStateAsync(this.agentId, state), Times.Once); + } + + [Fact] + public async Task SaveStateTest() + { + // Arrange + JsonElement expectedState = JsonDocument.Parse("{\"key\":\"value\"}").RootElement; + + this.mockRuntime.Setup(r => r.SaveAgentStateAsync(this.agentId)) + .ReturnsAsync(expectedState); + + // Act + JsonElement result = await this.agentProxy.SaveStateAsync(); + + // Assert + Assert.Equal(expectedState, result); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs new file mode 100644 index 000000000000..bd4b0ac2a514 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentTypeTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.Abstractions.Tests; + +[Trait("Category", "Unit")] +public class AgentTypeTests +{ + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("invalid type")] // Agent type must only contain alphanumeric letters or underscores + [InlineData("123invalidType")] // Agent type cannot start with a number + [InlineData("invalid@type")] // Agent type must only contain alphanumeric letters or underscores + [InlineData("invalid-type")] // Agent type cannot alphanumeric underscores. + public void AgentIdShouldThrowArgumentExceptionWithInvalidType(string? invalidType) + { + // Act & Assert + ArgumentException exception = Assert.Throws(() => new AgentType(invalidType!)); + Assert.Contains("Invalid AgentId type", exception.Message); + } + + [Fact] + public void ImplicitConversionFromStringTest() + { + // Arrange + string agentTypeName = "TestAgent"; + + // Act + AgentType agentType = agentTypeName; + + // Assert + Assert.Equal(agentTypeName, agentType.Name); + } + + [Fact] + public void ImplicitConversionToStringTest() + { + // Arrange + AgentType agentType = "TestAgent"; + + // Act + string agentTypeName = agentType; + + // Assert + Assert.Equal("TestAgent", agentTypeName); + } + + [Fact] + public void ExplicitConversionFromTypeTest() + { + // Arrange + Type type = typeof(string); + + // Act + AgentType agentType = (AgentType)type; + + // Assert + Assert.Equal(type.Name, agentType.Name); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs new file mode 100644 index 000000000000..3d5033e120c7 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageContextTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.Abstractions.Tests; + +[Trait("Category", "Unit")] +public class MessageContextTests +{ + [Fact] + public void ConstructWithMessageIdAndCancellationTokenTest() + { + // Arrange + string messageId = Guid.NewGuid().ToString(); + CancellationToken cancellationToken = new(); + + // Act + MessageContext messageContext = new(messageId, cancellationToken); + + // Assert + Assert.Equal(messageId, messageContext.MessageId); + Assert.Equal(cancellationToken, messageContext.CancellationToken); + } + + [Fact] + public void ConstructWithCancellationTokenTest() + { + // Arrange + CancellationToken cancellationToken = new(); + + // Act + MessageContext messageContext = new(cancellationToken); + + // Assert + Assert.NotNull(messageContext.MessageId); + Assert.Equal(cancellationToken, messageContext.CancellationToken); + } + + [Fact] + public void AssignSenderTest() + { + // Arrange + MessageContext messageContext = new(new CancellationToken()); + AgentId sender = new("type", "key"); + + // Act + messageContext.Sender = sender; + + // Assert + Assert.Equal(sender, messageContext.Sender); + } + + [Fact] + public void AssignTopicTest() + { + // Arrange + MessageContext messageContext = new(new CancellationToken()); + TopicId topic = new("type", "source"); + + // Act + messageContext.Topic = topic; + + // Assert + Assert.Equal(topic, messageContext.Topic); + } + + [Fact] + public void AssignIsRpcPropertyTest() + { + // Arrange + MessageContext messageContext = new(new CancellationToken()) + { + // Act + IsRpc = true + }; + + // Assert + Assert.True(messageContext.IsRpc); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj b/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj new file mode 100644 index 000000000000..95fafae135c4 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj @@ -0,0 +1,32 @@ + + + + Microsoft.Agents.Runtime.Abstractions.UnitTests + Microsoft.Agents.Runtime.Abstractions.UnitTests + net8.0 + enable + enable + True + $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001 + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs new file mode 100644 index 000000000000..71a9e4ca6723 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft. All rights reserved. +// TopicIdTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.Abstractions.Tests; + +[Trait("Category", "Unit")] +public class TopicIdTests +{ + [Fact] + public void ConstrWithTypeOnlyTest() + { + // Arrange & Act + TopicId topicId = new("testtype"); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal(TopicId.DefaultSource, topicId.Source); + } + + [Fact] + public void ConstrucWithTypeAndSourceTest() + { + // Arrange & Act + TopicId topicId = new("testtype", "customsource"); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Fact] + public void ConstructWithTupleTest() + { + // Arrange + (string, string) tuple = ("testtype", "customsource"); + + // Act + TopicId topicId = new(tuple); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Fact] + public void ConvertFromStringTest() + { + // Arrange + const string topicIdStr = "testtype/customsource"; + + // Act + TopicId topicId = TopicId.FromStr(topicIdStr); + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Theory] + [InlineData("invalid-format")] + [InlineData("too/many/parts")] + [InlineData("")] + public void InvalidFormatFromStringThrowsTest(string invalidInput) + { + // Act & Assert + Assert.Throws(() => TopicId.FromStr(invalidInput)); + } + + [Fact] + public void ToStringTest() + { + // Arrange + TopicId topicId = new("testtype", "customsource"); + + // Act + string result = topicId.ToString(); + + // Assert + Assert.Equal("testtype/customsource", result); + } + + [Fact] + public void EqualityTest() + { + // Arrange + TopicId topicId1 = new("testtype", "customsource"); + TopicId topicId2 = new("testtype", "customsource"); + + // Act & Assert + Assert.True(topicId1.Equals(topicId2)); + Assert.True(topicId1.Equals((object)topicId2)); + } + + [Fact] + public void InequalityTest() + { + // Arrange + TopicId topicId1 = new("testtype1", "source1"); + TopicId topicId2 = new("testtype2", "source2"); + TopicId topicId3 = new("testtype1", "source2"); + TopicId topicId4 = new("testtype2", "source1"); + + // Act & Assert + Assert.False(topicId1.Equals(topicId2)); + Assert.False(topicId1.Equals(topicId3)); + Assert.False(topicId1.Equals(topicId4)); + } + + [Fact] + public void NullEqualityTest() + { + // Arrange + TopicId topicId = new("testtype", "customsource"); + + // Act & Assert + Assert.False(topicId.Equals(null)); + } + + [Fact] + public void DifferentTypeEqualityTest() + { + // Arrange + TopicId topicId = new("testtype", "customsource"); + const string differentType = "not-a-topic-id"; + + // Act & Assert + Assert.False(topicId.Equals(differentType)); + } + + [Fact] + public void GetHashCodeTest() + { + // Arrange + TopicId topicId1 = new("testtype", "customsource"); + TopicId topicId2 = new("testtype", "customsource"); + + // Act + int hash1 = topicId1.GetHashCode(); + int hash2 = topicId2.GetHashCode(); + + // Assert + Assert.Equal(hash1, hash2); + } + + [Fact] + public void ExplicitConverstionTest() + { + // Arrange + string topicIdStr = "testtype/customsource"; + + // Act + TopicId topicId = (TopicId)topicIdStr; + + // Assert + Assert.Equal("testtype", topicId.Type); + Assert.Equal("customsource", topicId.Source); + } + + [Fact] + public void IsWildcardMatchTest() + { + // Arrange + TopicId topicId1 = new("testtype", "source1"); + TopicId topicId2 = new("testtype", "source2"); + + // Act & Assert + Assert.True(topicId1.IsWildcardMatch(topicId2)); + Assert.True(topicId2.IsWildcardMatch(topicId1)); + } + + [Fact] + public void IsWildcardMismatchTest() + { + // Arrange + TopicId topicId1 = new("testtype1", "source"); + TopicId topicId2 = new("testtype2", "source"); + + // Act & Assert + Assert.False(topicId1.IsWildcardMatch(topicId2)); + Assert.False(topicId2.IsWildcardMatch(topicId1)); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs new file mode 100644 index 000000000000..d3f496aad222 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentId.cs + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using Microsoft.AgentRuntime.Internal; + +namespace Microsoft.AgentRuntime; + +/// +/// 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. +/// \ +/// +/// See the Python equivalent: +/// AgentId in AutoGen (Python). +/// +[DebuggerDisplay($"AgentId(type=\"{{{nameof(Type)}}}\", key=\"{{{nameof(Key)}}}\")")] +public struct AgentId : IEquatable +{ + /// + /// The default source value used when no source is explicitly provided. + /// + public const string DefaultKey = "default"; + + private static readonly Regex KeyRegex = new(@"^[\x20-\x7E]+$", RegexOptions.Compiled); // ASCII 32-126 + + /// + /// 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 (_). + /// + public string Type { get; } + + /// + /// Agent instance identifier. + /// Strings may only be composed of alphanumeric letters (a-z) and (0-9), or underscores (_). + /// + public string Key { get; } + + internal static Regex KeyRegex1 => KeyRegex2; + + internal static Regex KeyRegex2 => KeyRegex; + + /// + /// Initializes a new instance of the struct. + /// + /// The agent type. + /// Agent instance identifier. + 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; + } + + /// + /// Initializes a new instance of the struct from a tuple. + /// + /// A tuple containing the agent type and key. + public AgentId((string Type, string Key) kvPair) + : this(kvPair.Type, kvPair.Key) + { + } + + /// + /// Initializes a new instance of the struct from an . + /// + /// The agent type. + /// Agent instance identifier. + public AgentId(AgentType type, string key) + : this(type.Name, key) + { + } + + /// + /// Convert a string of the format "type/key" into an . + /// + /// The agent ID string. + /// An instance of . + public static AgentId FromStr(string maybeAgentId) => new(maybeAgentId.ToKeyValuePair(nameof(Type), nameof(Key))); + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/key". + public override readonly string ToString() => $"{this.Type}/{this.Key}"; + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current ; otherwise, false. + public override readonly bool Equals([NotNullWhen(true)] object? obj) + { + return (obj is AgentId other && this.Equals(other)); + } + + /// + public readonly bool Equals(AgentId other) + { + return this.Type == other.Type && this.Key == other.Key; + } + + /// + /// Returns a hash code for this . + /// + /// A hash code for the current instance. + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Type, this.Key); + } + + /// + /// Explicitly converts a string to an . + /// + /// The string representation of an agent ID. + /// An instance of . + public static explicit operator AgentId(string id) => FromStr(id); + + /// + /// Equality operator for . + /// + public static bool operator ==(AgentId left, AgentId right) => left.Equals(right); + + /// + /// Inequality operator for . + /// + public static bool operator !=(AgentId left, AgentId right) => !left.Equals(right); +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs new file mode 100644 index 000000000000..c9b5b1a3f571 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentMetadata.cs + +namespace Microsoft.AgentRuntime; + +/// +/// Represents metadata associated with an agent, including its type, unique key, and description. +/// +public readonly struct AgentMetadata(string type, string key, string description) : IEquatable +{ + /// + /// 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 (_). + /// + public string Type { get; } = type; + + /// + /// A unique key identifying the agent instance. + /// Strings may only be composed of alphanumeric letters (a-z, 0-9), or underscores (_). + /// + public string Key { get; } = key; + + /// + /// A brief description of the agent's purpose or functionality. + /// + public string Description { get; } = description; + + /// + public override readonly bool Equals(object? obj) + { + return obj is AgentMetadata agentMetadata && this.Equals(agentMetadata); + } + + /// + public readonly bool Equals(AgentMetadata other) + { + return this.Type.Equals(other.Type, StringComparison.Ordinal) && this.Key.Equals(other.Key, StringComparison.Ordinal); + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Type, this.Key); + } + + /// + public static bool operator ==(AgentMetadata left, AgentMetadata right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(AgentMetadata left, AgentMetadata right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs new file mode 100644 index 000000000000..2b68312f9267 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentProxy.cs + +using System.Text.Json; + +namespace Microsoft.AgentRuntime; + +/// +/// A proxy that allows you to use an in place of its associated . +/// +public class AgentProxy +{ + /// + /// The runtime instance used to interact with agents. + /// + private readonly IAgentRuntime _runtime; + private AgentMetadata? _metadata; + + /// + /// Initializes a new instance of the class. + /// + public AgentProxy(AgentId agentId, IAgentRuntime runtime) + { + this.Id = agentId; + this._runtime = runtime; + } + + /// + /// The target agent for this proxy. + /// + public AgentId Id { get; } + + /// + /// Gets the metadata of the agent. + /// + /// + /// An instance of containing details about the agent. + /// + public AgentMetadata Metadata => this._metadata ??= this.QueryMetdataAndUnwrap(); + + /// + /// Sends a message to the agent and processes the response. + /// + /// The message to send to the agent. + /// The agent that is sending the message. + /// + /// The message ID. If null, a new message ID will be generated. + /// This message ID must be unique and is recommended to be a UUID. + /// + /// + /// A token used to cancel an in-progress operation. Defaults to null. + /// + /// A task representing the asynchronous operation, returning the response from the agent. + public ValueTask SendMessageAsync(object message, AgentId sender, string? messageId = null, CancellationToken cancellationToken = default) + { + return this._runtime.SendMessageAsync(message, this.Id, sender, messageId, cancellationToken); + } + + /// + /// Loads the state of the agent from a previously saved state. + /// + /// A dictionary representing the state of the agent. Must be JSON serializable. + /// A task representing the asynchronous operation. + public ValueTask LoadStateAsync(JsonElement state) + { + return this._runtime.LoadAgentStateAsync(this.Id, state); + } + + /// + /// Saves the state of the agent. The result must be JSON serializable. + /// + /// A task representing the asynchronous operation, returning a dictionary containing the saved state. + public ValueTask SaveStateAsync() + { + return this._runtime.SaveAgentStateAsync(this.Id); + } + + private AgentMetadata QueryMetdataAndUnwrap() + { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); // %%% PRAGMA +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs new file mode 100644 index 000000000000..9261d1258ea4 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.RegularExpressions; + +namespace Microsoft.AgentRuntime; + +/// +/// 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. +/// +/// +/// This struct is immutable and provides implicit conversion to and from . +/// +public readonly struct AgentType : IEquatable +{ + private static readonly Regex TypeRegex = new(@"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); + + 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."); + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The agent type. + public AgentType(string type) + { + Validate(type); + this.Name = type; + } + + /// + /// The string representation of this agent type. + /// + public string Name { get; } + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/source". + public override readonly string ToString() => this.Name; + + /// + /// Explicitly converts a to an . + /// + /// The .NET to convert. + /// An instance with the name of the provided type. + public static explicit operator AgentType(Type type) => new(type.Name); + + /// + /// Implicitly converts a to an . + /// + /// The string representation of the agent type. + /// An instance with the given name. + public static implicit operator AgentType(string type) => new(type); + + /// + /// Implicitly converts an to a . + /// + /// The instance. + /// The string representation of the agent type. + public static implicit operator string(AgentType type) => type.ToString(); + + /// + public override bool Equals(object? obj) + { + return obj is AgentType other && this.Equals(other); + } + + /// + public bool Equals(AgentType other) + { + return this.Name.Equals(other.Name, StringComparison.Ordinal); + } + + /// + public override int GetHashCode() + { + return this.Name.GetHashCode(); + } + + /// + public static bool operator ==(AgentType left, AgentType right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(AgentType left, AgentType right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs new file mode 100644 index 000000000000..9f19b12534cc --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// CantHandleException.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime; + +/// +/// Exception thrown when a handler cannot process the given message. +/// +[ExcludeFromCodeCoverage] +public class CantHandleException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public CantHandleException() : base("The handler cannot process the given message.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public CantHandleException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public CantHandleException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs new file mode 100644 index 000000000000..0e6a570e2928 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageDroppedException.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime; + +/// +/// Exception thrown when a message is dropped. +/// +[ExcludeFromCodeCoverage] +public class MessageDroppedException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public MessageDroppedException() : base("The message was dropped.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public MessageDroppedException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public MessageDroppedException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs new file mode 100644 index 000000000000..5c8c493aec05 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// NotAccessibleError.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime; + +/// +/// Exception thrown when an attempt is made to access an unavailable value, such as a remote resource. +/// +[ExcludeFromCodeCoverage] +public class NotAccessibleException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public NotAccessibleException() : base("The requested value is not accessible.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public NotAccessibleException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public NotAccessibleException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs new file mode 100644 index 000000000000..73946e7a2475 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// UndeliverableException.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime; + +/// +/// Exception thrown when a message cannot be delivered. +/// +[ExcludeFromCodeCoverage] +public class UndeliverableException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public UndeliverableException() : base("The message cannot be delivered.") { } + + /// + /// Initializes a new instance of the class with a custom error message. + /// + /// The custom error message. + public UndeliverableException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a custom error message and an inner exception. + /// + /// The custom error message. + /// The inner exception that caused this error. + public UndeliverableException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs new file mode 100644 index 000000000000..fc70227ae91e --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// IAgent.cs + +namespace Microsoft.AgentRuntime; + +/// +/// Represents an agent within the runtime that can process messages, maintain state, and be closed when no longer needed. +/// +public interface IAgent : ISaveState +{ + /// + /// Gets the unique identifier of the agent. + /// + public AgentId Id { get; } + + /// + /// Gets metadata associated with the agent. + /// + public AgentMetadata Metadata { get; } + + /// + /// Handles an incoming message for the agent. + /// This should only be called by the runtime, not by other agents. + /// + /// The received message. The type should match one of the expected subscription types. + /// The context of the message, providing additional metadata. + /// + /// A task representing the asynchronous operation, returning a response to the message. + /// The response can be null if no reply is necessary. + /// + /// Thrown if the message was cancelled. + /// Thrown if the agent cannot handle the message. + public ValueTask OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET? +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs new file mode 100644 index 000000000000..88e6fc0714c2 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +// IAgentRuntime.cs + +using System.Text.Json; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AgentRuntime; + +/// +/// Defines the runtime environment for agents, managing message sending, subscriptions, agent resolution, and state persistence. +/// +public interface IAgentRuntime : IHostedService, ISaveState +{ + /// + /// Sends a message to an agent and gets a response. + /// This method should be used to communicate directly with an agent. + /// + /// The message to send. + /// The agent to send the message to. + /// The agent sending the message. Should be null if sent from an external source. + /// A unique identifier for the message. If null, a new ID will be generated. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation, returning the response from the agent. + /// Thrown if the recipient cannot handle the message. + /// Thrown if the message cannot be delivered. + ValueTask SendMessageAsync(object message, AgentId recepient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + + /// + /// Publishes a message to all agents subscribed to the given topic. + /// No responses are expected from publishing. + /// + /// The message to publish. + /// The topic to publish the message to. + /// The agent sending the message. Defaults to null. + /// A unique message ID. If null, a new one will be generated. + /// A token to cancel the operation if needed. + /// A task representing the asynchronous operation. + /// Thrown if the message cannot be delivered. + ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + + /// + /// Retrieves an agent by its unique identifier. + /// + /// The unique identifier of the agent. + /// If true, the agent is fetched lazily. + /// A task representing the asynchronous operation, returning the agent's ID. + ValueTask GetAgentAsync(AgentId agentId, bool lazy = true/*, CancellationToken? = default*/); + + /// + /// Retrieves an agent by its type. + /// + /// The type of the agent. + /// An optional key to specify variations of the agent. Defaults to "default". + /// If true, the agent is fetched lazily. + /// A task representing the asynchronous operation, returning the agent's ID. + ValueTask GetAgentAsync(AgentType agentType, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + + /// + /// Retrieves an agent by its string representation. + /// + /// The string representation of the agent. + /// An optional key to specify variations of the agent. Defaults to "default". + /// If true, the agent is fetched lazily. + /// A task representing the asynchronous operation, returning the agent's ID. + ValueTask GetAgentAsync(string agent, string key = "default", bool lazy = true/*, CancellationToken? = default*/); + + /// + /// Saves the state of an agent. + /// The result must be JSON serializable. + /// + /// The ID of the agent whose state is being saved. + /// A task representing the asynchronous operation, returning a dictionary of the saved state. + ValueTask SaveAgentStateAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/); + + /// + /// Loads the saved state into an agent. + /// + /// The ID of the agent whose state is being restored. + /// The state dictionary to restore. + /// A task representing the asynchronous operation. + ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state/*, CancellationToken? cancellationToken = default*/); + + /// + /// Retrieves metadata for an agent. + /// + /// The ID of the agent. + /// A task representing the asynchronous operation, returning the agent's metadata. + ValueTask GetAgentMetadataAsync(AgentId agentId/*, CancellationToken? cancellationToken = default*/); + + /// + /// Adds a new subscription for the runtime to handle when processing published messages. + /// + /// The subscription to add. + /// A task representing the asynchronous operation. + ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription/*, CancellationToken? cancellationToken = default*/); + + /// + /// Removes a subscription from the runtime. + /// + /// The unique identifier of the subscription to remove. + /// A task representing the asynchronous operation. + /// Thrown if the subscription does not exist. + ValueTask RemoveSubscriptionAsync(string subscriptionId/*, CancellationToken? cancellationToken = default*/); + + /// + /// Registers an agent factory with the runtime, associating it with a specific agent type. + /// The type must be unique. + /// + /// The agent type to associate with the factory. + /// A function that asynchronously creates the agent instance. + /// A task representing the asynchronous operation, returning the registered . + ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc); + + /// + /// Attempts to retrieve an for the specified agent. + /// + /// The ID of the agent. + /// A task representing the asynchronous operation, returning an if successful. + ValueTask TryGetAgentProxyAsync(AgentId agentId); +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs new file mode 100644 index 000000000000..edcf204b8415 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// IHostableAgent.cs + +namespace Microsoft.AgentRuntime; + +/// +/// Represents an agent that can be explicitly hosted and closed when the runtime shuts down. +/// +public interface IHostableAgent : IAgent +{ + /// + /// Called when the runtime is closing. + /// + /// A task representing the asynchronous operation. + public ValueTask CloseAsync(); +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs b/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs new file mode 100644 index 000000000000..1eb910007f59 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// ISaveState.cs + +using System.Text.Json; + +namespace Microsoft.AgentRuntime; + +/// +/// Defines a contract for saving and loading the state of an object. +/// The state must be JSON serializable. +/// +public interface ISaveState +{ + /// + /// Saves the current state of the object. + /// + /// + /// 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. + /// + ValueTask SaveStateAsync(); + + /// + /// Loads a previously saved state into the object. + /// + /// + /// A dictionary representing the saved state. The structure of the state + /// is implementation-defined but must be JSON serializable. + /// + /// A task representing the asynchronous operation. + ValueTask LoadStateAsync(JsonElement state); +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs b/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs new file mode 100644 index 000000000000..f37b0fa74b35 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// ISubscriptionDefinition.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime; + +/// +/// Defines a subscription that matches topics and maps them to agents. +/// +public interface ISubscriptionDefinition +{ + /// + /// Gets the unique identifier of the subscription. + /// + public string Id { get; } + + /// + /// Determines whether the specified object is equal to the current subscription. + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to this instance; otherwise, false. + public bool Equals([NotNullWhen(true)] object? obj); + + /// + /// Determines whether the specified subscription is equal to the current subscription. + /// + /// The subscription to compare. + /// true if the subscriptions are equal; otherwise, false. + public bool Equals(ISubscriptionDefinition? other); + + /// + /// Returns a hash code for this subscription. + /// + /// A hash code for the subscription. + public int GetHashCode(); + + /// + /// Checks if a given matches the subscription. + /// + /// The topic to check. + /// true if the topic matches the subscription; otherwise, false. + public bool Matches(TopicId topic); + + /// + /// Maps a to an . + /// Should only be called if returns true. + /// + /// The topic to map. + /// The that should handle the topic. + public AgentId MapToAgent(TopicId topic); +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs new file mode 100644 index 000000000000..f6e67df78d77 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// KeyValueParserExtensions.cs + +using System.Text.RegularExpressions; + +namespace Microsoft.AgentRuntime.Internal; + +/// +/// Provides helper methods for parsing key-value string representations. +/// +internal static class KeyValueParserExtensions +{ + /// + /// The regular expression pattern used to match key-value pairs in the format "key/value". + /// + private const string KVPairPattern = @"^(?\w+)/(?\w+)$"; + + /// + /// The compiled regex used for extracting key-value pairs from a string. + /// + private static readonly Regex KVPairRegex = new Regex(KVPairPattern, RegexOptions.Compiled); + + /// + /// Parses a string in the format "key/value" into a tuple containing the key and value. + /// + /// The input string containing a key-value pair. + /// The expected name of the key component. + /// The expected name of the value component. + /// A tuple containing the extracted key and value. + /// + /// Thrown if the input string does not match the expected "key/value" format. + /// + /// + /// Example usage: + /// + /// string input = "agent1/12345"; + /// var result = input.ToKVPair("Type", "Key"); + /// Console.WriteLine(result.Item1); // Outputs: agent1 + /// Console.WriteLine(result.Item2); // Outputs: 12345 + /// + /// + public static (string, string) ToKeyValuePair(this string inputPair, string keyName, string valueName) + { + Match match = KVPairRegex.Match(inputPair); + if (match.Success) + { + return (match.Groups["key"].Value, match.Groups["value"].Value); + } + + throw new FormatException($"Invalid key-value pair format: {inputPair}; expecting \"{{{keyName}}}/{{{valueName}}}\""); + } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs b/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs new file mode 100644 index 000000000000..04493abd5f6b --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageContext.cs + +namespace Microsoft.AgentRuntime; + +/// +/// 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. +/// +public class MessageContext(string messageId, CancellationToken cancellationToken) +{ + /// + /// Initializes a new instance of the class. + /// + public MessageContext(CancellationToken cancellation) : this(Guid.NewGuid().ToString(), cancellation) + { } + + /// + /// Gets or sets the unique identifier for this message. + /// + public string MessageId { get; } = messageId; + + /// + /// Gets or sets the cancellation token associated with this message. + /// This can be used to cancel the operation if necessary. + /// + public CancellationToken CancellationToken { get; } = cancellationToken; + + /// + /// Gets or sets the sender of the message. + /// If null, the sender is unspecified. + /// + public AgentId? Sender { get; set; } + + /// + /// Gets or sets the topic associated with the message. + /// If null, the message is not tied to a specific topic. + /// + public TopicId? Topic { get; set; } + + /// + /// Gets or sets a value indicating whether this message is part of an RPC (Remote Procedure Call). + /// + public bool IsRpc { get; set; } +} diff --git a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj new file mode 100644 index 000000000000..46451ba41e23 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj @@ -0,0 +1,42 @@ + + + + Microsoft.Agents.Runtime.Abstractions + Microsoft.Agents.Runtime.Abstractions + net8.0;netstandard2.0 + enable + enable + $(NoWarn);IDE1006;IDE0130 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs new file mode 100644 index 000000000000..ac67ba20e26b --- /dev/null +++ b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft. All rights reserved. +// TopicId.cs + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AgentRuntime.Internal; + +namespace Microsoft.AgentRuntime; + +/// +/// Represents a topic identifier that defines the scope of a broadcast message. +/// 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: +/// CloudEvents Type Specification. +/// +public struct TopicId : IEquatable +{ + /// + /// The default source value used when no source is explicitly provided. + /// + public const string DefaultSource = "default"; + + /// + /// The separator character for the string representation of the topic. + /// + public const string Separator = "/"; + + /// + /// Gets the type of the event that this represents. + /// This adheres to the CloudEvents specification. + /// + /// Must match the pattern: ^[\w\-\.\:\=]+$. + /// + /// Learn more here: + /// CloudEvents Type. + /// + public string Type { get; } + + /// + /// Gets the source that identifies the context in which an event happened. + /// This adheres to the CloudEvents specification. + /// + /// Learn more here: + /// CloudEvents Source. + /// + public string Source { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The type of the topic. + /// The source of the event. Defaults to if not specified. + public TopicId(string type, string source = DefaultSource) + { + this.Type = type; + this.Source = source; + } + + /// + /// Initializes a new instance of the struct from a tuple. + /// + /// A tuple containing the topic type and source. + public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.Source) + { + } + + /// + /// Converts a string in the format "type/source" into a . + /// + /// The topic ID string. + /// An instance of . + /// Thrown when the string is not in the valid "type/source" format. + public static TopicId FromStr(string maybeTopicId) => new TopicId(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source))); + + /// + /// Returns the string representation of the . + /// + /// A string in the format "type/source". + public override readonly string ToString() => $"{this.Type}{Separator}{this.Source}"; + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current ; otherwise, false. + public override readonly bool Equals([NotNullWhen(true)] object? obj) + { + if (obj is TopicId other) + { + return this.Type == other.Type && this.Source == other.Source; + } + + return false; + } + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current ; otherwise, false. + public readonly bool Equals([NotNullWhen(true)] TopicId other) + { + return this.Type == other.Type && this.Source == other.Source; + } + + /// + /// Returns a hash code for this . + /// + /// A hash code for the current instance. + public override readonly int GetHashCode() + { + return HashCode.Combine(this.Type, this.Source); + } + + /// + /// Explicitly converts a string to a . + /// + /// The string representation of a topic ID. + /// An instance of . + public static explicit operator TopicId(string id) => FromStr(id); + + // TODO: Implement < for wildcard matching (type, *) + // == => < + // Type == other.Type => < + /// + /// Determines whether the given matches another topic. + /// + /// The topic ID to compare against. + /// + /// true if the topic types are equal; otherwise, false. + /// + public readonly bool IsWildcardMatch(TopicId other) + { + return this.Type == other.Type; + } + + /// + public static bool operator ==(TopicId left, TopicId right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(TopicId left, TopicId right) + { + return !(left == right); + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs new file mode 100644 index 000000000000..7e7be39cd41a --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentRuntimeExtensionsTests.cs + +using System.Text.Json; +using Microsoft.AgentRuntime.InProcess; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class AgentRuntimeExtensionsTests +{ + private const string TestTopic1 = "test.1.topic"; + private const string TestTopic2 = "test.2.topic"; + private const string TestTopicPrefix = "test.2"; + + [Fact] + public async Task RegisterAgentTypeWithStringAsync_WithBaseAgent() + { + // Arrange + string agentTypeName = nameof(TestAgent); + Guid value = Guid.NewGuid(); + ServiceProvider serviceProvider = new ServiceCollection().BuildServiceProvider(); + + await using InProcessRuntime runtime = new(); + + // Act + AgentType registeredType = await runtime.RegisterAgentTypeAsync(agentTypeName, serviceProvider, [value]); + AgentId registeredId = await runtime.GetAgentAsync(agentTypeName, lazy: false); + + // Assert + Assert.Equal(agentTypeName, registeredType.Name); + Assert.Equal(agentTypeName, registeredId.Type); + + // Act + TestAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(registeredId); + + // Assert + Assert.NotNull(agent); + Assert.Equal(agentTypeName, agent.Id.Type); + TestAgent testAgent = Assert.IsType(agent); + Assert.Equal(value, testAgent.Value); + } + + [Fact] + public async Task RegisterAgentTypeWithStringAsync_NotWithBaseAgent() + { + // Arrange + string agentTypeName = nameof(NotBaseAgent); + ServiceProvider serviceProvider = new ServiceCollection().BuildServiceProvider(); + + await using InProcessRuntime runtime = new(); + + // Act + AgentType registeredType = await runtime.RegisterAgentTypeAsync(agentTypeName, typeof(NotBaseAgent), serviceProvider); + + // Assert + await Assert.ThrowsAsync(async () => await runtime.GetAgentAsync(agentTypeName, lazy: false)); + } + + [Fact] + public async Task RegisterImplicitAgentSubscriptionsAsync() + { + // Arrange + string agentTypeName = nameof(TestAgent); + TopicId topic1 = new(TestTopic1); + TopicId topic2 = new(TestTopic2); + + ServiceProvider serviceProvider = new ServiceCollection().BuildServiceProvider(); + await using InProcessRuntime runtime = new(); + + // Act + AgentType registeredType = await runtime.RegisterAgentTypeAsync(agentTypeName, serviceProvider, [Guid.Empty]); + await runtime.RegisterImplicitAgentSubscriptionsAsync(agentTypeName); + + // Arrange + await runtime.StartAsync(); + + try + { + // Act - publish messages to each topic + string messageText1 = "Test message #1"; + string messageText2 = "Test message #1"; + await runtime.PublishMessageAsync(messageText1, topic1); + await runtime.PublishMessageAsync(messageText2, topic2); + + // Get agent and verify it received messages + AgentId registeredId = await runtime.GetAgentAsync(agentTypeName, lazy: false); + TestAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(registeredId); + + // Assert + Assert.NotNull(agent); + Assert.Equal(2, agent.ReceivedMessages.Count); + Assert.Contains(messageText1, agent.ReceivedMessages); + Assert.Contains(messageText2, agent.ReceivedMessages); + } + finally + { + // Arrange + await runtime.StopAsync(); + } + } + + [TypeSubscription(TestTopic1)] + [TypePrefixSubscription(TestTopicPrefix)] + private sealed class TestAgent : BaseAgent, IHandle + { + public List ReceivedMessages { get; } = []; + + public TestAgent(AgentId id, IAgentRuntime runtime, Guid value) + : base(id, runtime, "Test Subscribing Agent", null) + { + this.Value = value; + } + + public Guid Value { get; } + + public ValueTask HandleAsync(string item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item); + + return ValueTask.CompletedTask; + } + } + + private sealed class NotBaseAgent : IHostableAgent + { + public AgentId Id => throw new NotImplementedException(); + + public AgentMetadata Metadata => throw new NotImplementedException(); + + public ValueTask CloseAsync() + { + throw new NotImplementedException(); + } + + public ValueTask LoadStateAsync(JsonElement state) + { + throw new NotImplementedException(); + } + + public ValueTask OnMessageAsync(object message, MessageContext messageContext) + { + throw new NotImplementedException(); + } + + public ValueTask SaveStateAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs new file mode 100644 index 000000000000..9bacbd4dcc33 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentsAppBuilderTests.cs + +using System.Reflection; +using FluentAssertions; +using Microsoft.AgentRuntime.InProcess; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class AgentsAppBuilderTests +{ + [Fact] + public void Constructor_WithoutParameters_ShouldCreateNewHostApplicationBuilder() + { + // Act + AgentsAppBuilder builder = new(); + + // Assert + builder.Services.Should().NotBeNull(); + builder.Configuration.Should().NotBeNull(); + } + + [Fact] + public void Constructor_WithBaseBuilder_ShouldUseProvidedBuilder() + { + // Arrange + HostApplicationBuilder baseBuilder = new(); + + // Add a test service to verify it's the same builder + baseBuilder.Services.AddSingleton(); + + // Act + AgentsAppBuilder builder = new(baseBuilder); + + // Assert + builder.Services.Should().BeSameAs(baseBuilder.Services); + builder.Services.BuildServiceProvider().GetService().Should().NotBeNull(); + } + + [Fact] + public void Services_ShouldReturnBuilderServices() + { + // Arrange + AgentsAppBuilder builder = new(); + + // Act + IServiceCollection services = builder.Services; + + // Assert + services.Should().NotBeNull(); + } + + [Fact] + public void Configuration_ShouldReturnBuilderConfiguration() + { + // Arrange + AgentsAppBuilder builder = new(); + + // Act + IConfiguration configuration = builder.Configuration; + + // Assert + configuration.Should().NotBeNull(); + } + + [Fact] + public async Task UseRuntime_ShouldRegisterRuntimeInServices() + { + // Arrange + AgentsAppBuilder builder = new(); + await using InProcessRuntime runtime = new(); + + // Act + AgentsAppBuilder result = builder.UseRuntime(runtime); + + // Assert + result.Should().BeSameAs(builder); + IAgentRuntime? resolvedRuntime = builder.Services.BuildServiceProvider().GetService(); + resolvedRuntime.Should().BeSameAs(runtime); + + // Verify it's also registered as a hosted service + IHostedService? hostedService = builder.Services.BuildServiceProvider().GetService(); + hostedService.Should().BeSameAs(runtime); + } + + [Fact] + public void AddAgentsFromAssemblies_WithoutParameters_ShouldScanCurrentDomain() + { + // Arrange + AgentsAppBuilder builder = new(); + + // Act - using the parameterless version calls AppDomain.CurrentDomain.GetAssemblies() + builder.AddAgentsFromAssemblies(); + + // Assert + // We just verify it doesn't throw, as the actual agents registered depend on the loaded assemblies + } + + [Fact] + public void AddAgentsFromAssemblies_WithAssemblies_ShouldRegisterAgentsFromProvidedAssemblies() + { + // Arrange + AgentsAppBuilder builder = new(); + Assembly testAssembly = typeof(TestAgent).Assembly; + + // Act + AgentsAppBuilder result = builder.AddAgentsFromAssemblies(testAssembly); + + // Assert + result.Should().BeSameAs(builder); + // The assertion on actual agent registration is done in BuildAsync test + } + + [Fact] + public void AddAgent_ShouldRegisterAgentType() + { + // Arrange + AgentsAppBuilder builder = new(); + AgentType agentType = new("TestAgent"); + + // Act + AgentsAppBuilder result = builder.AddAgent(agentType); + + // Assert + result.Should().BeSameAs(builder); + // Actual agent registration is tested in BuildAsync + } + + [Fact] + public async Task BuildAsync_ShouldReturnAgentsAppWithRegisteredAgents() + { + // Arrange + AgentsAppBuilder builder = new(); + await using InProcessRuntime runtime = new(); + builder.UseRuntime(runtime); + + AgentType testAgentType = new("TestAgent"); + builder.AddAgent(testAgentType); + + // Act + AgentsApp app = await builder.BuildAsync(); + AgentId agentId = await runtime.GetAgentAsync(testAgentType); + + // Assert + app.Should().NotBeNull(); + app.Host.Should().NotBeNull(); + app.AgentRuntime.Should().BeSameAs(runtime); + agentId.Type.Should().BeSameAs(testAgentType.Name); + } + + // Private test interfaces and classes to support the tests + private interface ITestService { } + + private sealed class TestService : ITestService { } + + private sealed class TestAgent : BaseAgent + { + public TestAgent(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) + : base(id, runtime, description, logger) { } + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs new file mode 100644 index 000000000000..f2ae6f1afe5c --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentsAppTests.cs + +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class AgentsAppTests +{ + [Fact] + public void Constructor_ShouldInitializeHost() + { + // Arrange + Mock mockHost = new(); + + // Act + AgentsApp agentsApp = new(mockHost.Object); + + // Assert + agentsApp.Host.Should().BeSameAs(mockHost.Object); + } + + [Fact] + public void Services_ShouldReturnHostServices() + { + // Arrange + Mock mockServiceProvider = new(); + Mock mockHost = new(); + mockHost.Setup(h => h.Services).Returns(mockServiceProvider.Object); + AgentsApp agentsApp = new(mockHost.Object); + + // Act + IServiceProvider result = agentsApp.Services; + + // Assert + result.Should().BeSameAs(mockServiceProvider.Object); + } + + [Fact] + public void ApplicationLifetime_ShouldGetFromServices() + { + // Arrange + Mock mockLifetime = new(); + ServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(mockLifetime.Object) + .BuildServiceProvider(); + + Mock mockHost = new(); + mockHost.Setup(h => h.Services).Returns(serviceProvider); + + AgentsApp agentsApp = new(mockHost.Object); + + // Act + IHostApplicationLifetime result = agentsApp.ApplicationLifetime; + + // Assert + result.Should().BeSameAs(mockLifetime.Object); + } + + [Fact] + public void AgentRuntime_ShouldGetFromServices() + { + // Arrange + Mock mockAgentRuntime = new(); + ServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(mockAgentRuntime.Object) + .BuildServiceProvider(); + + Mock mockHost = new(); + mockHost.Setup(h => h.Services).Returns(serviceProvider); + + AgentsApp agentsApp = new(mockHost.Object); + + // Act + IAgentRuntime result = agentsApp.AgentRuntime; + + // Assert + result.Should().BeSameAs(mockAgentRuntime.Object); + } + + [Fact] + public async Task StartAsync_ShouldStartHost() + { + // Arrange + Mock mockHost = new(); + mockHost.Setup(h => h.StartAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + AgentsApp agentsApp = new(mockHost.Object); + + // Act + await agentsApp.StartAsync(); + + // Assert + mockHost.Verify(h => h.StartAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task StartAsync_WhenAlreadyRunning_ShouldThrowInvalidOperationException() + { + // Arrange + Mock mockHost = new(); + AgentsApp agentsApp = new(mockHost.Object); + + // Act & Assert + await agentsApp.StartAsync(); + await Assert.ThrowsAsync(() => agentsApp.StartAsync().AsTask()); + } + + [Fact] + public async Task ShutdownAsync_ShouldStopHost() + { + // Arrange + Mock mockHost = new(); + mockHost.Setup(h => h.StopAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + AgentsApp agentsApp = new(mockHost.Object); + await agentsApp.StartAsync(); // Start first so we can shut down + + // Act + await agentsApp.ShutdownAsync(); + + // Assert + mockHost.Verify(h => h.StopAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task ShutdownAsync_WhenNotRunning_ShouldThrowInvalidOperationException() + { + // Arrange + Mock mockHost = new(); + AgentsApp agentsApp = new(mockHost.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => agentsApp.ShutdownAsync().AsTask()); + } + + [Fact] + public async Task PublishMessageAsync_WhenNotRunning_ShouldStartHostFirst() + { + // Arrange + Mock mockAgentRuntime = new(); + ServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(mockAgentRuntime.Object) + .BuildServiceProvider(); + + Mock mockHost = new(); + mockHost.Setup(h => h.Services).Returns(serviceProvider); + mockHost.Setup(h => h.StartAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + AgentsApp agentsApp = new(mockHost.Object); + + string message = "test message"; + TopicId topic = new("test-topic"); + + // Act + await agentsApp.PublishMessageAsync(message, topic); + + // Assert + mockHost.Verify(h => h.StartAsync(It.IsAny()), Times.Once); + mockAgentRuntime.Verify( + r => + r.PublishMessageAsync( + message, + topic, + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task PublishMessageAsync_WhenRunning_ShouldNotStartHostAgain() + { + // Arrange + Mock mockAgentRuntime = new(); + ServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(mockAgentRuntime.Object) + .BuildServiceProvider(); + + Mock mockHost = new(); + mockHost.Setup(h => h.Services).Returns(serviceProvider); + mockHost.Setup(h => h.StartAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + AgentsApp agentsApp = new(mockHost.Object); + await agentsApp.StartAsync(); // Start first + + string message = "test message"; + TopicId topic = new("test-topic"); + + // Act + await agentsApp.PublishMessageAsync(message, topic); + + // Assert + mockHost.Verify(h => h.StartAsync(It.IsAny()), Times.Once); + mockAgentRuntime.Verify( + r => + r.PublishMessageAsync( + message, + topic, + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task PublishMessageAsync_ShouldPassAllParameters() + { + // Arrange + Mock mockAgentRuntime = new(); + ServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(mockAgentRuntime.Object) + .BuildServiceProvider(); + + Mock mockHost = new(); + mockHost.Setup(h => h.Services).Returns(serviceProvider); + + AgentsApp agentsApp = new(mockHost.Object); + await agentsApp.StartAsync(); + + string message = "test message"; + TopicId topic = new("test-topic"); + string messageId = "test-message-id"; + + // Act + await agentsApp.PublishMessageAsync(message, topic, messageId, CancellationToken.None); + + // Assert + mockAgentRuntime.Verify( + r => + r.PublishMessageAsync( + message, + topic, + It.IsAny(), + messageId, + CancellationToken.None), + Times.Once); + } + + [Fact] + public async Task WaitForShutdownAsync_ShouldBlock() + { + // Arrange + IHost host = new HostApplicationBuilder().Build(); + + AgentsApp agentsApp = new(host); + await agentsApp.StartAsync(); + + ValueTask shutdownTask = ValueTask.CompletedTask; + try + { + // Assert - Verify initial state + agentsApp.ApplicationLifetime.ApplicationStopped.IsCancellationRequested.Should().BeFalse(); + + // Act + shutdownTask = agentsApp.ShutdownAsync(); + await agentsApp.WaitForShutdownAsync(); + + // Assert + agentsApp.ApplicationLifetime.ApplicationStopped.IsCancellationRequested.Should().BeTrue(); + } + finally + { + await shutdownTask; // Ensure shutdown completes + } + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs new file mode 100644 index 000000000000..c76862402da7 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft. All rights reserved. +// BaseAgentTests.cs + +using System.Text.Json; +using FluentAssertions; +using Microsoft.AgentRuntime.InProcess; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class BaseAgentTests +{ + [Fact] + public void Constructor_InitializesActivitySource_Correctly() + { + BaseAgent.TraceSource.Name.Should().Be("Microsoft.AgentRuntime"); + } + + [Fact] + public void Constructor_InitializesProperties_Correctly() + { + // Arrange + using ILoggerFactory loggerFactory = LoggerFactory.Create(_ => { }); + ILogger logger = loggerFactory.CreateLogger(); + AgentId agentId = new("TestType", "TestKey"); + const string description = "Test Description"; + Mock runtimeMock = new(); + + // Act + TestAgentA agent = new(agentId, runtimeMock.Object, description, logger); + + // Assert + agent.Id.Should().Be(agentId); + agent.Metadata.Type.Should().Be(agentId.Type); + agent.Metadata.Key.Should().Be(agentId.Key); + agent.Metadata.Description.Should().Be(description); + agent.Logger.Should().Be(logger); + } + + [Fact] + public void Constructor_WithNoLogger_CreatesNullLogger() + { + // Arrange + AgentId agentId = new("TestType", "TestKey"); + string description = "Test Description"; + Mock runtimeMock = new(); + + // Act + TestAgentA agent = new(agentId, runtimeMock.Object, description); + + // Assert + agent.Logger.Should().Be(NullLogger.Instance); + } + + [Fact] + public async Task OnMessageAsync_WithoutMatchingHandler() + { + // Arrange + Mock runtimeMock = new(); + AgentId agentId = new("TestType", "TestKey"); + TestAgentA agent = new(agentId, runtimeMock.Object, "Test Agent"); + MessageContext context = new(CancellationToken.None); + + // Act + const string message = "This is a TestMessage"; + object? result = await agent.OnMessageAsync(message, context); + + // Assert + result.Should().BeNull(); + agent.ReceivedMessages.Should().BeEmpty(); + } + + [Fact] + public async Task OnMessageAsync_WithMatchingHandler_NoResult() + { + // Arrange + Mock runtimeMock = new(); + AgentId agentId = new("TestType", "TestKey"); + TestAgentA agent = new(agentId, runtimeMock.Object, "Test Agent"); + + // Act + TestMessage message = new() { Content = "Hello World" }; + MessageContext context = new(CancellationToken.None); + object? result = await agent.OnMessageAsync(message, context); + + // Assert + result.Should().BeNull(); + agent.ReceivedMessages.Should().ContainSingle(); + } + + [Fact] + public async Task OnMessageAsync_WithMatchingHandler_HasResult() + { + // Arrange + Mock runtimeMock = new(); + AgentId agentId = new("TestType", "TestKey"); + TestAgentB agent = new(agentId, runtimeMock.Object); + + // Act + TestMessage message = new() { Content = "Hello World" }; + MessageContext context = new(CancellationToken.None); + object? result = await agent.OnMessageAsync(message, context); + + // Assert + result.Should().Be(message.Content); + agent.ReceivedMessages.Should().ContainSingle(); + agent.ReceivedMessages[0].Should().Contain(message.Content); + } + + [Fact] + public async Task CloseAsync_ReturnsCompletedTask() + { + // Arrange + await using InProcessRuntime runtime = new(); + AgentId agentId = new("TestType", "TestKey"); + TestAgentA agent = new(agentId, runtime, "Test Agent"); + + // Act + await agent.CloseAsync(); + + // Assert + agent.IsClosed.Should().BeTrue(); + } + + [Fact] + public async Task PublishMessageAsync_Recieved() + { + // Arrange + ServiceProvider services = new ServiceCollection().BuildServiceProvider(); + await using InProcessRuntime runtime = new(); + TopicId topic = new("TestTopic"); + AgentType senderType = nameof(TestAgentC); + AgentType recieverType = nameof(TestAgentB); + await runtime.RegisterAgentTypeAsync(recieverType, services); + await runtime.AddSubscriptionAsync(new TypeSubscription(topic.Type, recieverType)); + AgentId recieverId = await runtime.GetAgentAsync(recieverType, lazy: false); + await runtime.RegisterAgentTypeAsync(senderType, services, [topic]); + AgentId senderId = await runtime.GetAgentAsync(senderType, lazy: false); + + // Act + await runtime.StartAsync(); + TestMessage message = new() { Content = "Hello World" }; + try + { + await runtime.SendMessageAsync(message, senderId); + } + finally + { + await runtime.RunUntilIdleAsync(); + } + + // Assert + await VerifyMessagHandled(runtime, senderId, message.Content); + await VerifyMessagHandled(runtime, recieverId, message.Content); + } + + [Fact] + public async Task SendMessageAsync_Recieved() + { + // Arrange + ServiceProvider services = new ServiceCollection().BuildServiceProvider(); + await using InProcessRuntime runtime = new(); + AgentType senderType = nameof(TestAgentD); + AgentType recieverType = nameof(TestAgentB); + await runtime.RegisterAgentTypeAsync(recieverType, services); + AgentId recieverId = await runtime.GetAgentAsync(recieverType, lazy: false); + await runtime.RegisterAgentTypeAsync(senderType, services, [recieverId]); + AgentId senderId = await runtime.GetAgentAsync(senderType, lazy: false); + + // Act + await runtime.StartAsync(); + TestMessage message = new() { Content = "Hello World" }; + try + { + await runtime.SendMessageAsync(message, senderId); + } + finally + { + await runtime.RunUntilIdleAsync(); + } + + // Assert + await VerifyMessagHandled(runtime, senderId, message.Content); + await VerifyMessagHandled(runtime, recieverId, message.Content); + } + + private static async Task VerifyMessagHandled(InProcessRuntime runtime, AgentId agentId, string expectedContent) + { + TestAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(agentId); + agent.ReceivedMessages.Should().ContainSingle(); + agent.ReceivedMessages[0].Should().Be(expectedContent); + } + + [Fact] + public async Task SaveStateAsync_ReturnsEmptyJsonElement() + { + // Arrange + await using InProcessRuntime runtime = new(); + AgentId agentId = new("TestType", "TestKey"); + TestAgentA agent = new(agentId, runtime, "Test Agent"); + + // Act + var state = await agent.SaveStateAsync(); + + // Assert + state.ValueKind.Should().Be(JsonValueKind.Object); + state.EnumerateObject().Count().Should().Be(0); + } + + [Fact] + public async Task LoadStateAsync_WithValidState_HandlesStateCorrectly() + { + // Arrange + await using InProcessRuntime runtime = new(); + AgentId agentId = new("TestType", "TestKey"); + TestAgentA agent = new(agentId, runtime, "Test Agent"); + + JsonElement state = JsonDocument.Parse("{ }").RootElement; + + // Act + await agent.LoadStateAsync(state); + + // Assert + // BaseAgent's default implementation just accepts any state without error + // This is primarily testing that the default method doesn't throw exceptions + } + + [Fact] + public async Task GetAgentAsync_WithValidType_ReturnsAgentId() + { + // Arrange + ServiceProvider services = new ServiceCollection().BuildServiceProvider(); + await using InProcessRuntime runtime = new(); + AgentType agentType = nameof(TestAgentB); + await runtime.RegisterAgentTypeAsync(agentType, services); + + AgentId callingAgentId = new("CallerType", "CallerKey"); + TestAgentB callingAgent = new(callingAgentId, runtime); + + // Act + await runtime.StartAsync(); + AgentId? retrievedAgentId = await callingAgent.GetAgentAsync(agentType); + + // Assert + retrievedAgentId.Should().NotBeNull(); + retrievedAgentId!.Value.Type.Should().Be(agentType.Name); + retrievedAgentId!.Value.Key.Should().Be(AgentId.DefaultKey); + + // Act + retrievedAgentId = await callingAgent.GetAgentAsync("badtype"); + + // Assert + retrievedAgentId.Should().BeNull(); + } + + // Custom test message + private sealed class TestMessage + { + public string Content { get; set; } = string.Empty; + } + + // TestAgent that collects the messages it receives + protected abstract class TestAgent : BaseAgent + { + public List ReceivedMessages { get; } = []; + + protected TestAgent(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) + : base(id, runtime, description, logger) + { + } + } + + private sealed class TestAgentA : TestAgent, IHandle + { + public bool IsClosed { get; private set; } + + public TestAgentA(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) + : base(id, runtime, description, logger) + { + } + + public ValueTask HandleAsync(TestMessage item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item.Content); + return ValueTask.CompletedTask; + } + + public override ValueTask CloseAsync() + { + this.IsClosed = true; + return base.CloseAsync(); + } + } + + // TestAgent that implements handler for TestMessage that produces a result + private sealed class TestAgentB : TestAgent, IHandle + { + public TestAgentB(AgentId id, IAgentRuntime runtime) + : base(id, runtime, "Test agent with handler result") + { + } + + public ValueTask HandleAsync(TestMessage item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item.Content); + return ValueTask.FromResult(item.Content); + } + + public new ValueTask GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default) => base.GetAgentAsync(agent, cancellationToken); + } + + // TestAgent that implements handler for TestMessage that responds by publishing to a topic + private sealed class TestAgentC : TestAgent, IHandle + { + private readonly TopicId _broadcastTopic; + + public TestAgentC(AgentId id, IAgentRuntime runtime, TopicId broadcastTopic) + : base(id, runtime, "Test agent that publishes") + { + this._broadcastTopic = broadcastTopic; + } + + public async ValueTask HandleAsync(TestMessage item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item.Content); + await this.PublishMessageAsync(item, this._broadcastTopic, messageContext.MessageId, messageContext.CancellationToken); + } + } + + // TestAgent that implements handler for TestMessage that responds by messaging another agent + private sealed class TestAgentD : TestAgent, IHandle + { + private readonly AgentId _recieverId; + + public TestAgentD(AgentId id, IAgentRuntime runtime, AgentId recieverId) + : base(id, runtime, "Test agent that sends") + { + this._recieverId = recieverId; + } + + public async ValueTask HandleAsync(TestMessage item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item.Content); + await this.SendMessageAsync(item, this._recieverId, messageContext.MessageId, messageContext.CancellationToken); + } + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj b/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj new file mode 100644 index 000000000000..576c5eaabb7f --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj @@ -0,0 +1,33 @@ + + + + Microsoft.Agents.Runtime.Core.Tests + Microsoft.Agents.Runtime.Core.Tests + net8.0 + enable + enable + True + $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001 + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs new file mode 100644 index 000000000000..c23ce46f9161 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypePrefixSubscriptionAttributeTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class TypePrefixSubscriptionAttributeTests +{ + [Fact] + public void Constructor_SetsTopicCorrectly() + { + // Arrange & Act + TypePrefixSubscriptionAttribute attribute = new("test-topic"); + + // Assert + Assert.Equal("test-topic", attribute.Topic); + } + + [Fact] + public void Bind_CreatesTypeSubscription() + { + // Arrange + TypePrefixSubscriptionAttribute attribute = new("test"); + AgentType agentType = new("testagent"); + + // Act + ISubscriptionDefinition subscription = attribute.Bind(agentType); + + // Assert + Assert.NotNull(subscription); + TypePrefixSubscription typeSubscription = Assert.IsType(subscription); + Assert.Equal("test", typeSubscription.TopicTypePrefix); + Assert.Equal(agentType, typeSubscription.AgentType); + } + + [Fact] + public void AttributeUsage_AllowsOnlyClasses() + { + // Arrange + Type attributeType = typeof(TypePrefixSubscriptionAttribute); + + // Act + AttributeUsageAttribute usageAttribute = + (AttributeUsageAttribute)Attribute.GetCustomAttribute( + attributeType, + typeof(AttributeUsageAttribute))!; + + // Assert + Assert.NotNull(usageAttribute); + Assert.Equal(AttributeTargets.Class, usageAttribute.ValidOn); + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs new file mode 100644 index 000000000000..0180e4ca503e --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypePrefixSubscriptionTests.cs + +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class TypePrefixSubscriptionTests +{ + [Fact] + public void Constructor_WithProvidedId_ShouldSetProperties() + { + // Arrange + string topicTypePrefix = "testPrefix"; + AgentType agentType = new("testAgent"); + string id = "custom-id"; + + // Act + TypePrefixSubscription subscription = new(topicTypePrefix, agentType, id); + + // Assert + subscription.TopicTypePrefix.Should().Be(topicTypePrefix); + subscription.AgentType.Should().Be(agentType); + subscription.Id.Should().Be(id); + } + + [Fact] + public void Constructor_WithoutId_ShouldGenerateGuid() + { + // Arrange + string topicTypePrefix = "testPrefix"; + AgentType agentType = new("testAgent"); + + // Act + TypePrefixSubscription subscription = new(topicTypePrefix, agentType); + + // Assert + subscription.TopicTypePrefix.Should().Be(topicTypePrefix); + subscription.AgentType.Should().Be(agentType); + subscription.Id.Should().NotBeNullOrEmpty(); + Guid.TryParse(subscription.Id, out _).Should().BeTrue(); + } + + [Fact] + public void Matches_TopicWithMatchingPrefix_ShouldReturnTrue() + { + // Arrange + string topicTypePrefix = "testPrefix"; + TypePrefixSubscription subscription = new(topicTypePrefix, new AgentType("testAgent")); + TopicId topic = new(topicTypePrefix, "source1"); + + // Act + bool result = subscription.Matches(topic); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void Matches_TopicWithMatchingPrefixAndAdditionalSuffix_ShouldReturnTrue() + { + // Arrange + string topicTypePrefix = "testPrefix"; + TypePrefixSubscription subscription = new(topicTypePrefix, new AgentType("testAgent")); + TopicId topic = new($"{topicTypePrefix}Suffix", "source1"); + + // Act + bool result = subscription.Matches(topic); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void Matches_TopicWithDifferentPrefix_ShouldReturnFalse() + { + // Arrange + TypePrefixSubscription subscription = new("testPrefix", new AgentType("testAgent")); + TopicId topic = new("differentPrefix", "source1"); + + // Act + bool result = subscription.Matches(topic); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void MapToAgent_MatchingTopic_ShouldReturnCorrectAgentId() + { + // Arrange + string topicTypePrefix = "testPrefix"; + string source = "source1"; + AgentType agentType = new("testAgent"); + TypePrefixSubscription subscription = new(topicTypePrefix, agentType); + TopicId topic = new(topicTypePrefix, source); + + // Act + var agentId = subscription.MapToAgent(topic); + + // Assert + agentId.Type.Should().Be(agentType.Name); + agentId.Key.Should().Be(source); + } + + [Fact] + public void MapToAgent_TopicWithMatchingPrefixAndSuffix_ShouldReturnCorrectAgentId() + { + // Arrange + string topicTypePrefix = "testPrefix"; + string source = "source1"; + AgentType agentType = new("testAgent"); + TypePrefixSubscription subscription = new(topicTypePrefix, agentType); + TopicId topic = new($"{topicTypePrefix}Suffix", source); + + // Act + var agentId = subscription.MapToAgent(topic); + + // Assert + agentId.Type.Should().Be(agentType.Name); + agentId.Key.Should().Be(source); + } + + [Fact] + public void MapToAgent_NonMatchingTopic_ShouldThrowInvalidOperationException() + { + // Arrange + TypePrefixSubscription subscription = new("testPrefix", new AgentType("testAgent")); + TopicId topic = new("differentPrefix", "source1"); + + // Act & Assert + Action action = () => subscription.MapToAgent(topic); + action.Should().Throw() + .WithMessage("TopicId does not match the subscription."); + } + + [Fact] + public void Equals_SameId_ShouldReturnTrue() + { + // Arrange + string id = "custom-id"; + TypePrefixSubscription subscription1 = new("prefix1", new AgentType("agent1"), id); + TypePrefixSubscription subscription2 = new("prefix2", new AgentType("agent2"), id); + + // Act & Assert + subscription1.Equals((object)subscription2).Should().BeTrue(); + subscription1.Equals(subscription2 as ISubscriptionDefinition).Should().BeTrue(); + } + + [Fact] + public void Equals_SameTypeAndAgentType_ShouldReturnTrue() + { + // Arrange + string topicTypePrefix = "prefix1"; + AgentType agentType = new("agent1"); + TypePrefixSubscription subscription1 = new(topicTypePrefix, agentType, "id1"); + TypePrefixSubscription subscription2 = new(topicTypePrefix, agentType, "id2"); + + // Act & Assert + subscription1.Equals((object)subscription2).Should().BeTrue(); + } + + [Fact] + public void Equals_DifferentIdAndProperties_ShouldReturnFalse() + { + // Arrange + TypePrefixSubscription subscription1 = new("prefix1", new AgentType("agent1"), "id1"); + TypePrefixSubscription subscription2 = new("prefix2", new AgentType("agent2"), "id2"); + + // Act & Assert + subscription1.Equals((object)subscription2).Should().BeFalse(); + } + + [Fact] + public void Equals_ISubscriptionDefinition_WithDifferentId_ShouldReturnFalse() + { + // Arrange + TypePrefixSubscription subscription1 = new("prefix1", new AgentType("agent1"), "id1"); + TypePrefixSubscription subscription2 = new("prefix1", new AgentType("agent1"), "id2"); + + // Act & Assert + subscription1.Equals(subscription2 as ISubscriptionDefinition).Should().BeFalse(); + } + + [Fact] + public void Equals_WithNull_ShouldReturnFalse() + { + // Arrange + TypePrefixSubscription subscription = new("prefix1", new AgentType("agent1")); + + // Act & Assert + subscription.Equals(null as object).Should().BeFalse(); + subscription.Equals(null as ISubscriptionDefinition).Should().BeFalse(); + } + + [Fact] + public void Equals_WithDifferentType_ShouldReturnFalse() + { + // Arrange + TypePrefixSubscription subscription = new("prefix1", new AgentType("agent1")); + object differentObject = new(); + + // Act & Assert + subscription.Equals(differentObject).Should().BeFalse(); + } + + [Fact] + public void GetHashCode_SameValues_ShouldReturnSameHashCode() + { + // Arrange + string id = "custom-id"; + string topicTypePrefix = "prefix1"; + AgentType agentType = new("agent1"); + TypePrefixSubscription subscription1 = new(topicTypePrefix, agentType, id); + TypePrefixSubscription subscription2 = new(topicTypePrefix, agentType, id); + + // Act & Assert + subscription1.GetHashCode().Should().Be(subscription2.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentValues_ShouldReturnDifferentHashCodes() + { + // Arrange + TypePrefixSubscription subscription1 = new("prefix1", new AgentType("agent1"), "id1"); + TypePrefixSubscription subscription2 = new("prefix2", new AgentType("agent2"), "id2"); + + // Act & Assert + subscription1.GetHashCode().Should().NotBe(subscription2.GetHashCode()); + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs new file mode 100644 index 000000000000..414ac054180f --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypeSubscriptionAttributeTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class TypeSubscriptionAttributeTests +{ + [Fact] + public void Constructor_SetsTopicCorrectly() + { + // Arrange & Act + TypeSubscriptionAttribute attribute = new("test-topic"); + + // Assert + Assert.Equal("test-topic", attribute.Topic); + } + + [Fact] + public void Bind_CreatesTypeSubscription() + { + // Arrange + TypeSubscriptionAttribute attribute = new("test-topic"); + AgentType agentType = new("testagent"); + + // Act + ISubscriptionDefinition subscription = attribute.Bind(agentType); + + // Assert + Assert.NotNull(subscription); + TypeSubscription typeSubscription = Assert.IsType(subscription); + Assert.Equal("test-topic", typeSubscription.TopicType); + Assert.Equal(agentType, typeSubscription.AgentType); + } + + [Fact] + public void AttributeUsage_AllowsOnlyClasses() + { + // Arrange + Type attributeType = typeof(TypeSubscriptionAttribute); + + // Act + AttributeUsageAttribute usageAttribute = + (AttributeUsageAttribute)Attribute.GetCustomAttribute( + attributeType, + typeof(AttributeUsageAttribute))!; + + // Assert + Assert.NotNull(usageAttribute); + Assert.Equal(AttributeTargets.Class, usageAttribute.ValidOn); + } +} diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs new file mode 100644 index 000000000000..fff5982cb9b4 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypeSubscriptionTests.cs + +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.Core.Tests; + +[Trait("Category", "Unit")] +public class TypeSubscriptionTests +{ + [Fact] + public void Constructor_WithProvidedId_ShouldSetProperties() + { + // Arrange + string topicType = "testTopic"; + AgentType agentType = new("testAgent"); + string id = "custom-id"; + + // Act + TypeSubscription subscription = new(topicType, agentType, id); + + // Assert + subscription.TopicType.Should().Be(topicType); + subscription.AgentType.Should().Be(agentType); + subscription.Id.Should().Be(id); + } + + [Fact] + public void Constructor_WithoutId_ShouldGenerateGuid() + { + // Arrange + string topicType = "testTopic"; + AgentType agentType = new("testAgent"); + + // Act + TypeSubscription subscription = new(topicType, agentType); + + // Assert + subscription.TopicType.Should().Be(topicType); + subscription.AgentType.Should().Be(agentType); + subscription.Id.Should().NotBeNullOrEmpty(); + Guid.TryParse(subscription.Id, out _).Should().BeTrue(); + } + + [Fact] + public void Matches_TopicWithMatchingType_ShouldReturnTrue() + { + // Arrange + string topicType = "testTopic"; + TypeSubscription subscription = new(topicType, new AgentType("testAgent")); + TopicId topic = new(topicType, "source1"); + + // Act + bool result = subscription.Matches(topic); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void Matches_TopicWithDifferentType_ShouldReturnFalse() + { + // Arrange + TypeSubscription subscription = new("testTopic", new AgentType("testAgent")); + TopicId topic = new("differentTopic", "source1"); + + // Act + bool result = subscription.Matches(topic); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void MapToAgent_MatchingTopic_ShouldReturnCorrectAgentId() + { + // Arrange + string topicType = "testTopic"; + string source = "source1"; + AgentType agentType = new("testAgent"); + TypeSubscription subscription = new(topicType, agentType); + TopicId topic = new(topicType, source); + + // Act + var agentId = subscription.MapToAgent(topic); + + // Assert + agentId.Type.Should().Be(agentType.Name); + agentId.Key.Should().Be(source); + } + + [Fact] + public void MapToAgent_NonMatchingTopic_ShouldThrowInvalidOperationException() + { + // Arrange + TypeSubscription subscription = new("testTopic", new AgentType("testAgent")); + TopicId topic = new("differentTopic", "source1"); + + // Act & Assert + Action action = () => subscription.MapToAgent(topic); + action.Should().Throw() + .WithMessage("TopicId does not match the subscription."); + } + + [Fact] + public void Equals_SameId_ShouldReturnTrue() + { + // Arrange + string id = "custom-id"; + TypeSubscription subscription1 = new("topic1", new AgentType("agent1"), id); + TypeSubscription subscription2 = new("topic2", new AgentType("agent2"), id); + + // Act & Assert + subscription1.Equals((object)subscription2).Should().BeTrue(); + subscription1.Equals(subscription2 as ISubscriptionDefinition).Should().BeTrue(); + } + + [Fact] + public void Equals_SameTypeAndAgentType_ShouldReturnTrue() + { + // Arrange + string topicType = "topic1"; + AgentType agentType = new("agent1"); + TypeSubscription subscription1 = new(topicType, agentType, "id1"); + TypeSubscription subscription2 = new(topicType, agentType, "id2"); + + // Act & Assert + subscription1.Equals((object)subscription2).Should().BeTrue(); + } + + [Fact] + public void Equals_DifferentIdAndProperties_ShouldReturnFalse() + { + // Arrange + TypeSubscription subscription1 = new("topic1", new AgentType("agent1"), "id1"); + TypeSubscription subscription2 = new("topic2", new AgentType("agent2"), "id2"); + + // Act & Assert + subscription1.Equals((object)subscription2).Should().BeFalse(); + subscription1.Equals(subscription2 as ISubscriptionDefinition).Should().BeFalse(); + } + + [Fact] + public void Equals_WithNull_ShouldReturnFalse() + { + // Arrange + TypeSubscription subscription = new("topic1", new AgentType("agent1")); + + // Act & Assert + subscription.Equals(null as object).Should().BeFalse(); + subscription.Equals(null as ISubscriptionDefinition).Should().BeFalse(); + } + + [Fact] + public void Equals_WithDifferentType_ShouldReturnFalse() + { + // Arrange + TypeSubscription subscription = new("topic1", new AgentType("agent1")); + object differentObject = new(); + + // Act & Assert + subscription.Equals(differentObject).Should().BeFalse(); + } + + [Fact] + public void GetHashCode_SameValues_ShouldReturnSameHashCode() + { + // Arrange + string id = "custom-id"; + string topicType = "topic1"; + AgentType agentType = new("agent1"); + TypeSubscription subscription1 = new(topicType, agentType, id); + TypeSubscription subscription2 = new(topicType, agentType, id); + + // Act & Assert + subscription1.GetHashCode().Should().Be(subscription2.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentValues_ShouldReturnDifferentHashCodes() + { + // Arrange + TypeSubscription subscription1 = new("topic1", new AgentType("agent1"), "id1"); + TypeSubscription subscription2 = new("topic2", new AgentType("agent2"), "id2"); + + // Act & Assert + subscription1.GetHashCode().Should().NotBe(subscription2.GetHashCode()); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs b/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs new file mode 100644 index 000000000000..11765a4e72de --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentRuntimeExtensions.cs + +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Provides extension methods for managing and registering agents within an . +/// +public static class AgentRuntimeExtensions +{ + internal const string DirectMessageTopicSuffix = ":"; + + /// + /// Registers an agent type with the runtime, providing a factory function to create instances of the agent. + /// + /// The type of agent being registered. Must implement . + /// The where the agent will be registered. + /// The representing the type of agent. + /// The service provider used for dependency injection. + /// Additional arguments to pass to the agent's constructor. + /// A representing the asynchronous operation of registering the agent. + public static ValueTask RegisterAgentTypeAsync(this IAgentRuntime runtime, AgentType type, IServiceProvider serviceProvider, params object[] additionalArguments) + where TAgent : BaseAgent + => RegisterAgentTypeAsync(runtime, type, typeof(TAgent), serviceProvider, additionalArguments); + + /// + /// Registers an agent type with the runtime using the specified runtime type and additional constructor arguments. + /// + /// The agent runtime instance to register the agent with. + /// The agent type to register. + /// The .NET type of the agent to activate. + /// The service provider for dependency injection. + /// Additional arguments to pass to the agent's constructor. + /// A representing the asynchronous registration operation containing the registered agent type. + public static ValueTask RegisterAgentTypeAsync(this IAgentRuntime runtime, AgentType type, Type runtimeType, IServiceProvider serviceProvider, params object[] additionalArguments) + { + ValueTask factory(AgentId id, IAgentRuntime runtime) => ActivateAgentAsync(serviceProvider, runtimeType, [id, runtime, .. additionalArguments]); + + return runtime.RegisterAgentFactoryAsync(type, factory); + } + + /// + /// Registers implicit subscriptions for an agent type based on the type's custom attributes. + /// + /// The type of the agent. + /// The agent runtime instance. + /// The agent type to register subscriptions for. + /// If true, class-level subscriptions are skipped. + /// If true, the direct message subscription is skipped. + /// A representing the asynchronous subscription registration operation. + public static ValueTask RegisterImplicitAgentSubscriptionsAsync(this IAgentRuntime runtime, AgentType type, bool skipClassSubscriptions = false, bool skipDirectMessageSubscription = false) + where TAgent : BaseAgent + => RegisterImplicitAgentSubscriptionsAsync(runtime, type, typeof(TAgent), skipClassSubscriptions, skipDirectMessageSubscription); + + /// + /// Registers implicit subscriptions for the specified agent type using runtime type information. + /// + /// The agent runtime instance. + /// The agent type for which to register subscriptions. + /// The .NET type of the agent. + /// If true, class-level subscriptions are not registered. + /// If true, the direct message subscription is not registered. + /// A representing the asynchronous subscription registration operation. + public static async ValueTask RegisterImplicitAgentSubscriptionsAsync(this IAgentRuntime runtime, AgentType type, Type runtimeType, bool skipClassSubscriptions = false, bool skipDirectMessageSubscription = false) + { + ISubscriptionDefinition[] subscriptions = BindSubscriptionsForAgentType(type, runtimeType, skipClassSubscriptions, skipDirectMessageSubscription); + foreach (ISubscriptionDefinition subscription in subscriptions) + { + await runtime.AddSubscriptionAsync(subscription).ConfigureAwait(false); + } + } + + /// + /// Binds subscription definitions for the given agent type based on the custom attributes applied to the runtime type. + /// + /// The agent type to bind subscriptions for. + /// The .NET type of the agent. + /// If true, class-level subscriptions are skipped. + /// If true, the direct message subscription is skipped. + /// An array of subscription definitions for the agent type. + private static ISubscriptionDefinition[] BindSubscriptionsForAgentType(AgentType agentType, Type runtimeType, bool skipClassSubscriptions = false, bool skipDirectMessageSubscription = false) + { + List subscriptions = []; + + if (!skipClassSubscriptions) + { + subscriptions.AddRange(runtimeType.GetCustomAttributes().Select(t => t.Bind(agentType))); + + subscriptions.AddRange(runtimeType.GetCustomAttributes().Select(t => t.Bind(agentType))); + } + + if (!skipDirectMessageSubscription) + { + // Direct message subscription using agent name as prefix. + subscriptions.Add(new TypePrefixSubscription(agentType.Name + DirectMessageTopicSuffix, agentType)); + } + + return [.. subscriptions]; + } + + /// + /// Instantiates and activates an agent asynchronously using dependency injection. + /// + /// The service provider used for dependency injection. + /// The .NET type of the agent being activated. + /// Additional arguments to pass to the agent's constructor. + /// A representing the asynchronous activation of the agent. + private static ValueTask ActivateAgentAsync(IServiceProvider serviceProvider, Type runtimeType, params object[] additionalArguments) + { + try + { + IHostableAgent agent = (BaseAgent)ActivatorUtilities.CreateInstance(serviceProvider, runtimeType, additionalArguments); + +#if !NETCOREAPP + return agent.AsValueTask(); +#else + return ValueTask.FromResult(agent); +#endif + } + catch (Exception e) when (!e.IsCriticalException()) + { +#if !NETCOREAPP + return e.AsValueTask(); +#else + return ValueTask.FromException(e); +#endif + } + } +} diff --git a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs new file mode 100644 index 000000000000..c634685987e9 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentsApp.cs + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Represents the core application hosting the agent runtime. +/// Manages the application lifecycle including startup, shutdown, and message publishing. +/// +public class AgentsApp +{ + private int runningCount; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying application host. + internal AgentsApp(IHost host) + { + this.Host = host; + } + + /// + /// Gets the underlying host responsible for managing application lifetime. + /// + public IHost Host { get; } + + /// + /// Gets the service provider for dependency resolution. + /// + public IServiceProvider Services => this.Host.Services; + + /// + /// Gets the application lifetime object to manage startup and shutdown events. + /// + public IHostApplicationLifetime ApplicationLifetime => this.Services.GetRequiredService(); + + /// + /// Gets the agent runtime responsible for handling agent messaging and operations. + /// + public IAgentRuntime AgentRuntime => this.Services.GetRequiredService(); + + /// + /// Starts the application by initiating the host. + /// Throws an exception if the application is already running. + /// + public async ValueTask StartAsync() + { + if (Interlocked.Exchange(ref this.runningCount, 1) != 0) + { + throw new InvalidOperationException("Application is already running."); + } + + await this.Host.StartAsync().ConfigureAwait(false); + } + + /// + /// Shuts down the application by stopping the host. + /// Throws an exception if the application is not running. + /// + public async ValueTask ShutdownAsync() + { + if (Interlocked.Exchange(ref this.runningCount, 0) != 1) + { + throw new InvalidOperationException("Application is already stopped."); + } + + await this.Host.StopAsync().ConfigureAwait(false); + } + + /// + /// Publishes a message to the specified topic. + /// If the application is not running, it starts the host first. + /// + /// The type of the message being published. + /// The message to publish. + /// The topic to which the message will be published. + /// An optional unique identifier for the message. + /// A token to cancel the operation if needed. + public async ValueTask PublishMessageAsync(TMessage message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) + where TMessage : notnull + { + if (Volatile.Read(ref this.runningCount) == 0) + { + await StartAsync().ConfigureAwait(false); + } + + await this.AgentRuntime.PublishMessageAsync(message, topic, messageId: messageId, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Waits for the host to complete its shutdown process. + /// + /// A token to cancel the operation if needed. + public Task WaitForShutdownAsync(CancellationToken cancellationToken = default) + { + return this.Host.WaitForShutdownAsync(cancellationToken); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs b/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs new file mode 100644 index 000000000000..2ed67257c049 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. +// AgentsAppBuilder.cs + +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Provides a fluent API to configure and build an instance. +/// +public class AgentsAppBuilder +{ + private readonly HostApplicationBuilder _builder; + private readonly List>> _agentTypeRegistrations; + + /// + /// Initializes a new instance of the class using the specified . + /// + /// An optional host application builder to use; if null, a new instance is created. + public AgentsAppBuilder(HostApplicationBuilder? baseBuilder = null) + { + this._builder = baseBuilder ?? new HostApplicationBuilder(); + this._agentTypeRegistrations = []; + } + + /// + /// Gets the dependency injection service collection. + /// + public IServiceCollection Services => this._builder.Services; + + /// + /// Gets the application's configuration. + /// + public IConfiguration Configuration => this._builder.Configuration; + + /// + /// Scans all assemblies loaded in the current application domain to register available agents. + /// + public void AddAgentsFromAssemblies() + { + this.AddAgentsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()); + } + + /// + /// Configures the AgentsApp to use the specified agent runtime. + /// + /// The type of the runtime. + /// The runtime instance to use. + /// The modified instance of . + public AgentsAppBuilder UseRuntime(TRuntime runtime) where TRuntime : class, IAgentRuntime + { + this.Services.AddSingleton(_ => runtime); + this.Services.AddHostedService(services => runtime); + + return this; + } + + /// + /// Registers agents from the provided assemblies. + /// + /// An array of assemblies to scan for agents. + /// The modified instance of . + public AgentsAppBuilder AddAgentsFromAssemblies(params Assembly[] assemblies) + { + IEnumerable agentTypes = + assemblies.SelectMany(assembly => assembly.GetTypes()) + .Where( + type => + typeof(BaseAgent).IsAssignableFrom(type) && + !type.IsAbstract); + + foreach (Type agentType in agentTypes) + { + // TODO: Expose skipClassSubscriptions and skipDirectMessageSubscription as parameters? + this.AddAgent(agentType.Name, agentType); + } + + return this; + } + + /// + /// Registers an agent of type with the associated agent type and subscription options. + /// + /// The .NET type of the agent. + /// The agent type identifier. + /// Option to skip class subscriptions. + /// Option to skip direct message subscriptions. + /// The modified instance of . + public AgentsAppBuilder AddAgent(AgentType agentType, bool skipClassSubscriptions = false, bool skipDirectMessageSubscription = false) where TAgent : IHostableAgent + => this.AddAgent(agentType, typeof(TAgent), skipClassSubscriptions, skipDirectMessageSubscription); + + /// + /// Builds the AgentsApp instance by constructing the host and registering all agent types. + /// + /// A task representing the asynchronous operation, returning the built . + public async ValueTask BuildAsync() + { + IHost host = this._builder.Build(); + + AgentsApp app = new(host); + + foreach (Func> registration in this._agentTypeRegistrations) + { + await registration(app).ConfigureAwait(false); + } + + return app; + } + + /// + /// Registers an agent with the runtime using the specified agent type and runtime type. + /// + /// The agent type identifier. + /// The .NET type representing the agent. + /// Option to skip class subscriptions. + /// Option to skip direct message subscriptions. + /// The modified instance of . + private AgentsAppBuilder AddAgent(AgentType agentType, Type runtimeType, bool skipClassSubscriptions = false, bool skipDirectMessageSubscription = false) + { + this._agentTypeRegistrations.Add( + async app => + { + await app.AgentRuntime.RegisterAgentTypeAsync(agentType, runtimeType, app.Services).ConfigureAwait(false); + + await app.AgentRuntime.RegisterImplicitAgentSubscriptionsAsync(agentType, runtimeType, skipClassSubscriptions, skipDirectMessageSubscription).ConfigureAwait(false); + + return agentType; + }); + + return this; + } +} diff --git a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs new file mode 100644 index 000000000000..f74d77b4844f --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs @@ -0,0 +1,162 @@ + +// Copyright (c) Microsoft. All rights reserved. +// BaseAgent.cs + +using System.Diagnostics; +using System.Text.Json; +using Microsoft.AgentRuntime.Core.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Represents the base class for an agent in the AutoGen system. +/// +public abstract class BaseAgent : IHostableAgent, ISaveState +{ + /// + /// The activity source for tracing. + /// + public static readonly ActivitySource TraceSource = new($"{typeof(IAgent).Namespace}"); + + private readonly Dictionary _handlerInvokers; + private readonly IAgentRuntime _runtime; + + /// + /// Provides logging capabilities used for diagnostic and operational information. + /// + protected internal ILogger Logger { get; } + + /// + /// Gets the description of the agent. + /// + protected string Description { get; } + + /// + /// Gets the unique identifier of the agent. + /// + public AgentId Id { get; } + + /// + /// Gets the metadata of the agent. + /// + public AgentMetadata Metadata { get; } + + /// + /// Initializes a new instance of the BaseAgent class with the specified identifier, runtime, description, and optional logger. + /// + /// The unique identifier of the agent. + /// The runtime environment in which the agent operates. + /// A brief description of the agent's purpose. + /// An optional logger for recording diagnostic information. + 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); + } + + /// + /// Handles an incoming message by determining its type and invoking the corresponding handler method if available. + /// + /// The message object to be handled. + /// The context associated with the message. + /// A ValueTask that represents the asynchronous operation, containing the response object or null. + public async ValueTask 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; + } + + /// + public virtual ValueTask SaveStateAsync() + { +#if !NETCOREAPP + return JsonDocument.Parse("{}").RootElement.AsValueTask(); +#else + return ValueTask.FromResult(JsonDocument.Parse("{}").RootElement); +#endif + } + + /// + public virtual ValueTask LoadStateAsync(JsonElement state) + { +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + /// + /// Closes this agent gracefully by releasing allocated resources and performing any necessary cleanup. + /// + public virtual ValueTask CloseAsync() + { +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + /// + /// Sends a message to a specified recipient agent through the runtime. + /// + /// The requested agent's type. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, returning the response object or null. + protected async ValueTask GetAgentAsync(AgentType agent, CancellationToken cancellationToken = default) + { + try + { + return await this._runtime.GetAgentAsync(agent, lazy: false).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + return null; + } + } + + /// + /// Sends a message to a specified recipient agent through the runtime. + /// + /// The message object to send. + /// The recipient agent's identifier. + /// An optional identifier for the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous operation, returning the response object or null. + protected ValueTask SendMessageAsync(object message, AgentId recepient, string? messageId = null, CancellationToken cancellationToken = default) + { + return this._runtime.SendMessageAsync(message, recepient, sender: this.Id, messageId, cancellationToken); + } + + /// + /// Publishes a message to all agents subscribed to a specific topic through the runtime. + /// + /// The message object to publish. + /// The topic identifier to which the message is published. + /// An optional identifier for the message. + /// A token used to cancel the operation if needed. + /// A ValueTask that represents the asynchronous publish operation. + protected ValueTask PublishMessageAsync(object message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) + { + return this._runtime.PublishMessageAsync(message, topic, sender: this.Id, messageId, cancellationToken); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/IHandle.cs b/dotnet/src/Agents/Runtime/Core/IHandle.cs new file mode 100644 index 000000000000..bfa71a5a75e3 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/IHandle.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// IHandle.cs + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Defines a handler interface for processing items of type . +/// +/// The type of item to be handled. +public interface IHandle +{ + /// + /// Handles the specified item asynchronously. + /// + /// The item to be handled. + /// The context of the message being handled. + /// A task that represents the asynchronous operation. + ValueTask HandleAsync(T item, MessageContext messageContext); +} + +/// +/// Defines a handler interface for processing items of type and . +/// +/// The input type +/// The output type +public interface IHandle +{ + /// + /// Handles the specified item asynchronously. + /// + /// The item to be handled. + /// The context of the message being handled. + /// A task that represents the asynchronous operation. + ValueTask HandleAsync(TIn item, MessageContext messageContext); +} diff --git a/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs b/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs new file mode 100644 index 000000000000..aaf36ce8769e --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft. All rights reserved. +// HandlerInvoker.cs + +using System.Diagnostics; +using System.Reflection; + +namespace Microsoft.AgentRuntime.Core.Internal; + +/// +/// 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. +/// +internal sealed class HandlerInvoker +{ + /// + /// Scans the provided agent for implemented handler interfaces (IHandle<> and IHandle<,>) via reflection, + /// creates a corresponding for each handler method, and returns a dictionary that maps + /// the message type (first generic argument of the interface) to its invoker. + /// + /// The agent instance whose handler interfaces will be reflected. + /// A dictionary mapping message types to their corresponding instances. + public static Dictionary ReflectAgentHandlers(BaseAgent agent) + { + Type realType = agent.GetType(); + + IEnumerable candidateInterfaces = + realType.GetInterfaces() + .Where(i => i.IsGenericType && + (i.GetGenericTypeDefinition() == typeof(IHandle<>) || + (i.GetGenericTypeDefinition() == typeof(IHandle<,>)))); + + Dictionary invokers = new(); + foreach (Type interface_ in candidateInterfaces) + { + MethodInfo handleAsync = + interface_.GetMethod(nameof(IHandle.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; + } + + /// + /// Represents the asynchronous invocation function. + /// + private Func> Invocation { get; } + + /// + /// Initializes a new instance of the class with the specified method information and target object. + /// + /// The MethodInfo representing the handler method to be invoked. + /// The target instance of the agent. + /// Thrown if the target is missing for a non-static method or if the method's return type is not supported. + private HandlerInvoker(MethodInfo methodInfo, BaseAgent target) + { + object? invocation(object? message, MessageContext messageContext) => methodInfo.Invoke(target, [message, messageContext]); + + Func> 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 + else if (methodInfo.ReturnType.IsGenericType && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + // Obtain the generic type argument for ValueTask + 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 to ValueTask + object valueTask = invocation(message, messageContext)!; + object? typelessValueTask = typeEraseAwait.Invoke(null, [valueTask]); + + Debug.Assert(typelessValueTask is ValueTask, "Expected ValueTask after type erasure."); + + return await ((ValueTask)typelessValueTask).ConfigureAwait(false); + }; + } + else + { + throw new InvalidOperationException($"Method {methodInfo.Name} must return a ValueTask or ValueTask"); + } + + this.Invocation = getResultAsync; + } + + /// + /// Invokes the handler method asynchronously with the provided message and context. + /// + /// The message to be passed as the first argument to the handler. + /// The contextual information associated with the message. + /// A ValueTask representing the asynchronous operation, which yields the handler's result. + public async ValueTask 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; + } + } + + /// + /// Awaits a generic ValueTask and returns its result as an object. + /// This method is used to convert a ValueTask{T} to ValueTask{object?}. + /// + /// The type of the result contained in the ValueTask. + /// The ValueTask to be awaited. + /// A ValueTask containing the result as an object. + private static async ValueTask TypeEraseAwaitAsync(ValueTask vt) + { + return await vt.ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj new file mode 100644 index 000000000000..54f576b33da6 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj @@ -0,0 +1,43 @@ + + + + Microsoft.Agents.Runtime.Core + Microsoft.Agents.Runtime.Core + net8.0;netstandard2.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs new file mode 100644 index 000000000000..1039a2ef80f3 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypePrefixSubscription.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime.Core; + +/// +/// This subscription matches on topics based on a prefix of the 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. +/// +/// +/// Example: +/// +/// var subscription = new TypePrefixSubscription("t1", "a1"); +/// +/// In this case: +/// - A with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`. +/// - A with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`. +/// - A with type `"t1SUFFIX"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`. +/// +public class TypePrefixSubscription : ISubscriptionDefinition +{ + /// + /// Initializes a new instance of the class. + /// + /// Topic type prefix to match against. + /// Agent type to handle this subscription. + /// Unique identifier for the subscription. If not provided, a new UUID will be generated. + public TypePrefixSubscription(string topicTypePrefix, AgentType agentType, string? id = null) + { + this.TopicTypePrefix = topicTypePrefix; + this.AgentType = agentType; + this.Id = id ?? Guid.NewGuid().ToString(); + } + + /// + /// Gets the unique identifier of the subscription. + /// + public string Id { get; } + + /// + /// Gets the topic type prefix used for matching. + /// + public string TopicTypePrefix { get; } + + /// + /// Gets the agent type that handles this subscription. + /// + public AgentType AgentType { get; } + + /// + /// Checks if a given matches the subscription based on its type prefix. + /// + /// The topic to check. + /// true if the topic's type starts with the subscription's prefix, false otherwise. + public bool Matches(TopicId topic) + { + return topic.Type.StartsWith(this.TopicTypePrefix, StringComparison.Ordinal); + } + + /// + /// Maps a to an . Should only be called if returns true. + /// + /// The topic to map. + /// An representing the agent that should handle the topic. + /// Thrown if the topic does not match the subscription. + public AgentId MapToAgent(TopicId topic) + { + if (!Matches(topic)) + { + throw new InvalidOperationException("TopicId does not match the subscription."); + } + + return new AgentId(this.AgentType, topic.Source); // No need for .Name, since AgentType implicitly converts to string + } + + /// + /// Determines whether the specified object is equal to the current subscription. + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to this instance; otherwise, false. + public override bool Equals([NotNullWhen(true)] object? obj) + { + return + obj is TypePrefixSubscription other && + (this.Id == other.Id || + (this.AgentType == other.AgentType && + this.TopicTypePrefix == other.TopicTypePrefix)); + } + + /// + /// Determines whether the specified subscription is equal to the current subscription. + /// + /// The subscription to compare. + /// true if the subscriptions are equal; otherwise, false. + public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures. + public override int GetHashCode() + { + return HashCode.Combine(this.Id, this.AgentType, this.TopicTypePrefix); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs new file mode 100644 index 000000000000..d9ece8f0804e --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypePrefixSubscriptionAttribute.cs + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Specifies that the attributed class subscribes to topics based on a type prefix. +/// +/// The topic prefix used for matching incoming messages. +[AttributeUsage(AttributeTargets.Class)] +public sealed class TypePrefixSubscriptionAttribute(string topic) : Attribute +{ + /// + /// Gets the topic prefix that this subscription listens for. + /// + public string Topic => topic; + + /// + /// Creates a subscription definition that binds the topic to the specified agent type. + /// + /// The agent type to bind to this topic. + /// An representing the binding. + internal ISubscriptionDefinition Bind(AgentType agentType) + { + return new TypePrefixSubscription(this.Topic, agentType); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs new file mode 100644 index 000000000000..025079f32dc0 --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypeSubscription.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime.Core; + +/// +/// 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. +/// +/// +/// Example: +/// +/// var subscription = new TypeSubscription("t1", "a1"); +/// +/// In this case: +/// - A with type `"t1"` and source `"s1"` will be handled by an agent of type `"a1"` with key `"s1"`. +/// - A with type `"t1"` and source `"s2"` will be handled by an agent of type `"a1"` with key `"s2"`. +/// +public class TypeSubscription : ISubscriptionDefinition +{ + /// + /// Initializes a new instance of the class. + /// + /// The exact topic type to match against. + /// Agent type to handle this subscription. + /// Unique identifier for the subscription. If not provided, a new UUID will be generated. + public TypeSubscription(string topicType, AgentType agentType, string? id = null) + { + this.TopicType = topicType; + this.AgentType = agentType; + this.Id = id ?? Guid.NewGuid().ToString(); + } + + /// + /// Gets the unique identifier of the subscription. + /// + public string Id { get; } + + /// + /// Gets the exact topic type used for matching. + /// + public string TopicType { get; } + + /// + /// Gets the agent type that handles this subscription. + /// + public AgentType AgentType { get; } + + /// + /// Checks if a given matches the subscription based on an exact type match. + /// + /// The topic to check. + /// true if the topic's type matches exactly, false otherwise. + public bool Matches(TopicId topic) + { + return topic.Type == this.TopicType; + } + + /// + /// Maps a to an . Should only be called if returns true. + /// + /// The topic to map. + /// An representing the agent that should handle the topic. + /// Thrown if the topic does not match the subscription. + public AgentId MapToAgent(TopicId topic) + { + if (!Matches(topic)) + { + throw new InvalidOperationException("TopicId does not match the subscription."); + } + + return new AgentId(this.AgentType, topic.Source); + } + + /// + /// Determines whether the specified object is equal to the current subscription. + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to this instance; otherwise, false. + public override bool Equals([NotNullWhen(true)] object? obj) + { + return + obj is TypeSubscription other && + (this.Id == other.Id || + (this.AgentType == other.AgentType && + this.TopicType == other.TopicType)); + } + + /// + /// Determines whether the specified subscription is equal to the current subscription. + /// + /// The subscription to compare. + /// true if the subscriptions are equal; otherwise, false. + public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures. + public override int GetHashCode() + { + return HashCode.Combine(this.Id, this.AgentType, this.TopicType); + } +} diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs new file mode 100644 index 000000000000..5bc383b8c47a --- /dev/null +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// TypeSubscriptionAttribute.cs + +namespace Microsoft.AgentRuntime.Core; + +/// +/// Specifies that the attributed class subscribes to a particular topic for agent message handling. +/// +/// The topic identifier that this class subscribes to. +[AttributeUsage(AttributeTargets.Class)] +public sealed class TypeSubscriptionAttribute(string topic) : Attribute +{ + /// + /// Gets the topic identifier associated with this subscription. + /// + public string Topic => topic; + + /// + /// Creates a subscription definition that binds the topic to the specified agent type. + /// + /// The agent type to bind to this topic. + /// An representing the binding. + internal ISubscriptionDefinition Bind(AgentType agentType) + { + return new TypeSubscription(this.Topic, agentType); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs new file mode 100644 index 000000000000..a4216eade224 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft. All rights reserved. +// InProcessRuntimeTests.cs +using System.Text.Json; +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +[Trait("Category", "Unit")] +public class InProcessRuntimeTests() +{ + [Fact] + [Trait("Category", "Unit")] + public async Task RuntimeStatusLifecycleTest() + { + // Arrange & Act + await using InProcessRuntime runtime = new(); + + // Assert + Assert.False(runtime.DeliverToSelf); + Assert.Equal(0, runtime.messageQueueCount); + + // Act + await runtime.StopAsync(); // Already stopped + await runtime.RunUntilIdleAsync(); // Never throws + + await runtime.StartAsync(); + + // Assert + // Invalid to start runtime that is already started + await Assert.ThrowsAsync(() => runtime.StartAsync()); + Assert.Equal(0, runtime.messageQueueCount); + + // Act + await runtime.StopAsync(); + + // Assert + Assert.Equal(0, runtime.messageQueueCount); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task SubscriptionRegistrationLifecycleTest() + { + // Arrange + await using InProcessRuntime runtime = new(); + TestSubscription subscription = new("TestTopic", "MyAgent"); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.RemoveSubscriptionAsync(subscription.Id)); + + // Arrange + await runtime.AddSubscriptionAsync(subscription); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.AddSubscriptionAsync(subscription)); + + // Act + await runtime.RemoveSubscriptionAsync(subscription.Id); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task AgentRegistrationLifecycleTest() + { + // Arrange + const string agentType = "MyAgent"; + const string agentDescription = "A test agent"; + List agents = []; + await using InProcessRuntime runtime = new(); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.GetAgentAsync(agentType, lazy: false)); + + // Arrange + await runtime.RegisterAgentFactoryAsync(agentType, factoryFunc); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.RegisterAgentFactoryAsync(agentType, factoryFunc)); + + // Act: Lookup by type + AgentId agentId = await runtime.GetAgentAsync(agentType, lazy: false); + + // Assert + Assert.Single(agents); + Assert.Single(runtime.agentInstances); + + // Act + MockAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(agentId); + + // Assert + Assert.Equal(agentId, agent.Id); + + // Act & Assert + await Assert.ThrowsAsync(async () => await runtime.TryGetUnderlyingAgentInstanceAsync(agentId)); + + // Act: Lookup by ID + AgentId sameId = await runtime.GetAgentAsync(agentId, lazy: false); + + // Assert + Assert.Equal(agentId, sameId); + + // Act: Lookup by Type + sameId = await runtime.GetAgentAsync((AgentType)agent.Id.Type, lazy: false); + + // Assert + Assert.Equal(agentId, sameId); + + // Act: Lookup metadata + AgentMetadata metadata = await runtime.GetAgentMetadataAsync(agentId); + + // Assert + Assert.Equal(agentId.Type, metadata.Type); + Assert.Equal(agentDescription, metadata.Description); + Assert.Equal(agentId.Key, metadata.Key); + + // Act: Access proxy + AgentProxy proxy = await runtime.TryGetAgentProxyAsync(agentId); + + // Assert + Assert.Equal(agentId, proxy.Id); + Assert.Equal(metadata.Type, proxy.Metadata.Type); + Assert.Equal(metadata.Description, proxy.Metadata.Description); + Assert.Equal(metadata.Key, proxy.Metadata.Key); + + ValueTask factoryFunc(AgentId id, IAgentRuntime runtime) + { + MockAgent agent = new(id, runtime, agentDescription); + agents.Add(agent); + return ValueTask.FromResult(agent); + } + } + + [Fact] + [Trait("Category", "Unit")] + public async Task AgentStateLifecycleTest() + { + // Arrange + const string agentType = "MyAgent"; + const string testMessage = "test message"; + + await using InProcessRuntime firstRuntime = new(); + await firstRuntime.RegisterAgentFactoryAsync(agentType, factoryFunc); + + // Act + AgentId agentId = await firstRuntime.GetAgentAsync(agentType, lazy: false); + + // Assert + Assert.Single(firstRuntime.agentInstances); + + // Arrange + MockAgent agent = (MockAgent)firstRuntime.agentInstances[agentId]; + agent.ReceivedMessages.Add(testMessage); + + // Act + JsonElement agentState = await firstRuntime.SaveAgentStateAsync(agentId); + + // Arrange + await using InProcessRuntime secondRuntime = new(); + await secondRuntime.RegisterAgentFactoryAsync(agentType, factoryFunc); + + // Act + await secondRuntime.LoadAgentStateAsync(agentId, agentState); + + // Assert + Assert.Single(secondRuntime.agentInstances); + MockAgent copy = (MockAgent)secondRuntime.agentInstances[agentId]; + Assert.Single(copy.ReceivedMessages); + Assert.Equal(testMessage, copy.ReceivedMessages.Single().ToString()); + + static ValueTask factoryFunc(AgentId id, IAgentRuntime runtime) + { + MockAgent agent = new(id, runtime, "A test agent"); + return ValueTask.FromResult(agent); + } + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RuntimeSendMessageTest() + { + // Arrange + await using InProcessRuntime runtime = new(); + MockAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + { + agent = new MockAgent(id, runtime, "A test agent"); + return ValueTask.FromResult(agent); + }); + + // Act: Ensure the agent is actually created + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + + // Assert + Assert.NotNull(agent); + Assert.Empty(agent.ReceivedMessages); + + // Act: Send message + await runtime.StartAsync(); + await runtime.SendMessageAsync("TestMessage", agent.Id); + await runtime.RunUntilIdleAsync(); + + // Assert + Assert.Equal(0, runtime.messageQueueCount); + Assert.Single(agent.ReceivedMessages); + } + + // Agent will not deliver to self will success when runtime.DeliverToSelf is false (default) + [Theory] + [InlineData(false, 0)] + [InlineData(true, 1)] + [Trait("Category", "Unit")] + public async Task RuntimeAgentPublishToSelfTest(bool selfPublish, int recieveCount) + { + // Arrange + await using InProcessRuntime runtime = new() + { + DeliverToSelf = selfPublish + }; + + MockAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + { + agent = new MockAgent(id, runtime, "A test agent"); + return ValueTask.FromResult(agent); + }); + + // Assert + runtime.agentInstances.Count.Should().Be(0, "No Agent should be registered in the runtime"); + + // Act: Ensure the agent is actually created + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + + // Assert + Assert.NotNull(agent); + runtime.agentInstances.Count.Should().Be(1, "Agent should be registered in the runtime"); + + const string TopicType = "TestTopic"; + + // Arrange + await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); + + // Act + await runtime.StartAsync(); + await runtime.PublishMessageAsync("SelfMessage", new TopicId(TopicType), sender: agentId); + await runtime.RunUntilIdleAsync(); + + // Assert + Assert.Equal(recieveCount, agent.ReceivedMessages.Count); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RuntimeShouldSaveLoadStateCorrectlyTest() + { + // Arrange: Create a runtime and register an agent + await using InProcessRuntime runtime = new(); + MockAgent? agent = null; + await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + { + agent = new MockAgent(id, runtime, "test agent"); + return ValueTask.FromResult(agent); + }); + + // Get agent ID and instantiate agent by publishing + AgentId agentId = await runtime.GetAgentAsync("MyAgent", lazy: false); + const string TopicType = "TestTopic"; + await runtime.AddSubscriptionAsync(new TestSubscription(TopicType, agentId.Type)); + + await runtime.StartAsync(); + await runtime.PublishMessageAsync("test", new TopicId(TopicType)); + await runtime.RunUntilIdleAsync(); + + // Act: Save the state + JsonElement savedState = await runtime.SaveStateAsync(); + + // Assert: Ensure the agent's state is stored as a valid JSON type + Assert.NotNull(agent); + savedState.TryGetProperty(agentId.ToString(), out JsonElement agentState).Should().BeTrue("Agent state should be saved"); + agentState.ValueKind.Should().Be(JsonValueKind.Array, "Agent state should be stored as a JSON array"); + agent.ReceivedMessages.Count.Should().Be(1, "Agent should be have state restored"); + + // Arrange: Serialize and Deserialize the state to simulate persistence + string json = JsonSerializer.Serialize(savedState); + json.Should().NotBeNullOrEmpty("Serialized state should not be empty"); + IDictionary deserializedState = JsonSerializer.Deserialize>(json) + ?? throw new InvalidOperationException("Deserialized state is unexpectedly null"); + deserializedState.Should().ContainKey(agentId.ToString()); + + // Act: Start new runtime and restore the state + agent = null; + await using InProcessRuntime newRuntime = new(); + await newRuntime.StartAsync(); + await newRuntime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => + { + agent = new MockAgent(id, runtime, "another agent"); + return ValueTask.FromResult(agent); + }); + + // Assert: Show that no agent instances exist in the new runtime + newRuntime.agentInstances.Count.Should().Be(0, "Agent should be registered in the new runtime"); + + // Act: Load the state into the new runtime and show that agent is now instantiated + await newRuntime.LoadStateAsync(savedState); + + // Assert + Assert.NotNull(agent); + newRuntime.agentInstances.Count.Should().Be(1, "Agent should be registered in the new runtime"); + newRuntime.agentInstances.Should().ContainKey(agentId, "Agent should be loaded into the new runtime"); + agent.ReceivedMessages.Count.Should().Be(1, "Agent should be have state restored"); + } + + private sealed class TextMessage + { + public string Source { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + } + + private sealed class WrongAgent : IAgent, IHostableAgent + { + public AgentId Id => throw new NotImplementedException(); + + public AgentMetadata Metadata => throw new NotImplementedException(); + + public ValueTask CloseAsync() => ValueTask.CompletedTask; + + public ValueTask LoadStateAsync(JsonElement state) + { + throw new NotImplementedException(); + } + + public ValueTask OnMessageAsync(object message, MessageContext messageContext) + { + throw new NotImplementedException(); + } + + public ValueTask SaveStateAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs new file mode 100644 index 000000000000..7af53609268c --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageDeliveryTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +[Trait("Category", "Unit")] +public class MessageDeliveryTests +{ + [Fact] + public void Constructor_InitializesProperties() + { + // Arrange + MessageEnvelope message = new(new object()); + Func servicer = (_, _) => new ValueTask(); + ResultSink resultSink = new(); + + // Act + MessageDelivery delivery = new(message, servicer, resultSink); + + // Assert + Assert.Same(message, delivery.Message); + Assert.Same(servicer, delivery.Servicer); + Assert.Same(resultSink, delivery.ResultSink); + } + + [Fact] + public async Task Future_WithResultSink_ReturnsSinkFuture() + { + // Arrange + MessageEnvelope message = new(new object()); + Func servicer = (_, _) => new ValueTask(); + + ResultSink resultSink = new(); + int expectedResult = 42; + resultSink.SetResult(expectedResult); + + // Act + MessageDelivery delivery = new(message, servicer, resultSink); + object? result = await delivery.ResultSink.Future; + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public async Task InvokeAsync_CallsServicerWithCorrectParameters() + { + // Arrange + MessageEnvelope message = new(new object()); + CancellationToken cancellationToken = new(); + + bool servicerCalled = false; + MessageEnvelope? passedMessage = null; + CancellationToken? passedToken = null; + + Func servicer = (msg, token) => + { + servicerCalled = true; + passedMessage = msg; + passedToken = token; + return ValueTask.CompletedTask; + }; + + ResultSink sink = new(); + MessageDelivery delivery = new(message, servicer, sink); + + // Act + await delivery.InvokeAsync(cancellationToken); + + // Assert + Assert.True(servicerCalled); + Assert.Same(message, passedMessage); + Assert.Equal(cancellationToken, passedToken); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs new file mode 100644 index 000000000000..0be3ee492c19 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageEnvelopeTests.cs + +using Xunit; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +[Trait("Category", "Unit")] +public class MessageEnvelopeTests +{ + [Fact] + public void ConstructAllParametersTest() + { + // Arrange + object message = new { Content = "Test message" }; + const string messageId = "testid"; + CancellationToken cancellation = new(); + + // Act + MessageEnvelope envelope = new(message, messageId, cancellation); + + // Assert + Assert.Same(message, envelope.Message); + Assert.Equal(messageId, envelope.MessageId); + Assert.Equal(cancellation, envelope.Cancellation); + Assert.Null(envelope.Sender); + Assert.Null(envelope.Receiver); + Assert.Null(envelope.Topic); + } + + [Fact] + public void ConstructOnlyRequiredParametersTest() + { + // Arrange & Act + MessageEnvelope envelope = new("test"); + + // Assert + Assert.NotNull(envelope.MessageId); + Assert.NotEmpty(envelope.MessageId); + // Verify it's a valid GUID + Assert.True(Guid.TryParse(envelope.MessageId, out _)); + } + + [Fact] + public void WithSenderTest() + { + // Arrange + MessageEnvelope envelope = new("test"); + AgentId sender = new("testtype", "testkey"); + + // Act + MessageEnvelope result = envelope.WithSender(sender); + + // Assert + Assert.Same(envelope, result); + Assert.Equal(sender, envelope.Sender); + } + + [Fact] + public async Task ForSendTest() + { + // Arrange + MessageEnvelope envelope = new("test"); + AgentId receiver = new("receivertype", "receiverkey"); + object expectedResult = new { Response = "Success" }; + + ValueTask servicer(MessageEnvelope env, CancellationToken ct) => ValueTask.FromResult(expectedResult); + + // Act + MessageDelivery delivery = envelope.ForSend(receiver, servicer); + + // Assert + Assert.NotNull(delivery); + Assert.Same(envelope, delivery.Message); + Assert.Equal(receiver, envelope.Receiver); + + // Invoke the servicer to verify result sink works + await delivery.InvokeAsync(CancellationToken.None); + Assert.True(delivery.ResultSink.Future.IsCompleted); + object? result = await delivery.ResultSink.Future; + Assert.Same(expectedResult, result); + } + + [Fact] + public void ForPublishTest() + { + // Arrange + MessageEnvelope envelope = new("test"); + TopicId topic = new("testtopic"); + + static ValueTask servicer(MessageEnvelope env, CancellationToken ct) => ValueTask.CompletedTask; + + // Act + MessageDelivery delivery = envelope.ForPublish(topic, servicer); + + // Assert + Assert.NotNull(delivery); + Assert.Same(envelope, delivery.Message); + Assert.Equal(topic, envelope.Topic); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs new file mode 100644 index 000000000000..917673807adf --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessagingTestFixture.cs + +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +public sealed class BasicMessage +{ + public string Content { get; set; } = string.Empty; +} + +#pragma warning disable RCS1194 // Implement exception constructors +public sealed class TestException : Exception { } +#pragma warning restore RCS1194 // Implement exception constructors + +public sealed class PublisherAgent : TestAgent, IHandle +{ + private IList targetTopics; + + public PublisherAgent(AgentId id, IAgentRuntime runtime, string description, IList targetTopics) + : base(id, runtime, description) + { + this.targetTopics = targetTopics; + } + + public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item); + foreach (TopicId targetTopic in this.targetTopics) + { + await this.PublishMessageAsync( + new BasicMessage { Content = $"@{targetTopic}: {item.Content}" }, + targetTopic); + } + } +} + +public sealed class SendOnAgent : TestAgent, IHandle +{ + private readonly IList targetKeys; + + public SendOnAgent(AgentId id, IAgentRuntime runtime, string description, IList targetKeys) + : base(id, runtime, description) + { + this.targetKeys = targetKeys; + } + + public async ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + foreach (Guid targetKey in this.targetKeys) + { + AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); + BasicMessage response = new() { Content = $"@{targetKey}: {item.Content}" }; + await this.SendMessageAsync(response, targetId); + } + } +} + +public sealed class ReceiverAgent : TestAgent, IHandle +{ + public List Messages { get; } = []; + + public ReceiverAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + this.Messages.Add(item); + return ValueTask.CompletedTask; + } +} + +public sealed class ProcessorAgent : TestAgent, IHandle +{ + private Func ProcessFunc { get; } + + public ProcessorAgent(AgentId id, IAgentRuntime runtime, Func processFunc, string description) + : base(id, runtime, description) + { + this.ProcessFunc = processFunc; + } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + BasicMessage result = new() { Content = this.ProcessFunc.Invoke(((BasicMessage)item).Content) }; + + return ValueTask.FromResult(result); + } +} + +public sealed class CancelAgent : TestAgent, IHandle +{ + public CancelAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + CancellationToken cancelledToken = new(canceled: true); + cancelledToken.ThrowIfCancellationRequested(); + + return ValueTask.CompletedTask; + } +} + +public sealed class ErrorAgent : TestAgent, IHandle +{ + public ErrorAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } + + public bool DidThrow { get; private set; } + + public ValueTask HandleAsync(BasicMessage item, MessageContext messageContext) + { + this.DidThrow = true; + + throw new TestException(); + } +} + +public sealed class MessagingTestFixture +{ + private Dictionary AgentsTypeMap { get; } = []; + public InProcessRuntime Runtime { get; } = new(); + + public ValueTask RegisterFactoryMapInstances(AgentType type, Func> factory) + where TAgent : IHostableAgent + { + async ValueTask WrappedFactory(AgentId id, IAgentRuntime runtime) + { + TAgent agent = await factory(id, runtime); + this.GetAgentInstances()[id] = agent; + return agent; + } + + return this.Runtime.RegisterAgentFactoryAsync(type, WrappedFactory); + } + + public Dictionary GetAgentInstances() where TAgent : IHostableAgent + { + if (!this.AgentsTypeMap.TryGetValue(typeof(TAgent), out object? maybeAgentMap) || + maybeAgentMap is not Dictionary result) + { + this.AgentsTypeMap[typeof(TAgent)] = result = []; + } + + return result; + } + public async ValueTask RegisterReceiverAgent(string? agentNameSuffix = null, params string[] topicTypes) + { + await this.RegisterFactoryMapInstances( + $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}", + (id, runtime) => ValueTask.FromResult(new ReceiverAgent(id, runtime, string.Empty))); + + foreach (string topicType in topicTypes) + { + await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ReceiverAgent)}{agentNameSuffix ?? string.Empty}")); + } + } + + public async ValueTask RegisterErrorAgent(string? agentNameSuffix = null, params string[] topicTypes) + { + await this.RegisterFactoryMapInstances( + $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}", + (id, runtime) => ValueTask.FromResult(new ErrorAgent(id, runtime, string.Empty))); + + foreach (string topicType in topicTypes) + { + await this.Runtime.AddSubscriptionAsync(new TestSubscription(topicType, $"{nameof(ErrorAgent)}{agentNameSuffix ?? string.Empty}")); + } + } + + public async ValueTask RunPublishTestAsync(TopicId sendTarget, object message, string? messageId = null) + { + messageId ??= Guid.NewGuid().ToString(); + + await this.Runtime.StartAsync(); + await this.Runtime.PublishMessageAsync(message, sendTarget, messageId: messageId); + await this.Runtime.RunUntilIdleAsync(); + } + + public async ValueTask RunSendTestAsync(AgentId sendTarget, object message, string? messageId = null) + { + messageId ??= Guid.NewGuid().ToString(); + + await this.Runtime.StartAsync(); + + object? result = await this.Runtime.SendMessageAsync(message, sendTarget, messageId: messageId); + + await this.Runtime.RunUntilIdleAsync(); + + return result; + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs new file mode 100644 index 000000000000..d882ad21b635 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +// PublishMessageTests.cs + +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +[Trait("Category", "Unit")] +public class PublishMessageTests +{ + [Fact] + public async Task Test_PublishMessage_Success() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterReceiverAgent(topicTypes: "TestTopic"); + await fixture.RegisterReceiverAgent("2", topicTypes: "TestTopic"); + + await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }); + + fixture.GetAgentInstances().Values + .Should().HaveCount(2, "Two agents should have been created") + .And.AllSatisfy(receiverAgent => receiverAgent.Messages + .Should().NotBeNull() + .And.HaveCount(1) + .And.ContainSingle(m => m.Content == "1")); + } + + [Fact] + public async Task Test_PublishMessage_SingleFailure() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterErrorAgent(topicTypes: "TestTopic"); + + Func publishTask = async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }); + + // Test that we wrap single errors appropriately + await publishTask.Should().ThrowAsync(); + + fixture.GetAgentInstances().Values.Should().ContainSingle() + .Which.DidThrow.Should().BeTrue("Agent should have thrown an exception"); + } + + [Fact] + public async Task Test_PublishMessage_MultipleFailures() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterErrorAgent(topicTypes: "TestTopic"); + await fixture.RegisterErrorAgent("2", topicTypes: "TestTopic"); + + Func publishTask = async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }); + + // What we are really testing here is that a single exception does not prevent sending to the remaining agents + (await publishTask.Should().ThrowAsync()) + .Which.Should().Match( + exception => exception.InnerExceptions.Count == 2 && + exception.InnerExceptions.All(exception => exception is TestException)); + + fixture.GetAgentInstances().Values + .Should().HaveCount(2) + .And.AllSatisfy( + agent => agent.DidThrow.Should().BeTrue("Agent should have thrown an exception")); + } + + [Fact] + public async Task Test_PublishMessage_MixedSuccessFailure() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterReceiverAgent(topicTypes: "TestTopic"); + await fixture.RegisterReceiverAgent("2", topicTypes: "TestTopic"); + + await fixture.RegisterErrorAgent(topicTypes: "TestTopic"); + await fixture.RegisterErrorAgent("2", topicTypes: "TestTopic"); + + Func publicTask = async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" }); + + // What we are really testing here is that raising exceptions does not prevent sending to the remaining agents + (await publicTask.Should().ThrowAsync()) + .Which.Should().Match( + exception => exception.InnerExceptions.Count == 2 && + exception.InnerExceptions.All( + exception => exception is TestException)); + + fixture.GetAgentInstances().Values + .Should().HaveCount(2, "Two ReceiverAgents should have been created") + .And.AllSatisfy(receiverAgent => receiverAgent.Messages + .Should().NotBeNull() + .And.HaveCount(1) + .And.ContainSingle(m => m.Content == "1"), + "ReceiverAgents should get published message regardless of ErrorAgents throwing exception."); + + fixture.GetAgentInstances().Values + .Should().HaveCount(2, "Two ErrorAgents should have been created") + .And.AllSatisfy(agent => agent.DidThrow.Should().BeTrue("ErrorAgent should have thrown an exception")); + } + + [Fact] + public async Task Test_PublishMessage_RecurrentPublishSucceeds() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances( + nameof(PublisherAgent), + (id, runtime) => ValueTask.FromResult(new PublisherAgent(id, runtime, string.Empty, new List { new TopicId("TestTopic") }))); + + await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", nameof(PublisherAgent))); + + await fixture.RegisterReceiverAgent(topicTypes: "TestTopic"); + await fixture.RegisterReceiverAgent("2", topicTypes: "TestTopic"); + + await fixture.RunPublishTestAsync(new TopicId("RunTest"), new BasicMessage { Content = "1" }); + + TopicId testTopicId = new("TestTopic"); + fixture.GetAgentInstances().Values + .Should().HaveCount(2, "Two ReceiverAgents should have been created") + .And.AllSatisfy(receiverAgent => receiverAgent.Messages + .Should().NotBeNull() + .And.HaveCount(1) + .And.ContainSingle(m => m.Content == $"@{testTopicId}: 1")); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs new file mode 100644 index 000000000000..66e9d233589c --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. +// ResultSinkTests.cs + +using System.Threading.Tasks.Sources; +using Xunit; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +[Trait("Category", "Unit")] +public class ResultSinkTests +{ + [Fact] + public void GetResultTest() + { + // Arrange + ResultSink sink = new(); + const int expectedResult = 42; + + // Act + sink.SetResult(expectedResult); + int result = sink.GetResult(0); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); + } + + [Fact] + public async Task FutureResultTest() + { + // Arrange + ResultSink sink = new(); + const string expectedResult = "test"; + + // Act + sink.SetResult(expectedResult); + string result = await sink.Future; + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); + } + + [Fact] + public async Task SetExceptionTest() + { + // Arrange + ResultSink sink = new(); + InvalidOperationException expectedException = new("Test exception"); + + // Act + sink.SetException(expectedException); + + // Assert + Exception exception = await Assert.ThrowsAsync(async () => await sink.Future); + Assert.Equal(expectedException.Message, exception.Message); + exception = Assert.Throws(() => sink.GetResult(0)); + Assert.Equal(expectedException.Message, exception.Message); + Assert.Equal(ValueTaskSourceStatus.Faulted, sink.GetStatus(0)); + } + + [Fact] + public async Task SetCancelledTest() + { + // Arrange + ResultSink sink = new(); + + // Act + sink.SetCancelled(); + + // Assert + Assert.True(sink.IsCancelled); + Assert.Throws(() => sink.GetResult(0)); + await Assert.ThrowsAsync(async () => await sink.Future); + Assert.Equal(ValueTaskSourceStatus.Canceled, sink.GetStatus(0)); + } + + [Fact] + public void OnCompletedTest() + { + // Arrange + ResultSink sink = new(); + bool continuationCalled = false; + const int expectedResult = 42; + + // Register the continuation + sink.OnCompleted( + state => continuationCalled = true, + state: null, + token: 0, + ValueTaskSourceOnCompletedFlags.None); + + // Assert + Assert.False(continuationCalled, "Continuation should have been called"); + + // Act + sink.SetResult(expectedResult); + + // Assert + Assert.Equal(expectedResult, sink.GetResult(0)); + Assert.Equal(ValueTaskSourceStatus.Succeeded, sink.GetStatus(0)); + Assert.True(continuationCalled, "Continuation should have been called"); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj b/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj new file mode 100644 index 000000000000..6447455af153 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj @@ -0,0 +1,33 @@ + + + + Microsoft.Agents.Runtime.InProcess.Tests + Microsoft.Agents.Runtime.InProcess.Tests + net8.0 + enable + enable + True + $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001 + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs new file mode 100644 index 000000000000..8b0f3e963cb8 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +// SendMessageTests.cs + +using System.Diagnostics; +using FluentAssertions; +using Xunit; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +[Trait("Category", "Unit")] +public class SendMessageTests +{ + [Fact] + public async Task Test_SendMessage_ReturnsValue() + { + static string ProcessFunc(string s) => $"Processed({s})"; + + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances(nameof(ProcessorAgent), + (id, runtime) => ValueTask.FromResult(new ProcessorAgent(id, runtime, ProcessFunc, string.Empty))); + + AgentId targetAgent = new(nameof(ProcessorAgent), Guid.NewGuid().ToString()); + object? maybeResult = await fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }); + + maybeResult.Should().NotBeNull() + .And.BeOfType() + .And.Match(m => m.Content == "Processed(1)"); + } + + [Fact] + public async Task Test_SendMessage_Cancellation() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances(nameof(CancelAgent), + (id, runtime) => ValueTask.FromResult(new CancelAgent(id, runtime, string.Empty))); + + AgentId targetAgent = new(nameof(CancelAgent), Guid.NewGuid().ToString()); + Func testAction = () => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask(); + + await testAction.Should().ThrowAsync(); + } + + [Fact] + public async Task Test_SendMessage_Error() + { + MessagingTestFixture fixture = new(); + + await fixture.RegisterFactoryMapInstances(nameof(ErrorAgent), + (id, runtime) => ValueTask.FromResult(new ErrorAgent(id, runtime, string.Empty))); + + AgentId targetAgent = new(nameof(ErrorAgent), Guid.NewGuid().ToString()); + Func testAction = () => fixture.RunSendTestAsync(targetAgent, new BasicMessage { Content = "1" }).AsTask(); + + await testAction.Should().ThrowAsync(); + } + + [Fact] + public async Task Test_SendMessage_FromSendMessageHandler() + { + Guid[] targetGuids = [Guid.NewGuid(), Guid.NewGuid()]; + + MessagingTestFixture fixture = new(); + + Dictionary sendAgents = fixture.GetAgentInstances(); + Dictionary receiverAgents = fixture.GetAgentInstances(); + + await fixture.RegisterFactoryMapInstances(nameof(SendOnAgent), + (id, runtime) => ValueTask.FromResult(new SendOnAgent(id, runtime, string.Empty, targetGuids))); + + await fixture.RegisterFactoryMapInstances(nameof(ReceiverAgent), + (id, runtime) => ValueTask.FromResult(new ReceiverAgent(id, runtime, string.Empty))); + + AgentId targetAgent = new(nameof(SendOnAgent), Guid.NewGuid().ToString()); + BasicMessage input = new() { Content = "Hello" }; + Task testTask = fixture.RunSendTestAsync(targetAgent, input).AsTask(); + + // We do not actually expect to wait the timeout here, but it is still better than waiting the 10 min + // timeout that the tests default to. A failure will fail regardless of what timeout value we set. + TimeSpan timeout = Debugger.IsAttached ? TimeSpan.FromSeconds(120) : TimeSpan.FromSeconds(10); + Task timeoutTask = Task.Delay(timeout); + + Task completedTask = await Task.WhenAny([testTask, timeoutTask]); + completedTask.Should().Be(testTask, "SendOnAgent should complete before timeout"); + + // Check that each of the target agents received the message + foreach (Guid targetKey in targetGuids) + { + AgentId targetId = new(nameof(ReceiverAgent), targetKey.ToString()); + receiverAgents[targetId].Messages.Should().ContainSingle(m => m.Content == $"@{targetKey}: {input.Content}"); + } + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs new file mode 100644 index 000000000000..9007661bc6de --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// TestAgents.cs + +using System.Text.Json; +using Microsoft.AgentRuntime.Core; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +public abstract class TestAgent : BaseAgent +{ + internal List ReceivedMessages = []; + + protected TestAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) + { + } +} + +/// +/// A test agent that captures the messages it receives and +/// is able to save and load its state. +/// +public sealed class MockAgent : TestAgent, IHandle +{ + public MockAgent(AgentId id, IAgentRuntime runtime, string description) + : base(id, runtime, description) { } + + public ValueTask HandleAsync(string item, MessageContext messageContext) + { + this.ReceivedMessages.Add(item); + return ValueTask.CompletedTask; + } + + public override ValueTask SaveStateAsync() + { + JsonElement json = JsonSerializer.SerializeToElement(this.ReceivedMessages); + return ValueTask.FromResult(json); + } + + public override ValueTask LoadStateAsync(JsonElement state) + { + this.ReceivedMessages = JsonSerializer.Deserialize>(state) ?? throw new InvalidOperationException("Failed to deserialize state"); + return ValueTask.CompletedTask; + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs new file mode 100644 index 000000000000..24899c74e355 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// TestSubscription.cs + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AgentRuntime.InProcess.Tests; + +public class TestSubscription(string topicType, string agentType, string? id = null) : ISubscriptionDefinition +{ + public string Id { get; } = id ?? Guid.NewGuid().ToString(); + + public string TopicType { get; } = topicType; + + public AgentId MapToAgent(TopicId topic) + { + if (!this.Matches(topic)) + { + throw new InvalidOperationException("TopicId does not match the subscription."); + } + + return new AgentId(agentType, topic.Source); + } + + public bool Equals(ISubscriptionDefinition? other) => this.Id == other?.Id; + + public override bool Equals([NotNullWhen(true)] object? obj) => obj is TestSubscription other && other.Equals(this); + + public override int GetHashCode() => this.Id.GetHashCode(); + + public bool Matches(TopicId topic) + { + return topic.Type == this.TopicType; + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs new file mode 100644 index 000000000000..80cfdc484bdf --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft. All rights reserved. +// InProcessRuntime.cs + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection; +using System.Text.Json; + +namespace Microsoft.AgentRuntime.InProcess; + +/// +/// Provides an in-process/in-memory implementation of the agent runtime. +/// +public sealed class InProcessRuntime : IAgentRuntime, IAsyncDisposable +{ + private readonly Dictionary>> _agentFactories = []; + private readonly Dictionary _subscriptions = []; + private readonly ConcurrentQueue _messageDeliveryQueue = new(); + + private CancellationTokenSource? _shutdownSource; + private CancellationTokenSource? _finishSource; + private Task _messageDeliveryTask = Task.CompletedTask; + private Func _shouldContinue = () => true; + + // Exposed for testing purposes. + internal int messageQueueCount; + internal readonly Dictionary agentInstances = []; + + /// + /// Gets or sets a value indicating whether agents should receive messages they send themselves. + /// + public bool DeliverToSelf { get; set; } //= false; + + /// + public async ValueTask DisposeAsync() + { + await this.RunUntilIdleAsync().ConfigureAwait(false); + this._shutdownSource?.Dispose(); + this._finishSource?.Dispose(); + } + + /// + /// Starts the runtime service. + /// + /// Token to monitor for shutdown requests. + /// A task representing the asynchronous operation. + /// Thrown if the runtime is already started. + public Task StartAsync(CancellationToken cancellationToken = default) + { + if (this._shutdownSource != null) + { + throw new InvalidOperationException("Runtime is already running."); + } + + this._shutdownSource = new CancellationTokenSource(); + this._messageDeliveryTask = Task.Run(() => this.RunAsync(this._shutdownSource.Token), cancellationToken); + + return Task.CompletedTask; + } + + /// + /// Stops the runtime service. + /// + /// Token to propagate when stopping the runtime. + /// A task representing the asynchronous operation. + /// Thrown if the runtime is in the process of stopping. + public Task StopAsync(CancellationToken cancellationToken = default) + { + if (this._shutdownSource != null) + { + if (this._finishSource != null) + { + throw new InvalidOperationException("Runtime is already stopping."); + } + + this._finishSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + this._shutdownSource.Cancel(); + } + + return Task.CompletedTask; + } + + /// + /// This will run until the message queue is empty and then stop the runtime. + /// + public async Task RunUntilIdleAsync() + { + Func oldShouldContinue = this._shouldContinue; + this._shouldContinue = () => !this._messageDeliveryQueue.IsEmpty; + + // TODO: Do we want detach semantics? + await this._messageDeliveryTask.ConfigureAwait(false); + + this._shouldContinue = oldShouldContinue; + } + + /// + public ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + return this.ExecuteTracedAsync(async () => + { + MessageDelivery delivery = + new MessageEnvelope(message, messageId, cancellationToken) + .WithSender(sender) + .ForPublish(topic, this.PublishMessageServicer); + + this._messageDeliveryQueue.Enqueue(delivery); + Interlocked.Increment(ref this.messageQueueCount); + + await delivery.ResultSink.Future.ConfigureAwait(false); + }); + } + + /// + public async ValueTask SendMessageAsync(object message, AgentId recepient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + { + return await this.ExecuteTracedAsync(async () => + { + MessageDelivery delivery = + new MessageEnvelope(message, messageId, cancellationToken) + .WithSender(sender) + .ForSend(recepient, this.SendMessageServicerAsync); + + this._messageDeliveryQueue.Enqueue(delivery); + 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); + } + }).ConfigureAwait(false); + } + + /// + public async ValueTask GetAgentAsync(AgentId agentId, bool lazy = true) + { + if (!lazy) + { + await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + } + + return agentId; + } + + /// + public ValueTask GetAgentAsync(AgentType agentType, string key = AgentId.DefaultKey, bool lazy = true) + => this.GetAgentAsync(new AgentId(agentType, key), lazy); + + /// + public ValueTask GetAgentAsync(string agent, string key = AgentId.DefaultKey, bool lazy = true) + => this.GetAgentAsync(new AgentId(agent, key), lazy); + + /// + public async ValueTask GetAgentMetadataAsync(AgentId agentId) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + return agent.Metadata; + } + + /// + public async ValueTask TryGetUnderlyingAgentInstanceAsync(AgentId agentId) where TAgent : IHostableAgent + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + + if (agent is not TAgent concreteAgent) + { + throw new InvalidOperationException($"Agent with name {agentId.Type} is not of type {typeof(TAgent).Name}."); + } + + return concreteAgent; + } + + /// + public async ValueTask LoadAgentStateAsync(AgentId agentId, JsonElement state) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + await agent.LoadStateAsync(state).ConfigureAwait(false); + } + + /// + public async ValueTask SaveAgentStateAsync(AgentId agentId) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + return await agent.SaveStateAsync().ConfigureAwait(false); + } + + /// + public ValueTask AddSubscriptionAsync(ISubscriptionDefinition subscription) + { + if (this._subscriptions.ContainsKey(subscription.Id)) + { + throw new InvalidOperationException($"Subscription with id {subscription.Id} already exists."); + } + + this._subscriptions.Add(subscription.Id, subscription); + +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + /// + public ValueTask RemoveSubscriptionAsync(string subscriptionId) + { + if (!this._subscriptions.ContainsKey(subscriptionId)) + { + throw new InvalidOperationException($"Subscription with id {subscriptionId} does not exist."); + } + + this._subscriptions.Remove(subscriptionId); + +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + /// + public async ValueTask LoadStateAsync(JsonElement state) + { + foreach (JsonProperty agentIdStr in state.EnumerateObject()) + { + AgentId agentId = AgentId.FromStr(agentIdStr.Name); + + if (this._agentFactories.ContainsKey(agentId.Type)) + { + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + await agent.LoadStateAsync(agentIdStr.Value).ConfigureAwait(false); + } + } + } + + /// + public async ValueTask SaveStateAsync() + { + Dictionary state = []; + foreach (AgentId agentId in this.agentInstances.Keys) + { + JsonElement agentState = await this.agentInstances[agentId].SaveStateAsync().ConfigureAwait(false); + state[agentId.ToString()] = agentState; + } + return JsonSerializer.SerializeToElement(state); + } + + /// + /// Registers an agent factory with the runtime, associating it with a specific agent type. + /// + /// The type of agent created by the factory. + /// The agent type to associate with the factory. + /// A function that asynchronously creates the agent instance. + /// A task representing the asynchronous operation, returning the registered agent type. + public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) where TAgent : IHostableAgent + // Declare the lambda return type explicitly, as otherwise the compiler will infer 'ValueTask' + // and recurse into the same call, causing a stack overflow. + => this.RegisterAgentFactoryAsync(type, async ValueTask (agentId, runtime) => await factoryFunc(agentId, runtime).ConfigureAwait(false)); + + /// + public ValueTask RegisterAgentFactoryAsync(AgentType type, Func> factoryFunc) + { + if (this._agentFactories.ContainsKey(type)) + { + throw new InvalidOperationException($"Agent with type {type} already exists."); + } + + this._agentFactories.Add(type, factoryFunc); + +#if !NETCOREAPP + return type.AsValueTask(); +#else + return ValueTask.FromResult(type); +#endif + } + + /// + public ValueTask TryGetAgentProxyAsync(AgentId agentId) + { + AgentProxy proxy = new(agentId, this); + +#if !NETCOREAPP + return proxy.AsValueTask(); +#else + return ValueTask.FromResult(proxy); +#endif + } + + private ValueTask ProcessNextMessageAsync(CancellationToken cancellation = default) + { + if (this._messageDeliveryQueue.TryDequeue(out MessageDelivery? delivery)) + { + Interlocked.Decrement(ref this.messageQueueCount); + Debug.WriteLine($"Processing message {delivery.Message.MessageId}..."); + return delivery.InvokeAsync(cancellation); + } + +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else + return ValueTask.CompletedTask; +#endif + } + + private async Task RunAsync(CancellationToken cancellation) + { + Dictionary pendingTasks = []; + while (!cancellation.IsCancellationRequested && this._shouldContinue()) + { + // Get a unique task id + Guid taskId; + do + { + taskId = Guid.NewGuid(); + } while (pendingTasks.ContainsKey(taskId)); + + // 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) + { + continue; + } + + Task actualTask = processTask.AsTask(); + pendingTasks.Add(taskId, actualTask.ContinueWith(t => pendingTasks.Remove(taskId), TaskScheduler.Current)); + } + + // The pending task dictionary may contain null values when a race condition is experienced during + // the prior "ContinueWith" call. This could be solved with a ConcurrentDictionary, but locking + // is entirely undesirable in this context. + await Task.WhenAll([.. pendingTasks.Values.Where(task => task is not null)]).ConfigureAwait(false); + await this.FinishAsync(this._finishSource?.Token ?? CancellationToken.None).ConfigureAwait(false); + } + + private async ValueTask PublishMessageServicer(MessageEnvelope envelope, CancellationToken deliveryToken) + { + if (!envelope.Topic.HasValue) + { + throw new InvalidOperationException("Message must have a topic to be published."); + } + + List exceptions = []; + TopicId topic = envelope.Topic.Value; + foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic))) + { + try + { + deliveryToken.ThrowIfCancellationRequested(); + + AgentId? sender = envelope.Sender; + + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); // %%% CHANGE - USING + MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) + { + Sender = sender, + Topic = topic, + IsRpc = false + }; + + AgentId agentId = subscription.MapToAgent(topic); + if (!this.DeliverToSelf && sender.HasValue && sender == agentId) + { + continue; + } + + IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false); + + // TODO: Cancellation propagation! + await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + exceptions.Add(ex); + } + } + + if (exceptions.Count > 0) + { + // TODO: Unwrap TargetInvocationException? + throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions); + } + } + + private async ValueTask SendMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken) + { + if (!envelope.Receiver.HasValue) + { + throw new InvalidOperationException("Message must have a receiver to be sent."); + } + + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); // %%% CHANGE - USING + MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) + { + Sender = envelope.Sender, + IsRpc = false + }; + + AgentId receiver = envelope.Receiver.Value; + IHostableAgent agent = await this.EnsureAgentAsync(receiver).ConfigureAwait(false); + + return await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false); + } + + private async ValueTask EnsureAgentAsync(AgentId agentId) + { + if (!this.agentInstances.TryGetValue(agentId, out IHostableAgent? agent)) + { + if (!this._agentFactories.TryGetValue(agentId.Type, out Func>? factoryFunc)) + { + throw new InvalidOperationException($"Agent with name {agentId.Type} not found."); + } + + agent = await factoryFunc(agentId, this).ConfigureAwait(false); + this.agentInstances.Add(agentId, agent); + } + + return this.agentInstances[agentId]; + } + + private async Task FinishAsync(CancellationToken token) + { + foreach (IHostableAgent agent in this.agentInstances.Values) + { + if (!token.IsCancellationRequested) + { + await agent.CloseAsync().ConfigureAwait(false); + } + } + + this._shutdownSource?.Dispose(); + this._finishSource?.Dispose(); + this._finishSource = null; + this._shutdownSource = null; + } + +#pragma warning disable CA1822 // Mark members as static + private ValueTask ExecuteTracedAsync(Func> 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 func) +#pragma warning restore CA1822 // Mark members as static + { + // TODO: Bind tracing + return func(); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs b/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs new file mode 100644 index 000000000000..c91d75d8b430 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageDelivery.cs + +namespace Microsoft.AgentRuntime.InProcess; + +internal sealed class MessageDelivery(MessageEnvelope message, Func servicer, IResultSink resultSink) +{ + public MessageEnvelope Message { get; } = message; + public Func Servicer { get; } = servicer; + public IResultSink ResultSink { get; } = resultSink; + + public ValueTask InvokeAsync(CancellationToken cancellation) + { + return this.Servicer(this.Message, cancellation); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs b/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs new file mode 100644 index 000000000000..3caecb0810db --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +// MessageEnvelope.cs + +namespace Microsoft.AgentRuntime.InProcess; + +internal sealed class MessageEnvelope +{ + public object Message { get; } + public string MessageId { get; } + public TopicId? Topic { get; private set; } + public AgentId? Sender { get; private set; } + public AgentId? Receiver { get; private set; } + public CancellationToken Cancellation { get; } + + 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) + { + this.Sender = sender; + return this; + } + + public MessageDelivery ForSend(AgentId receiver, Func> servicer) + { + this.Receiver = receiver; + + ResultSink resultSink = new(); + + return new MessageDelivery(this, BoundServicer, resultSink); + + async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation) + { + try + { + object? result = await servicer(envelope, cancellation).ConfigureAwait(false); + resultSink.SetResult(result); + } + catch (OperationCanceledException exception) + { + resultSink.SetCancelled(exception); + } + catch (Exception exception) when (!exception.IsCriticalException()) + { + resultSink.SetException(exception); + } + } + } + + public MessageDelivery ForPublish(TopicId topic, Func servicer) + { + this.Topic = topic; + + ResultSink waitForPublish = new(); + + async ValueTask BoundServicer(MessageEnvelope envelope, CancellationToken cancellation) + { + try + { + await servicer(envelope, cancellation).ConfigureAwait(false); + waitForPublish.SetResult(null); + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + waitForPublish.SetException(ex); + } + } + + return new MessageDelivery(this, BoundServicer, waitForPublish); + } +} diff --git a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs new file mode 100644 index 000000000000..47db12339988 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +// ResultSink.cs + +using System.Threading.Tasks.Sources; + +namespace Microsoft.AgentRuntime.InProcess; + +internal interface IResultSink : IValueTaskSource +{ + public void SetResult(TResult result); + public void SetException(Exception exception); + public void SetCancelled(OperationCanceledException? exception = null); + + public ValueTask Future { get; } +} + +internal sealed class ResultSink : IResultSink +{ + private ManualResetValueTaskSourceCore 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 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 Future => new(this, this.core.Version); +} diff --git a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj new file mode 100644 index 000000000000..073a93e217f2 --- /dev/null +++ b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj @@ -0,0 +1,36 @@ + + + + Microsoft.Agents.Runtime.InProcess + Microsoft.Agents.Runtime.InProcess + net8.0;netstandard2.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs b/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs new file mode 100644 index 000000000000..e34d157e3e8e --- /dev/null +++ b/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +// ValueTaskExtensions.cs + +#if !NETCOREAPP + +using System; +using System.Threading.Tasks; + +/// +/// Convenience extensions for ValueTask patterns within .netstandard2.0 projects. +/// +internal static class ValueTaskExtensions +{ + /// + /// Creates a that's completed successfully with the specified result. + /// + /// + /// + /// int value = 33; + /// return value.AsValueTask(); + /// + /// + public static ValueTask AsValueTask(this TValue value) => new ValueTask(value); + + /// + /// Creates a that's failed and is associated with an exception. + /// + /// + /// + /// int value = 33; + /// return value.AsValueTask(); + /// + /// + public static ValueTask AsValueTask(this Exception exception) => new ValueTask(Task.FromException(exception)); + + /// + /// Present a regular task as a ValueTask. + /// + /// + /// return Task.CompletedTask.AsValueTask(); + /// + public static ValueTask AsValueTask(this Task task) => new ValueTask(task); +} + +#endif From f54b0f8285c960da309a9c53f8693b89edabae8b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:11:06 -0700 Subject: [PATCH 17/57] Typos --- .../Agents/Runtime/Abstractions/AgentProxy.cs | 4 +-- .../Runtime/Abstractions/IAgentRuntime.cs | 4 +-- .../Runtime/Core.Tests/BaseAgentTests.cs | 28 +++++++++---------- dotnet/src/Agents/Runtime/Core/BaseAgent.cs | 6 ++-- .../Runtime/InProcess/InProcessRuntime.cs | 4 +-- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs index 2b68312f9267..533939b68958 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs @@ -36,7 +36,7 @@ public AgentProxy(AgentId agentId, IAgentRuntime runtime) /// /// An instance of containing details about the agent. /// - public AgentMetadata Metadata => this._metadata ??= this.QueryMetdataAndUnwrap(); + public AgentMetadata Metadata => this._metadata ??= this.QueryMetadataAndUnwrap(); /// /// Sends a message to the agent and processes the response. @@ -75,7 +75,7 @@ public ValueTask SaveStateAsync() return this._runtime.SaveAgentStateAsync(this.Id); } - private AgentMetadata QueryMetdataAndUnwrap() + private AgentMetadata QueryMetadataAndUnwrap() { #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); // %%% PRAGMA diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs index 88e6fc0714c2..5075e3c0307e 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs @@ -16,14 +16,14 @@ public interface IAgentRuntime : IHostedService, ISaveState /// This method should be used to communicate directly with an agent. /// /// The message to send. - /// The agent to send the message to. + /// The agent to send the message to. /// The agent sending the message. Should be null if sent from an external source. /// A unique identifier for the message. If null, a new ID will be generated. /// A token to cancel the operation if needed. /// A task representing the asynchronous operation, returning the response from the agent. /// Thrown if the recipient cannot handle the message. /// Thrown if the message cannot be delivered. - ValueTask SendMessageAsync(object message, AgentId recepient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); + ValueTask SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default); /// /// Publishes a message to all agents subscribed to the given topic. diff --git a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs index c76862402da7..d87d7c08030c 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs @@ -135,10 +135,10 @@ public async Task PublishMessageAsync_Recieved() await using InProcessRuntime runtime = new(); TopicId topic = new("TestTopic"); AgentType senderType = nameof(TestAgentC); - AgentType recieverType = nameof(TestAgentB); - await runtime.RegisterAgentTypeAsync(recieverType, services); - await runtime.AddSubscriptionAsync(new TypeSubscription(topic.Type, recieverType)); - AgentId recieverId = await runtime.GetAgentAsync(recieverType, lazy: false); + AgentType receiverType = nameof(TestAgentB); + await runtime.RegisterAgentTypeAsync(receiverType, services); + await runtime.AddSubscriptionAsync(new TypeSubscription(topic.Type, receiverType)); + AgentId receiverId = await runtime.GetAgentAsync(receiverType, lazy: false); await runtime.RegisterAgentTypeAsync(senderType, services, [topic]); AgentId senderId = await runtime.GetAgentAsync(senderType, lazy: false); @@ -156,7 +156,7 @@ public async Task PublishMessageAsync_Recieved() // Assert await VerifyMessagHandled(runtime, senderId, message.Content); - await VerifyMessagHandled(runtime, recieverId, message.Content); + await VerifyMessagHandled(runtime, receiverId, message.Content); } [Fact] @@ -166,10 +166,10 @@ public async Task SendMessageAsync_Recieved() ServiceProvider services = new ServiceCollection().BuildServiceProvider(); await using InProcessRuntime runtime = new(); AgentType senderType = nameof(TestAgentD); - AgentType recieverType = nameof(TestAgentB); - await runtime.RegisterAgentTypeAsync(recieverType, services); - AgentId recieverId = await runtime.GetAgentAsync(recieverType, lazy: false); - await runtime.RegisterAgentTypeAsync(senderType, services, [recieverId]); + AgentType receiverType = nameof(TestAgentB); + await runtime.RegisterAgentTypeAsync(receiverType, services); + AgentId receiverId = await runtime.GetAgentAsync(receiverType, lazy: false); + await runtime.RegisterAgentTypeAsync(senderType, services, [receiverId]); AgentId senderId = await runtime.GetAgentAsync(senderType, lazy: false); // Act @@ -186,7 +186,7 @@ public async Task SendMessageAsync_Recieved() // Assert await VerifyMessagHandled(runtime, senderId, message.Content); - await VerifyMessagHandled(runtime, recieverId, message.Content); + await VerifyMessagHandled(runtime, receiverId, message.Content); } private static async Task VerifyMessagHandled(InProcessRuntime runtime, AgentId agentId, string expectedContent) @@ -335,18 +335,18 @@ public async ValueTask HandleAsync(TestMessage item, MessageContext messageConte // TestAgent that implements handler for TestMessage that responds by messaging another agent private sealed class TestAgentD : TestAgent, IHandle { - private readonly AgentId _recieverId; + private readonly AgentId _receiverId; - public TestAgentD(AgentId id, IAgentRuntime runtime, AgentId recieverId) + public TestAgentD(AgentId id, IAgentRuntime runtime, AgentId receiverId) : base(id, runtime, "Test agent that sends") { - this._recieverId = recieverId; + this._receiverId = receiverId; } public async ValueTask HandleAsync(TestMessage item, MessageContext messageContext) { this.ReceivedMessages.Add(item.Content); - await this.SendMessageAsync(item, this._recieverId, messageContext.MessageId, messageContext.CancellationToken); + await this.SendMessageAsync(item, this._receiverId, messageContext.MessageId, messageContext.CancellationToken); } } } diff --git a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs index f74d77b4844f..e8538da200a4 100644 --- a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs +++ b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs @@ -138,13 +138,13 @@ public virtual ValueTask CloseAsync() /// Sends a message to a specified recipient agent through the runtime. /// /// The message object to send. - /// The recipient agent's identifier. + /// The recipient agent's identifier. /// An optional identifier for the message. /// A token used to cancel the operation if needed. /// A ValueTask that represents the asynchronous operation, returning the response object or null. - protected ValueTask SendMessageAsync(object message, AgentId recepient, string? messageId = null, CancellationToken cancellationToken = default) + protected ValueTask SendMessageAsync(object message, AgentId recipient, string? messageId = null, CancellationToken cancellationToken = default) { - return this._runtime.SendMessageAsync(message, recepient, sender: this.Id, messageId, cancellationToken); + return this._runtime.SendMessageAsync(message, recipient, sender: this.Id, messageId, cancellationToken); } /// diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs index 80cfdc484bdf..30b468115c7f 100644 --- a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs +++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs @@ -113,14 +113,14 @@ public ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sen } /// - public async ValueTask SendMessageAsync(object message, AgentId recepient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) + public async ValueTask SendMessageAsync(object message, AgentId recipient, AgentId? sender = null, string? messageId = null, CancellationToken cancellationToken = default) { return await this.ExecuteTracedAsync(async () => { MessageDelivery delivery = new MessageEnvelope(message, messageId, cancellationToken) .WithSender(sender) - .ForSend(recepient, this.SendMessageServicerAsync); + .ForSend(recipient, this.SendMessageServicerAsync); this._messageDeliveryQueue.Enqueue(delivery); Interlocked.Increment(ref this.messageQueueCount); From fc06f0a145088a12ed970922372091b95e2294ae Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:12:52 -0700 Subject: [PATCH 18/57] Typos --- .../Runtime/Abstractions.Tests/TopicIdTests.cs | 2 +- .../Agents/Runtime/Core.Tests/BaseAgentTests.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs index 71a9e4ca6723..deab4ee89215 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs @@ -145,7 +145,7 @@ public void GetHashCodeTest() } [Fact] - public void ExplicitConverstionTest() + public void ExplicitConversionTest() { // Arrange string topicIdStr = "testtype/customsource"; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs index d87d7c08030c..9d83ab4e9f9a 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs @@ -128,7 +128,7 @@ public async Task CloseAsync_ReturnsCompletedTask() } [Fact] - public async Task PublishMessageAsync_Recieved() + public async Task PublishMessageAsync_Received() { // Arrange ServiceProvider services = new ServiceCollection().BuildServiceProvider(); @@ -155,12 +155,12 @@ public async Task PublishMessageAsync_Recieved() } // Assert - await VerifyMessagHandled(runtime, senderId, message.Content); - await VerifyMessagHandled(runtime, receiverId, message.Content); + await VerifyMessageHandled(runtime, senderId, message.Content); + await VerifyMessageHandled(runtime, receiverId, message.Content); } [Fact] - public async Task SendMessageAsync_Recieved() + public async Task SendMessageAsync_Received() { // Arrange ServiceProvider services = new ServiceCollection().BuildServiceProvider(); @@ -185,11 +185,11 @@ public async Task SendMessageAsync_Recieved() } // Assert - await VerifyMessagHandled(runtime, senderId, message.Content); - await VerifyMessagHandled(runtime, receiverId, message.Content); + await VerifyMessageHandled(runtime, senderId, message.Content); + await VerifyMessageHandled(runtime, receiverId, message.Content); } - private static async Task VerifyMessagHandled(InProcessRuntime runtime, AgentId agentId, string expectedContent) + private static async Task VerifyMessageHandled(InProcessRuntime runtime, AgentId agentId, string expectedContent) { TestAgent agent = await runtime.TryGetUnderlyingAgentInstanceAsync(agentId); agent.ReceivedMessages.Should().ContainSingle(); From 5e95e698526da5027b91a783edc75c3732d47688 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:14:24 -0700 Subject: [PATCH 19/57] Typos --- dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs | 2 +- .../Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs index deab4ee89215..bb7acad6d640 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs @@ -20,7 +20,7 @@ public void ConstrWithTypeOnlyTest() } [Fact] - public void ConstrucWithTypeAndSourceTest() + public void ConstructWithTypeAndSourceTest() { // Arrange & Act TopicId topicId = new("testtype", "customsource"); diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs index a4216eade224..ae7d067cb270 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs @@ -210,7 +210,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => [InlineData(false, 0)] [InlineData(true, 1)] [Trait("Category", "Unit")] - public async Task RuntimeAgentPublishToSelfTest(bool selfPublish, int recieveCount) + public async Task RuntimeAgentPublishToSelfTest(bool selfPublish, int receiveCount) { // Arrange await using InProcessRuntime runtime = new() @@ -246,7 +246,7 @@ await runtime.RegisterAgentFactoryAsync("MyAgent", (id, runtime) => await runtime.RunUntilIdleAsync(); // Assert - Assert.Equal(recieveCount, agent.ReceivedMessages.Count); + Assert.Equal(receiveCount, agent.ReceivedMessages.Count); } [Fact] From e5c0dd55697afa3631d26235fcdbf45348c7f9f7 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:16:43 -0700 Subject: [PATCH 20/57] Headers --- dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs | 2 +- .../src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs | 2 +- dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs | 1 - dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs | 1 - .../Agents/Runtime/Abstractions.Tests/MessageContextTests.cs | 1 - dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs | 1 - .../Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs | 1 - dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs | 1 - dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs | 1 - dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs | 1 - .../Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs | 1 - .../Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs | 1 - .../Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs | 1 - dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs | 1 - 14 files changed, 2 insertions(+), 14 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs index f10da7c9a3f8..c96a259365ff 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentIdTests.cs + using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs index 4a22551c43d3..caa68c2d658b 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentMetaDataTests.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentMetaDataTests.cs + using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs index 21fd2b686add..c08291913d5e 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentProxyTests.cs using System.Text.Json; using Moq; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs index bd4b0ac2a514..63c28ed18e99 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentTypeTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs index 3d5033e120c7..f18181475b97 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageContextTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs index bb7acad6d640..8791709b3f53 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TopicIdTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs index 7e7be39cd41a..f6513957f993 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentRuntimeExtensionsTests.cs using System.Text.Json; using Microsoft.AgentRuntime.InProcess; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs index 9bacbd4dcc33..bad1b1ebde04 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentsAppBuilderTests.cs using System.Reflection; using FluentAssertions; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs index f2ae6f1afe5c..298848333d2e 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentsAppTests.cs using FluentAssertions; using Microsoft.Extensions.DependencyInjection; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs index 9d83ab4e9f9a..ddb8b3ec871c 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// BaseAgentTests.cs using System.Text.Json; using FluentAssertions; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs index c23ce46f9161..953da0188154 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypePrefixSubscriptionAttributeTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs index 0180e4ca503e..a21f9b9ac6e2 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypePrefixSubscriptionTests.cs using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs index 414ac054180f..4d0ad7991c24 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypeSubscriptionAttributeTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs index fff5982cb9b4..a2f7a865833b 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypeSubscriptionTests.cs using FluentAssertions; using Xunit; From a105a8431d4e5ece9b029066949a156d72c621d4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:17:28 -0700 Subject: [PATCH 21/57] Headers --- .../src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs | 2 +- .../src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs | 1 - .../src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs | 1 - .../src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs | 1 - .../src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs | 1 - dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs | 1 - dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs | 1 - dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs | 1 - dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs | 1 - 9 files changed, 1 insertion(+), 9 deletions(-) diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs index ae7d067cb270..79acc4f81d4b 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -// InProcessRuntimeTests.cs + using System.Text.Json; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs index 7af53609268c..c5fc727b238f 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageDeliveryTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs index 0be3ee492c19..1e33dbd404f3 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageEnvelopeTests.cs using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs index 917673807adf..0e9c18e54230 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessagingTestFixture.cs using Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs index d882ad21b635..96994c54888e 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// PublishMessageTests.cs using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs index 66e9d233589c..43ac060ecb6b 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// ResultSinkTests.cs using System.Threading.Tasks.Sources; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs index 8b0f3e963cb8..8aa774fefe42 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// SendMessageTests.cs using System.Diagnostics; using FluentAssertions; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs index 9007661bc6de..464557c36e8b 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TestAgents.cs using System.Text.Json; using Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs index 24899c74e355..3d7f1d69f610 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TestSubscription.cs using System.Diagnostics.CodeAnalysis; From 76dfbcf06f4d5b59c1310531d3b2adf29f7314c8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:24:48 -0700 Subject: [PATCH 22/57] RegEx --- dotnet/src/Agents/Runtime/Abstractions/AgentType.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs index 9261d1258ea4..f14879d7a1f9 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs @@ -11,13 +11,18 @@ namespace Microsoft.AgentRuntime; /// /// This struct is immutable and provides implicit conversion to and from . /// -public readonly struct AgentType : IEquatable +public readonly partial struct AgentType : IEquatable { - private static readonly Regex TypeRegex = new(@"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); +#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)) + 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."); } From ad5d8c8748951b99b94ecff84059dc0c9db11ce3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:31:01 -0700 Subject: [PATCH 23/57] Headers --- dotnet/src/Agents/Runtime/Abstractions/AgentId.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/IAgent.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs | 1 - .../Agents/Runtime/Abstractions/ISubscriptionDefinition.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs | 1 - dotnet/src/Agents/Runtime/Abstractions/TopicId.cs | 1 - dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs | 1 - dotnet/src/Agents/Runtime/Core/AgentsApp.cs | 1 - dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs | 1 - dotnet/src/Agents/Runtime/Core/BaseAgent.cs | 4 +--- dotnet/src/Agents/Runtime/Core/IHandle.cs | 1 - dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs | 1 - .../Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs | 1 - dotnet/src/Agents/Runtime/Core/TypeSubscription.cs | 1 - dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs | 1 - dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs | 1 - dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs | 1 - dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs | 1 - dotnet/src/Agents/Runtime/InProcess/ResultSink.cs | 1 - 23 files changed, 1 insertion(+), 25 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs index d3f496aad222..26e733b44c5a 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentId.cs using System.Diagnostics; using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs index c9b5b1a3f571..d5994c29c69c 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentMetadata.cs namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs index 533939b68958..cd159bf233e2 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentProxy.cs using System.Text.Json; diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs index fc70227ae91e..8959281db68d 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// IAgent.cs namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs index 5075e3c0307e..4896d23cd6b0 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// IAgentRuntime.cs using System.Text.Json; using Microsoft.Extensions.Hosting; diff --git a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs index edcf204b8415..c34ef6446a6e 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// IHostableAgent.cs namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs b/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs index 1eb910007f59..997938812dbc 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// ISaveState.cs using System.Text.Json; diff --git a/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs b/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs index f37b0fa74b35..5cc665868e0e 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// ISubscriptionDefinition.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs b/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs index 04493abd5f6b..10293964a697 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageContext.cs namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs index ac67ba20e26b..bda32841599a 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TopicId.cs using System.Diagnostics.CodeAnalysis; using Microsoft.AgentRuntime.Internal; diff --git a/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs b/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs index 11765a4e72de..fb80831285d8 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentRuntimeExtensions.cs using System.Reflection; using Microsoft.Extensions.DependencyInjection; diff --git a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs index c634685987e9..f6faa526a1b7 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentsApp.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs b/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs index 2ed67257c049..a4f5cfb49b63 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// AgentsAppBuilder.cs using System.Reflection; using Microsoft.Extensions.Configuration; diff --git a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs index e8538da200a4..d323a6830830 100644 --- a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs +++ b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs @@ -1,6 +1,4 @@ - -// Copyright (c) Microsoft. All rights reserved. -// BaseAgent.cs +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; using System.Text.Json; diff --git a/dotnet/src/Agents/Runtime/Core/IHandle.cs b/dotnet/src/Agents/Runtime/Core/IHandle.cs index bfa71a5a75e3..90527f581014 100644 --- a/dotnet/src/Agents/Runtime/Core/IHandle.cs +++ b/dotnet/src/Agents/Runtime/Core/IHandle.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// IHandle.cs namespace Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs index 1039a2ef80f3..b7eb7f7b5e70 100644 --- a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypePrefixSubscription.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs index d9ece8f0804e..56fbbdf19816 100644 --- a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypePrefixSubscriptionAttribute.cs namespace Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs index 025079f32dc0..57cbb9ccbd79 100644 --- a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypeSubscription.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs index 5bc383b8c47a..3fd658d1f126 100644 --- a/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// TypeSubscriptionAttribute.cs namespace Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs index 30b468115c7f..e4859755dcf9 100644 --- a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs +++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// InProcessRuntime.cs using System.Collections.Concurrent; using System.Diagnostics; diff --git a/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs b/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs index c91d75d8b430..a7900fea2770 100644 --- a/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs +++ b/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageDelivery.cs namespace Microsoft.AgentRuntime.InProcess; diff --git a/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs b/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs index 3caecb0810db..7dba9ba3b3be 100644 --- a/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs +++ b/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageEnvelope.cs namespace Microsoft.AgentRuntime.InProcess; diff --git a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs index 47db12339988..59f42075c863 100644 --- a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs +++ b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// ResultSink.cs using System.Threading.Tasks.Sources; From a6137c3a4bc591072532ffd6f1b7551edb7553bf Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:31:57 -0700 Subject: [PATCH 24/57] Naming --- .../Agents/Runtime/InProcess/ResultSink.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs index 59f42075c863..20099986481e 100644 --- a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs +++ b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs @@ -6,49 +6,49 @@ namespace Microsoft.AgentRuntime.InProcess; internal interface IResultSink : IValueTaskSource { - public void SetResult(TResult result); - public void SetException(Exception exception); - public void SetCancelled(OperationCanceledException? exception = null); + void SetResult(TResult result); + void SetException(Exception exception); + void SetCancelled(OperationCanceledException? exception = null); - public ValueTask Future { get; } + ValueTask Future { get; } } internal sealed class ResultSink : IResultSink { - private ManualResetValueTaskSourceCore core; + private ManualResetValueTaskSourceCore _core; public bool IsCancelled { get; private set; } public TResult GetResult(short token) { - return this.core.GetResult(token); + return this._core.GetResult(token); } public ValueTaskSourceStatus GetStatus(short token) { - return this.core.GetStatus(token); + return this._core.GetStatus(token); } public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) { - this.core.OnCompleted(continuation, state, token, flags); + this._core.OnCompleted(continuation, state, token, flags); } public void SetCancelled(OperationCanceledException? exception = null) { this.IsCancelled = true; - this.core.SetException(exception ?? new OperationCanceledException()); + this._core.SetException(exception ?? new OperationCanceledException()); } public void SetException(Exception exception) { - this.core.SetException(exception); + this._core.SetException(exception); } public void SetResult(TResult result) { - this.core.SetResult(result); + this._core.SetResult(result); } - public ValueTask Future => new(this, this.core.Version); + public ValueTask Future => new(this, this._core.Version); } From a3ca06cba8344044793581c5ec5225093bb6ea53 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:35:31 -0700 Subject: [PATCH 25/57] Mark preview --- .../Runtime/Abstractions/Exceptions/CantHandleException.cs | 1 - .../Runtime/Abstractions/Exceptions/MessageDroppedException.cs | 1 - .../Runtime/Abstractions/Exceptions/NotAccessibleException.cs | 1 - .../Runtime/Abstractions/Exceptions/UndeliverableException.cs | 1 - .../src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj | 1 + dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj | 1 + dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj | 1 + 7 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs index 9f19b12534cc..e2e464ae2302 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// CantHandleException.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs index 0e6a570e2928..fddfa8e13700 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// MessageDroppedException.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs index 5c8c493aec05..662c6fcf7d91 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// NotAccessibleError.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs index 73946e7a2475..a50bbb82987d 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// UndeliverableException.cs using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj index 46451ba41e23..9a2882801891 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj +++ b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj @@ -7,6 +7,7 @@ enable enable $(NoWarn);IDE1006;IDE0130 + preview diff --git a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj index 54f576b33da6..0d866120f588 100644 --- a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj +++ b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj @@ -6,6 +6,7 @@ net8.0;netstandard2.0 enable enable + preview diff --git a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj index 073a93e217f2..81e14b9813f4 100644 --- a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj +++ b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj @@ -6,6 +6,7 @@ net8.0;netstandard2.0 enable enable + preview From 7a1d8caea901b3abb40d62910c62810eacdff9ba Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:36:15 -0700 Subject: [PATCH 26/57] Headers --- .../Runtime/Abstractions/Internal/KeyValueParserExtensions.cs | 1 - dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs index f6e67df78d77..61c88973e7ed 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// KeyValueParserExtensions.cs using System.Text.RegularExpressions; diff --git a/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs b/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs index aaf36ce8769e..f66d565e3a9a 100644 --- a/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs +++ b/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// HandlerInvoker.cs using System.Diagnostics; using System.Reflection; From c3931c76f0d8841326989977559ae5d221b1b899 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 18 Apr 2025 16:40:39 -0700 Subject: [PATCH 27/57] Scope --- dotnet/src/Agents/Runtime/Abstractions/IAgent.cs | 6 +++--- .../Runtime/Abstractions/ISubscriptionDefinition.cs | 12 ++++++------ .../Internal/KeyValueParserExtensions.cs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs index 8959281db68d..fd4eb21e9332 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs @@ -10,12 +10,12 @@ public interface IAgent : ISaveState /// /// Gets the unique identifier of the agent. /// - public AgentId Id { get; } + AgentId Id { get; } /// /// Gets metadata associated with the agent. /// - public AgentMetadata Metadata { get; } + AgentMetadata Metadata { get; } /// /// Handles an incoming message for the agent. @@ -29,5 +29,5 @@ public interface IAgent : ISaveState /// /// Thrown if the message was cancelled. /// Thrown if the agent cannot handle the message. - public ValueTask OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET? + ValueTask OnMessageAsync(object message, MessageContext messageContext); // TODO: How do we express this properly in .NET? } diff --git a/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs b/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs index 5cc665868e0e..fef0f253db57 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/ISubscriptionDefinition.cs @@ -12,34 +12,34 @@ public interface ISubscriptionDefinition /// /// Gets the unique identifier of the subscription. /// - public string Id { get; } + string Id { get; } /// /// Determines whether the specified object is equal to the current subscription. /// /// The object to compare with the current instance. /// true if the specified object is equal to this instance; otherwise, false. - public bool Equals([NotNullWhen(true)] object? obj); + bool Equals([NotNullWhen(true)] object? obj); /// /// Determines whether the specified subscription is equal to the current subscription. /// /// The subscription to compare. /// true if the subscriptions are equal; otherwise, false. - public bool Equals(ISubscriptionDefinition? other); + bool Equals(ISubscriptionDefinition? other); /// /// Returns a hash code for this subscription. /// /// A hash code for the subscription. - public int GetHashCode(); + int GetHashCode(); /// /// Checks if a given matches the subscription. /// /// The topic to check. /// true if the topic matches the subscription; otherwise, false. - public bool Matches(TopicId topic); + bool Matches(TopicId topic); /// /// Maps a to an . @@ -47,5 +47,5 @@ public interface ISubscriptionDefinition /// /// The topic to map. /// The that should handle the topic. - public AgentId MapToAgent(TopicId topic); + AgentId MapToAgent(TopicId topic); } diff --git a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs index 61c88973e7ed..aade46df5a5e 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs @@ -17,7 +17,7 @@ internal static class KeyValueParserExtensions /// /// The compiled regex used for extracting key-value pairs from a string. /// - private static readonly Regex KVPairRegex = new Regex(KVPairPattern, RegexOptions.Compiled); + private static readonly Regex KVPairRegex = new(KVPairPattern, RegexOptions.Compiled); /// /// Parses a string in the format "key/value" into a tuple containing the key and value. From e7bdf3b5eaa40ea12cde81ec8ed435cd84af5960 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 15:17:17 -0700 Subject: [PATCH 28/57] Clean-up --- dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs | 2 +- dotnet/src/Agents/Runtime/Abstractions/TopicId.cs | 2 +- dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs index c34ef6446a6e..e69142237403 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs @@ -11,5 +11,5 @@ public interface IHostableAgent : IAgent /// Called when the runtime is closing. /// /// A task representing the asynchronous operation. - public ValueTask CloseAsync(); + ValueTask CloseAsync(); } diff --git a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs index bda32841599a..4e846fd0684c 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs @@ -70,7 +70,7 @@ public TopicId((string Type, string Source) kvPair) : this(kvPair.Type, kvPair.S /// The topic ID string. /// An instance of . /// Thrown when the string is not in the valid "type/source" format. - public static TopicId FromStr(string maybeTopicId) => new TopicId(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source))); + public static TopicId FromStr(string maybeTopicId) => new(maybeTopicId.ToKeyValuePair(nameof(Type), nameof(Source))); /// /// Returns the string representation of the . diff --git a/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs b/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs index e34d157e3e8e..b69db78ad8f7 100644 --- a/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs +++ b/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -// ValueTaskExtensions.cs #if !NETCOREAPP From a8ef1a32dbb3997fb96c962adc49e5b778dcb10c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 15:25:27 -0700 Subject: [PATCH 29/57] Resolve merge from main --- .../Runtime/Abstractions/Runtime.Abstractions.csproj | 12 ++++++------ dotnet/src/Agents/Runtime/Core/AgentsApp.cs | 2 +- dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj | 12 ++++++------ .../Agents/Runtime/Core/TypePrefixSubscription.cs | 2 +- dotnet/src/Agents/Runtime/Core/TypeSubscription.cs | 2 +- .../Runtime/InProcess/Runtime.InProcess.csproj | 12 ++++++------ .../src/System/ValueTaskExtensions.cs | 6 +++--- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj index 9a2882801891..cf763dd81cb2 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj +++ b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj @@ -13,12 +13,12 @@ - - - - - - + + + + + + diff --git a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs index f6faa526a1b7..8bf0059e3b7d 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs @@ -84,7 +84,7 @@ public async ValueTask PublishMessageAsync(TMessage message, TopicId t { if (Volatile.Read(ref this.runningCount) == 0) { - await StartAsync().ConfigureAwait(false); + await this.StartAsync().ConfigureAwait(false); } await this.AgentRuntime.PublishMessageAsync(message, topic, messageId: messageId, cancellationToken: cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj index 0d866120f588..b010327e1021 100644 --- a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj +++ b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs index b7eb7f7b5e70..7f0abe5630c0 100644 --- a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs @@ -66,7 +66,7 @@ public bool Matches(TopicId topic) /// Thrown if the topic does not match the subscription. public AgentId MapToAgent(TopicId topic) { - if (!Matches(topic)) + if (!this.Matches(topic)) { throw new InvalidOperationException("TopicId does not match the subscription."); } diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs index 57cbb9ccbd79..7816a432f2ab 100644 --- a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs @@ -65,7 +65,7 @@ public bool Matches(TopicId topic) /// Thrown if the topic does not match the subscription. public AgentId MapToAgent(TopicId topic) { - if (!Matches(topic)) + if (!this.Matches(topic)) { throw new InvalidOperationException("TopicId does not match the subscription."); } diff --git a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj index 81e14b9813f4..26cfbba16ef7 100644 --- a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj +++ b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs b/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs index b69db78ad8f7..1bb738b1ced3 100644 --- a/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs +++ b/dotnet/src/InternalUtilities/src/System/ValueTaskExtensions.cs @@ -19,7 +19,7 @@ internal static class ValueTaskExtensions /// return value.AsValueTask(); /// /// - public static ValueTask AsValueTask(this TValue value) => new ValueTask(value); + public static ValueTask AsValueTask(this TValue value) => new(value); /// /// Creates a that's failed and is associated with an exception. @@ -30,7 +30,7 @@ internal static class ValueTaskExtensions /// return value.AsValueTask(); /// /// - public static ValueTask AsValueTask(this Exception exception) => new ValueTask(Task.FromException(exception)); + public static ValueTask AsValueTask(this Exception exception) => new(Task.FromException(exception)); /// /// Present a regular task as a ValueTask. @@ -38,7 +38,7 @@ internal static class ValueTaskExtensions /// /// return Task.CompletedTask.AsValueTask(); /// - public static ValueTask AsValueTask(this Task task) => new ValueTask(task); + public static ValueTask AsValueTask(this Task task) => new(task); } #endif From 9c3a9998d37070ccf7f7e6df9ac21d633ea67687 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 15:50:29 -0700 Subject: [PATCH 30/57] Namespaces --- dotnet/Directory.Packages.props | 1 - .../Agents/Runtime/Abstractions.Tests/AgentIdTests.cs | 1 + .../Runtime/Abstractions.Tests/AgentProxyTests.cs | 2 ++ .../Runtime/Abstractions.Tests/AgentTypeTests.cs | 1 + .../Runtime/Abstractions.Tests/MessageContextTests.cs | 2 ++ .../Runtime.Abstractions.Tests.csproj | 2 -- .../Agents/Runtime/Abstractions.Tests/TopicIdTests.cs | 1 + dotnet/src/Agents/Runtime/Abstractions/AgentId.cs | 1 + .../src/Agents/Runtime/Abstractions/AgentMetadata.cs | 2 ++ dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs | 2 ++ dotnet/src/Agents/Runtime/Abstractions/AgentType.cs | 1 + .../Abstractions/Exceptions/CantHandleException.cs | 1 + .../Exceptions/MessageDroppedException.cs | 1 + .../Abstractions/Exceptions/NotAccessibleException.cs | 1 + .../Abstractions/Exceptions/UndeliverableException.cs | 1 + dotnet/src/Agents/Runtime/Abstractions/IAgent.cs | 3 +++ .../src/Agents/Runtime/Abstractions/IAgentRuntime.cs | 4 ++++ .../src/Agents/Runtime/Abstractions/IHostableAgent.cs | 2 ++ dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs | 1 + .../Abstractions/Internal/KeyValueParserExtensions.cs | 1 + .../src/Agents/Runtime/Abstractions/MessageContext.cs | 3 +++ .../Runtime/Abstractions/Runtime.Abstractions.csproj | 2 -- dotnet/src/Agents/Runtime/Abstractions/TopicId.cs | 1 + .../Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs | 3 +++ .../Runtime/Core.Tests/AgentsAppBuilderTests.cs | 1 + .../src/Agents/Runtime/Core.Tests/AgentsAppTests.cs | 3 +++ .../src/Agents/Runtime/Core.Tests/BaseAgentTests.cs | 4 ++++ .../Runtime/Core.Tests/Runtime.Core.Tests.csproj | 2 -- .../TypePrefixSubscriptionAttributeTests.cs | 1 + .../Runtime/Core.Tests/TypePrefixSubscriptionTests.cs | 1 + .../Core.Tests/TypeSubscriptionAttributeTests.cs | 1 + .../Runtime/Core.Tests/TypeSubscriptionTests.cs | 1 + .../src/Agents/Runtime/Core/AgentRuntimeExtensions.cs | 4 ++++ dotnet/src/Agents/Runtime/Core/AgentsApp.cs | 11 +++++++---- dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs | 4 ++++ dotnet/src/Agents/Runtime/Core/BaseAgent.cs | 4 ++++ dotnet/src/Agents/Runtime/Core/IHandle.cs | 2 ++ .../Agents/Runtime/Core/Internal/HandlerInvoker.cs | 4 ++++ dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj | 2 -- .../src/Agents/Runtime/Core/TypePrefixSubscription.cs | 1 + .../Runtime/Core/TypePrefixSubscriptionAttribute.cs | 2 ++ dotnet/src/Agents/Runtime/Core/TypeSubscription.cs | 1 + .../Agents/Runtime/Core/TypeSubscriptionAttribute.cs | 2 ++ .../Runtime/InProcess.Tests/InProcessRuntimeTests.cs | 4 ++++ .../Runtime/InProcess.Tests/MessageDeliveryTests.cs | 9 ++++++--- .../Runtime/InProcess.Tests/MessageEnvelopeTests.cs | 3 +++ .../Runtime/InProcess.Tests/MessagingTestFixture.cs | 6 +++++- .../Runtime/InProcess.Tests/PublishMessageTests.cs | 4 ++++ .../Agents/Runtime/InProcess.Tests/ResultSinkTests.cs | 2 ++ .../InProcess.Tests/Runtime.InProcess.Tests.csproj | 2 -- .../Runtime/InProcess.Tests/SendMessageTests.cs | 3 +++ .../src/Agents/Runtime/InProcess.Tests/TestAgents.cs | 3 +++ .../Runtime/InProcess.Tests/TestSubscription.cs | 1 + .../src/Agents/Runtime/InProcess/InProcessRuntime.cs | 5 +++++ .../src/Agents/Runtime/InProcess/MessageDelivery.cs | 4 ++++ .../src/Agents/Runtime/InProcess/MessageEnvelope.cs | 4 ++++ dotnet/src/Agents/Runtime/InProcess/ResultSink.cs | 2 ++ .../Agents/Runtime/InProcess/Runtime.InProcess.csproj | 2 -- 58 files changed, 124 insertions(+), 21 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 9950fcc9affd..c0150982d16f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,7 +5,6 @@ true - diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs index c96a259365ff..ba10db9ab2aa 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentIdTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs index c08291913d5e..4d79e9f0b5c3 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentProxyTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Moq; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs index 63c28ed18e99..a31b7ccda79a 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/AgentTypeTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Xunit; namespace Microsoft.AgentRuntime.Abstractions.Tests; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs index f18181475b97..2cab72fa4714 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/MessageContextTests.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; using Xunit; namespace Microsoft.AgentRuntime.Abstractions.Tests; diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj b/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj index 95fafae135c4..51401f0d71a0 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/Runtime.Abstractions.Tests.csproj @@ -4,8 +4,6 @@ Microsoft.Agents.Runtime.Abstractions.UnitTests Microsoft.Agents.Runtime.Abstractions.UnitTests net8.0 - enable - enable True $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001 diff --git a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs index 8791709b3f53..24580515dd2b 100644 --- a/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs +++ b/dotnet/src/Agents/Runtime/Abstractions.Tests/TopicIdTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Xunit; namespace Microsoft.AgentRuntime.Abstractions.Tests; diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs index 26e733b44c5a..cdc6653a6dea 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentId.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs index d5994c29c69c..a41af63e6097 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentMetadata.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs index cd159bf233e2..010e0a984ca8 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs index f14879d7a1f9..a38ca5f17177 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentType.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Text.RegularExpressions; namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs index e2e464ae2302..07808eb95112 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/CantHandleException.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs index fddfa8e13700..4c12673cd6ef 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/MessageDroppedException.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs index 662c6fcf7d91..50785a2cfdbb 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; +using System; namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs index a50bbb82987d..178ae1d5916c 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; +using System; namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs index fd4eb21e9332..b7e062f06be2 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgent.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading.Tasks; + namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs index 4896d23cd6b0..059253d2ce8c 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IAgentRuntime.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Hosting; namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs index e69142237403..10564da12ee6 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/IHostableAgent.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; + namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs b/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs index 997938812dbc..1e7a4d5f7925 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/ISaveState.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using System.Threading.Tasks; namespace Microsoft.AgentRuntime; diff --git a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs index aade46df5a5e..e1885225d6e9 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Internal/KeyValueParserExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Text.RegularExpressions; namespace Microsoft.AgentRuntime.Internal; diff --git a/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs b/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs index 10293964a697..6a5e1b1fd9c5 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/MessageContext.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; + namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj index cf763dd81cb2..7909d2b780c1 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj +++ b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj @@ -4,8 +4,6 @@ Microsoft.Agents.Runtime.Abstractions Microsoft.Agents.Runtime.Abstractions net8.0;netstandard2.0 - enable - enable $(NoWarn);IDE1006;IDE0130 preview diff --git a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs index 4e846fd0684c..72956060cef1 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/TopicId.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AgentRuntime.Internal; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs index f6513957f993..50c555381823 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentRuntimeExtensionsTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Text.Json; +using System.Threading.Tasks; using Microsoft.AgentRuntime.InProcess; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs index bad1b1ebde04..d9d6ec882b0b 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppBuilderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Reflection; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.AgentRuntime.InProcess; using Microsoft.Extensions.Configuration; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs index 298848333d2e..9114e91ec561 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/AgentsAppTests.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs index ddb8b3ec871c..d5943cecdefd 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/BaseAgentTests.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Linq; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.AgentRuntime.InProcess; using Microsoft.Extensions.DependencyInjection; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj b/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj index 576c5eaabb7f..076804a6f427 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj +++ b/dotnet/src/Agents/Runtime/Core.Tests/Runtime.Core.Tests.csproj @@ -4,8 +4,6 @@ Microsoft.Agents.Runtime.Core.Tests Microsoft.Agents.Runtime.Core.Tests net8.0 - enable - enable True $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001 diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs index 953da0188154..0ea23b962f75 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionAttributeTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Xunit; namespace Microsoft.AgentRuntime.Core.Tests; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs index a21f9b9ac6e2..4033addd97dc 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypePrefixSubscriptionTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs index 4d0ad7991c24..684b8c7a57ad 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionAttributeTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Xunit; namespace Microsoft.AgentRuntime.Core.Tests; diff --git a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs index a2f7a865833b..f0f4ea55de2e 100644 --- a/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs +++ b/dotnet/src/Agents/Runtime/Core.Tests/TypeSubscriptionTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs b/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs index fb80831285d8..dad520758f25 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentRuntimeExtensions.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs index 8bf0059e3b7d..0f46bc814fb0 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentsApp.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentsApp.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -11,7 +14,7 @@ namespace Microsoft.AgentRuntime.Core; /// public class AgentsApp { - private int runningCount; + private int _runningCount; /// /// Initializes a new instance of the class. @@ -48,7 +51,7 @@ internal AgentsApp(IHost host) /// public async ValueTask StartAsync() { - if (Interlocked.Exchange(ref this.runningCount, 1) != 0) + if (Interlocked.Exchange(ref this._runningCount, 1) != 0) { throw new InvalidOperationException("Application is already running."); } @@ -62,7 +65,7 @@ public async ValueTask StartAsync() /// public async ValueTask ShutdownAsync() { - if (Interlocked.Exchange(ref this.runningCount, 0) != 1) + if (Interlocked.Exchange(ref this._runningCount, 0) != 1) { throw new InvalidOperationException("Application is already stopped."); } @@ -82,7 +85,7 @@ public async ValueTask ShutdownAsync() public async ValueTask PublishMessageAsync(TMessage message, TopicId topic, string? messageId = null, CancellationToken cancellationToken = default) where TMessage : notnull { - if (Volatile.Read(ref this.runningCount) == 0) + if (Volatile.Read(ref this._runningCount) == 0) { await this.StartAsync().ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs b/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs index a4f5cfb49b63..ce5e4d082f7a 100644 --- a/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs +++ b/dotnet/src/Agents/Runtime/Core/AgentsAppBuilder.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs index d323a6830830..7a7ba124cb0d 100644 --- a/dotnet/src/Agents/Runtime/Core/BaseAgent.cs +++ b/dotnet/src/Agents/Runtime/Core/BaseAgent.cs @@ -1,7 +1,11 @@ // 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.AgentRuntime.Core.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/dotnet/src/Agents/Runtime/Core/IHandle.cs b/dotnet/src/Agents/Runtime/Core/IHandle.cs index 90527f581014..460a5a10acda 100644 --- a/dotnet/src/Agents/Runtime/Core/IHandle.cs +++ b/dotnet/src/Agents/Runtime/Core/IHandle.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; + namespace Microsoft.AgentRuntime.Core; /// diff --git a/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs b/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs index f66d565e3a9a..867297f4aed6 100644 --- a/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs +++ b/dotnet/src/Agents/Runtime/Core/Internal/HandlerInvoker.cs @@ -1,7 +1,11 @@ // 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.AgentRuntime.Core.Internal; diff --git a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj index b010327e1021..542a343445a2 100644 --- a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj +++ b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj @@ -4,8 +4,6 @@ Microsoft.Agents.Runtime.Core Microsoft.Agents.Runtime.Core net8.0;netstandard2.0 - enable - enable preview diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs index 7f0abe5630c0..e23d39cd331c 100644 --- a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscription.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs index 56fbbdf19816..279ef1ff49a2 100644 --- a/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs +++ b/dotnet/src/Agents/Runtime/Core/TypePrefixSubscriptionAttribute.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.AgentRuntime.Core; /// diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs index 7816a432f2ab..437d7732e3b7 100644 --- a/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscription.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.AgentRuntime.Core; diff --git a/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs b/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs index 3fd658d1f126..037ec2570442 100644 --- a/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs +++ b/dotnet/src/Agents/Runtime/Core/TypeSubscriptionAttribute.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; + namespace Microsoft.AgentRuntime.Core; /// diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs index 79acc4f81d4b..61936146714d 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/InProcessRuntimeTests.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs index c5fc727b238f..1577a64226d8 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.AgentRuntime.InProcess.Tests; @@ -29,7 +32,7 @@ public async Task Future_WithResultSink_ReturnsSinkFuture() { // Arrange MessageEnvelope message = new(new object()); - Func servicer = (_, _) => new ValueTask(); + static ValueTask servicer(MessageEnvelope msg, CancellationToken token) => new ValueTask(); ResultSink resultSink = new(); int expectedResult = 42; @@ -54,13 +57,13 @@ public async Task InvokeAsync_CallsServicerWithCorrectParameters() MessageEnvelope? passedMessage = null; CancellationToken? passedToken = null; - Func servicer = (msg, token) => + ValueTask servicer(MessageEnvelope msg, CancellationToken token) { servicerCalled = true; passedMessage = msg; passedToken = token; return ValueTask.CompletedTask; - }; + } ResultSink sink = new(); MessageDelivery delivery = new(message, servicer, sink); diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs index 1e33dbd404f3..904a2853a33d 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageEnvelopeTests.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.AgentRuntime.InProcess.Tests; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs index 0e9c18e54230..e32f40f4061f 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessagingTestFixture.cs @@ -1,5 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AgentRuntime.Core; namespace Microsoft.AgentRuntime.InProcess.Tests; @@ -15,7 +19,7 @@ public sealed class TestException : Exception { } public sealed class PublisherAgent : TestAgent, IHandle { - private IList targetTopics; + private readonly IList targetTopics; public PublisherAgent(AgentId id, IAgentRuntime runtime, string description, IList targetTopics) : base(id, runtime, description) diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs index 96994c54888e..ecca606645af 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs @@ -1,5 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs index 43ac060ecb6b..6aa8d9b444b1 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/ResultSinkTests.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading.Tasks; using System.Threading.Tasks.Sources; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj b/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj index 6447455af153..a3e050d5fc00 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/Runtime.InProcess.Tests.csproj @@ -4,8 +4,6 @@ Microsoft.Agents.Runtime.InProcess.Tests Microsoft.Agents.Runtime.InProcess.Tests net8.0 - enable - enable True $(NoWarn);CA1707;CA2007;CA1812;CA1861;CA1063;CS0618;CS1591;IDE1006;VSTHRD111;SKEXP0001;SKEXP0050;SKEXP0110;OPENAI001 diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs index 8aa774fefe42..c60f77563cd2 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/SendMessageTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Threading.Tasks; using FluentAssertions; using Xunit; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs index 464557c36e8b..d0ec0d24ba58 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/TestAgents.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; using System.Text.Json; +using System.Threading.Tasks; using Microsoft.AgentRuntime.Core; namespace Microsoft.AgentRuntime.InProcess.Tests; diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs index 3d7f1d69f610..e225ee71a1dd 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/TestSubscription.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.AgentRuntime.InProcess.Tests; diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs index e4859755dcf9..050fa9d8c016 100644 --- a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs +++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs @@ -1,9 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.AgentRuntime.InProcess; diff --git a/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs b/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs index a7900fea2770..dd9b3fb20071 100644 --- a/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs +++ b/dotnet/src/Agents/Runtime/InProcess/MessageDelivery.cs @@ -1,5 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; + namespace Microsoft.AgentRuntime.InProcess; internal sealed class MessageDelivery(MessageEnvelope message, Func servicer, IResultSink resultSink) diff --git a/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs b/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs index 7dba9ba3b3be..75c651463698 100644 --- a/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs +++ b/dotnet/src/Agents/Runtime/InProcess/MessageEnvelope.cs @@ -1,5 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading; +using System.Threading.Tasks; + namespace Microsoft.AgentRuntime.InProcess; internal sealed class MessageEnvelope diff --git a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs index 20099986481e..2274a412efa3 100644 --- a/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs +++ b/dotnet/src/Agents/Runtime/InProcess/ResultSink.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading.Tasks; using System.Threading.Tasks.Sources; namespace Microsoft.AgentRuntime.InProcess; diff --git a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj index 26cfbba16ef7..dd662f59b0e9 100644 --- a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj +++ b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj @@ -4,8 +4,6 @@ Microsoft.Agents.Runtime.InProcess Microsoft.Agents.Runtime.InProcess net8.0;netstandard2.0 - enable - enable preview From 6afe1fa642aae9cc7317daff3e7c103af7994b8c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 15:53:15 -0700 Subject: [PATCH 31/57] Namespace ordering --- .../Runtime/Abstractions/Exceptions/NotAccessibleException.cs | 2 +- .../Runtime/Abstractions/Exceptions/UndeliverableException.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs index 50785a2cfdbb..ea2388bafa44 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/NotAccessibleException.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; -using System; namespace Microsoft.AgentRuntime; /// diff --git a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs index 178ae1d5916c..3fa4c06be5cd 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/Exceptions/UndeliverableException.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; - using System; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AgentRuntime; /// From 3e673ae1a6851d7b09a937555905e75c16f37d0a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 16:01:07 -0700 Subject: [PATCH 32/57] A couple more --- .../Runtime/InProcess.Tests/MessageDeliveryTests.cs | 10 +++++----- .../Runtime/InProcess.Tests/PublishMessageTests.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs index 1577a64226d8..1dbdfd4ce018 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/MessageDeliveryTests.cs @@ -10,20 +10,21 @@ namespace Microsoft.AgentRuntime.InProcess.Tests; [Trait("Category", "Unit")] public class MessageDeliveryTests { + private static readonly Func EmptyServicer = (_, _) => new ValueTask(); + [Fact] public void Constructor_InitializesProperties() { // Arrange MessageEnvelope message = new(new object()); - Func servicer = (_, _) => new ValueTask(); ResultSink resultSink = new(); // Act - MessageDelivery delivery = new(message, servicer, resultSink); + MessageDelivery delivery = new(message, EmptyServicer, resultSink); // Assert Assert.Same(message, delivery.Message); - Assert.Same(servicer, delivery.Servicer); + Assert.Same(EmptyServicer, delivery.Servicer); Assert.Same(resultSink, delivery.ResultSink); } @@ -32,14 +33,13 @@ public async Task Future_WithResultSink_ReturnsSinkFuture() { // Arrange MessageEnvelope message = new(new object()); - static ValueTask servicer(MessageEnvelope msg, CancellationToken token) => new ValueTask(); ResultSink resultSink = new(); int expectedResult = 42; resultSink.SetResult(expectedResult); // Act - MessageDelivery delivery = new(message, servicer, resultSink); + MessageDelivery delivery = new(message, EmptyServicer, resultSink); object? result = await delivery.ResultSink.Future; // Assert diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs index ecca606645af..b40852cc83a2 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs @@ -108,7 +108,7 @@ public async Task Test_PublishMessage_RecurrentPublishSucceeds() await fixture.RegisterFactoryMapInstances( nameof(PublisherAgent), - (id, runtime) => ValueTask.FromResult(new PublisherAgent(id, runtime, string.Empty, new List { new TopicId("TestTopic") }))); + (id, runtime) => ValueTask.FromResult(new PublisherAgent(id, runtime, string.Empty, [new TopicId("TestTopic")]))); await fixture.Runtime.AddSubscriptionAsync(new TestSubscription("RunTest", nameof(PublisherAgent))); From 5c131ffd9afb17519f0ce9b00e07cc00e99e0ab4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 16:03:56 -0700 Subject: [PATCH 33/57] Remove namespace --- dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs index b40852cc83a2..a3aeb531efa4 100644 --- a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs +++ b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; From d3375ae04a55f68d23e0e2bbf1dde0de7fed001f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 16:08:35 -0700 Subject: [PATCH 34/57] Comment cleanup --- dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs | 2 +- dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs index 010e0a984ca8..106c1cbcefc2 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs +++ b/dotnet/src/Agents/Runtime/Abstractions/AgentProxy.cs @@ -79,7 +79,7 @@ public ValueTask SaveStateAsync() private AgentMetadata QueryMetadataAndUnwrap() { #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); // %%% PRAGMA + return this._runtime.GetAgentMetadataAsync(this.Id).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); #pragma warning restore VSTHRD002 // Avoid problematic synchronous waits } } diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs index 050fa9d8c016..c3238a1f9614 100644 --- a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs +++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs @@ -362,7 +362,7 @@ private async ValueTask PublishMessageServicer(MessageEnvelope envelope, Cancell AgentId? sender = envelope.Sender; - using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); // %%% CHANGE - USING + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) { Sender = sender, @@ -401,7 +401,7 @@ private async ValueTask PublishMessageServicer(MessageEnvelope envelope, Cancell throw new InvalidOperationException("Message must have a receiver to be sent."); } - using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); // %%% CHANGE - USING + using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken); MessageContext messageContext = new(envelope.MessageId, combinedSource.Token) { Sender = envelope.Sender, From 66c7944f9de00779216712cb49e470f056e01b00 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 16:21:24 -0700 Subject: [PATCH 35/57] Naming --- dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs index c3238a1f9614..cce3611d139e 100644 --- a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs +++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs @@ -107,7 +107,7 @@ public ValueTask PublishMessageAsync(object message, TopicId topic, AgentId? sen MessageDelivery delivery = new MessageEnvelope(message, messageId, cancellationToken) .WithSender(sender) - .ForPublish(topic, this.PublishMessageServicer); + .ForPublish(topic, this.PublishMessageServicerAsync); this._messageDeliveryQueue.Enqueue(delivery); Interlocked.Increment(ref this.messageQueueCount); @@ -345,7 +345,7 @@ private async Task RunAsync(CancellationToken cancellation) await this.FinishAsync(this._finishSource?.Token ?? CancellationToken.None).ConfigureAwait(false); } - private async ValueTask PublishMessageServicer(MessageEnvelope envelope, CancellationToken deliveryToken) + private async ValueTask PublishMessageServicerAsync(MessageEnvelope envelope, CancellationToken deliveryToken) { if (!envelope.Topic.HasValue) { From e730ff1c16fb29497f68d6d2baeb43ec88d3af64 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 16:29:26 -0700 Subject: [PATCH 36/57] Logging --- .../Orchestration/Step02_Sequential.cs | 6 ++-- .../Orchestration/Step03_GroupChat.cs | 6 ++-- .../Orchestration/AgentOrchestration.cs | 35 ++++++++++++------- .../ConcurrentOrchestration.String.cs | 12 +++++-- .../Concurrent/ConcurrentOrchestration.cs | 8 ++--- .../GroupChat/GroupChatManagerActor.cs | 15 ++++---- .../GroupChat/GroupChatOrchestration.cs | 10 +++--- .../Logging/AgentOrchestrationLogMessages.cs | 28 +++++++-------- .../ConcurrentOrchestrationLogMessages.cs | 2 +- .../Magentic/MagenticOrchestration.cs | 8 ++--- .../Agents/Orchestration/Orchestratable.cs | 6 ++-- .../Sequential/SequentialOrchestration.cs | 6 ++-- .../AgentUtilities/BaseOrchestrationTest.cs | 2 +- 13 files changed, 79 insertions(+), 65 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index f4dcfe0b4c65..44547938df3e 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -10,10 +10,10 @@ namespace GettingStarted.Orchestration; /// /// Demonstrates how to use the . /// -public class Step02_Sequentail(ITestOutputHelper output) : BaseOrchestrationTest(output) +public class Step02_Sequential(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleSequentailAsync() + public async Task SimpleSequentialAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); @@ -36,7 +36,7 @@ public async Task SimpleSequentailAsync() } [Fact] - public async Task NestedSequentailAsync() + public async Task NestedSequentialAsync() { // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index c114362ef047..9385f481e918 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.AgentRuntime.InProcess; -using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; @@ -73,8 +71,8 @@ public async Task SingleNestedActorAsync() InProcessRuntime runtime = new(); GroupChatOrchestration orchestrationInner = new(runtime, agent) { - InputTransform = (ChatMessages.InputTask input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input.Message.ToString()).ToInputTask()), - ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToResult()) + InputTransform = (ChatMessages.InputTask input) => ValueTask.FromResult(input), + ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result), }; GroupChatOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 842df90bd6df..0e7e26b63912 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -83,7 +83,7 @@ public async ValueTask> InvokeAsync(TInput input, T TaskCompletionSource completion = new(); - AgentType orchestrationType = await this.RegisterAsync(topic, completion, targetActor: null, logger).ConfigureAwait(false); + AgentType orchestrationType = await this.RegisterAsync(topic, completion, handoff: null, this.LoggerFactory).ConfigureAwait(false); logger.LogOrchestrationInvoke(this._orchestrationRoot, topic); @@ -116,21 +116,22 @@ public async ValueTask> InvokeAsync(TInput input, T /// The topic identifier for the orchestration session. /// The orchestration type used in registration. /// The entry AgentType for the orchestration, if any. + /// The active logger factory. /// The logger to use during registration - protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger); + protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger); // %%% TODO - CLASS LEVEL /// /// Registers the orchestration with the runtime using an external topic and an optional target actor. /// /// The external topic identifier to register with. - /// An optional target actor that may influence registration behavior. + /// The actor type used for handoff. Only defined for nested orchestrations. + /// The active logger factory. /// A ValueTask containing the AgentType that indicates the registered agent. - /// The logger to use during registration - protected internal override ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor, ILogger logger) + protected internal override ValueTask RegisterAsync(TopicId externalTopic, AgentType? handoff, ILoggerFactory loggerFactory) { TopicId orchestrationTopic = new($"{externalTopic.Type}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); - return this.RegisterAsync(orchestrationTopic, completion: null, targetActor, logger); + return this.RegisterAsync(orchestrationTopic, completion: null, handoff, loggerFactory); } /// @@ -151,11 +152,19 @@ protected async Task SubscribeAsync(string agentType, params TopicId[] topics) /// /// The unique topic for the orchestration session. /// A TaskCompletionSource for the final result output, if applicable. - /// An optional target actor for routing results. - /// The orchestration logger (for use during registration) + /// The actor type used for handoff. Only defined for nested orchestrations. + /// The logger factory to use during initialization. /// The AgentType representing the orchestration entry point. - private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? targetActor, ILogger logger) + private async ValueTask RegisterAsync(TopicId topic, TaskCompletionSource? completion, AgentType? handoff, ILoggerFactory loggerFactory) { + // Use the orchestration's logger factory, if assigned; otherwise, use the provided factory. + if (this.LoggerFactory.GetType() != typeof(NullLoggerFactory)) + { + loggerFactory = this.LoggerFactory; + } + // Create a logger for the orchestration registration. + ILogger logger = loggerFactory.CreateLogger(this.GetType()); + logger.LogOrchestrationRegistrationStart(this._orchestrationRoot, topic); if (this.InputTransform == null) @@ -173,13 +182,13 @@ await this.Runtime.RegisterAgentFactoryAsync( this.FormatAgentType(topic, "Root"), (agentId, runtime) => ValueTask.FromResult( - new ResultActor(agentId, runtime, this._orchestrationRoot, this.ResultTransform, completion, this.LoggerFactory.CreateLogger()) + new ResultActor(agentId, runtime, this._orchestrationRoot, this.ResultTransform, completion, loggerFactory.CreateLogger()) { - CompletionTarget = targetActor, + CompletionTarget = handoff, })).ConfigureAwait(false); // Register orchestration members - AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal, logger).ConfigureAwait(false); + AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal, loggerFactory, logger).ConfigureAwait(false); // Register actor for orchestration entry-point AgentType orchestrationEntry = @@ -187,7 +196,7 @@ await this.Runtime.RegisterAgentFactoryAsync( this.FormatAgentType(topic, "Boot"), (agentId, runtime) => ValueTask.FromResult( - new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), this.LoggerFactory.CreateLogger())) + new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), loggerFactory.CreateLogger())) ).ConfigureAwait(false); logger.LogOrchestrationRegistrationDone(this._orchestrationRoot, topic); diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs index f9d024298c4e..2ab42feb59cb 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -19,7 +19,15 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration ValueTask.FromResult(input.ToRequest()); - this.ResultTransform = (ConcurrentMessages.Result[] result) => ValueTask.FromResult([.. result.Select(r => r.Message.Content ?? string.Empty)]); + this.InputTransform = (string input) => + { + System.Console.WriteLine("*** TRANSFORM INPUT - OUTER"); + return ValueTask.FromResult(input.ToRequest()); + }; + this.ResultTransform = (ConcurrentMessages.Result[] result) => + { + System.Console.WriteLine("*** TRANSFORM OUTPUT - OUTER"); + return ValueTask.FromResult([.. result.Select(r => r.Message.Content ?? string.Empty)]); + }; } } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index af464ca768ac..69d86eae32ec 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -32,7 +32,7 @@ protected override ValueTask StartAsync(TopicId topic, ConcurrentMessages.Reques } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) { // Register result actor AgentType resultType = this.FormatAgentType(topic, "Results"); @@ -40,7 +40,7 @@ await this.Runtime.RegisterAgentFactoryAsync( resultType, (agentId, runtime) => ValueTask.FromResult( - new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count, loggerFactory.CreateLogger()))).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, resultType, "RESULTS"); // Register member actors - All agents respond to the same message. @@ -57,7 +57,7 @@ await this.Runtime.RegisterAgentFactoryAsync( } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - memberType = await orchestration.RegisterAsync(topic, resultType, logger).ConfigureAwait(false); + memberType = await orchestration.RegisterAsync(topic, resultType, loggerFactory).ConfigureAwait(false); } logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); @@ -73,7 +73,7 @@ ValueTask RegisterAgentAsync(Agent agent) this.Runtime.RegisterAgentFactoryAsync( this.FormatAgentType(topic, $"Agent_{agentCount}"), (agentId, runtime) => - ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType, this.LoggerFactory.CreateLogger()))); + ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType, loggerFactory.CreateLogger()))); } } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index 4b76924c6f98..8ef20a076043 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AgentRuntime; @@ -14,6 +13,8 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// internal sealed class GroupChatManagerActor : ChatManagerActor // %%% ABSTRACT { + private int _count = 0; + /// /// Initializes a new instance of the class. /// @@ -38,15 +39,13 @@ public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, protected override Task SelectAgentAsync() { // %%% PLACEHOLDER SELECTION LOGIC -#pragma warning disable CA5394 // Do not use insecure randomness - int index = Random.Shared.Next(this.Team.Count + 1); -#pragma warning restore CA5394 // Do not use insecure randomness - AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; - AgentType? agentType = null; - if (index < this.Team.Count) + if (this._count >= 2) { - agentType = agentTypes[index]; + return Task.FromResult(null); } + AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; + AgentType? agentType = agentTypes[this._count % this.Team.Count]; + ++this._count; return Task.FromResult(agentType); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 53c59195a444..1aac766c5124 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -16,7 +16,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; public class GroupChatOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = typeof(ConcurrentOrchestration<,>).Name.Split('`').First(); + internal static readonly string OrchestrationName = typeof(GroupChatOrchestration<,>).Name.Split('`').First(); /// /// Initializes a new instance of the class. @@ -35,7 +35,7 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) { AgentType managerType = this.FormatAgentType(topic, "Manager"); @@ -52,7 +52,7 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - memberType = await orchestration.RegisterAsync(topic, managerType, logger).ConfigureAwait(false); + memberType = await orchestration.RegisterAsync(topic, managerType, loggerFactory).ConfigureAwait(false); } team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID @@ -66,7 +66,7 @@ await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, loggerFactory.CreateLogger()))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); @@ -78,7 +78,7 @@ ValueTask RegisterAgentAsync(Agent agent) this.Runtime.RegisterAgentFactoryAsync( this.FormatAgentType(topic, $"Agent_{agentCount}"), (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, this.LoggerFactory.CreateLogger()))); + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerFactory.CreateLogger()))); } } } diff --git a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs index 23efa5f35990..0bb9ef027329 100644 --- a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; internal static partial class AgentOrchestrationLogMessages { /// - /// Logs awaiting the orchestration. + /// Logs the start of the registration phase for an orchestration. /// [LoggerMessage( EventId = 0, @@ -30,7 +30,7 @@ public static partial void LogOrchestrationRegistrationStart( TopicId topic); /// - /// Logs actor registration. + /// Logs pattern actor registration. /// [LoggerMessage( EventId = 0, @@ -43,7 +43,7 @@ public static partial void LogRegisterActor( string label); /// - /// Logs actor registration. + /// Logs agent actor registration. /// [LoggerMessage( EventId = 0, @@ -57,7 +57,7 @@ public static partial void LogRegisterActor( int count); /// - /// Logs awaiting the orchestration. + /// Logs the end of the registration phase for an orchestration. /// [LoggerMessage( EventId = 0, @@ -69,7 +69,7 @@ public static partial void LogOrchestrationRegistrationDone( TopicId topic); /// - /// Logs orchestration invocation. + /// Logs an orchestration invocation /// [LoggerMessage( EventId = 0, @@ -81,8 +81,8 @@ public static partial void LogOrchestrationInvoke( TopicId topic); /// - /// Logs that the orchestration - /// has started successfully and yielded control back to the caller. + /// Logs that the orchestration has started successfully and + /// yielded control back to the caller. /// [LoggerMessage( EventId = 0, @@ -94,7 +94,7 @@ public static partial void LogOrchestrationYield( TopicId topic); /// - /// Logs the start of the outer orchestration. + /// Logs the start an orchestration (top/outer). /// [LoggerMessage( EventId = 0, @@ -106,7 +106,7 @@ public static partial void LogOrchestrationStart( AgentId agentId); /// - /// %%% COMMENT + /// Logs that orchestration request actor is active /// [LoggerMessage( EventId = 0, @@ -118,12 +118,12 @@ public static partial void LogOrchestrationRequestInvoke( AgentId agentId); /// - /// %%% COMMENT + /// Logs that orchestration request actor experienced an unexpected failure. /// [LoggerMessage( EventId = 0, Level = LogLevel.Error, - Message = "{Orchestration} request failed: {AgentId}")] + Message = "FAILURE {Orchestration}: {AgentId}")] public static partial void LogOrchestrationRequestFailure( this ILogger logger, string orchestration, @@ -131,7 +131,7 @@ public static partial void LogOrchestrationRequestFailure( Exception exception); /// - /// %%% COMMENT + /// Logs that orchestration result actor is active /// [LoggerMessage( EventId = 0, @@ -143,12 +143,12 @@ public static partial void LogOrchestrationResultInvoke( AgentId agentId); /// - /// %%% COMMENT + /// Logs that orchestration result actor experienced an unexpected failure. /// [LoggerMessage( EventId = 0, Level = LogLevel.Error, - Message = "{Orchestration} result failed: {AgentId}")] + Message = "FAILURE {Orchestration}: {AgentId}")] public static partial void LogOrchestrationResultFailure( this ILogger logger, string orchestration, diff --git a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs index 6faae339a0fc..a9e4a8075ea5 100644 --- a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs @@ -41,7 +41,7 @@ public static partial void LogConcurrentAgentResult( [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "COLLECT Concurrent result [{AgentId}]: ({ResultCount} / {ExpectedCount})")] + Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")] public static partial void LogConcurrentResultCapture( this ILogger logger, AgentId agentId, diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs index ac6b5fcd5a63..1bdce9e3e718 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs @@ -35,7 +35,7 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) { AgentType managerType = this.FormatAgentType(topic, "Manager"); @@ -52,7 +52,7 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - memberType = await orchestration.RegisterAsync(topic, managerType, logger).ConfigureAwait(false); + memberType = await orchestration.RegisterAsync(topic, managerType, loggerFactory).ConfigureAwait(false); } team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID @@ -66,7 +66,7 @@ await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic, this.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic, loggerFactory.CreateLogger()))).ConfigureAwait(false); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); @@ -78,7 +78,7 @@ ValueTask RegisterAgentAsync(Agent agent) this.Runtime.RegisterAgentFactoryAsync( this.FormatAgentType(topic, $"Agent_{agentCount}"), (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, this.LoggerFactory.CreateLogger()))); + ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerFactory.CreateLogger()))); } } } diff --git a/dotnet/src/Agents/Orchestration/Orchestratable.cs b/dotnet/src/Agents/Orchestration/Orchestratable.cs index f8d82f809f57..c3a5f2b677bd 100644 --- a/dotnet/src/Agents/Orchestration/Orchestratable.cs +++ b/dotnet/src/Agents/Orchestration/Orchestratable.cs @@ -16,8 +16,8 @@ public abstract class Orchestratable /// Registers the orchestratable component with the external system using a specified topic and an optional target actor. /// /// The topic identifier to be used for registration. - /// An optional target actor type, if applicable, that may influence registration behavior. - /// The logger to use during registration + /// The actor type used for handoff. Only defined for nested orchestrations. + /// The active logger factory. /// A ValueTask containing the AgentType that indicates the registered agent. - protected internal abstract ValueTask RegisterAsync(TopicId externalTopic, AgentType? targetActor, ILogger logger); + protected internal abstract ValueTask RegisterAsync(TopicId externalTopic, AgentType? handoff, ILoggerFactory loggerFactory); } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index f9546a6a72de..540cee90072f 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -33,7 +33,7 @@ protected override async ValueTask StartAsync(TopicId topic, SequentialMessage i } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILogger logger) + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) { // Each agent handsoff its result to the next agent. AgentType nextAgent = orchestrationType; @@ -47,7 +47,7 @@ protected override async ValueTask StartAsync(TopicId topic, SequentialMessage i } else if (member.IsOrchestration(out Orchestratable? orchestration)) { - nextAgent = await orchestration.RegisterAsync(topic, nextAgent, logger).ConfigureAwait(false); + nextAgent = await orchestration.RegisterAsync(topic, nextAgent, loggerFactory).ConfigureAwait(false); } logger.LogRegisterActor(OrchestrationName, nextAgent, "MEMBER", index + 1); } @@ -59,7 +59,7 @@ ValueTask RegisterAgentAsync(TopicId topic, AgentType nextAgent, int return this.Runtime.RegisterAgentFactoryAsync( this.GetAgentType(topic, index), - (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent, this.LoggerFactory.CreateLogger()))); + (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent, loggerFactory.CreateLogger()))); } } diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs index 114ee31c22b3..d50c35e15d1e 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs @@ -8,7 +8,7 @@ /// public abstract class BaseOrchestrationTest(ITestOutputHelper output) : BaseAgentsTest(output) { - protected const int ResultTimeoutInSeconds = 10; + protected const int ResultTimeoutInSeconds = 15; protected ChatCompletionAgent CreateAgent(string instructions, string? name = null, string? description = null) { From ffb4bf61f84141873f1607fa274d1bb693034599 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 16:41:46 -0700 Subject: [PATCH 37/57] Clean dependencies --- .../Agents/Runtime/Abstractions/Runtime.Abstractions.csproj | 5 +---- dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj | 2 +- dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj | 2 +- dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs | 4 ++++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj index 7909d2b780c1..73b62bd4b648 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj +++ b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj @@ -6,6 +6,7 @@ net8.0;netstandard2.0 $(NoWarn);IDE1006;IDE0130 preview + SKIPSKABSTRACTION @@ -29,10 +30,6 @@ - - - - diff --git a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj index 542a343445a2..ea9df2bc8197 100644 --- a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj +++ b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj @@ -5,6 +5,7 @@ Microsoft.Agents.Runtime.Core net8.0;netstandard2.0 preview + SKIPSKABSTRACTION @@ -30,7 +31,6 @@ - diff --git a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj index dd662f59b0e9..a7630930141a 100644 --- a/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj +++ b/dotnet/src/Agents/Runtime/InProcess/Runtime.InProcess.csproj @@ -5,6 +5,7 @@ Microsoft.Agents.Runtime.InProcess net8.0;netstandard2.0 preview + SKIPSKABSTRACTION @@ -23,7 +24,6 @@ - diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs b/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs index b2aba234fb67..c792d50d13e0 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/Verify.cs @@ -75,6 +75,7 @@ public static void True(bool condition, string message, [CallerArgumentExpressio } } +#if !SKIPSKABSTRACTION internal static void ValidPluginName([NotNull] string? pluginName, IReadOnlyKernelPluginCollection? plugins = null, [CallerArgumentExpression(nameof(pluginName))] string? paramName = null) { NotNullOrWhiteSpace(pluginName); @@ -88,6 +89,7 @@ internal static void ValidPluginName([NotNull] string? pluginName, IReadOnlyKern throw new ArgumentException($"A plugin with the name '{pluginName}' already exists."); } } +#endif internal static void ValidFunctionName([NotNull] string? functionName, [CallerArgumentExpression(nameof(functionName))] string? paramName = null) { @@ -146,6 +148,7 @@ internal static void DirectoryExists(string path) } } +#if !SKIPSKABSTRACTION /// /// Make sure every function parameter name is unique /// @@ -179,6 +182,7 @@ internal static void ParametersUniqueness(IReadOnlyList } } } +#endif [DoesNotReturn] private static void ThrowArgumentInvalidName(string kind, string name, string? paramName) => From 1b7e488021890b8bc62c99f23a09c39562658865 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 17:05:15 -0700 Subject: [PATCH 38/57] Clean-up --- dotnet/nuget.config | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dotnet/nuget.config b/dotnet/nuget.config index c97fa9a1705c..ed145feca307 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,8 +3,7 @@ - - + From 78d15f47d97e1648a770d093d4d81000fafe8ab6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 17:05:57 -0700 Subject: [PATCH 39/57] Whitespace --- dotnet/nuget.config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/nuget.config b/dotnet/nuget.config index ed145feca307..143754718558 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -1,9 +1,9 @@  - + - + @@ -11,5 +11,5 @@ - + From 5a47a9d878a5dd66d6a50e74b1c4aaece8baf17c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 17:08:02 -0700 Subject: [PATCH 40/57] Clean-it --- dotnet/nuget.config | 2 -- .../GettingStartedWithAgents.csproj | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 143754718558..296a5db4e511 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -1,6 +1,5 @@  - @@ -11,5 +10,4 @@ - diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index 0fc2e42ffeee..555751348dae 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -25,9 +25,9 @@ - - - + + + From ce35f0d9092f5ffbb5703b947685a28d35d53beb Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 20 Apr 2025 17:09:19 -0700 Subject: [PATCH 41/57] Once more --- dotnet/nuget.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 296a5db4e511..143754718558 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -1,5 +1,6 @@  + @@ -10,4 +11,5 @@ + From c92c5caad50de35475f2a1eacc725c80afd2b785 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Apr 2025 08:06:32 -0700 Subject: [PATCH 42/57] Package version sync --- dotnet/Directory.Packages.props | 4 ++-- dotnet/SK-dotnet.sln | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 7a40bbdeb3f0..493c99301914 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -7,7 +7,7 @@ - + @@ -64,7 +64,7 @@ - + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index bb8f8995ae7e..6c3487d87ef7 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -1712,8 +1712,8 @@ Global {12C7E0C7-A7DF-3BC3-0D4B-1A706BCE6981} = {879545ED-D429-49B1-96F1-2EC55FFED31D} {B06770D5-2F3E-4271-9F6B-3AA9E716176F} = {879545ED-D429-49B1-96F1-2EC55FFED31D} {7C092DD9-9985-4D18-A817-15317D984149} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} - {31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {7C092DD9-9985-4D18-A817-15317D984149} - {08D84994-794A-760F-95FD-4EFA8998A16D} = {7C092DD9-9985-4D18-A817-15317D984149} + {31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {08D84994-794A-760F-95FD-4EFA8998A16D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {A70ED5A7-F8E1-4A57-9455-3C05989542DA} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} {B9C86C5D-EB4C-8A16-E567-27025AC59A28} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} {BB74EEE2-F048-A1A4-F53E-2B384A6F8BC4} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} @@ -1721,10 +1721,6 @@ Global {A4F05541-7D23-A5A9-033D-382F1E13D0FE} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} {CCC909E4-5269-A31E-0BFD-4863B4B29BBB} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} {DA6B4ED4-ED0B-D25C-889C-9F940E714891} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA} - {12C7E0C7-A7DF-3BC3-0D4B-1A706BCE6981} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} - {B06770D5-2F3E-4271-9F6B-3AA9E716176F} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} - {31F6608A-FD36-F529-A5FC-C954A0B5E29E} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} - {08D84994-794A-760F-95FD-4EFA8998A16D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {D1A02387-FA60-22F8-C2ED-4676568B6CC3} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From 79afea5ed3666783c337adf13124da274700ae96 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Apr 2025 14:31:32 -0700 Subject: [PATCH 43/57] Clean --- .../src/InternalUtilities/samples/InternalUtilities/BaseTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index c30bdb430e64..78816c97e2e2 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using System.Reflection; using System.Text; using System.Text.Json; From fcd39dbadaf992f01d2e4e95171c0d753ae7ad76 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Apr 2025 14:58:48 -0700 Subject: [PATCH 44/57] Header comment --- dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index e86fc9c86546..214754e7287b 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -1,4 +1,4 @@ -//// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Threading.Tasks; using Microsoft.Extensions.Logging; From bdad5f050d8d56ef9f949c5dd490caaa0d67aa9e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Apr 2025 15:01:31 -0700 Subject: [PATCH 45/57] Typos --- .github/_typos.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/_typos.toml b/.github/_typos.toml index aaba7f3b7291..51ab82591703 100644 --- a/.github/_typos.toml +++ b/.github/_typos.toml @@ -47,6 +47,7 @@ asend = "asend" # Async generator method [default.extend-identifiers] ags = "ags" # Azure Graph Service +magnetic "magnetic" # Agent orchestration demo [type.jupyter] extend-ignore-re = [ From 913bd2158703644a3329d6eabdf1a8188bf3e8e1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Apr 2025 15:03:18 -0700 Subject: [PATCH 46/57] Typos --- .github/_typos.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/_typos.toml b/.github/_typos.toml index 51ab82591703..9968ce9edda6 100644 --- a/.github/_typos.toml +++ b/.github/_typos.toml @@ -47,7 +47,7 @@ asend = "asend" # Async generator method [default.extend-identifiers] ags = "ags" # Azure Graph Service -magnetic "magnetic" # Agent orchestration demo +magnetic = "magnetic" # Agent orchestration demo [type.jupyter] extend-ignore-re = [ From ccf95480cf18fe7b78c2eaeb9a3dda2d92ae3556 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 21 Apr 2025 15:07:02 -0700 Subject: [PATCH 47/57] Typos --- .github/_typos.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/_typos.toml b/.github/_typos.toml index 9968ce9edda6..e399272a2c1e 100644 --- a/.github/_typos.toml +++ b/.github/_typos.toml @@ -47,7 +47,7 @@ asend = "asend" # Async generator method [default.extend-identifiers] ags = "ags" # Azure Graph Service -magnetic = "magnetic" # Agent orchestration demo +magentic = "magentic" # Agent orchestration demo [type.jupyter] extend-ignore-re = [ From 264558b99856fab3f6b84414515ad00b2d1cd38a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Apr 2025 07:45:08 -0700 Subject: [PATCH 48/57] Typo config --- .github/_typos.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/_typos.toml b/.github/_typos.toml index e399272a2c1e..1b584578115a 100644 --- a/.github/_typos.toml +++ b/.github/_typos.toml @@ -44,10 +44,10 @@ dall = "dall" # OpenAI model name pn = "pn" # Kiota parameter nin = "nin" # MongoDB "not in" operator asend = "asend" # Async generator method +magentic = "magentic" # Agent orchestration demo [default.extend-identifiers] ags = "ags" # Azure Graph Service -magentic = "magentic" # Agent orchestration demo [type.jupyter] extend-ignore-re = [ From 0dc5630d24df3655997d6e5ccb09c8b1c27a1cef Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 22 Apr 2025 09:24:06 -0700 Subject: [PATCH 49/57] Comments and logging --- .../Orchestration/Step03_GroupChat.cs | 2 +- .../Orchestration/Step05_Custom.cs | 2 +- .../AgentOrchestration.RequestActor.cs | 2 +- .../AgentOrchestration.ResultActor.cs | 2 +- .../Agents/Orchestration/AgentOrchestration.cs | 8 ++++---- .../GroupChat/GroupChatManagerActor.cs | 10 +++++----- .../GroupChat/GroupChatOrchestration.cs | 1 + .../Magentic/MagenticManagerActor.cs | 15 +++++++-------- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index ba8e2fecc00b..306eac66f56d 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -62,7 +62,7 @@ public async Task SingleActorAsync() } [Fact] - public async Task SingleNestedActorAsync() + public async Task SingleNestedActorAsync() // %%% BROKEN { // Define the agents ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs index 14e922917953..7271ad76fe29 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs @@ -10,7 +10,7 @@ namespace GettingStarted.Orchestration; public class Step05_Custom(ITestOutputHelper output) : BaseAgentsTest(output) { [Fact] - public Task UseCustomPatternAsync() // %%% TODO + public Task UseCustomPatternAsync() // %%% SAMPLE - CUSTOM { return Task.CompletedTask; } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index ffcaf4210c61..b1266cfd9fb6 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -24,7 +24,7 @@ private sealed class RequestActor : PatternActor, IHandle /// /// The unique identifier of the agent. /// The runtime associated with the agent. - /// // %%% COMMENT + /// A descriptive root label for the orchestration. /// A function that transforms an input of type TInput into a source type TSource. /// An asynchronous function that processes the resulting source. /// The logger to use for the actor diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index a0512f8fd068..aaeab7f10857 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -24,7 +24,7 @@ private sealed class ResultActor : PatternActor, IHandle /// /// The unique identifier of the agent. /// The runtime associated with the agent. - /// // %%% COMMENT + /// A descriptive root label for the orchestration. /// A delegate that transforms a TResult instance into a TOutput instance. /// Optional TaskCompletionSource to signal orchestration completion. /// The logger to use for the actor diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index e9541ad46f37..215687312dcd 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -21,16 +21,16 @@ public abstract partial class AgentOrchestration /// Initializes a new instance of the class. /// - /// // %%% COMMENT + /// A descriptive root label for the orchestration. /// The runtime associated with the orchestration. /// Specifies the member agents or orchestrations participating in this orchestration. - protected AgentOrchestration(string name, IAgentRuntime runtime, params OrchestrationTarget[] members) + protected AgentOrchestration(string orchestrationRoot, IAgentRuntime runtime, params OrchestrationTarget[] members) { Verify.NotNull(runtime, nameof(runtime)); this.Runtime = runtime; this.Members = members; - this._orchestrationRoot = name; + this._orchestrationRoot = orchestrationRoot; } /// @@ -118,7 +118,7 @@ public async ValueTask> InvokeAsync(TInput input, T /// The entry AgentType for the orchestration, if any. /// The active logger factory. /// The logger to use during registration - protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger); // %%% TODO - CLASS LEVEL + protected abstract ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger); /// /// Registers the orchestration with the runtime using an external topic and an optional target actor. diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index daf53eaa134c..2a9c77f3b9d5 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -11,9 +11,9 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An used to manage a . /// -internal sealed class GroupChatManagerActor : ChatManagerActor // %%% ABSTRACT +internal sealed class GroupChatManagerActor : ChatManagerActor // %%% ABSTRACT ??? { - private int _count = 0; + private int _index = 0; /// /// Initializes a new instance of the class. @@ -39,13 +39,13 @@ public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, protected override Task SelectAgentAsync() { // %%% PLACEHOLDER SELECTION LOGIC - if (this._count >= 2) + if (this._index >= 2) { return Task.FromResult(null); } AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; - AgentType? agentType = agentTypes[this._count % this.Team.Count]; - ++this._count; + AgentType? agentType = agentTypes[this._index % this.Team.Count]; + ++this._index; return Task.FromResult(agentType); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 8b54bd693a77..2ab97a261ec0 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -66,6 +66,7 @@ await this.Runtime.RegisterAgentFactoryAsync( (agentId, runtime) => ValueTask.FromResult( new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, loggerFactory.CreateLogger()))).ConfigureAwait(false); + logger.LogRegisterActor(OrchestrationName, managerType, "MANAGER"); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs index 2bc1a7d8d292..d7de6bb1c911 100644 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -14,6 +13,8 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; /// internal sealed class MagenticManagerActor : ChatManagerActor { + private int _index; + /// /// Initializes a new instance of the class. /// @@ -38,15 +39,13 @@ public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, A protected override Task SelectAgentAsync() { // %%% PLACEHOLDER SELECTION LOGIC -#pragma warning disable CA5394 // Do not use insecure randomness - int index = Random.Shared.Next(this.Team.Count + 1); -#pragma warning restore CA5394 // Do not use insecure randomness - AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; - AgentType? agentType = null; - if (index < this.Team.Count) + if (this._index >= 2) { - agentType = agentTypes[index]; + return Task.FromResult(null); } + AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; + AgentType? agentType = agentTypes[this._index % this.Team.Count]; + ++this._index; return Task.FromResult(agentType); } } From a3de83d086b3914cdadcc4dc0056889af0461832 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 09:35:03 -0700 Subject: [PATCH 50/57] Unit-tests and clean-up --- .../Orchestration/Step01_Concurrent.cs | 84 +------ .../Orchestration/Step02_Sequential.cs | 84 +------ .../Orchestration/Step03_GroupChat.cs | 68 ++---- .../Orchestration/Step04_Nested.cs | 34 +-- .../Orchestration/Step05_Custom.cs | 17 -- dotnet/src/Agents/Orchestration/AgentActor.cs | 4 +- .../AgentOrchestration.RequestActor.cs | 8 + .../AgentOrchestration.ResultActor.cs | 8 +- .../Orchestration/AgentOrchestration.cs | 12 +- .../Orchestration/Agents.Orchestration.csproj | 1 - .../Agents/Orchestration/Chat/ChatGroup.cs | 7 +- .../Agents/Orchestration/Chat/ChatHandoff.cs | 35 +++ .../Orchestration/Chat/ChatManagerActor.cs | 11 +- .../GroupChat/GroupChatContext.cs | 60 +++++ .../GroupChat/GroupChatManagerActor.cs | 26 +- .../GroupChatOrchestration.String.cs | 5 +- .../GroupChat/GroupChatOrchestration.cs | 25 +- .../GroupChat/GroupChatStrategy.cs | 41 ++++ .../Magentic/MagenticManagerActor.cs | 51 ---- .../Magentic/MagenticOrchestration.String.cs | 26 -- .../Magentic/MagenticOrchestration.cs | 84 ------- .../Agents/Orchestration/Orchestratable.cs | 10 + .../Agents/UnitTests/Agents.UnitTests.csproj | 18 +- dotnet/src/Agents/UnitTests/MockAgent.cs | 6 + .../Orchestration/ChatGroupExtensionsTests.cs | 91 +++++++ .../ConcurrentOrchestrationTests.cs | 114 +++++++++ .../GroupChatOrchestrationTests.cs | 136 +++++++++++ .../Orchestration/OrchestrationResultTests.cs | 104 ++++++++ .../Orchestration/OrchestrationTargetTests.cs | 223 ++++++++++++++++++ .../SequentialOrchestrationTests.cs | 110 +++++++++ 30 files changed, 1024 insertions(+), 479 deletions(-) delete mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs create mode 100644 dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs delete mode 100644 dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs delete mode 100644 dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs delete mode 100644 dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/SequentialOrchestrationTests.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index 639c8e1140ad..89d575286bd5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -18,7 +18,7 @@ public async Task SimpleConcurrentAsync() // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); - ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of onsonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of consonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); // Define the pattern InProcessRuntime runtime = new(); @@ -35,86 +35,4 @@ public async Task SimpleConcurrentAsync() await runtime.RunUntilIdleAsync(); } - - [Fact] - public async Task NestedConcurrentAsync() - { - // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); - ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); - ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); - - // Define the pattern - InProcessRuntime runtime = new(); - - ConcurrentOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); - ConcurrentOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); - ConcurrentOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestrationMain.InvokeAsync(input); - - string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); - - await runtime.RunUntilIdleAsync(); - } - - [Fact] - public async Task SingleActorAsync() - { - // Define the agents - ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - - // Define the pattern - InProcessRuntime runtime = new(); - ConcurrentOrchestration orchestration = new(runtime, agent) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); - - string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); - - await runtime.RunUntilIdleAsync(); - } - - [Fact] - public async Task SingleNestedActorAsync() - { - // Define the agents - ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - - // Define the pattern - InProcessRuntime runtime = new(); - ConcurrentOrchestration orchestrationInner = CreateNested(runtime, agent); - ConcurrentOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestrationOuter.InvokeAsync(input); - - string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); - - await runtime.RunUntilIdleAsync(); - } - - private static ConcurrentOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) - { - return new(runtime, targets) - { - InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(input), - ResultTransform = (ConcurrentMessages.Result[] results) => ValueTask.FromResult(string.Join("\n", results.Select(result => $"{result.Message}")).ToResult()), - }; - } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index b83b70ae179f..495b4da4b221 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -18,7 +18,7 @@ public async Task SimpleSequentialAsync() // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); - ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of onsonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of consonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); // Define the pattern InProcessRuntime runtime = new(); @@ -34,86 +34,4 @@ public async Task SimpleSequentialAsync() await runtime.RunUntilIdleAsync(); } - - [Fact] - public async Task NestedSequentialAsync() - { - // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); - ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); - ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); - - // Define the pattern - InProcessRuntime runtime = new(); - - SequentialOrchestration orchestrationLeft = CreateNested(runtime, agent1, agent2); - SequentialOrchestration orchestrationRight = CreateNested(runtime, agent3, agent4); - SequentialOrchestration orchestrationMain = new(runtime, orchestrationLeft, orchestrationRight) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestrationMain.InvokeAsync(input); - - string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {output}"); - - await runtime.RunUntilIdleAsync(); - } - - [Fact] - public async Task SingleActorAsync() - { - // Define the agents - ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - - // Define the pattern - InProcessRuntime runtime = new(); - SequentialOrchestration orchestration = new(runtime, agent) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); - - string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {output}"); - - await runtime.RunUntilIdleAsync(); - } - - [Fact] - public async Task SingleNestedActorAsync() - { - // Define the agents - ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - - // Define the pattern - InProcessRuntime runtime = new(); - SequentialOrchestration orchestrationInner = CreateNested(runtime, agent); - SequentialOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestrationOuter.InvokeAsync(input); - - string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {output}"); - - await runtime.RunUntilIdleAsync(); - } - - private static SequentialOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) - { - return new(runtime, targets) - { - InputTransform = (SequentialMessage input) => ValueTask.FromResult(input), - ResultTransform = (SequentialMessage results) => ValueTask.FromResult(results), - }; - } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 306eac66f56d..3c0340d6132e 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -2,7 +2,6 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; @@ -19,11 +18,11 @@ public async Task SimpleGroupChatAsync() // Define the agents ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); - ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of onsonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of consonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); // Define the pattern InProcessRuntime runtime = new(); - GroupChatOrchestration orchestration = new(runtime, agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; + GroupChatOrchestration orchestration = new(runtime, new SimpleGroupChatStrategy(), agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; // Start the runtime await runtime.StartAsync(); @@ -37,54 +36,25 @@ public async Task SimpleGroupChatAsync() await runtime.RunUntilIdleAsync(); } - // %%% MORE SAMPLES - GROUPCHAT - - [Fact] - public async Task SingleActorAsync() - { - // Define the agents - ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - - // Define the pattern - InProcessRuntime runtime = new(); - GroupChatOrchestration orchestration = new(runtime, agent) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); - - string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {output}"); - - await runtime.RunUntilIdleAsync(); - } - - [Fact] - public async Task SingleNestedActorAsync() // %%% BROKEN + private sealed class SimpleGroupChatStrategy : GroupChatStrategy { - // Define the agents - ChatCompletionAgent agent = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); + private int _count; - // Define the pattern - InProcessRuntime runtime = new(); - GroupChatOrchestration orchestrationInner = new(runtime, agent) + public override ValueTask SelectAsync(GroupChatContext context, CancellationToken cancellationToken = default) { - InputTransform = (ChatMessages.InputTask input) => ValueTask.FromResult(input), - ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result), - }; - GroupChatOrchestration orchestrationOuter = new(runtime, orchestrationInner) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestrationOuter.InvokeAsync(input); - - string output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {output}"); - - await runtime.RunUntilIdleAsync(); + try + { + if (this._count < context.Team.Count) + { + context.SelectAgent(context.Team.Skip(this._count).First().Key); + } + + return ValueTask.CompletedTask; + } + finally + { + ++this._count; + } + } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs index bb5f2458eb46..26626b381a65 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs @@ -1,50 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; -using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to nest an orchestration within another orchestration. /// public class Step04_Nested(ITestOutputHelper output) : BaseOrchestrationTest(output) { - [Fact] - public async Task NestSequentialGroupsAsync() - { - // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); - ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); - ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); - - // Define the pattern - InProcessRuntime runtime = new(); - ConcurrentOrchestration innerOrchestration = - new(runtime, agent3, agent4) - { - InputTransform = (SequentialMessage input) => ValueTask.FromResult(new ConcurrentMessages.Request { Message = input.Message }), - ResultTransform = (ConcurrentMessages.Result[] output) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.Assistant, string.Join("\n", output.Select(item => item.Message.Content))))) - }; - SequentialOrchestration outerOrchestration = new(runtime, agent1, innerOrchestration, agent2) { LoggerFactory = this.LoggerFactory }; - - // Start the runtime - await runtime.StartAsync(); - string input = "1"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await outerOrchestration.InvokeAsync(input); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n> RESULT:\n{text}"); - - await runtime.RunUntilIdleAsync(); - } - [Fact] public async Task NestConcurrentGroupsAsync() { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs deleted file mode 100644 index 7271ad76fe29..000000000000 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Custom.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel.Agents.Orchestration; - -namespace GettingStarted.Orchestration; - -/// -/// Demonstrates how to build a custom . -/// -public class Step05_Custom(ITestOutputHelper output) : BaseAgentsTest(output) -{ - [Fact] - public Task UseCustomPatternAsync() // %%% SAMPLE - CUSTOM - { - return Task.CompletedTask; - } -} diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index 26937aa38ede..f3503bf46b64 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -75,7 +75,7 @@ protected async ValueTask DeleteThreadAsync(CancellationToken cancellationToken) /// A task that returns the response . protected ValueTask InvokeAsync(ChatMessageContent input, CancellationToken cancellationToken) { - return this.InvokeAsync(new[] { input }, cancellationToken); + return this.InvokeAsync([input], cancellationToken); } /// @@ -113,7 +113,7 @@ await this.Agent.InvokeAsync( /// An asynchronous stream of responses. protected async IAsyncEnumerable InvokeStreamingAsync(ChatMessageContent input, [EnumeratorCancellation] CancellationToken cancellationToken) { - var responseStream = this.Agent.InvokeStreamingAsync(new[] { input }, this.Thread, options: null, cancellationToken); + var responseStream = this.Agent.InvokeStreamingAsync([input], this.Thread, options: null, cancellationToken); await foreach (AgentResponseItem response in responseStream.ConfigureAwait(false)) { diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index b1266cfd9fb6..4ffd035b03f6 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -18,6 +18,7 @@ private sealed class RequestActor : PatternActor, IHandle private readonly string _orchestrationRoot; private readonly Func> _transform; private readonly Func _action; + private readonly TaskCompletionSource? _completionSource; /// /// Initializes a new instance of the class. @@ -27,6 +28,7 @@ private sealed class RequestActor : PatternActor, IHandle /// A descriptive root label for the orchestration. /// A function that transforms an input of type TInput into a source type TSource. /// An asynchronous function that processes the resulting source. + /// Optional TaskCompletionSource to signal orchestration completion. /// The logger to use for the actor public RequestActor( AgentId id, @@ -34,12 +36,14 @@ public RequestActor( string orchestrationRoot, Func> transform, Func action, + TaskCompletionSource? completionSource = null, ILogger? logger = null) : base(id, runtime, $"{id.Type}_Actor", logger) { this._orchestrationRoot = orchestrationRoot; this._transform = transform; this._action = action; + this._completionSource = completionSource; } /// @@ -62,6 +66,10 @@ public async ValueTask HandleAsync(TInput item, MessageContext messageContext) { // Log exception details and allow orchestration to fail this.Logger.LogOrchestrationRequestFailure(this._orchestrationRoot, this.Id, exception); + if (this._completionSource != null) + { + this._completionSource.SetException(exception); + } throw; } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index aaeab7f10857..459f0e171c2e 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -74,7 +74,13 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) { // Log exception details and fail orchestration as per design. this.Logger.LogOrchestrationResultFailure(this._orchestrationRoot, this.Id, exception); - throw; + + if (this._completionSource == null) + { + throw; + } + + this._completionSource.SetException(exception); } } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 215687312dcd..2bd6eb3734d1 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -33,16 +33,6 @@ protected AgentOrchestration(string orchestrationRoot, IAgentRuntime runtime, pa this._orchestrationRoot = orchestrationRoot; } - /// - /// Gets the name of the orchestration. - /// - public string Name { get; init; } = string.Empty; - - /// - /// Gets the description of the orchestration. - /// - public string Description { get; init; } = string.Empty; - /// /// Gets the associated logger. /// @@ -196,7 +186,7 @@ await this.Runtime.RegisterAgentFactoryAsync( this.FormatAgentType(topic, "Boot"), (agentId, runtime) => ValueTask.FromResult( - new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), loggerFactory.CreateLogger())) + new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), completion, loggerFactory.CreateLogger())) ).ConfigureAwait(false); logger.LogOrchestrationRegistrationDone(this._orchestrationRoot, topic); diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index bbfceccf769c..57d984d19c45 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -4,7 +4,6 @@ Microsoft.SemanticKernel.Agents.Orchestration Microsoft.SemanticKernel.Agents.Orchestration - net8.0 $(NoWarn);SKEXP0110;SKEXP0001 diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs b/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs index b2fa364e8d15..12a84ba4846d 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.SemanticKernel.Agents.Runtime; namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; /// /// Describes a team of agents participating in a group chat. /// -public class ChatGroup : Dictionary; // %%% TODO: ANONYMOUS TYPE => EXPLICIT +public class ChatGroup : Dictionary; /// /// Extensions for . @@ -21,12 +20,12 @@ public static class ChatGroupExtensions /// /// The agent team /// A comma delimimted list of agent name. - public static string FormatNames(this ChatGroup team) => string.Join(",", team.Select(t => t.Key)); + public static string FormatNames(this ChatGroup team) => string.Join(",", team.Select(t => t.Value.Name)); /// /// Format the names and descriptions of the agents in the team as a markdown list. /// /// The agent team /// A markdown list of agent names and descriptions. - public static string FormatList(this ChatGroup team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); + public static string FormatList(this ChatGroup team) => string.Join("\n", team.Select(t => $"- {t.Value.Name}: {t.Value.Description}")); } diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs b/dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs new file mode 100644 index 000000000000..b31994bbf912 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; + +/// +/// Define how the chat history is translated into a singular response. +/// (i.e. What is the result of the chat?) +/// +public abstract class ChatHandoff +{ + /// + /// Provide the final message to be returned to the user based on the entire chat history. + /// + /// The chat history + /// The to monitor for cancellation requests. + /// The final response + public abstract ValueTask ProcessAsync(IReadOnlyList history, CancellationToken cancellationToken); + + /// + /// Default behavior for chat handoff: copy the final message in the history. + /// + public static readonly ChatHandoff Default = new DefaultChatHandoff(); + + /// + /// Provide final message, as default behavior. + /// + private sealed class DefaultChatHandoff : ChatHandoff + { + public override ValueTask ProcessAsync(IReadOnlyList history, CancellationToken cancellationToken) => ValueTask.FromResult(history[^1]); + } +} diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs index d9883824e32d..94fa80fb1c7c 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs @@ -26,6 +26,7 @@ public abstract class ChatManagerActor : private readonly AgentType _orchestrationType; private readonly TopicId _groupTopic; + private readonly ChatHandoff _handoff; /// /// Initializes a new instance of the class. @@ -35,14 +36,16 @@ public abstract class ChatManagerActor : /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. + /// Defines how the group-chat is translated into a singular response. /// The logger to use for the actor - protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) + protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ChatHandoff handoff, ILogger? logger = null) : base(id, runtime, DefaultDescription, logger) { this.Chat = []; this.Team = team; this._orchestrationType = orchestrationType; this._groupTopic = groupTopic; + this._handoff = handoff; } /// @@ -105,7 +108,8 @@ public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext m else { this.Logger.LogChatManagerTerminate(this.Id); - await this.SendMessageAsync(item.Message.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); // %%% PLACEHOLDER - FINAL MESSAGE + ChatMessageContent handoff = await this._handoff.ProcessAsync(this.Chat, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(handoff.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); } } @@ -123,7 +127,8 @@ public async ValueTask HandleAsync(ChatMessages.Group item, MessageContext messa else { this.Logger.LogChatManagerTerminate(this.Id); - await this.SendMessageAsync(item.Message.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); // %%% PLACEHOLDER - FINAL MESSAGE + ChatMessageContent handoff = await this._handoff.ProcessAsync(this.Chat, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(handoff.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs new file mode 100644 index 000000000000..bb4dea05f763 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// An expression of the state of a group chat for use during agent selection. +/// This includes the chat history and a list of agent names. +/// +public sealed class GroupChatContext +{ + internal string? Selection { get; private set; } + + internal bool HasSelection => !string.IsNullOrWhiteSpace(this.Selection); + + /// + /// The group chat history for consideration during agent selection. + /// + public IReadOnlyList History { get; } + + /// + /// The agents that are part of the group chat. + /// + public ChatGroup Team { get; } + + internal GroupChatContext(ChatGroup team, IReadOnlyList history) + { + this.Team = team; + this.History = history; + } + + /// + /// Indicates the next agent to be selected. Not selecting will result + /// in the chat terminating. A null result can be used to indicate that + /// the conversation is over, or it may signal that user input is needed. + /// + /// The agent to be selected. + /// When the specified agent isn't part of the group chat. + public void SelectAgent(string name) + { + if (this.Team.ContainsKey(name)) + { + this.Selection = name; + return; + } + + foreach (var team in this.Team) + { + if (team.Value.Name == name) + { + this.Selection = team.Key; + return; + } + } + + throw new KeyNotFoundException($"Agent unknown to the group chat: {name}."); + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index 2a9c77f3b9d5..a2e52715c6f8 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; @@ -11,9 +10,9 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An used to manage a . /// -internal sealed class GroupChatManagerActor : ChatManagerActor // %%% ABSTRACT ??? +internal sealed class GroupChatManagerActor : ChatManagerActor { - private int _index = 0; + private readonly GroupChatStrategy _strategy; /// /// Initializes a new instance of the class. @@ -23,10 +22,13 @@ internal sealed class GroupChatManagerActor : ChatManagerActor // %%% ABSTRACT ? /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The unique topic used to broadcast to the entire chat. + /// The strategy that determines how the chat shall proceed. + /// Defines how the group-chat is translated into a singular response. /// The logger to use for the actor - public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) - : base(id, runtime, team, orchestrationType, groupTopic, logger) + public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, GroupChatStrategy strategy, ChatHandoff handoff, ILogger? logger = null) + : base(id, runtime, team, orchestrationType, groupTopic, handoff, logger) { + this._strategy = strategy; } /// @@ -36,16 +38,10 @@ public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, } /// - protected override Task SelectAgentAsync() + protected override async Task SelectAgentAsync() { - // %%% PLACEHOLDER SELECTION LOGIC - if (this._index >= 2) - { - return Task.FromResult(null); - } - AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; - AgentType? agentType = agentTypes[this._index % this.Team.Count]; - ++this._index; - return Task.FromResult(agentType); + GroupChatContext context = new(this.Team, this.Chat); + await this._strategy.SelectAsync(context).ConfigureAwait(false); + return context.HasSelection ? context.Selection! : (AgentType?)null; } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs index 4d4a18c71915..ba19caa56898 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs @@ -16,9 +16,10 @@ public sealed partial class GroupChatOrchestration : GroupChatOrchestration class. /// /// The runtime associated with the orchestration. + /// The strategy that determines how the chat shall proceed. /// The agents to be orchestrated. - public GroupChatOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) - : base(runtime, members) + public GroupChatOrchestration(IAgentRuntime runtime, GroupChatStrategy strategy, params OrchestrationTarget[] members) + : base(runtime, strategy, members) { this.InputTransform = (string input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input).ToInputTask()); this.ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToString()); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 2ab97a261ec0..726477c7c755 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -15,18 +15,31 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; public class GroupChatOrchestration : AgentOrchestration { + internal const string DefaultAgentDescription = "A helpful agent."; + internal static readonly string OrchestrationName = typeof(GroupChatOrchestration<,>).Name.Split('`').First(); + private readonly GroupChatStrategy _strategy; + /// /// Initializes a new instance of the class. /// /// The runtime associated with the orchestration. + /// The strategy that determines how the chat shall proceed. /// The agents participating in the orchestration. - public GroupChatOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) + public GroupChatOrchestration(IAgentRuntime runtime, GroupChatStrategy strategy, params OrchestrationTarget[] agents) : base(OrchestrationName, runtime, agents) { + Verify.NotNull(strategy, nameof(strategy)); + + this._strategy = strategy; } + /// + /// Defines how the group-chat is translated into the orchestration result (or handoff). + /// + public ChatHandoff Handoff { get; init; } = ChatHandoff.Default; + /// protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask input, AgentType? entryAgent) { @@ -45,16 +58,22 @@ protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask in ++agentCount; AgentType memberType = default; + string? name = null; + string? description = null; if (member.IsAgent(out Agent? agent)) { memberType = await RegisterAgentAsync(agent).ConfigureAwait(false); + description = agent.Description; + name = agent.Name ?? agent.Id; } else if (member.IsOrchestration(out Orchestratable? orchestration)) { memberType = await orchestration.RegisterAsync(topic, managerType, loggerFactory).ConfigureAwait(false); + description = orchestration.Description; + name = orchestration.Name; } - team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID + team[memberType] = (name ?? memberType, description ?? DefaultAgentDescription); logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); @@ -65,7 +84,7 @@ await this.Runtime.RegisterAgentFactoryAsync( managerType, (agentId, runtime) => ValueTask.FromResult( - new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, loggerFactory.CreateLogger()))).ConfigureAwait(false); + new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, this._strategy, this.Handoff, loggerFactory.CreateLogger()))).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, managerType, "MANAGER"); await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs new file mode 100644 index 000000000000..96a9b9a87489 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// Strategy that determines how the group chat shall proceed. Does it +/// select another agent for its response? Is the response complete? +/// Is input requested? +/// +public abstract class GroupChatStrategy +{ + /// + /// Callback used to evaluate the chat state and determine the next agent to be invoked. + /// + /// The group chat context + /// The to monitor for cancellation requests. + /// The next agent to respond. Null results in no response. + public delegate ValueTask CallbackAsync(GroupChatContext context, CancellationToken cancellationToken = default); + + /// + /// Implicitly converts a to a . + /// + /// The callback being cast + public static implicit operator GroupChatStrategy(CallbackAsync callback) => new CallbackStrategy(callback); + + /// + /// Method used to evaluate the chat state and determine the next agent to be invoked. + /// + /// The group chat context + /// The to monitor for cancellation requests. + public abstract ValueTask SelectAsync(GroupChatContext context, CancellationToken cancellationToken = default); + + private sealed class CallbackStrategy(CallbackAsync selectCallback) : GroupChatStrategy + { + public override ValueTask SelectAsync(GroupChatContext context, CancellationToken cancellationToken = default) => + selectCallback.Invoke(context, cancellationToken); + } +} diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs deleted file mode 100644 index d7de6bb1c911..000000000000 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticManagerActor.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; -using Microsoft.SemanticKernel.Agents.Runtime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; - -/// -/// An used to manage a . -/// -internal sealed class MagenticManagerActor : ChatManagerActor -{ - private int _index; - - /// - /// Initializes a new instance of the class. - /// - /// The unique identifier of the agent. - /// The runtime associated with the agent. - /// The team of agents being orchestrated - /// Identifies the orchestration agent. - /// The unique topic used to broadcast to the entire chat. - /// The logger to use for the actor - public MagenticManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ILogger? logger = null) - : base(id, runtime, team, orchestrationType, groupTopic, logger) - { - } - - /// - protected override Task PrepareTaskAsync() - { - return this.SelectAgentAsync(); - } - - /// - protected override Task SelectAgentAsync() - { - // %%% PLACEHOLDER SELECTION LOGIC - if (this._index >= 2) - { - return Task.FromResult(null); - } - AgentType[] agentTypes = [.. this.Team.Keys.Select(value => new AgentType(value))]; - AgentType? agentType = agentTypes[this._index % this.Team.Count]; - ++this._index; - return Task.FromResult(agentType); - } -} diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs deleted file mode 100644 index f44166cbb342..000000000000 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.String.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; - -/// -/// An orchestration that broadcasts the input message to each agent. -/// -public sealed partial class MagenticOrchestration : MagenticOrchestration -{ - /// - /// Initializes a new instance of the class. - /// - /// The runtime associated with the orchestration. - /// The agents to be orchestrated. - public MagenticOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) - : base(runtime, members) - { - this.InputTransform = (string input) => ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input).ToInputTask()); - this.ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result.Message.ToString()); - } -} diff --git a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs b/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs deleted file mode 100644 index 37e783e63830..000000000000 --- a/dotnet/src/Agents/Orchestration/Magentic/MagenticOrchestration.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; -using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; -using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; -using Microsoft.SemanticKernel.Agents.Runtime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Magentic; - -/// -/// An orchestration that coordinates a group-chat. -/// -public class MagenticOrchestration : - AgentOrchestration -{ - internal static readonly string OrchestrationName = typeof(ConcurrentOrchestration<,>).Name.Split('`').First(); - - /// - /// Initializes a new instance of the class. - /// - /// The runtime associated with the orchestration. - /// The agents participating in the orchestration. - public MagenticOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] agents) - : base(OrchestrationName, runtime, agents) - { - } - - /// - protected override ValueTask StartAsync(TopicId topic, ChatMessages.InputTask input, AgentType? entryAgent) - { - return this.Runtime.SendMessageAsync(input, entryAgent!.Value); - } - - /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) - { - AgentType managerType = this.FormatAgentType(topic, "Manager"); - - int agentCount = 0; - ChatGroup team = []; - foreach (OrchestrationTarget member in this.Members) - { - ++agentCount; - - AgentType memberType = default; - if (member.IsAgent(out Agent? agent)) - { - memberType = await RegisterAgentAsync(agent).ConfigureAwait(false); - } - else if (member.IsOrchestration(out Orchestratable? orchestration)) - { - memberType = await orchestration.RegisterAsync(topic, managerType, loggerFactory).ConfigureAwait(false); - } - - team[memberType] = (memberType, "an agent"); // %%% DESCRIPTION & NAME ID - - logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); - - await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); - } - - await this.Runtime.RegisterAgentFactoryAsync( - managerType, - (agentId, runtime) => - ValueTask.FromResult( - new MagenticManagerActor(agentId, runtime, team, orchestrationType, topic, loggerFactory.CreateLogger()))).ConfigureAwait(false); - - await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); - - return managerType; - - ValueTask RegisterAgentAsync(Agent agent) - { - return - this.Runtime.RegisterAgentFactoryAsync( - this.FormatAgentType(topic, $"Agent_{agentCount}"), - (agentId, runtime) => - ValueTask.FromResult(new ChatAgentActor(agentId, runtime, agent, topic, loggerFactory.CreateLogger()))); - } - } -} diff --git a/dotnet/src/Agents/Orchestration/Orchestratable.cs b/dotnet/src/Agents/Orchestration/Orchestratable.cs index 097ba4affd84..4c9039abde14 100644 --- a/dotnet/src/Agents/Orchestration/Orchestratable.cs +++ b/dotnet/src/Agents/Orchestration/Orchestratable.cs @@ -12,6 +12,16 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// public abstract class Orchestratable { + /// + /// Gets the description of the orchestration. + /// + public string Description { get; init; } = string.Empty; + + /// + /// Gets the name of the orchestration. + /// + public string Name { get; init; } = string.Empty; + /// /// Registers the orchestratable component with the external system using a specified topic and an optional target actor. /// diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index a0222fac89cf..5cd08cdae28a 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -8,15 +8,9 @@ true false 12 - $(NoWarn);CA2007,CA1812,CA1861,CA1063,CS0618,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110;OPENAI001 + $(NoWarn);CA2007,CA1812,CA1861,CA1707,CA1063,CS0618,CS1591,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110;OPENAI001 - - - - - - @@ -35,14 +29,16 @@ - - + + - + + - + + diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index bdb5a6dc8868..e986f7e8b6cd 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -8,6 +8,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Moq; namespace SemanticKernel.Agents.UnitTests; @@ -27,6 +28,11 @@ public override IAsyncEnumerable> InvokeAs CancellationToken cancellationToken = default) { this.InvokeCount++; + if (thread == null) + { + Mock mockThread = new(); + thread = mockThread.Object; + } return this.Response.Select(x => new AgentResponseItem(x, thread!)).ToAsyncEnumerable(); } diff --git a/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs new file mode 100644 index 000000000000..3d4d46d1e6cf --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +public class ChatGroupExtensionsTests +{ + [Fact] + public void FormatNames_WithMultipleAgents_ReturnsCommaSeparatedList() + { + // Arrange + ChatGroup group = new() + { + { "agent1", ("Agent One", "First agent description") }, + { "agent2", ("Agent Two", "Second agent description") }, + { "agent3", ("Agent Three", "Third agent description") } + }; + + // Act + string result = group.FormatNames(); + + // Assert + Assert.Equal("Agent One,Agent Two,Agent Three", result); + } + + [Fact] + public void FormatNames_WithSingleAgent_ReturnsSingleName() + { + // Arrange + ChatGroup group = new() + { + { "agent1", ("Agent One", "First agent description") } + }; + + // Act + string result = group.FormatNames(); + + // Assert + Assert.Equal("Agent One", result); + } + + [Fact] + public void FormatNames_WithEmptyGroup_ReturnsEmptyString() + { + // Arrange + ChatGroup group = []; + + // Act + string result = group.FormatNames(); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void FormatList_WithMultipleAgents_ReturnsMarkdownList() + { + // Arrange + ChatGroup group = new() + { + { "agent1", ("Agent One", "First agent description") }, + { "agent2", ("Agent Two", "Second agent description") }, + { "agent3", ("Agent Three", "Third agent description") } + }; + + // Act + string result = group.FormatList(); + + // Assert + const string Expected = + """ + - Agent One: First agent description + - Agent Two: Second agent description + - Agent Three: Third agent description + """; + Assert.Equal(Expected, result); + } + + [Fact] + public void FormatList_WithEmptyGroup_ReturnsEmptyString() + { + // Arrange + ChatGroup group = []; + + // Act & Assert + Assert.Equal(string.Empty, group.FormatNames()); + Assert.Equal(string.Empty, group.FormatList()); + } +} diff --git a/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs new file mode 100644 index 000000000000..0b7a89120d5f --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +/// +/// Tests for the class. +/// +public class ConcurrentOrchestrationTests +{ + [Fact] + public async Task ConcurrentOrchestrationWithSingleAgentAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + MockAgent mockAgent1 = CreateMockAgent(1, "xyz"); + + // Act: Create and execute the orchestration + string[] response = await ExecuteOrchestrationAsync(runtime, mockAgent1); + + // Assert + Assert.Contains("xyz", response); + Assert.Equal(1, mockAgent1.InvokeCount); + } + + [Fact] + public async Task ConcurrentOrchestrationWithMultipleAgentsAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgent1 = CreateMockAgent(1, "abc"); + MockAgent mockAgent2 = CreateMockAgent(2, "xyz"); + MockAgent mockAgent3 = CreateMockAgent(3, "lmn"); + + // Act: Create and execute the orchestration + string[] response = await ExecuteOrchestrationAsync(runtime, mockAgent1, mockAgent2, mockAgent3); + + // Assert + Assert.Contains("lmn", response); + Assert.Contains("xyz", response); + Assert.Contains("abc", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgent2.InvokeCount); + Assert.Equal(1, mockAgent3.InvokeCount); + } + + [Fact] + public async Task ConcurrentOrchestrationWithNestedMemberAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgentB = CreateMockAgent(2, "efg"); + ConcurrentOrchestration orchestration = CreateNested(runtime, mockAgentB); + MockAgent mockAgent1 = CreateMockAgent(1, "xyz"); + + // Act: Create and execute the orchestration + string[] response = await ExecuteOrchestrationAsync(runtime, mockAgent1, orchestration); + + // Assert + Assert.Contains("efg", response); + Assert.Contains("xyz", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgentB.InvokeCount); + } + + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params OrchestrationTarget[] mockAgents) + { + // Act + await runtime.StartAsync(); + + ConcurrentOrchestration orchestration = new(runtime, mockAgents); + + const string InitialInput = "123"; + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + + // Assert + Assert.NotNull(result); + + // Act + string[] response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); + + await runtime.RunUntilIdleAsync(); + + return response; + } + + private static MockAgent CreateMockAgent(int index, string response) + { + return new() + { + Description = $"test {index}", + Response = [new(AuthorRole.Assistant, response)] + }; + } + + private static ConcurrentOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + { + return new(runtime, targets) + { + InputTransform = (ConcurrentMessages.Request input) => ValueTask.FromResult(input), + ResultTransform = (ConcurrentMessages.Result[] results) => ValueTask.FromResult(string.Join("\n", results.Select(result => $"{result.Message}")).ToResult()), + }; + } +} diff --git a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs new file mode 100644 index 000000000000..31f0fd340ee1 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +using Microsoft.SemanticKernel.Agents.Runtime; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +/// +/// Tests for the class. +/// +public class GroupChatOrchestrationTests +{ + [Fact] + public async Task GroupChatOrchestrationWithSingleAgentAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + MockAgent mockAgent1 = CreateMockAgent(2, "xyz"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, mockAgent1); + + // Assert + Assert.Equal("xyz", response); + Assert.Equal(1, mockAgent1.InvokeCount); + } + + [Fact] + public async Task GroupChatOrchestrationWithMultipleAgentsAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgent1 = CreateMockAgent(1, "abc"); + MockAgent mockAgent2 = CreateMockAgent(2, "xyz"); + MockAgent mockAgent3 = CreateMockAgent(3, "lmn"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, mockAgent1, mockAgent2, mockAgent3); + + // Assert + Assert.Equal("lmn", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgent2.InvokeCount); + Assert.Equal(1, mockAgent3.InvokeCount); + } + + [Fact(Skip = "Not functional until root issue with nested protocol is fixed")] + public async Task GroupChatOrchestrationWithNestedMemberAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgentB = CreateMockAgent(2, "efg"); + GroupChatOrchestration orchestration = CreateNested(runtime, mockAgentB); + MockAgent mockAgent1 = CreateMockAgent(2, "xyz"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, mockAgent1, orchestration); + + // Assert + Assert.Equal("efg", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgentB.InvokeCount); + } + + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params OrchestrationTarget[] mockAgents) + { + // Act + await runtime.StartAsync(); + + GroupChatOrchestration orchestration = new(runtime, new SimpleGroupChatStrategy(), mockAgents); + + const string InitialInput = "123"; + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + + // Assert + Assert.NotNull(result); + + // Act + string response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); + + await runtime.RunUntilIdleAsync(); + + return response; + } + + private static MockAgent CreateMockAgent(int index, string response) + { + return new() + { + Description = $"test {index}", + Response = [new(AuthorRole.Assistant, response)] + }; + } + + private static GroupChatOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + { + return new(runtime, new SimpleGroupChatStrategy(), targets) + { + InputTransform = (ChatMessages.InputTask input) => ValueTask.FromResult(input), + ResultTransform = (ChatMessages.Result result) => ValueTask.FromResult(result), + }; + } + + private sealed class SimpleGroupChatStrategy : GroupChatStrategy + { + private int _count; + + public override ValueTask SelectAsync(GroupChatContext context, CancellationToken cancellationToken = default) + { + try + { + if (this._count < context.Team.Count) + { + context.SelectAgent(context.Team.Skip(this._count).First().Key); + } + + return ValueTask.CompletedTask; + } + finally + { + ++this._count; + } + } + } +} diff --git a/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs new file mode 100644 index 000000000000..4e872ef2b436 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Runtime; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +public class OrchestrationResultTests +{ + [Fact] + public void Constructor_InitializesPropertiesCorrectly() + { + // Arrange + string orchestrationName = "TestOrchestration"; + TopicId topic = new("testTopic"); + TaskCompletionSource tcs = new(); + + // Act + OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + + // Assert + Assert.Equal(topic, result.Topic); + } + + [Fact] + public async Task GetValueAsync_ReturnsCompletedValue_WhenTaskIsCompletedAsync() + { + // Arrange + string orchestrationName = "TestOrchestration"; + TopicId topic = new("testTopic"); + TaskCompletionSource tcs = new(); + OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + string expectedValue = "Result value"; + + // Act + tcs.SetResult(expectedValue); + string actualValue = await result.GetValueAsync(); + + // Assert + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public async Task GetValueAsync_WithTimeout_ReturnsCompletedValue_WhenTaskCompletesWithinTimeoutAsync() + { + // Arrange + string orchestrationName = "TestOrchestration"; + TopicId topic = new("testTopic"); + TaskCompletionSource tcs = new(); + OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + string expectedValue = "Result value"; + TimeSpan timeout = TimeSpan.FromSeconds(1); + + // Act + tcs.SetResult(expectedValue); + string actualValue = await result.GetValueAsync(timeout); + + // Assert + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public async Task GetValueAsync_WithTimeout_ThrowsTimeoutException_WhenTaskDoesNotCompleteWithinTimeoutAsync() + { + // Arrange + string orchestrationName = "TestOrchestration"; + TopicId topic = new("testTopic"); + TaskCompletionSource tcs = new(); + OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + TimeSpan timeout = TimeSpan.FromMilliseconds(50); + + // Act & Assert + TimeoutException exception = await Assert.ThrowsAsync(() => result.GetValueAsync(timeout).AsTask()); + Assert.Contains("Orchestration did not complete within the allowed duration", exception.Message); + } + + [Fact] + public async Task GetValueAsync_ReturnsCompletedValue_WhenCompletionIsDelayedAsync() + { + // Arrange + string orchestrationName = "TestOrchestration"; + TopicId topic = new("testTopic"); + TaskCompletionSource tcs = new(); + OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + int expectedValue = 42; + + // Act + // Simulate delayed completion in a separate task + Task delayTask = Task.Run(async () => + { + await Task.Delay(100); + tcs.SetResult(expectedValue); + }); + + int actualValue = await result.GetValueAsync(); + + // Assert + Assert.Equal(expectedValue, actualValue); + } +} diff --git a/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs new file mode 100644 index 000000000000..05ed3f283a75 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +/// +/// Unit tests for the class. +/// +public sealed class OrchestrationTargetTests +{ + [Fact] + public void ConstructWithAgent_SetsCorrectProperties() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + + // Act + OrchestrationTarget target = new(mockAgent.Object); + + // Assert + Assert.Equal(OrchestrationTargetType.Agent, target.TargetType); + Assert.Same(mockAgent.Object, target.Agent); + Assert.Null(target.Orchestration); + } + + [Fact] + public void ConstructWithOrchestration_SetsCorrectProperties() + { + // Arrange + Mock mockOrchestration = new(MockBehavior.Strict); + + // Act + OrchestrationTarget target = new(mockOrchestration.Object); + + // Assert + Assert.Equal(OrchestrationTargetType.Orchestratable, target.TargetType); + Assert.Same(mockOrchestration.Object, target.Orchestration); + Assert.Null(target.Agent); + } + + [Fact] + public void ImplicitConversionFromAgent_CreatesValidTarget() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + + // Act + OrchestrationTarget target = mockAgent.Object; + + // Assert + Assert.Equal(OrchestrationTargetType.Agent, target.TargetType); + Assert.Same(mockAgent.Object, target.Agent); + } + + [Fact] + public void ImplicitConversionFromOrchestration_CreatesValidTarget() + { + // Arrange + Mock mockOrchestration = new(MockBehavior.Strict); + + // Act + OrchestrationTarget target = mockOrchestration.Object; + + // Assert + Assert.Equal(OrchestrationTargetType.Orchestratable, target.TargetType); + Assert.Same(mockOrchestration.Object, target.Orchestration); + } + + [Fact] + public void IsAgent_ReturnsTrueAndAgent_WhenTargetIsAgent() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + OrchestrationTarget target = new(mockAgent.Object); + + // Act + bool isAgent = target.IsAgent(out Agent? agent); + + // Assert + Assert.True(isAgent); + Assert.Same(mockAgent.Object, agent); + } + + [Fact] + public void IsAgent_ReturnsFalseAndNull_WhenTargetIsNotAgent() + { + // Arrange + Mock mockOrchestration = new(MockBehavior.Strict); + OrchestrationTarget target = new(mockOrchestration.Object); + + // Act + bool isAgent = target.IsAgent(out Agent? agent); + + // Assert + Assert.False(isAgent); + Assert.Null(agent); + } + + [Fact] + public void IsOrchestration_ReturnsTrueAndOrchestration_WhenTargetIsOrchestration() + { + // Arrange + Mock mockOrchestration = new(MockBehavior.Strict); + OrchestrationTarget target = new(mockOrchestration.Object); + + // Act + bool isOrchestration = target.IsOrchestration(out Orchestratable? orchestration); + + // Assert + Assert.True(isOrchestration); + Assert.Same(mockOrchestration.Object, orchestration); + } + + [Fact] + public void IsOrchestration_ReturnsFalseAndNull_WhenTargetIsNotOrchestration() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + OrchestrationTarget target = new(mockAgent.Object); + + // Act + bool isOrchestration = target.IsOrchestration(out Orchestratable? orchestration); + + // Assert + Assert.False(isOrchestration); + Assert.Null(orchestration); + } + + [Fact] + public void Equals_ReturnsTrueForSameAgentReference() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + OrchestrationTarget target1 = new(mockAgent.Object); + OrchestrationTarget target2 = new(mockAgent.Object); + + // Act & Assert + Assert.True(target1.Equals(target2)); + Assert.True(target1 == target2); + Assert.False(target1 != target2); + } + + [Fact] + public void Equals_ReturnsTrueForSameOrchestrationReference() + { + // Arrange + Mock mockOrchestration = new(MockBehavior.Strict); + OrchestrationTarget target1 = new(mockOrchestration.Object); + OrchestrationTarget target2 = new(mockOrchestration.Object); + + // Act & Assert + Assert.True(target1.Equals(target2)); + Assert.True(target1 == target2); + Assert.False(target1 != target2); + } + + [Fact] + public void Equals_ReturnsFalseForDifferentReferences() + { + // Arrange + Mock mockAgent1 = new(MockBehavior.Strict); + Mock mockAgent2 = new(MockBehavior.Strict); + OrchestrationTarget target1 = new(mockAgent1.Object); + OrchestrationTarget target2 = new(mockAgent2.Object); + + // Act & Assert + Assert.False(target1.Equals(target2)); + Assert.False(target1 == target2); + Assert.True(target1 != target2); + } + + [Fact] + public void Equals_ReturnsFalseForDifferentTypes() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + Mock mockOrchestration = new(MockBehavior.Strict); + OrchestrationTarget target1 = new(mockAgent.Object); + OrchestrationTarget target2 = new(mockOrchestration.Object); + + // Act & Assert + Assert.False(target1.Equals(target2)); + Assert.False(target1 == target2); + Assert.True(target1 != target2); + } + + [Fact] + public void GetHashCode_ReturnsSameValueForEqualObjects() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + OrchestrationTarget target1 = new(mockAgent.Object); + OrchestrationTarget target2 = new(mockAgent.Object); + + // Act + int hashCode1 = target1.GetHashCode(); + int hashCode2 = target2.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void GetHashCode_ReturnsDifferentValuesForDifferentObjects() + { + // Arrange + Mock mockAgent = new(MockBehavior.Strict); + Mock mockOrchestration = new(MockBehavior.Strict); + OrchestrationTarget target1 = new(mockAgent.Object); + OrchestrationTarget target2 = new(mockOrchestration.Object); + + // Act + int hashCode1 = target1.GetHashCode(); + int hashCode2 = target2.GetHashCode(); + + // Assert + Assert.NotEqual(hashCode1, hashCode2); + } +} diff --git a/dotnet/src/Agents/UnitTests/Orchestration/SequentialOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/SequentialOrchestrationTests.cs new file mode 100644 index 000000000000..884b402400f3 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/SequentialOrchestrationTests.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +/// +/// Tests for the class. +/// +public class SequentialOrchestrationTests +{ + [Fact] + public async Task SequentialOrchestrationWithSingleAgentAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + MockAgent mockAgent1 = CreateMockAgent(2, "xyz"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, mockAgent1); + + // Assert + Assert.Equal("xyz", response); + Assert.Equal(1, mockAgent1.InvokeCount); + } + + [Fact] + public async Task SequentialOrchestrationWithMultipleAgentsAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgent1 = CreateMockAgent(1, "abc"); + MockAgent mockAgent2 = CreateMockAgent(2, "xyz"); + MockAgent mockAgent3 = CreateMockAgent(3, "lmn"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, mockAgent1, mockAgent2, mockAgent3); + + // Assert + Assert.Equal("lmn", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgent2.InvokeCount); + Assert.Equal(1, mockAgent3.InvokeCount); + } + + [Fact] + public async Task SequentialOrchestrationWithNestedMemberAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgentB = CreateMockAgent(2, "efg"); + SequentialOrchestration orchestration = CreateNested(runtime, mockAgentB); + MockAgent mockAgent1 = CreateMockAgent(2, "xyz"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, mockAgent1, orchestration); + + // Assert + Assert.Equal("efg", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgentB.InvokeCount); + } + + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params OrchestrationTarget[] mockAgents) + { + // Act + await runtime.StartAsync(); + + SequentialOrchestration orchestration = new(runtime, mockAgents); + + const string InitialInput = "123"; + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + + // Assert + Assert.NotNull(result); + + // Act + string response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); + + await runtime.RunUntilIdleAsync(); + + return response; + } + + private static MockAgent CreateMockAgent(int index, string response) + { + return new() + { + Description = $"test {index}", + Response = [new(AuthorRole.Assistant, response)] + }; + } + + private static SequentialOrchestration CreateNested(InProcessRuntime runtime, params OrchestrationTarget[] targets) + { + return new(runtime, targets) + { + InputTransform = (SequentialMessage input) => ValueTask.FromResult(input), + ResultTransform = (SequentialMessage results) => ValueTask.FromResult(results), + }; + } +} From d93823e1deae924117c46f8d6d4f0d37dfc1306f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 09:43:27 -0700 Subject: [PATCH 51/57] Namespace --- .../UnitTests/Orchestration/GroupChatOrchestrationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs index 31f0fd340ee1..568e2bc3fbdf 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs @@ -7,7 +7,6 @@ using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; using Microsoft.SemanticKernel.ChatCompletion; using Xunit; From e0f3b744e82253ef0909a095690361ceb07e8621 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 10:26:50 -0700 Subject: [PATCH 52/57] Sync cleanup --- dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs | 4 ++-- .../Agents/Orchestration/Concurrent/ConcurrentActor.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs b/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs index ed778ed2bb0a..bdef421968b0 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs @@ -26,7 +26,7 @@ public sealed class Group /// /// Reset/clear the conversation history for all . /// - public sealed class Reset { } + public sealed class Reset; /// /// The final result. @@ -42,7 +42,7 @@ public sealed class Result /// /// Signal a to respond. /// - public sealed class Speak { } + public sealed class Speak; /// /// The input task. diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index 11ddae767b80..264e23606cce 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -12,7 +12,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// internal sealed class ConcurrentActor : AgentActor, IHandle { - private readonly AgentType _orchestrationType; + private readonly AgentType _handoffActor; /// /// Initializes a new instance of the class. @@ -20,12 +20,12 @@ internal sealed class ConcurrentActor : AgentActor, IHandleThe unique identifier of the agent. /// The runtime associated with the agent. /// An . - /// Identifies the orchestration agent. + /// Identifies the actor collecting results. /// The logger to use for the actor - public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType orchestrationType, ILogger? logger = null) : + public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType resultActor, ILogger? logger = null) : base(id, runtime, agent, noThread: true, logger) { - this._orchestrationType = orchestrationType; + this._handoffActor = resultActor; } /// @@ -37,6 +37,6 @@ public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageConte this.Logger.LogConcurrentAgentResult(this.Id, response.Content); - await this.SendMessageAsync(response.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(response.ToResult(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false); } } From ebfab0b5855995515317278c09c9d62cbf8418ad Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 10:46:10 -0700 Subject: [PATCH 53/57] Scope topic --- .../Agents/Orchestration/Chat/ChatManagerActor.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs index 94fa80fb1c7c..e8f6919294d2 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs @@ -25,7 +25,6 @@ public abstract class ChatManagerActor : public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task."; private readonly AgentType _orchestrationType; - private readonly TopicId _groupTopic; private readonly ChatHandoff _handoff; /// @@ -41,11 +40,12 @@ public abstract class ChatManagerActor : protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, AgentType orchestrationType, TopicId groupTopic, ChatHandoff handoff, ILogger? logger = null) : base(id, runtime, DefaultDescription, logger) { - this.Chat = []; - this.Team = team; this._orchestrationType = orchestrationType; - this._groupTopic = groupTopic; this._handoff = handoff; + + this.Chat = []; + this.Team = team; + this.GroupTopic = groupTopic; } /// @@ -53,6 +53,11 @@ protected ChatManagerActor(AgentId id, IAgentRuntime runtime, ChatGroup team, Ag /// protected ChatHistory Chat { get; } + /// + /// The agent type used to identify the orchestration agent. + /// + protected TopicId GroupTopic { get; } + /// /// The input task. /// @@ -103,7 +108,7 @@ public async ValueTask HandleAsync(ChatMessages.InputTask item, MessageContext m if (agentType != null) { await this.RequestAgentResponseAsync(agentType.Value, messageContext.CancellationToken).ConfigureAwait(false); - await this.PublishMessageAsync(item.Message.ToGroup(), this._groupTopic).ConfigureAwait(false); + await this.PublishMessageAsync(item.Message.ToGroup(), this.GroupTopic).ConfigureAwait(false); } else { From 42ffb79fc610d076c8978e5931d3514b00081082 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 10:51:32 -0700 Subject: [PATCH 54/57] Add test reference --- dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index 5cd08cdae28a..28e51bfd9105 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -35,6 +35,7 @@ + From 34549f591f00d06810728b70549b5071b709cbe2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 11:05:36 -0700 Subject: [PATCH 55/57] Fix test base --- .../samples/AgentUtilities/BaseOrchestrationTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs index d50c35e15d1e..515fb079885b 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs @@ -17,7 +17,7 @@ protected ChatCompletionAgent CreateAgent(string instructions, string? name = nu { Instructions = instructions, Name = name, - Description = "test agent", + Description = description, Kernel = this.CreateKernelWithChatCompletion(), }; } From 53787cf4788b9319eeaa3661af6867bbe52d7c41 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 15:40:12 -0700 Subject: [PATCH 56/57] Test update - description --- .../Orchestration/Step01_Concurrent.cs | 30 +++++++++++++++++-- .../Orchestration/Step02_Sequential.cs | 30 +++++++++++++++++-- .../Orchestration/Step03_GroupChat.cs | 30 +++++++++++++++++-- .../{Step04_Nested.cs => Step05_Nested.cs} | 22 ++++++++++---- .../GroupChatOrchestration.String.cs | 2 +- .../SequentialOrchestration.String.cs | 2 +- .../Sequential/SequentialOrchestration.cs | 7 ++++- 7 files changed, 106 insertions(+), 17 deletions(-) rename dotnet/samples/GettingStartedWithAgents/Orchestration/{Step04_Nested.cs => Step05_Nested.cs} (63%) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index 89d575286bd5..d7240cc4a978 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -16,9 +16,33 @@ public class Step01_Concurrent(ITestOutputHelper output) : BaseOrchestrationTest public async Task SimpleConcurrentAsync() { // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); - ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); - ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of consonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + ChatCompletionAgent agent1 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of words. + + ALWAYS report the count using numeric digits formatted as: Words: + """, + description: "Able to count the number of words in a message"); + ChatCompletionAgent agent2 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of vowels. + + ALWAYS report the count using numeric digits formatted as: Vowels: + """, + description: "Able to count the number of vowels in a message"); + ChatCompletionAgent agent3 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of consonants. + + ALWAYS report the count using numeric digits formatted as: Consonants: + """, + description: "Able to count the number of consonants in a message"); // Define the pattern InProcessRuntime runtime = new(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index 495b4da4b221..464207238139 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -16,9 +16,33 @@ public class Step02_Sequential(ITestOutputHelper output) : BaseOrchestrationTest public async Task SimpleSequentialAsync() { // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); - ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); - ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of consonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + ChatCompletionAgent agent1 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of words. + + ALWAYS report the count using numeric digits formatted as: Words: + """, + description: "Able to count the number of words in a message"); + ChatCompletionAgent agent2 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of vowels. + + ALWAYS report the count using numeric digits formatted as: Vowels: + """, + description: "Able to count the number of vowels in a message"); + ChatCompletionAgent agent3 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of consonants. + + ALWAYS report the count using numeric digits formatted as: Consonants: + """, + description: "Able to count the number of consonants in a message"); // Define the pattern InProcessRuntime runtime = new(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 3c0340d6132e..b3d7deb2aa2a 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -16,9 +16,33 @@ public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest( public async Task SimpleGroupChatAsync() { // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("Analyze the previous message to determine count of words. ALWAYS report the count using numeric digits formatted as:\nWords: "); - ChatCompletionAgent agent2 = this.CreateAgent("Analyze the previous message to determine count of vowels. ALWAYS report the count using numeric digits formatted as:\nVowels: "); - ChatCompletionAgent agent3 = this.CreateAgent("Analyze the previous message to determine count of consonants. ALWAYS report the count using numeric digits formatted as:\nConsonants: "); + ChatCompletionAgent agent1 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of words. + + ALWAYS report the count using numeric digits formatted as: Words: + """, + description: "Able to count the number of words in a message"); + ChatCompletionAgent agent2 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of vowels. + + ALWAYS report the count using numeric digits formatted as: Vowels: + """, + description: "Able to count the number of vowels in a message"); + ChatCompletionAgent agent3 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of consonants. + + ALWAYS report the count using numeric digits formatted as: Consonants: + """, + description: "Able to count the number of consonants in a message"); // Define the pattern InProcessRuntime runtime = new(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs similarity index 63% rename from dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs rename to dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs index 26626b381a65..23d99fb4bf26 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Nested.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs @@ -11,16 +11,28 @@ namespace GettingStarted.Orchestration; /// /// Demonstrates how to nest an orchestration within another orchestration. /// -public class Step04_Nested(ITestOutputHelper output) : BaseOrchestrationTest(output) +public class Step05_Nested(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] public async Task NestConcurrentGroupsAsync() { // Define the agents - ChatCompletionAgent agent1 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 1"); - ChatCompletionAgent agent2 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 2"); - ChatCompletionAgent agent3 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 3"); - ChatCompletionAgent agent4 = this.CreateAgent("When the input is a number, N, respond with a number that is N + 4"); + ChatCompletionAgent agent1 = + this.CreateAgent( + instructions: "When the input is a number, N, respond with a number that is N + 1", + description: "Increments the current value by +1"); + ChatCompletionAgent agent2 = + this.CreateAgent( + instructions: "When the input is a number, N, respond with a number that is N + 2", + description: "Increments the current value by +2"); + ChatCompletionAgent agent3 = + this.CreateAgent( + instructions: "When the input is a number, N, respond with a number that is N + 3", + description: "Increments the current value by +3"); + ChatCompletionAgent agent4 = + this.CreateAgent( + instructions: "When the input is a number, N, respond with a number that is N + 4", + description: "Increments the current value by +4"); // Define the pattern InProcessRuntime runtime = new(); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs index ba19caa56898..c4730a133014 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs @@ -10,7 +10,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An orchestration that broadcasts the input message to each agent. /// -public sealed partial class GroupChatOrchestration : GroupChatOrchestration +public sealed class GroupChatOrchestration : GroupChatOrchestration { /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs index 33090fa80a56..9442a11d5292 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs @@ -10,7 +10,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// An orchestration that passes the input message to the first agent, and /// then the subsequent result to the next agent, etc... /// -public sealed partial class SequentialOrchestration : SequentialOrchestration +public sealed class SequentialOrchestration : SequentialOrchestration { /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 542861795eef..0869916b5d03 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -29,7 +30,11 @@ public SequentialOrchestration(IAgentRuntime runtime, params OrchestrationTarget /// protected override async ValueTask StartAsync(TopicId topic, SequentialMessage input, AgentType? entryAgent) { - await this.Runtime.SendMessageAsync(input, entryAgent!.Value).ConfigureAwait(false); // NULL OVERRIDE + if (!entryAgent.HasValue) + { + throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); + } + await this.Runtime.SendMessageAsync(input, entryAgent.Value).ConfigureAwait(false); } /// From 538045f49300e322f799c241d3e364e06e2862cc Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 24 Apr 2025 20:44:48 -0700 Subject: [PATCH 57/57] Handoff Orchestration --- .../Orchestration/Step04_Handoff.cs | 131 ++++++++++++++++++ dotnet/src/Agents/Orchestration/AgentActor.cs | 19 ++- .../Orchestration/Agents.Orchestration.csproj | 1 + .../Orchestration/Chat/ChatAgentActor.cs | 2 +- .../Concurrent/ConcurrentActor.cs | 2 +- .../Orchestration/Handoff/HandoffActor.cs | 119 ++++++++++++++++ .../Handoff/HandoffConnection.cs | 16 +++ .../Orchestration/Handoff/HandoffMessages.cs | 52 +++++++ .../Handoff/HandoffOrchestration.String.cs | 28 ++++ .../Handoff/HandoffOrchestration.cs | 98 +++++++++++++ .../HandoffOrchestrationLogMessages.cs | 54 ++++++++ .../Sequential/SequentialActor.cs | 2 +- .../HandoffOrchestrationTests.cs | 113 +++++++++++++++ 13 files changed, 632 insertions(+), 5 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs create mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs create mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs create mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs create mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs create mode 100644 dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs new file mode 100644 index 000000000000..dbc55027979b --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use the . +/// +public class Step04_Handoff(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task SimpleHandoffAsync() + { + // Initialize plugin + GithubPlugin githubPlugin = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(githubPlugin); + + // Define the agents + ChatCompletionAgent triageAgent = + this.CreateAgent( + instructions: "Given a GitHub issue, triage it.", + name: "TriageAgent", + description: "An agent that triages GitHub issues"); + ChatCompletionAgent pythonAgent = + this.CreateAgent( + instructions: "You are an agent that handles Python related GitHub issues.", + name: "PythonAgent", + description: "An agent that handles Python related issues"); + pythonAgent.Kernel.Plugins.Add(plugin); + ChatCompletionAgent dotnetAgent = + this.CreateAgent( + instructions: "You are an agent that handles .NET related GitHub issues.", + name: "DotNetAgent", + description: "An agent that handles .NET related issues"); + dotnetAgent.Kernel.Plugins.Add(plugin); + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestration = + new(runtime, + handoffs: + new() + { + { + triageAgent.Name!, + new() + { + { pythonAgent.Name!, pythonAgent.Description! }, + { dotnetAgent.Name!, dotnetAgent.Description! }, + } + } + }, + triageAgent, + pythonAgent, + dotnetAgent) + { + LoggerFactory = this.LoggerFactory + }; + + const string InputJson = + """ + { + "id": "12345", + "title": "Bug: SQLite Error 1: 'ambiguous column name:' when including VectorStoreRecordKey in VectorSearchOptions.Filter", + "body": "Describe the bug\nWhen using column names marked as [VectorStoreRecordData(IsFilterable = true)] in VectorSearchOptions.Filter, the query runs correctly.\nHowever, using the column name marked as [VectorStoreRecordKey] in VectorSearchOptions.Filter, the query throws exception 'SQLite Error 1: ambiguous column name: StartUTC'.\n\nTo Reproduce\nAdd a filter for the column marked [VectorStoreRecordKey]. Since that same column exists in both the vec_TestTable and TestTable, the data for both columns cannot be returned.\n\nExpected behavior\nThe query should explicitly list the vec_TestTable column names to retrieve and should omit the [VectorStoreRecordKey] column since it will be included in the primary TestTable columns.\n\nPlatform\nMicrosoft.SemanticKernel.Connectors.Sqlite v1.46.0-preview\n\nAdditional context\nNormal DBContext logging shows only normal context queries. Queries run by VectorizedSearchAsync() don't appear in those logs and I could not find a way to enable logging in semantic search so that I could actually see the exact query that is failing. It would have been very useful to see the failing semantic query.", + "labels": [] + } + """; + + // Start the runtime + await runtime.StartAsync(); + OrchestrationResult result = await orchestration.InvokeAsync(InputJson); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {text}"); + Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}"); + + await runtime.RunUntilIdleAsync(); + } + + [Fact] + public async Task SingleHandoffAsync() + { + // Define the agents + ChatCompletionAgent agent1 = + this.CreateAgent( + instructions: + """ + Analyze the previous message to determine count of words. + + ALWAYS report the count using numeric digits formatted as: Words: + """, + name: "Agent1", + description: "Able to count the number of words in a message"); + + // Define the pattern + InProcessRuntime runtime = new(); + HandoffOrchestration orchestration = + new(runtime, + handoffs: [], + agent1) + { + LoggerFactory = this.LoggerFactory + }; + + // Start the runtime + await runtime.StartAsync(); + string input = "Tell me the count of words, vowels, and consonants in: The quick brown fox jumps over the lazy dog"; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {text}"); + + await runtime.RunUntilIdleAsync(); + } + + private sealed class GithubPlugin + { + public Dictionary Labels { get; } = []; + + [KernelFunction] + public void AddLabels(string issueId, params string[] labels) + { + this.Labels[issueId] = labels; + } + } +} diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index f3503bf46b64..b71010dafe01 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -25,8 +25,9 @@ public abstract class AgentActor : PatternActor /// The runtime associated with the agent. /// An . /// Option to automatically clean-up agent thread + /// Option to enable function calling. /// The logger to use for the actor - protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThread = false, ILogger? logger = null) + protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThread = false, bool enableTools = false, ILogger? logger = null) : base( id, runtime, @@ -35,6 +36,7 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre { this.Agent = agent; this.NoThread = noThread; + this.EnableTools = enableTools; } /// @@ -47,6 +49,11 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre /// protected bool NoThread { get; } + /// + /// Gets a value indicating whether function calling is enabled. + /// + private bool EnableTools { get; } + /// /// Gets or sets the current conversation thread used during agent communication. /// @@ -87,11 +94,19 @@ protected ValueTask InvokeAsync(ChatMessageContent input, Ca /// A task that returns the response . protected async ValueTask InvokeAsync(IList input, CancellationToken cancellationToken) { + AgentInvokeOptions? options = null; + if (this.EnableTools) + { + options = new() + { + KernelArguments = new(new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }) + }; + } AgentResponseItem[] responses = await this.Agent.InvokeAsync( input, this.Thread, - options: null, + options, cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); AgentResponseItem response = responses[0]; diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index 57d984d19c45..5c7d3291ae93 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -30,6 +30,7 @@ + diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs index 84061a0e3ebd..bdc76e323e70 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs @@ -30,7 +30,7 @@ internal sealed class ChatAgentActor : /// The unique topic used to broadcast to the entire chat. /// The logger to use for the actor public ChatAgentActor(AgentId id, IAgentRuntime runtime, Agent agent, TopicId groupTopic, ILogger? logger = null) - : base(id, runtime, agent, noThread: false, logger) + : base(id, runtime, agent, noThread: false, enableTools: false, logger) { this._cache = []; this._groupTopic = groupTopic; diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index 264e23606cce..8b93a815a7f4 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -23,7 +23,7 @@ internal sealed class ConcurrentActor : AgentActor, IHandleIdentifies the actor collecting results. /// The logger to use for the actor public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType resultActor, ILogger? logger = null) : - base(id, runtime, agent, noThread: true, logger) + base(id, runtime, agent, noThread: true, enableTools: false, logger) { this._handoffActor = resultActor; } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs new file mode 100644 index 000000000000..d7eddd40404e --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Runtime; +using Microsoft.SemanticKernel.Agents.Runtime.Core; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// An actor used with the . +/// +internal sealed class HandoffActor : + AgentActor, + IHandle, + IHandle +{ + private readonly AgentType _resultHandoff; + private readonly TopicId _groupTopic; + private readonly List _cache; + + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the agent. + /// The runtime associated with the agent. + /// An . + /// The handoffs available to this agent + /// The handoff agent for capturing the result. + /// The unique topic for the orchestration session. + /// The logger to use for the actor + public HandoffActor(AgentId id, IAgentRuntime runtime, Agent agent, HandoffLookup handoffs, AgentType resultHandoff, TopicId groupTopic, ILogger? logger = null) + : base(id, runtime, agent, noThread: true, enableTools: true, logger) + { + this._cache = []; + this._groupTopic = groupTopic; + this._resultHandoff = resultHandoff; + agent.Kernel.AutoFunctionInvocationFilters.Add(new HandoffInvocationFilter()); // %%% CLONE KERNEL AND OVERRIDE ??? + agent.Kernel.Plugins.Add(this.CreateHandoffPlugin(handoffs)); // %%% CLONE KERNEL + } + + /// + public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext) + { + this.Logger.LogHandoffAgentInvoke(this.Id); + + ChatMessageContent response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); + this._cache.Clear(); + + this.Logger.LogHandoffAgentResult(this.Id, response.Content); + + await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this._groupTopic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false); + } + + /// + public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext) + { + this._cache.Add(item.Message); + + return ValueTask.CompletedTask; + } + + private KernelPlugin CreateHandoffPlugin(HandoffLookup handoffs) + { + return KernelPluginFactory.CreateFromFunctions(HandoffInvocationFilter.HandoffPlugin, CreateHandoffFunctions()); + + IEnumerable CreateHandoffFunctions() + { + yield return KernelFunctionFactory.CreateFromMethod( + this.EndAsync, + functionName: "end_task_with_summary", + description: "End the task with a summary when there is no further action to take."); + + foreach ((string name, (AgentType type, string description)) in handoffs) + { + KernelFunction kernelFunction = + KernelFunctionFactory.CreateFromMethod( + (CancellationToken cancellationToken) => this.HandoffAsync(type, cancellationToken), + functionName: $"transfer_to_{name}", + description: description); + + yield return kernelFunction; + } + } + } + + private async ValueTask HandoffAsync(AgentType agentType, CancellationToken cancellationToken = default) + { + this.Logger.LogHandoffFunctionCall(this.Id, agentType); + await this.SendMessageAsync(new HandoffMessages.Request(), agentType, cancellationToken).ConfigureAwait(false); + } + + private async ValueTask EndAsync(string summary, CancellationToken cancellationToken) + { + this.Logger.LogHandoffSummary(this.Id, summary); + await this.SendMessageAsync(new HandoffMessages.Result { Message = new ChatMessageContent(AuthorRole.User, summary) }, this._resultHandoff, cancellationToken).ConfigureAwait(false); + } +} + +internal sealed class HandoffInvocationFilter() : IAutoFunctionInvocationFilter +{ + public const string HandoffPlugin = nameof(HandoffPlugin); + + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + // Execution the function + await next(context).ConfigureAwait(false); + + // Signal termination if the function is part of the handoff plugin + if (context.Function.PluginName == HandoffPlugin) + { + context.Terminate = true; + } + } +} diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs new file mode 100644 index 000000000000..eeffca284c66 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel.Agents.Runtime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// Defines the handoff relationships for a given agent. +/// +public sealed class HandoffConnections : Dictionary; + +/// +/// Handoff relationships post-processed into a name based lookup table that includes the agent type. +/// +internal sealed class HandoffLookup : Dictionary; diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs new file mode 100644 index 000000000000..6dd457f0f341 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// A message that describes the input task and captures results for a . +/// +public sealed class HandoffMessages +{ + /// + /// An empty message instance as a default. + /// + internal static readonly ChatMessageContent Empty = new(); + + /// + /// The input message. + /// + public sealed class Input + { + /// + /// The orchestration input message. + /// + public ChatMessageContent Message { get; init; } = Empty; + } + + /// + /// The final result. + /// + public sealed class Result + { + /// + /// The orchestration result message. + /// + public ChatMessageContent Message { get; init; } = Empty; + } + + /// + /// Signals the handoff to another agent. + /// + public sealed class Request; + + /// + /// Broadcast an agent response to all actors in the orchestration. + /// + public sealed class Response + { + /// + /// The chat response message. + /// + public ChatMessageContent Message { get; init; } = Empty; + } +} diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs new file mode 100644 index 000000000000..862bc399c1ce --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Runtime; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// An orchestration that passes the input message to the first agent, and +/// then the subsequent result to the next agent, etc... +/// +public sealed class HandoffOrchestration : HandoffOrchestration +{ + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// Defines the handoff connections for each agent. + /// The agents to be orchestrated. + public HandoffOrchestration(IAgentRuntime runtime, Dictionary handoffs, params OrchestrationTarget[] members) + : base(runtime, handoffs, members) + { + this.InputTransform = (string input) => ValueTask.FromResult(new HandoffMessages.Input { Message = new ChatMessageContent(AuthorRole.User, input) }); + this.ResultTransform = (HandoffMessages.Result result) => ValueTask.FromResult(result.Message.ToString()); + } +} diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs new file mode 100644 index 000000000000..c8d1e4906a22 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; +using Microsoft.SemanticKernel.Agents.Runtime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +/// +/// An orchestration that provides the input message to the first agent +/// and Handoffly passes each agent result to the next agent. +/// +public class HandoffOrchestration : AgentOrchestration +{ + internal static readonly string OrchestrationName = typeof(HandoffOrchestration<,>).Name.Split('`').First(); + + private readonly Dictionary _handoffs; + + /// + /// Initializes a new instance of the class. + /// + /// The runtime associated with the orchestration. + /// Defines the handoff connections for each agent. + /// The agents participating in the orchestration. + public HandoffOrchestration(IAgentRuntime runtime, Dictionary handoffs, params OrchestrationTarget[] agents) + : base(OrchestrationName, runtime, agents) + { + this._handoffs = handoffs; + } + + /// + protected override async ValueTask StartAsync(TopicId topic, HandoffMessages.Input input, AgentType? entryAgent) + { + if (!entryAgent.HasValue) + { + throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); + } + await this.Runtime.PublishMessageAsync(new HandoffMessages.Response { Message = input.Message }, topic).ConfigureAwait(false); + await this.Runtime.SendMessageAsync(new HandoffMessages.Request(), entryAgent.Value).ConfigureAwait(false); + } + + /// + protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) + { + // Each agent handsoff its result to the next agent. + Dictionary agentMap = []; + Dictionary handoffMap = []; + AgentType nextAgent = orchestrationType; + for (int index = this.Members.Count - 1; index >= 0; --index) + { + OrchestrationTarget member = this.Members[index]; + + if (member.IsAgent(out Agent? agent)) + { + HandoffLookup map = []; + handoffMap[agent.Name ?? agent.Id] = map; + nextAgent = await RegisterAgentAsync(topic, nextAgent, index, agent, map).ConfigureAwait(false); + agentMap[agent.Name ?? agent.Id] = nextAgent; + } + //else if (member.IsOrchestration(out Orchestratable? orchestration)) // %%% IS POSSIBLE ??? + //{ + // nextAgent = await orchestration.RegisterAsync(topic, nextAgent, loggerFactory).ConfigureAwait(false); + //} + + await this.SubscribeAsync(nextAgent, topic).ConfigureAwait(false); + + logger.LogRegisterActor(OrchestrationName, nextAgent, "MEMBER", index + 1); + } + + // Complete the handoff model + foreach ((string agentName, HandoffConnections handoffs) in this._handoffs) + { + // Retrieve the map for the agent (every agent had an empty map created) + HandoffLookup agentHandoffs = handoffMap[agentName]; + foreach ((string handoffName, string description) in handoffs) + { + // name = (type,description) + agentHandoffs[handoffName] = (agentMap[handoffName], description); + } + } + + return nextAgent; + + ValueTask RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, Agent agent, HandoffLookup handoffs) + { + return + this.Runtime.RegisterAgentFactoryAsync( + this.GetAgentType(topic, index), + (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, agent, handoffs, orchestrationType, topic, loggerFactory.CreateLogger()))); + } + } + + private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); +} diff --git a/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs new file mode 100644 index 000000000000..f7865191d04d --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.Agents.Runtime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Extensions for logging . +/// +/// +/// This extension uses the to +/// generate logging code at compile time to achieve optimized code. +/// +[ExcludeFromCodeCoverage] +internal static partial class HandoffOrchestrationLogMessages +{ + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "REQUEST Handoff agent [{AgentId}]")] + public static partial void LogHandoffAgentInvoke( + this ILogger logger, + AgentId agentId); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "RESULT Handoff agent [{AgentId}]: {Message}")] + public static partial void LogHandoffAgentResult( + this ILogger logger, + AgentId agentId, + string? message); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "TOOL Handoff [{AgentId}]: {Handoff}")] + public static partial void LogHandoffFunctionCall( + this ILogger logger, + AgentId agentId, + AgentType handoff); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Trace, + Message = "RESULT Handoff summary [{AgentId}]: {Summary}")] + public static partial void LogHandoffSummary( + this ILogger logger, + AgentId agentId, + string? summary); +} diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index 214754e7287b..cfc878c58d4e 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -23,7 +23,7 @@ internal sealed class SequentialActor : AgentActor, IHandle /// The identifier of the next agent for which to handoff the result /// The logger to use for the actor public SequentialActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType nextAgent, ILogger? logger = null) - : base(id, runtime, agent, noThread: true, logger) + : base(id, runtime, agent, noThread: true, enableTools: false, logger) { this._nextAgent = nextAgent; } diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs new file mode 100644 index 000000000000..7fa3b0c79d6a --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +/// +/// Tests for the class. +/// +public class HandoffOrchestrationTests +{ + [Fact] + public async Task HandoffOrchestrationWithSingleAgentAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + MockAgent mockAgent1 = CreateMockAgent(2, "xyz"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync(runtime, handoffs: null, mockAgent1); + + // Assert + Assert.Equal("xyz", response); + Assert.Equal(1, mockAgent1.InvokeCount); + } + + [Fact] + public async Task HandoffOrchestrationWithMultipleAgentsAsync() + { + // Arrange + await using InProcessRuntime runtime = new(); + + MockAgent mockAgent1 = CreateMockAgent(1, "abc"); + MockAgent mockAgent2 = CreateMockAgent(2, "xyz"); + MockAgent mockAgent3 = CreateMockAgent(3, "lmn"); + + // Act: Create and execute the orchestration + string response = await ExecuteOrchestrationAsync( + runtime, + handoffs: + new() + { + { + mockAgent1.Name!, + new() + { + { mockAgent2.Name!, mockAgent2.Description! }, + } + }, + { + mockAgent2 .Name!, + new() + { + { mockAgent3.Name!, mockAgent3.Description! }, + } + }, + { + mockAgent3.Name!, + new() + { + { mockAgent1.Name!, mockAgent1.Description! }, + } + }, + }, + mockAgent1, + mockAgent2, + mockAgent3); + + // Assert + Assert.Equal("lmn", response); + Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal(1, mockAgent2.InvokeCount); + Assert.Equal(1, mockAgent3.InvokeCount); + } + + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, Dictionary? handoffs, params OrchestrationTarget[] mockAgents) + { + // Act + await runtime.StartAsync(); + + HandoffOrchestration orchestration = new(runtime, handoffs ?? [], mockAgents); + + const string InitialInput = "123"; + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + + // Assert + Assert.NotNull(result); + + // Act + string response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); + + await runtime.RunUntilIdleAsync(); + + return response; + } + + private static MockAgent CreateMockAgent(int index, string response) + { + return new() + { + Name = $"agent{index}", + Description = $"Provides a mock response", + Response = [new(AuthorRole.Assistant, response)] + }; + } +}