From 877dba17036a75a2d74aff7c7bd254f2ac89ea94 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 27 Mar 2025 09:28:18 -0700 Subject: [PATCH 01/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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/98] 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)] + }; + } +} From 1dc6fcb8b5b3cdf582bd9e1072378a4a8f6cce0a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 25 Apr 2025 10:33:04 -0700 Subject: [PATCH 58/98] Fix final note --- dotnet/src/Agents/Orchestration/AgentActor.cs | 31 +++++++++---------- .../Orchestration/Chat/ChatAgentActor.cs | 2 +- .../Concurrent/ConcurrentActor.cs | 2 +- .../Orchestration/Handoff/HandoffActor.cs | 29 ++++++++++++++--- .../Handoff/HandoffOrchestration.cs | 2 +- .../Sequential/SequentialActor.cs | 2 +- 6 files changed, 42 insertions(+), 26 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index b71010dafe01..d96af11f541e 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -18,6 +18,8 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// public abstract class AgentActor : PatternActor { + private AgentInvokeOptions? _options; + /// /// Initializes a new instance of the class. /// @@ -25,9 +27,8 @@ 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, bool enableTools = false, ILogger? logger = null) + protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThread = false, ILogger? logger = null) : base( id, runtime, @@ -36,7 +37,6 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre { this.Agent = agent; this.NoThread = noThread; - this.EnableTools = enableTools; } /// @@ -50,14 +50,17 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre protected bool NoThread { get; } /// - /// Gets a value indicating whether function calling is enabled. + /// Gets or sets the current conversation thread used during agent communication. /// - private bool EnableTools { get; } + protected AgentThread? Thread { get; set; } /// - /// Gets or sets the current conversation thread used during agent communication. + /// Optionally overridden to create custom invocation options for the agent. /// - protected AgentThread? Thread { get; set; } + protected virtual AgentInvokeOptions? CreateInvokeOptions() + { + return null; + } /// /// Deletes the agent thread. @@ -94,19 +97,11 @@ 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, + this.GetInvokeOptions(), cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); AgentResponseItem response = responses[0]; @@ -128,7 +123,7 @@ await this.Agent.InvokeAsync( /// 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([input], this.Thread, this.GetInvokeOptions(), cancellationToken); await foreach (AgentResponseItem response in responseStream.ConfigureAwait(false)) { @@ -144,6 +139,8 @@ protected async IAsyncEnumerable InvokeStreamingAsy } } + private AgentInvokeOptions? GetInvokeOptions() => this._options ??= this.CreateInvokeOptions(); + 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/Chat/ChatAgentActor.cs b/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs index bdc76e323e70..84061a0e3ebd 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, enableTools: false, logger) + : base(id, runtime, agent, noThread: 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 8b93a815a7f4..264e23606cce 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, enableTools: false, logger) + base(id, runtime, agent, noThread: true, logger) { this._handoffActor = resultActor; } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index d7eddd40404e..a322fee08b07 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -19,6 +19,7 @@ internal sealed class HandoffActor : IHandle, IHandle { + private readonly HandoffLookup _handoffs; private readonly AgentType _resultHandoff; private readonly TopicId _groupTopic; private readonly List _cache; @@ -34,13 +35,31 @@ internal sealed class HandoffActor : /// 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) + : base(id, runtime, agent, noThread: true, logger) { this._cache = []; this._groupTopic = groupTopic; + this._handoffs = handoffs; this._resultHandoff = resultHandoff; - agent.Kernel.AutoFunctionInvocationFilters.Add(new HandoffInvocationFilter()); // %%% CLONE KERNEL AND OVERRIDE ??? - agent.Kernel.Plugins.Add(this.CreateHandoffPlugin(handoffs)); // %%% CLONE KERNEL + } + + /// + protected override AgentInvokeOptions? CreateInvokeOptions() + { + // Clone kernel to avoid modifying the original + Kernel kernel = this.Agent.Kernel.Clone(); + kernel.AutoFunctionInvocationFilters.Add(new HandoffInvocationFilter()); + kernel.Plugins.Add(this.CreateHandoffPlugin()); + + // Create invocation options that use auto-function invocation and our modified kernel. + AgentInvokeOptions options = + new() + { + Kernel = kernel, + KernelArguments = new(new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }) + }; + + return options; } /// @@ -64,7 +83,7 @@ public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messa return ValueTask.CompletedTask; } - private KernelPlugin CreateHandoffPlugin(HandoffLookup handoffs) + private KernelPlugin CreateHandoffPlugin() { return KernelPluginFactory.CreateFromFunctions(HandoffInvocationFilter.HandoffPlugin, CreateHandoffFunctions()); @@ -75,7 +94,7 @@ IEnumerable CreateHandoffFunctions() 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) + foreach ((string name, (AgentType type, string description)) in this._handoffs) { KernelFunction kernelFunction = KernelFunctionFactory.CreateFromMethod( diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index d4a05f10ce6d..18a11fa92762 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -61,7 +61,7 @@ protected override async ValueTask StartAsync(TopicId topic, HandoffMessages.Inp 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 ??? + //else if (member.IsOrchestration(out Orchestratable? orchestration)) // TODO: IS POSSIBLE ? //{ // nextAgent = await orchestration.RegisterAsync(topic, nextAgent, loggerFactory).ConfigureAwait(false); //} diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index cfc878c58d4e..214754e7287b 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, enableTools: false, logger) + : base(id, runtime, agent, noThread: true, logger) { this._nextAgent = nextAgent; } From 1f6dcdc79dc7636664e9e453d02a4bf4402f2a5a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 25 Apr 2025 10:34:54 -0700 Subject: [PATCH 59/98] Skip test --- .../UnitTests/Orchestration/HandoffOrchestrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index 86bfa4d8577b..ed007d5f1171 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -16,7 +16,7 @@ namespace SemanticKernel.Agents.UnitTests.Orchestration; /// public class HandoffOrchestrationTests { - [Fact] + [Fact(Skip = "Mock agent unable to provide expected function calls")] public async Task HandoffOrchestrationWithSingleAgentAsync() { // Arrange @@ -31,7 +31,7 @@ public async Task HandoffOrchestrationWithSingleAgentAsync() Assert.Equal(1, mockAgent1.InvokeCount); } - [Fact] + [Fact(Skip = "Mock agent unable to provide expected function calls")] public async Task HandoffOrchestrationWithMultipleAgentsAsync() { // Arrange From bb315c64016db3b66c53f312258b42351e930192 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 May 2025 12:12:03 -0700 Subject: [PATCH 60/98] Fix merge --- dotnet/Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 68b75128d032..a8a2bc01b1ae 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -29,6 +29,7 @@ + From 6675050ede0cbc1daf4d9f7a2c0fde45a25d94e1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 16:52:12 -0700 Subject: [PATCH 61/98] Checkpoint --- .../Orchestration/Step01_Concurrent.cs | 45 ++-- .../Step01a_ConcurrentWithStructuredOutput.cs | 74 ++++++ .../Orchestration/Step02_Sequential.cs | 49 ++-- .../Orchestration/Step02a_Sequential.cs | 54 +++++ .../Orchestration/Step03_GroupChat.cs | 79 +++---- .../Step03a_GroupChatWithHumanInTheLoop.cs | 98 ++++++++ .../Step03b_GroupChatWithAIManager.cs | 207 ++++++++++++++++ .../Orchestration/Step04_Handoff.cs | 19 +- .../Step04b_HandoffWithStructuredInput.cs | 128 ++++++++++ .../Orchestration/Step05_Nested.cs | 58 ----- .../Resources/Hamlet_full_play_summary.txt | 13 + dotnet/src/Agents/Orchestration/AgentActor.cs | 53 ++--- .../AgentOrchestration.RequestActor.cs | 45 ++-- .../AgentOrchestration.ResultActor.cs | 40 ++-- .../Orchestration/AgentOrchestration.cs | 222 +++++++++-------- .../Agents/Orchestration/Chat/ChatGroup.cs | 6 +- .../Agents/Orchestration/Chat/ChatHandoff.cs | 35 --- .../Orchestration/Chat/ChatManagerActor.cs | 146 ------------ .../Concurrent/ConcurrentActor.cs | 11 +- .../Concurrent/ConcurrentMessages.cs | 27 ++- .../ConcurrentOrchestration.String.cs | 19 +- .../Concurrent/ConcurrentOrchestration.cs | 54 ++--- .../Concurrent/ConcurrentResultActor.cs | 6 +- .../Extensions/RuntimeExtensions.cs | 19 +- .../GroupChatAgentActor.cs} | 31 ++- .../GroupChat/GroupChatContext.cs | 60 ----- .../GroupChat/GroupChatManager.cs | 97 ++++++++ .../GroupChat/GroupChatManagerActor.cs | 84 +++++-- .../GroupChatMessages.cs} | 28 ++- .../GroupChatOrchestration.String.cs | 14 +- .../GroupChat/GroupChatOrchestration.cs | 94 +++----- .../GroupChat/GroupChatStrategy.cs | 41 ---- .../GroupChat/RoundRobinGroupChatManager.cs | 36 +++ .../Orchestration/Handoff/HandoffActor.cs | 35 ++- .../Orchestration/Handoff/HandoffMessages.cs | 18 +- .../Handoff/HandoffOrchestration.String.cs | 10 +- .../Handoff/HandoffOrchestration.cs | 54 ++--- .../Logging/AgentOrchestrationLogMessages.cs | 2 +- .../ConcurrentOrchestrationLogMessages.cs | 5 +- ...s => GroupChatOrchestrationLogMessages.cs} | 47 +++- .../Logging/OrchestrationResultLogMessages.cs | 16 +- .../SequentialOrchestrationLogMessages.cs | 5 +- .../Agents/Orchestration/Orchestratable.cs | 33 --- ...{PatternActor.cs => OrchestrationActor.cs} | 14 +- .../Orchestration/OrchestrationContext.cs | 55 +++++ .../Orchestration/OrchestrationResult.cs | 74 +++++- .../Orchestration/OrchestrationTarget.cs | 153 ------------ .../Sequential/SequentialActor.cs | 33 ++- .../Sequential/SequentialMessage.cs | 19 -- .../Sequential/SequentialMessages.cs | 59 +++++ .../SequentialOrchestration.String.cs | 11 +- .../Sequential/SequentialOrchestration.cs | 40 ++-- .../Transforms/DefaultTransforms.cs | 71 ++++++ .../Transforms/OrchestrationTransforms.cs | 33 +++ .../Transforms/StructuredOutputTransform.cs | 60 +++++ .../ConcurrentOrchestrationTests.cs | 36 +-- .../GroupChatOrchestrationTests.cs | 57 +---- .../HandoffOrchestrationTests.cs | 7 +- .../Orchestration/OrchestrationResultTests.cs | 34 +-- .../Orchestration/OrchestrationTargetTests.cs | 223 ------------------ .../SequentialOrchestrationTests.cs | 35 +-- 61 files changed, 1725 insertions(+), 1506 deletions(-) create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs delete mode 100644 dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Resources/Hamlet_full_play_summary.txt delete mode 100644 dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs delete mode 100644 dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs rename dotnet/src/Agents/Orchestration/{Chat/ChatAgentActor.cs => GroupChat/GroupChatAgentActor.cs} (54%) delete mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs rename dotnet/src/Agents/Orchestration/{Chat/ChatMessages.cs => GroupChat/GroupChatMessages.cs} (60%) delete mode 100644 dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs create mode 100644 dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs rename dotnet/src/Agents/Orchestration/Logging/{ChatOrchestrationLogMessages.cs => GroupChatOrchestrationLogMessages.cs} (65%) delete mode 100644 dotnet/src/Agents/Orchestration/Orchestratable.cs rename dotnet/src/Agents/Orchestration/{PatternActor.cs => OrchestrationActor.cs} (71%) create mode 100644 dotnet/src/Agents/Orchestration/OrchestrationContext.cs delete mode 100644 dotnet/src/Agents/Orchestration/OrchestrationTarget.cs delete mode 100644 dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs create mode 100644 dotnet/src/Agents/Orchestration/Sequential/SequentialMessages.cs create mode 100644 dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs create mode 100644 dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs create mode 100644 dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs delete mode 100644 dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index d7240cc4a978..0a29a23f1264 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -8,51 +8,34 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step01_Concurrent(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleConcurrentAsync() + public async Task ConcurrentTaskAsync() // %%% TODO { // Define the agents - ChatCompletionAgent agent1 = + ChatCompletionAgent physicist = 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 = + instructions: "You are an expert in physics. You answer questions from a physics perspective.", + description: "An expert in physics"); + ChatCompletionAgent chemist = 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"); + instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective.", + description: "An expert in chemistry"); - // Define the pattern - InProcessRuntime runtime = new(); - ConcurrentOrchestration orchestration = new(runtime, agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; + // Define the orchestration + ConcurrentOrchestration orchestration = new(physicist, chemist) { LoggerFactory = this.LoggerFactory }; // Start the runtime + InProcessRuntime runtime = new(); await runtime.StartAsync(); + + // Run the orchestration string input = "The quick brown fox jumps over the lazy dog"; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs new file mode 100644 index 000000000000..a0c5c5a625a8 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; +using Microsoft.SemanticKernel.Agents.Orchestration.Transforms; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Resources; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use the . +/// +public class Step01a_ConcurrentWithStructuredOutput(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; + + [Fact] + public async Task ConcurrentStructuredOutputAsync() // %%% TODO + { + // Define the agents + ChatCompletionAgent agent1 = + this.CreateAgent( + instructions: "You are an expert in identifying themes in articles. Given an article, identify the main themes.", + description: "An expert in identifying themes in articles"); + ChatCompletionAgent agent2 = + this.CreateAgent( + instructions: "You are an expert in sentiment analysis. Given an article, identify the sentiment.", + description: "An expert in sentiment analysis"); + ChatCompletionAgent agent3 = + this.CreateAgent( + instructions: "You are an expert in entity recognition. Given an article, extract the entities.", + description: "An expert in entity recognition"); + + // Define the orchestration with transform + Kernel kernel = this.CreateKernelWithChatCompletion(); + StructuredOutputTransform outputTransform = + new(kernel.GetRequiredService(), + new OpenAIPromptExecutionSettings { ResponseFormat = typeof(Analysis) }); + ConcurrentOrchestration orchestration = + new(agent1, agent2, agent3) + { + LoggerFactory = this.LoggerFactory, + ResultTransform = outputTransform.TransformAsync, + }; + + // Start the runtime + InProcessRuntime runtime = new(); + await runtime.StartAsync(); + + // Run the orchestration + const string resourceId = "Hamlet_full_play_summary.txt"; + string input = EmbeddedResource.Read(resourceId); + Console.WriteLine($"\n# INPUT: @{resourceId}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + + Analysis output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 2)); + Console.WriteLine($"\n# RESULT:\n{JsonSerializer.Serialize(output, s_options)}"); + + await runtime.RunUntilIdleAsync(); + } + + private sealed class Analysis + { + public IList Themes { get; set; } = []; + public IList Sentiments { get; set; } = []; + public IList Entities { get; set; } = []; + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index 464207238139..d6361996ca59 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -8,51 +8,50 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step02_Sequential(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleSequentialAsync() + public async Task SequentialTaskAsync() { // 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: + """ + You are a marketing analyst. Given a product description, identify: + - Key features + - Target audience + - Unique selling points """, - description: "Able to count the number of words in a message"); + description: "A agent that extracts key concepts from a product description."); ChatCompletionAgent agent2 = this.CreateAgent( - instructions: - """ - Analyze the previous message to determine count of vowels. - - ALWAYS report the count using numeric digits formatted as: Vowels: + """ + You are a marketing copywriter. Given a block of text describing features, audience, and USPs, + compose a compelling marketing copy (like a newsletter section) that highlights these points. + Output should be short (around 150 words), output just the copy as a single text block. """, - description: "Able to count the number of vowels in a message"); + description: "An agent that writes a marketing copy based on the extracted concepts."); ChatCompletionAgent agent3 = this.CreateAgent( - instructions: - """ - Analyze the previous message to determine count of consonants. - - ALWAYS report the count using numeric digits formatted as: Consonants: + """ + You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone, + give format and make it polished. Output the final improved copy as a single text block. """, - description: "Able to count the number of consonants in a message"); + description: "An agent that formats and proofreads the marketing copy."); - // Define the pattern - InProcessRuntime runtime = new(); - SequentialOrchestration orchestration = new(runtime, agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; + // Define the orchestration + SequentialOrchestration orchestration = new(agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; // Start the runtime + InProcessRuntime runtime = new(); await runtime.StartAsync(); - string input = "The quick brown fox jumps over the lazy dog"; + + // Run the orchestration + string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours"; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs new file mode 100644 index 000000000000..a38b3394e970 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use cancel a . +/// +public class Step02a_Sequential(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task SequentialCancelledAsync() + { + // Define the agents + ChatCompletionAgent agent = + this.CreateAgent( + """ + If the input message is a number, return the number incremented by one. + """, + description: "A agent that increments numbers."); + + // Define the orchestration + SequentialOrchestration orchestration = new(agent) { LoggerFactory = this.LoggerFactory }; + + // Start the runtime + InProcessRuntime runtime = new(); + await runtime.StartAsync(); + + // Run the orchestration + string input = "42"; + Console.WriteLine($"\n# INPUT: {input}\n"); + + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + + await result.CancelAsync(); + await Task.Delay(TimeSpan.FromSeconds(3)); + + try + { + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + Console.WriteLine($"\n# RESULT: {text}"); + } + catch + { + Console.WriteLine("\n# CANCELLED"); + } + + await runtime.RunUntilIdleAsync(); + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index b3d7deb2aa2a..29925be5963d 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -13,72 +13,47 @@ namespace GettingStarted.Orchestration; public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleGroupChatAsync() + public async Task AuthorCriticAsync() { // Define the agents - ChatCompletionAgent agent1 = + ChatCompletionAgent writer = this.CreateAgent( + name: "CopyWriter", + description: "A copy writer", 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 = + """ + You are a copywriter with ten years of experience and are known for brevity and a dry humor. + The goal is to refine and decide on the single best copy as an expert in the field. + Only provide a single proposal per response. + You're laser focused on the goal at hand. + Don't waste time with chit chat. + Consider suggestions when refining an idea. + """); + ChatCompletionAgent editor = this.CreateAgent( + name: "Reviewer", + description: "An editor.", 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"); + """ + You are an art director who has opinions about copywriting born of a love for David Ogilvy. + The goal is to determine if the given copy is acceptable to print. + If so, state that it is approved. + If not, provide insight on how to refine suggested copy without example. + """); - // Define the pattern - InProcessRuntime runtime = new(); - GroupChatOrchestration orchestration = new(runtime, new SimpleGroupChatStrategy(), agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; + // Define the orchestration + GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocations = 5 }, writer, editor) { LoggerFactory = this.LoggerFactory }; // Start the runtime + InProcessRuntime runtime = new(); await runtime.StartAsync(); - string input = "The quick brown fox jumps over the lazy dog"; + string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 4)); Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); } - - 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/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs new file mode 100644 index 000000000000..c02b574a2c20 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; +using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use the . +/// +public class Step03a_GroupChatWithHumanInTheLoop(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task GroupChatWithHumanAsync() + { + // Define the agents + ChatCompletionAgent writer = + this.CreateAgent( + name: "CopyWriter", + description: "A copy writer", + instructions: + """ + You are a copywriter with ten years of experience and are known for brevity and a dry humor. + The goal is to refine and decide on the single best copy as an expert in the field. + Only provide a single proposal per response. + You're laser focused on the goal at hand. + Don't waste time with chit chat. + Consider suggestions when refining an idea. + """); + ChatCompletionAgent editor = + this.CreateAgent( + name: "Reviewer", + description: "An editor.", + instructions: + """ + You are an art director who has opinions about copywriting born of a love for David Ogilvy. + The goal is to determine if the given copy is acceptable to print. + If so, state that it is approved. + If not, provide insight on how to refine suggested copy without example. + """); + + // Define the orchestration + GroupChatOrchestration orchestration = + new( + new CustomRoundRobinGroupChatManager() + { + MaximumInvocations = 5, + InteractiveCallback = () => + { + ChatMessageContent input = new(AuthorRole.User, "I like it"); + Console.WriteLine($"\n# INPUT: {input.Content}\n"); + return ValueTask.FromResult(input); + } + }, + writer, + editor) + { + LoggerFactory = this.LoggerFactory + }; + + // Start the runtime + InProcessRuntime runtime = new(); + await runtime.StartAsync(); + + // Run the orchestration + string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; + Console.WriteLine($"\n# INPUT: {input}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 4)); + Console.WriteLine($"\n# RESULT: {text}"); + + await runtime.RunUntilIdleAsync(); + } + + private sealed class CustomRoundRobinGroupChatManager : RoundRobinGroupChatManager + { + public override ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default) + { + string? lastAgent = history.LastOrDefault()?.AuthorName; + + if (lastAgent is null) + { + return ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "No agents have spoken yet." }); + } + + if (lastAgent == "Reviewer") + { + return ValueTask.FromResult(new GroupChatManagerResult(true) { Reason = "User input is needed after the reviewer's message." }); + } + + return ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "User input is not needed until the reviewer's message." }); + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs new file mode 100644 index 000000000000..8e7bca1b3362 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +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.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace GettingStarted.Orchestration; + +/// +/// Demonstrates how to use the . +/// +public class Step03b_GroupChatWithAIManager(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task GroupChatWithAIManagerAsync() + { + // Define the agents + ChatCompletionAgent farmer = + this.CreateAgent( + name: "Farmer", + description: "A rural farmer from Southeast Asia.", + instructions: + """ + You're a farmer from Southeast Asia. + Your life is deeply connected to land and family. + You value tradition and sustainability. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent developer = + this.CreateAgent( + name: "Developer", + description: "An urban software developer from the United States.", + instructions: + """ + You're a software developer from the United States. + Your life is fast-paced and technology-driven. + You value innovation, freedom, and work-life balance. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent teacher = + this.CreateAgent( + name: "Teacher", + description: "A retired history teacher from Eastern Europe", + instructions: + """ + You're a retired history teacher from Eastern Europe. + You bring historical and philosophical perspectives to discussions. + You value legacy, learning, and cultural continuity. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent activist = + this.CreateAgent( + name: "Activist", + description: "A young activist from South America.", + instructions: + """ + You're a young activist from South America. + You focus on social justice, environmental rights, and generational change. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent spiritual = + this.CreateAgent( + name: "SpiritualLeader", + description: "A spiritual leader from the Middle East.", + instructions: + """ + You're a spiritual leader from the Middle East. + You provide insights grounded in religion, morality, and community service. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent artist = + this.CreateAgent( + name: "Artist", + description: "An artist from Africa.", + instructions: + """ + You're an artist from Africa. + You view life through creative expression, storytelling, and collective memory. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent immigrant = + this.CreateAgent( + name: "Immigrant", + description: "An immigrant entrepreneur from Asia living in Canada.", + instructions: + """ + You're an immigrant entrepreneur from Asia living in Canada. + You balance trandition with adaption. + You focus on family success, risk, and opportunity. + You are in a debate. Feel free to challenge the other participants with respect. + """); + ChatCompletionAgent doctor = + this.CreateAgent( + name: "Doctor", + description: "A doctor from Scandinavia.", + instructions: + """ + You're a doctor from Scandinavia. + Your perspective is shaped by public health, equity, and structured societal support. + You are in a debate. Feel free to challenge the other participants with respect. + """); + + // Define the orchestration + const string topic = "What does a good life mean to you personally?"; + Kernel kernel = this.CreateKernelWithChatCompletion(); + GroupChatOrchestration orchestration = + new( + new AIGroupChatManager( + topic, + kernel.GetRequiredService()) + { + MaximumInvocations = 5 + }, + farmer, + developer, + teacher, + activist, + spiritual, + artist, + immigrant, + doctor) + { + LoggerFactory = this.LoggerFactory + }; + + // Start the runtime + InProcessRuntime runtime = new(); + await runtime.StartAsync(); + + // Run the orchestration + Console.WriteLine($"\n# INPUT: {topic}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(topic, runtime); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 4)); + Console.WriteLine($"\n# RESULT: {text}"); + + await runtime.RunUntilIdleAsync(); + } + + private sealed class AIGroupChatManager(string topic, IChatCompletionService chatCompletion) : GroupChatManager + { + private static class Prompts + { + public static string Termination(string topic) => + $""" + You are mediator that guides a discussion on the topic of '{topic}'. + You need to determine if the discussion has reached a conclusion. + If you would like to end the discussion, please respond with True. Otherwise, respond with False. + """; + + public static string Selection(string topic, string participants) => + $""" + You are mediator that guides a discussion on the topic of '{topic}'. + You need to select the next participant to speak. + Here are the names and descriptions of the participants: + {participants}\n + Please respond with only the name of the participant you would like to select. + """; + + public static string Filter(string topic) => + $""" + You are mediator that guides a discussion on the topic of '{topic}'. + You have just concluded the discussion. + Please summarize the discussion and provide a closing statement. + """; + } + + /// + public override ValueTask> FilterResults(ChatHistory history, CancellationToken cancellationToken = default) => + this.GetResponseAsync(history, Prompts.Filter(topic), cancellationToken); + + /// + public override ValueTask> SelectNextAgent(ChatHistory history, ChatGroup team, CancellationToken cancellationToken = default) => + this.GetResponseAsync(history, Prompts.Selection(topic, team.FormatList()), cancellationToken); + + /// + public override ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default) => + ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "The AI group chat manager does not request user input." }); + + /// + public override async ValueTask> ShouldTerminate(ChatHistory history, CancellationToken cancellationToken = default) + { + GroupChatManagerResult result = await base.ShouldTerminate(history, cancellationToken); + if (!result.Value) + { + result = await this.GetResponseAsync(history, Prompts.Termination(topic), cancellationToken); + } + return result; + } + + private async ValueTask> GetResponseAsync(ChatHistory history, string prompt, CancellationToken cancellationToken = default) + { + OpenAIPromptExecutionSettings executionSettings = new() { ResponseFormat = typeof(GroupChatManagerResult) }; + ChatHistory request = [.. history, new ChatMessageContent(AuthorRole.System, prompt)]; + ChatMessageContent response = await chatCompletion.GetChatMessageContentAsync(request, executionSettings, kernel: null, cancellationToken); + string responseText = response.ToString(); + return + JsonSerializer.Deserialize>(responseText) ?? + throw new InvalidOperationException($"Failed to parse response: {responseText}"); + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index dbc55027979b..d940bc57bfc4 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -39,11 +39,9 @@ public async Task SimpleHandoffAsync() description: "An agent that handles .NET related issues"); dotnetAgent.Kernel.Plugins.Add(plugin); - // Define the pattern - InProcessRuntime runtime = new(); + // Define the orchestration HandoffOrchestration orchestration = - new(runtime, - handoffs: + new(handoffs: new() { { @@ -73,8 +71,12 @@ public async Task SimpleHandoffAsync() """; // Start the runtime + InProcessRuntime runtime = new(); await runtime.StartAsync(); - OrchestrationResult result = await orchestration.InvokeAsync(InputJson); + + // Run the orchestration + Console.WriteLine($"\n# INPUT:\n{InputJson}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(InputJson, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}"); @@ -100,8 +102,7 @@ Analyze the previous message to determine count of words. // Define the pattern InProcessRuntime runtime = new(); HandoffOrchestration orchestration = - new(runtime, - handoffs: [], + new(handoffs: [], agent1) { LoggerFactory = this.LoggerFactory @@ -109,9 +110,9 @@ Analyze the previous message to determine count of words. // 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"; + string input = "Tell me the count of words in: The quick brown fox jumps over the lazy dog"; Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs new file mode 100644 index 000000000000..e156d39f1ee0 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +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 Step04b_HandoffWithStructuredInput(ITestOutputHelper output) : BaseOrchestrationTest(output) +{ + [Fact] + public async Task HandoffStructuredInputAsync() + { + // 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 orchestration + HandoffOrchestration orchestration = + new(handoffs: + new() + { + { + triageAgent.Name!, + new() + { + { pythonAgent.Name!, pythonAgent.Description! }, + { dotnetAgent.Name!, dotnetAgent.Description! }, + } + } + }, + triageAgent, + pythonAgent, + dotnetAgent) + { + LoggerFactory = this.LoggerFactory + }; + + GithubIssue input = + new() + { + Id = "12345", + Title = "Bug: SQLite Error 1: 'ambiguous column name:' when including VectorStoreRecordKey in VectorSearchOptions.Filter", + Body = + """ + Describe the bug + When using column names marked as [VectorStoreRecordData(IsFilterable = true)] in VectorSearchOptions.Filter, the query runs correctly. + However, using the column name marked as [VectorStoreRecordKey] in VectorSearchOptions.Filter, the query throws exception 'SQLite Error 1: ambiguous column name: StartUTC'. + To Reproduce + Add 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. + + Expected behavior + The 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. + + Platform + Microsoft.SemanticKernel.Connectors.Sqlite v1.46.0-preview + + Additional context + Normal 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 + InProcessRuntime runtime = new(); + await runtime.StartAsync(); + + // Run the orchestration + Console.WriteLine($"\n# INPUT:\n{input.Id}: {input.Title}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); + 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(); + } + + private sealed class GithubIssue + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("body")] + public string Body { get; set; } = string.Empty; + + [JsonPropertyName("labels")] + public string[] Labels { get; set; } = []; + } + + 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/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs deleted file mode 100644 index 23d99fb4bf26..000000000000 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Nested.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -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; - -namespace GettingStarted.Orchestration; - -/// -/// Demonstrates how to nest an orchestration within another orchestration. -/// -public class Step05_Nested(ITestOutputHelper output) : BaseOrchestrationTest(output) -{ - [Fact] - public async Task NestConcurrentGroupsAsync() - { - // Define the agents - 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(); - SequentialOrchestration innerOrchestration = - new(runtime, agent3, agent4) - { - 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) { 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[] 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/Resources/Hamlet_full_play_summary.txt b/dotnet/samples/GettingStartedWithAgents/Resources/Hamlet_full_play_summary.txt new file mode 100644 index 000000000000..9050a46e660f --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Resources/Hamlet_full_play_summary.txt @@ -0,0 +1,13 @@ +On a dark winter night, a ghost walks the ramparts of Elsinore Castle in Denmark. Discovered first by a pair of watchmen, then by the scholar Horatio, the ghost resembles the recently deceased King Hamlet, whose brother Claudius has inherited the throne and married the king’s widow, Queen Gertrude. When Horatio and the watchmen bring Prince Hamlet, the son of Gertrude and the dead king, to see the ghost, it speaks to him, declaring ominously that it is indeed his father’s spirit, and that he was murdered by none other than Claudius. Ordering Hamlet to seek revenge on the man who usurped his throne and married his wife, the ghost disappears with the dawn. + +Prince Hamlet devotes himself to avenging his father’s death, but, because he is contemplative and thoughtful by nature, he delays, entering into a deep melancholy and even apparent madness. Claudius and Gertrude worry about the prince’s erratic behavior and attempt to discover its cause. They employ a pair of Hamlet’s friends, Rosencrantz and Guildenstern, to watch him. When Polonius, the pompous Lord Chamberlain, suggests that Hamlet may be mad with love for his daughter, Ophelia, Claudius agrees to spy on Hamlet in conversation with the girl. But though Hamlet certainly seems mad, he does not seem to love Ophelia: he orders her to enter a nunnery and declares that he wishes to ban marriages. + +A group of traveling actors comes to Elsinore, and Hamlet seizes upon an idea to test his uncle’s guilt. He will have the players perform a scene closely resembling the sequence by which Hamlet imagines his uncle to have murdered his father, so that if Claudius is guilty, he will surely react. When the moment of the murder arrives in the theater, Claudius leaps up and leaves the room. Hamlet and Horatio agree that this proves his guilt. Hamlet goes to kill Claudius but finds him praying. Since he believes that killing Claudius while in prayer would send Claudius’s soul to heaven, Hamlet considers that it would be an inadequate revenge and decides to wait. Claudius, now frightened of Hamlet’s madness and fearing for his own safety, orders that Hamlet be sent to England at once. + +Hamlet goes to confront his mother, in whose bedchamber Polonius has hidden behind a tapestry. Hearing a noise from behind the tapestry, Hamlet believes the king is hiding there. He draws his sword and stabs through the fabric, killing Polonius. For this crime, he is immediately dispatched to England with Rosencrantz and Guildenstern. However, Claudius’s plan for Hamlet includes more than banishment, as he has given Rosencrantz and Guildenstern sealed orders for the King of England demanding that Hamlet be put to death. + +In the aftermath of her father’s death, Ophelia goes mad with grief and drowns in the river. Polonius’s son, Laertes, who has been staying in France, returns to Denmark in a rage. Claudius convinces him that Hamlet is to blame for his father’s and sister’s deaths. When Horatio and the king receive letters from Hamlet indicating that the prince has returned to Denmark after pirates attacked his ship en route to England, Claudius concocts a plan to use Laertes’ desire for revenge to secure Hamlet’s death. Laertes will fence with Hamlet in innocent sport, but Claudius will poison Laertes’ blade so that if he draws blood, Hamlet will die. As a backup plan, the king decides to poison a goblet, which he will give Hamlet to drink should Hamlet score the first or second hits of the match. Hamlet returns to the vicinity of Elsinore just as Ophelia’s funeral is taking place. Stricken with grief, he attacks Laertes and declares that he had in fact always loved Ophelia. Back at the castle, he tells Horatio that he believes one must be prepared to die, since death can come at any moment. A foolish courtier named Osric arrives on Claudius’s orders to arrange the fencing match between Hamlet and Laertes. + +The sword-fighting begins. Hamlet scores the first hit, but declines to drink from the king’s proffered goblet. Instead, Gertrude takes a drink from it and is swiftly killed by the poison. Laertes succeeds in wounding Hamlet, though Hamlet does not die of the poison immediately. First, Laertes is cut by his own sword’s blade, and, after revealing to Hamlet that Claudius is responsible for the queen’s death, he dies from the blade’s poison. Hamlet then stabs Claudius through with the poisoned sword and forces him to drink down the rest of the poisoned wine. Claudius dies, and Hamlet dies immediately after achieving his revenge. + +At this moment, a Norwegian prince named Fortinbras, who has led an army to Denmark and attacked Poland earlier in the play, enters with ambassadors from England, who report that Rosencrantz and Guildenstern are dead. Fortinbras is stunned by the gruesome sight of the entire royal family lying sprawled on the floor dead. He moves to take power of the kingdom. Horatio, fulfilling Hamlet’s last request, tells him Hamlet’s tragic story. Fortinbras orders that Hamlet be carried away in a manner befitting a fallen soldier. \ No newline at end of file diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index d96af11f541e..75d2bb129059 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.SemanticKernel.ChatCompletion; @@ -16,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// /// An actor that represents an . /// -public abstract class AgentActor : PatternActor +public abstract class AgentActor : OrchestrationActor { private AgentInvokeOptions? _options; @@ -25,18 +24,18 @@ public abstract class AgentActor : PatternActor /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// The orchestration context. /// An . - /// Option to automatically clean-up agent thread /// 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, OrchestrationContext context, Agent agent, ILogger? logger = null) : base( id, runtime, + context, VerifyDescription(agent), - logger ?? GetLogger(agent)) + logger) { this.Agent = agent; - this.NoThread = noThread; } /// @@ -44,11 +43,6 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre /// protected Agent Agent { get; } - /// - /// 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. /// @@ -65,8 +59,7 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, Agent agent, bool noThre /// /// Deletes the agent thread. /// - /// - /// + /// A cancellation token that can be used to cancel the operation. protected async ValueTask DeleteThreadAsync(CancellationToken cancellationToken) { if (this.Thread != null) @@ -97,6 +90,8 @@ protected ValueTask InvokeAsync(ChatMessageContent input, Ca /// A task that returns the response . protected async ValueTask InvokeAsync(IList input, CancellationToken cancellationToken) { + this.Context.Cancellation.ThrowIfCancellationRequested(); + AgentResponseItem[] responses = await this.Agent.InvokeAsync( input, @@ -104,14 +99,21 @@ await this.Agent.InvokeAsync( this.GetInvokeOptions(), cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); - AgentResponseItem response = responses[0]; - this.Thread ??= response.Thread; + AgentResponseItem? firstResponse = responses.FirstOrDefault(); + this.Thread ??= firstResponse?.Thread; // 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))) + ChatMessageContent response = new(firstResponse?.Message.Role ?? AuthorRole.Assistant, string.Join("\n\n", responses.Select(response => response.Message))) { - AuthorName = response.Message.AuthorName, + AuthorName = firstResponse?.Message.AuthorName, }; + + if (this.Context.ResponseCallback is not null) + { + await this.Context.ResponseCallback.Invoke(response).ConfigureAwait(false); + } + + return response; } /// @@ -123,18 +125,13 @@ await this.Agent.InvokeAsync( /// An asynchronous stream of responses. protected async IAsyncEnumerable InvokeStreamingAsync(ChatMessageContent input, [EnumeratorCancellation] CancellationToken cancellationToken) { + this.Context.Cancellation.ThrowIfCancellationRequested(); + var responseStream = this.Agent.InvokeStreamingAsync([input], this.Thread, this.GetInvokeOptions(), cancellationToken); await foreach (AgentResponseItem response in responseStream.ConfigureAwait(false)) { - if (this.NoThread) - { - // Do not block on thread clean-up - Task task = this.DeleteThreadAsync(cancellationToken).AsTask(); - } - { - this.Thread ??= response.Thread; - } + this.Thread ??= response.Thread; yield return response.Message; } } @@ -145,10 +142,4 @@ private static string VerifyDescription(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; - return loggerFactory.CreateLogger(); - } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index 4ffd035b03f6..e3ad0952f790 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -1,46 +1,46 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Transforms; using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.SemanticKernel.Agents.Orchestration; -public abstract partial class AgentOrchestration +public abstract partial class AgentOrchestration { /// /// Actor responsible for receiving final message and transforming it into the output type. /// - private sealed class RequestActor : PatternActor, IHandle + private sealed class RequestActor : OrchestrationActor, IHandle { - private readonly string _orchestrationRoot; - private readonly Func> _transform; - private readonly Func _action; - private readonly TaskCompletionSource? _completionSource; + private readonly OrchestrationInputTransform _transform; + private readonly Func, ValueTask> _action; + private readonly TaskCompletionSource _completionSource; /// - /// 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. - /// A descriptive root label for the orchestration. + /// The orchestration context. /// 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. + /// 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, - TaskCompletionSource? completionSource = null, + OrchestrationContext context, + OrchestrationInputTransform transform, + TaskCompletionSource completionSource, + Func, ValueTask> action, ILogger? logger = null) - : base(id, runtime, $"{id.Type}_Actor", logger) + : base(id, runtime, context, $"{id.Type}_Actor", logger) { - this._orchestrationRoot = orchestrationRoot; this._transform = transform; this._action = action; this._completionSource = completionSource; @@ -54,22 +54,19 @@ public RequestActor( /// A ValueTask representing the asynchronous operation. public async ValueTask HandleAsync(TInput item, MessageContext messageContext) { - this.Logger.LogOrchestrationRequestInvoke(this._orchestrationRoot, this.Id); + this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id); try { - TSource source = await this._transform.Invoke(item).ConfigureAwait(false); - Task task = this._action.Invoke(source).AsTask(); - this.Logger.LogOrchestrationStart(this._orchestrationRoot, this.Id); + IEnumerable input = await this._transform.Invoke(item).ConfigureAwait(false); + Task task = this._action.Invoke(input).AsTask(); + this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id); await task.ConfigureAwait(false); } catch (Exception exception) { // Log exception details and allow orchestration to fail - this.Logger.LogOrchestrationRequestFailure(this._orchestrationRoot, this.Id, exception); - if (this._completionSource != null) - { - this._completionSource.SetException(exception); - } + this.Logger.LogOrchestrationRequestFailure(this.Context.Orchestration, this.Id, exception); + this._completionSource.SetException(exception); throw; } } diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index 459f0e171c2e..25c91ee3ce7b 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -3,43 +3,46 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Orchestration.Transforms; using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.SemanticKernel.Agents.Orchestration; -public abstract partial class AgentOrchestration +public abstract partial class AgentOrchestration { /// /// Actor responsible for receiving the resultant message, transforming it, and handling further orchestration. /// - private sealed class ResultActor : PatternActor, IHandle + private sealed class ResultActor : OrchestrationActor, IHandle { - private readonly TaskCompletionSource? _completionSource; - private readonly string _orchestrationRoot; - private readonly Func> _transform; + private readonly TaskCompletionSource _completionSource; + private readonly OrchestrationResultTransform _transformResult; + private readonly OrchestrationOutputTransform _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. - /// A descriptive root label for the orchestration. - /// A delegate that transforms a TResult instance into a TOutput instance. + /// The orchestration context. + /// A delegate that transforms a TResult instance into a ChatMessageContent. + /// A delegate that transforms a ChatMessageContent 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) - : base(id, runtime, $"{id.Type}_Actor", logger) + OrchestrationContext context, + OrchestrationResultTransform transformResult, + OrchestrationOutputTransform transformOutput, + TaskCompletionSource completionSource, + ILogger>? logger = null) + : base(id, runtime, context, $"{id.Type}_Actor", logger) { this._completionSource = completionSource; - this._orchestrationRoot = orchestrationRoot; - this._transform = transform; + this._transformResult = transformResult; + this._transform = transformOutput; } /// @@ -57,11 +60,12 @@ public ResultActor( /// A ValueTask representing asynchronous operation. public async ValueTask HandleAsync(TResult item, MessageContext messageContext) { - this.Logger.LogOrchestrationResultInvoke(this._orchestrationRoot, this.Id); + this.Logger.LogOrchestrationResultInvoke(this.Context.Orchestration, this.Id); try { - TOutput output = await this._transform.Invoke(item).ConfigureAwait(false); + ChatMessageContent result = this._transformResult.Invoke(item); + TOutput output = await this._transform.Invoke(result).ConfigureAwait(false); if (this.CompletionTarget.HasValue) { @@ -73,7 +77,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._orchestrationRoot, this.Id, exception); + this.Logger.LogOrchestrationResultFailure(this.Context.Orchestration, this.Id, exception); if (this._completionSource == null) { diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 2bd6eb3734d1..4d644e4c9737 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -2,37 +2,68 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; +using Microsoft.SemanticKernel.Agents.Orchestration.Transforms; using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.SemanticKernel.Agents.Orchestration; +/// +/// Called for every response is produced by any agent. +/// +/// The agent response +public delegate ValueTask OrchestrationResponseCallback(ChatMessageContent response); + +/// +/// Called when human interaction is requested. +/// +public delegate ValueTask OrchestrationInteractiveCallback(); + /// /// Base class for multi-agent agent orchestration patterns. /// -public abstract partial class AgentOrchestration : Orchestratable +public abstract partial class AgentOrchestration { private readonly string _orchestrationRoot; /// - /// Initializes a new instance of the class. + /// Provides a properly formatted name based on the orchestration type (removes generic parameters). + /// + /// The orchestration type + /// + /// Need to respect naming restrictions around and . + /// + protected static string FormatOrchestrationName(Type orchestrationType) => orchestrationType.Name.Split('`').First(); + + /// + /// Initializes a new instance of the class. /// /// 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 orchestrationRoot, IAgentRuntime runtime, params OrchestrationTarget[] members) + protected AgentOrchestration(string orchestrationRoot, params Agent[] members) { - Verify.NotNull(runtime, nameof(runtime)); + Verify.NotNullOrWhiteSpace(orchestrationRoot, nameof(orchestrationRoot)); - this.Runtime = runtime; - this.Members = members; this._orchestrationRoot = orchestrationRoot; + + this.Members = members; } + /// + /// 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; + /// /// Gets the associated logger. /// @@ -41,156 +72,149 @@ protected AgentOrchestration(string orchestrationRoot, IAgentRuntime runtime, pa /// /// Transforms the orchestration input into a source input suitable for processing. /// - public Func>? InputTransform { get; init; } + public OrchestrationInputTransform InputTransform { get; init; } = DefaultTransforms.FromInput; /// /// Transforms the processed result into the final output form. /// - public Func>? ResultTransform { get; init; } + public OrchestrationOutputTransform ResultTransform { get; init; } = DefaultTransforms.ToOutput; /// - /// Gets the list of member targets involved in the orchestration. + /// Optional callback that is invoked for every agent response. /// - protected IReadOnlyList Members { get; } + public OrchestrationResponseCallback? ResponseCallback { get; init; } /// - /// Gets the runtime associated with the orchestration. + /// Gets the list of member targets involved in the orchestration. /// - protected IAgentRuntime Runtime { get; } + protected IReadOnlyList Members { get; } /// /// Initiates processing of the orchestration. /// /// The input message. - /// Optional timeout for the orchestration process. - public async ValueTask> InvokeAsync(TInput input, TimeSpan? timeout = null) + /// The runtime associated with the orchestration. + /// A cancellation token that can be used to cancel the operation. + public async ValueTask> InvokeAsync( + TInput input, + IAgentRuntime runtime, + CancellationToken cancellationToken = default) { - ILogger logger = this.LoggerFactory.CreateLogger(this.GetType()); - Verify.NotNull(input, nameof(input)); TopicId topic = new($"ID_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); + CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + OrchestrationContext context = new(this._orchestrationRoot, topic, this.ResponseCallback, this.LoggerFactory, cancellationToken); + + ILogger logger = this.LoggerFactory.CreateLogger(this.GetType()); + TaskCompletionSource completion = new(); - AgentType orchestrationType = await this.RegisterAsync(topic, completion, handoff: null, this.LoggerFactory).ConfigureAwait(false); + AgentType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); logger.LogOrchestrationInvoke(this._orchestrationRoot, topic); - Task task = this.Runtime.SendMessageAsync(input, orchestrationType).AsTask(); + Task task = runtime.SendMessageAsync(input, orchestrationType, cancellationToken).AsTask(); logger.LogOrchestrationYield(this._orchestrationRoot, topic); - return new OrchestrationResult(this._orchestrationRoot, topic, completion, logger); + return new OrchestrationResult(context, completion, orchestrationCancelSource, logger); } - /// - /// 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._orchestrationRoot}_{suffix}"); - /// /// Initiates processing according to the orchestration pattern. /// + /// The runtime associated with the orchestration. /// The unique identifier for the orchestration session. - /// The input message to be transformed and processed. + /// The input to be transformed and processed. /// The initial agent type used for starting the orchestration. - protected abstract ValueTask StartAsync(TopicId topic, TSource input, AgentType? entryAgent); + protected abstract ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent); /// - /// Registers additional orchestration members and returns the entry agent if available. + /// Orchestration specific registration, including members and returns an optional entry agent. /// - /// 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 runtime targeted for registration. + /// The orchestration context. + /// A registration context. /// The logger to use during registration - 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. - /// - /// The external topic identifier to register with. - /// 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 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, handoff, loggerFactory); - } + /// The entry AgentType for the orchestration, if any. + protected abstract ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger); /// - /// Subscribes the specified agent type to the provided topics. + /// Formats and returns a unique AgentType based on the provided topic and suffix. /// - /// 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) - { - await this.Runtime.AddSubscriptionAsync(new TypeSubscription(topics[index].Type, agentType)).ConfigureAwait(false); - } - } + /// 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._orchestrationRoot}_{suffix}"); /// /// 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. + /// The runtime targeted for registration. + /// The orchestration context. + /// A TaskCompletionSource for the orchestration. /// 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? handoff, ILoggerFactory loggerFactory) + private async ValueTask RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource completion, AgentType? handoff) { - // 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) - { - 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 = - await this.Runtime.RegisterAgentFactoryAsync( - this.FormatAgentType(topic, "Root"), - (agentId, runtime) => - ValueTask.FromResult( - new ResultActor(agentId, runtime, this._orchestrationRoot, this.ResultTransform, completion, loggerFactory.CreateLogger()) - { - CompletionTarget = handoff, - })).ConfigureAwait(false); + ILogger logger = context.LoggerFactory.CreateLogger(this.GetType()); + logger.LogOrchestrationRegistrationStart(context.Orchestration, context.Topic); - // Register orchestration members - AgentType? entryAgent = await this.RegisterMembersAsync(topic, orchestrationFinal, loggerFactory, logger).ConfigureAwait(false); + // Register orchestration + RegistrationContext registrar = new(this.FormatAgentType(context.Topic, "Root"), runtime, context, completion, this.ResultTransform); + AgentType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false); // Register actor for orchestration entry-point AgentType orchestrationEntry = - await this.Runtime.RegisterAgentFactoryAsync( - this.FormatAgentType(topic, "Boot"), + await runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(context.Topic, "Boot"), (agentId, runtime) => ValueTask.FromResult( - new RequestActor(agentId, runtime, this._orchestrationRoot, this.InputTransform, (TSource source) => this.StartAsync(topic, source, entryAgent), completion, loggerFactory.CreateLogger())) + new RequestActor(agentId, runtime, context, this.InputTransform, completion, StartAsync, context.LoggerFactory.CreateLogger())) ).ConfigureAwait(false); - logger.LogOrchestrationRegistrationDone(this._orchestrationRoot, topic); + logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic); return orchestrationEntry; + + ValueTask StartAsync(IEnumerable input) => this.StartAsync(runtime, context.Topic, input, entryAgent); + } + + /// + /// A context used during registration (). + /// + public sealed class RegistrationContext( + AgentType agentType, + IAgentRuntime runtime, + OrchestrationContext context, + TaskCompletionSource completion, + OrchestrationOutputTransform outputTransform) + { + /// + /// Register the final result type. + /// + public async ValueTask RegisterResultTypeAsync(OrchestrationResultTransform resultTransform) + { + // Register actor for final result + return + await runtime.RegisterAgentFactoryAsync( + agentType, + (agentId, runtime) => + ValueTask.FromResult( + new ResultActor( + agentId, + runtime, + context, + resultTransform, + outputTransform, + completion, + context.LoggerFactory.CreateLogger>()))).ConfigureAwait(false); + } } } diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs b/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs index 12a84ba4846d..3c362259b05c 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs +++ b/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; /// /// Describes a team of agents participating in a group chat. /// -public class ChatGroup : Dictionary; +public class ChatGroup : Dictionary; /// /// Extensions for . @@ -20,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.Value.Name)); + 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 ChatGroup team) => string.Join("\n", team.Select(t => $"- {t.Value.Name}: {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/Chat/ChatHandoff.cs b/dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs deleted file mode 100644 index b31994bbf912..000000000000 --- a/dotnet/src/Agents/Orchestration/Chat/ChatHandoff.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index e8f6919294d2..000000000000 --- a/dotnet/src/Agents/Orchestration/Chat/ChatManagerActor.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.Agents.Runtime.Core; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; - -/// -/// An used to manage a . -/// -public abstract class ChatManagerActor : - PatternActor, - IHandle, - IHandle, - IHandle -{ - /// - /// A common description for the manager. - /// - public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task."; - - private readonly AgentType _orchestrationType; - private readonly ChatHandoff _handoff; - - /// - /// 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. - /// 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, ChatHandoff handoff, ILogger? logger = null) - : base(id, runtime, DefaultDescription, logger) - { - this._orchestrationType = orchestrationType; - this._handoff = handoff; - - this.Chat = []; - this.Team = team; - this.GroupTopic = groupTopic; - } - - /// - /// The conversation history with the team. - /// - protected ChatHistory Chat { get; } - - /// - /// The agent type used to identify the orchestration agent. - /// - protected TopicId GroupTopic { get; } - - /// - /// The input task. - /// - protected ChatMessages.InputTask InputTask { get; private set; } = ChatMessages.InputTask.None; - - /// - /// Metadata that describes team of agents being orchestrated. - /// - protected ChatGroup Team { get; } - - /// - /// Message a specific agent, by topic. - /// - protected ValueTask RequestAgentResponseAsync(AgentType agentType, CancellationToken cancellationToken) - { - this.Logger.LogChatManagerSelect(this.Id, 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(); - - /// - /// 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) - { - this.Logger.LogChatManagerInit(this.Id); - this.InputTask = item; - AgentType? agentType = await this.PrepareTaskAsync().ConfigureAwait(false); - if (agentType != null) - { - await this.RequestAgentResponseAsync(agentType.Value, messageContext.CancellationToken).ConfigureAwait(false); - await this.PublishMessageAsync(item.Message.ToGroup(), this.GroupTopic).ConfigureAwait(false); - } - else - { - this.Logger.LogChatManagerTerminate(this.Id); - ChatMessageContent handoff = await this._handoff.ProcessAsync(this.Chat, messageContext.CancellationToken).ConfigureAwait(false); - await this.SendMessageAsync(handoff.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); - } - } - - /// - public async ValueTask HandleAsync(ChatMessages.Group item, MessageContext messageContext) - { - this.Logger.LogChatManagerInvoke(this.Id); - - 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 - { - this.Logger.LogChatManagerTerminate(this.Id); - ChatMessageContent handoff = await this._handoff.ProcessAsync(this.Chat, messageContext.CancellationToken).ConfigureAwait(false); - await this.SendMessageAsync(handoff.ToResult(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); - } - } - - /// - public ValueTask HandleAsync(ChatMessages.Result item, MessageContext messageContext) - { - this.Logger.LogChatManagerResult(this.Id); - return ValueTask.CompletedTask; - } -} diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs index 264e23606cce..fc57ecf236e4 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentActor.cs @@ -19,11 +19,12 @@ internal sealed class ConcurrentActor : AgentActor, IHandle /// The unique identifier of the agent. /// The runtime associated with the agent. + /// The orchestration context. /// An . /// Identifies 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) + public ConcurrentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType resultActor, ILogger? logger = null) + : base(id, runtime, context, agent, logger) { this._handoffActor = resultActor; } @@ -31,12 +32,12 @@ public ConcurrentActor(AgentId id, IAgentRuntime runtime, Agent agent, AgentType /// public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext) { - this.Logger.LogConcurrentAgentInvoke(this.Id, item.Message.Content); + this.Logger.LogConcurrentAgentInvoke(this.Id); - ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); + ChatMessageContent response = await this.InvokeAsync(item.Messages, messageContext.CancellationToken).ConfigureAwait(false); this.Logger.LogConcurrentAgentResult(this.Id, response.Content); - await this.SendMessageAsync(response.ToResult(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(response.AsResultMessage(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs index 1846081dca5b..29088f053ce6 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentMessages.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -7,17 +8,22 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// /// Common messages used by the . /// -public static class ConcurrentMessages +internal static class ConcurrentMessages { + /// + /// An empty message instance as a default. + /// + public static readonly ChatMessageContent Empty = new(); + /// /// The input task for a . /// public sealed class Request { /// - /// The request message. + /// The request input. /// - public ChatMessageContent Message { get; init; } = new(); + public IList Messages { get; init; } = []; } /// @@ -28,26 +34,21 @@ public sealed class Result /// /// The result message. /// - public ChatMessageContent Message { get; init; } = new(); + public ChatMessageContent Message { get; init; } = Empty; } /// /// 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) }; + public static Result AsResultMessage(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) }; + public static Result AsResultMessage(this ChatMessageContent message) => new() { Message = message }; /// - /// Extension method to convert a to a . + /// Extension method to convert a collection of to a . /// - public static Request ToInput(this ChatMessageContent message) => new() { Message = message }; + public static Request AsInputMessage(this IEnumerable messages) => new() { Messages = [.. messages] }; } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs index 902889b83c3e..ed724e5881a1 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -1,9 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Linq; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.Runtime; - namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// @@ -14,20 +10,9 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration /// Initializes a new instance of the class. /// - /// The runtime associated with the orchestration. /// The agents to be orchestrated. - public ConcurrentOrchestration(IAgentRuntime runtime, params OrchestrationTarget[] members) - : base(runtime, members) + public ConcurrentOrchestration(params Agent[] members) + : base(members) { - 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 c6c4a403ef39..8dd15a08d72f 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.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.Concurrent; @@ -11,69 +13,57 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// An orchestration that broadcasts the input message to each agent. /// public class ConcurrentOrchestration - : AgentOrchestration + : 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(OrchestrationName, runtime, agents) + public ConcurrentOrchestration(params Agent[] agents) + : base(OrchestrationName, agents) { + //this.OutputTransform= (message) => // %%% ??? } /// - protected override ValueTask StartAsync(TopicId topic, ConcurrentMessages.Request input, AgentType? entryAgent) + protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) { - return this.Runtime.PublishMessageAsync(input, topic); + return runtime.PublishMessageAsync(input.AsInputMessage(), topic); } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { + AgentType outputType = await registrar.RegisterResultTypeAsync(response => response[0].Message).ConfigureAwait(false); // %%% HACK + // Register result actor - AgentType resultType = this.FormatAgentType(topic, "Results"); - await this.Runtime.RegisterAgentFactoryAsync( + AgentType resultType = this.FormatAgentType(context.Topic, "Results"); + await runtime.RegisterAgentFactoryAsync( resultType, (agentId, runtime) => ValueTask.FromResult( - new ConcurrentResultActor(agentId, runtime, orchestrationType, this.Members.Count, loggerFactory.CreateLogger()))).ConfigureAwait(false); + new ConcurrentResultActor(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, resultType, "RESULTS"); // Register member actors - All agents respond to the same message. int agentCount = 0; - foreach (OrchestrationTarget member in this.Members) + foreach (Agent agent 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, resultType, loggerFactory).ConfigureAwait(false); - } + AgentType agentType = + await runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(context.Topic, $"Agent_{agentCount}"), + (agentId, runtime) => + ValueTask.FromResult(new ConcurrentActor(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); + logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", agentCount); - await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); + await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); } return null; - - ValueTask RegisterAgentAsync(Agent agent) - { - return - this.Runtime.RegisterAgentFactoryAsync( - this.FormatAgentType(topic, $"Agent_{agentCount}"), - (agentId, runtime) => - ValueTask.FromResult(new ConcurrentActor(agentId, runtime, agent, resultType, loggerFactory.CreateLogger()))); - } } } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs index 8af2b3957066..eb4e1f2994fe 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentResultActor.cs @@ -13,7 +13,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// Actor for capturing each message. /// internal sealed class ConcurrentResultActor : - PatternActor, + OrchestrationActor, IHandle { private readonly ConcurrentQueue _results; @@ -26,16 +26,18 @@ internal sealed class ConcurrentResultActor : /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// The orchestration context. /// 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, + OrchestrationContext context, AgentType orchestrationType, int expectedCount, ILogger logger) - : base(id, runtime, "Captures the results of the ConcurrentOrchestration", logger) + : base(id, runtime, context, "Captures the results of the ConcurrentOrchestration", logger) { this._orchestrationType = orchestrationType; this._expectedCount = expectedCount; diff --git a/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs b/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs index 10b0dcbeb007..033dd1e1059c 100644 --- a/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs +++ b/dotnet/src/Agents/Orchestration/Extensions/RuntimeExtensions.cs @@ -3,18 +3,19 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.Agents.Runtime; +using Microsoft.SemanticKernel.Agents.Runtime.Core; namespace Microsoft.SemanticKernel.Agents.Orchestration.Extensions; /// /// Extension methods for . /// -internal static class RuntimeExtensions +public 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) + internal 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) @@ -22,4 +23,18 @@ public static async ValueTask SendMessageAsync(this IAgentRuntime runtime, objec await runtime.SendMessageAsync(message, agentId.Value, sender: null, messageId: null, cancellationToken).ConfigureAwait(false); } } + + /// + /// Subscribes the specified agent type to the provided topics. + /// + /// The runtime for managing the subscription. + /// The agent type to subscribe. + /// A variable list of topics for subscription. + public static async Task SubscribeAsync(this IAgentRuntime runtime, string agentType, params TopicId[] topics) + { + for (int index = 0; index < topics.Length; ++index) + { + await runtime.AddSubscriptionAsync(new TypeSubscription(topics[index].Type, agentType)).ConfigureAwait(false); + } + } } diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs similarity index 54% rename from dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs rename to dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs index 84061a0e3ebd..320a9939d907 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatAgentActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs @@ -3,55 +3,52 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.SemanticKernel.Agents.Runtime.Core; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// An used with the . /// -internal sealed class ChatAgentActor : +internal sealed class GroupChatAgentActor : AgentActor, - IHandle, - IHandle, - IHandle + IHandle, + IHandle, + IHandle { private readonly List _cache; - 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 orchestration context. /// 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) - : base(id, runtime, agent, noThread: false, logger) + public GroupChatAgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null) + : base(id, runtime, context, agent, logger) { this._cache = []; - this._groupTopic = groupTopic; } /// - public ValueTask HandleAsync(ChatMessages.Group item, MessageContext messageContext) + public ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext) { - this._cache.Add(item.Message); + this._cache.AddRange(item.Messages); return ValueTask.CompletedTask; } /// - public async ValueTask HandleAsync(ChatMessages.Reset item, MessageContext messageContext) + public async ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext) { await this.DeleteThreadAsync(messageContext.CancellationToken).ConfigureAwait(false); } /// - public async ValueTask HandleAsync(ChatMessages.Speak item, MessageContext messageContext) + public async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext) { this.Logger.LogChatAgentInvoke(this.Id); @@ -60,6 +57,6 @@ public async ValueTask HandleAsync(ChatMessages.Speak item, MessageContext messa this.Logger.LogChatAgentResult(this.Id, response.Content); this._cache.Clear(); - await this.PublishMessageAsync(response.ToGroup(), this._groupTopic).ConfigureAwait(false); + await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs deleted file mode 100644 index bb4dea05f763..000000000000 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatContext.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs new file mode 100644 index 000000000000..c3b3b3663fb3 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// Represents the result of a group chat manager operation, including a value and a reason. +/// +/// The type of the value returned by the operation. +/// The value returned by the operation. +public sealed class GroupChatManagerResult(TValue value) +{ + /// + /// The reason for the result, providing additional context or explanation. + /// + public string Reason { get; init; } = string.Empty; + + /// + /// The value returned by the group chat manager operation. + /// + public TValue Value { get; } = value; +} + +/// +/// A manager that manages the flow of a group chat. +/// +public abstract class GroupChatManager +{ + private int _invocationsCount; + + /// + /// Initializes a new instance of the class. + /// + protected GroupChatManager() { } + + /// + /// Gets the number of times the group chat manager has been invoked. + /// + public int InvocationsCount => this._invocationsCount; + + /// + /// Gets or sets the maximum number of invocations allowed for the group chat manager. + /// + public int MaximumInvocations { get; init; } = int.MaxValue; + + /// + /// Gets or sets the callback to be invoked for interactive operations. + /// + public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } + + /// + /// Filters the results of the group chat based on the provided chat history. + /// + /// The chat history to filter. + /// A cancellation token that can be used to cancel the operation. + /// A containing the filtered result as a string. + public abstract ValueTask> FilterResults(ChatHistory history, CancellationToken cancellationToken = default); + + /// + /// Selects the next agent to participate in the group chat based on the provided chat history and team. + /// + /// The chat history to consider. + /// The group of agents participating in the chat. + /// A cancellation token that can be used to cancel the operation. + /// A containing the identifier of the next agent as a string. + public abstract ValueTask> SelectNextAgent(ChatHistory history, ChatGroup team, CancellationToken cancellationToken = default); + + /// + /// Determines whether user input should be requested based on the provided chat history. + /// + /// The chat history to consider. + /// A cancellation token that can be used to cancel the operation. + /// A indicating whether user input should be requested. + public abstract ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default); + + /// + /// Determines whether the group chat should be terminated based on the provided chat history and invocation count. + /// + /// The chat history to consider. + /// A cancellation token that can be used to cancel the operation. + /// A indicating whether the chat should be terminated. + public virtual ValueTask> ShouldTerminate(ChatHistory history, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref this._invocationsCount); + + if (this.InvocationsCount > this.MaximumInvocations) + { + return ValueTask.FromResult(new GroupChatManagerResult(true) { Reason = "Maximum number of invocations reached." }); + } + + return ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "Maximum number of invocations has not been reached." }); + } +} diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index a2e52715c6f8..12d7f126bb0c 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -4,44 +4,98 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Runtime; +using Microsoft.SemanticKernel.Agents.Runtime.Core; +using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// -/// An used to manage a . +/// An used to manage a . /// -internal sealed class GroupChatManagerActor : ChatManagerActor +internal sealed class GroupChatManagerActor : + OrchestrationActor, + IHandle, + IHandle { - private readonly GroupChatStrategy _strategy; + /// + /// A common description for the manager. + /// + public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task."; + + private readonly AgentType _orchestrationType; + private readonly GroupChatManager _manager; + private readonly ChatHistory _chat; + private readonly ChatGroup _team; /// /// Initializes a new instance of the class. /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// The orchestration context. + /// The manages the flow of the group-chat. /// 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, GroupChatStrategy strategy, ChatHandoff handoff, ILogger? logger = null) - : base(id, runtime, team, orchestrationType, groupTopic, handoff, logger) + public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, ChatGroup team, AgentType orchestrationType, ILogger? logger = null) + : base(id, runtime, context, DefaultDescription, logger) { - this._strategy = strategy; + this._chat = []; + this._manager = manager; + this._orchestrationType = orchestrationType; + this._team = team; } /// - protected override Task PrepareTaskAsync() + public async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext) { - return this.SelectAgentAsync(); + this.Logger.LogChatManagerInit(this.Id); + + this._chat.AddRange(item.Messages); + + await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false); + + await this.ManageAsync(messageContext).ConfigureAwait(false); } /// - protected override async Task SelectAgentAsync() + public async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext) { - GroupChatContext context = new(this.Team, this.Chat); - await this._strategy.SelectAsync(context).ConfigureAwait(false); - return context.HasSelection ? context.Selection! : (AgentType?)null; + this.Logger.LogChatManagerInvoke(this.Id); + + this._chat.AddRange(item.Messages); + + await this.ManageAsync(messageContext).ConfigureAwait(false); + } + + private async ValueTask ManageAsync(MessageContext messageContext) + { + if (this._manager.InteractiveCallback != null) + { + GroupChatManagerResult inputResult = await this._manager.ShouldRequestUserInput(this._chat, messageContext.CancellationToken).ConfigureAwait(false); + this.Logger.LogChatManagerInput(this.Id, inputResult.Value, inputResult.Reason); + if (inputResult.Value) + { + ChatMessageContent input = await this._manager.InteractiveCallback.Invoke().ConfigureAwait(false); + this.Logger.LogChatManagerUserInput(this.Id, input.Content); + this._chat.Add(input); + await this.PublishMessageAsync(input.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false); + } + } + + GroupChatManagerResult terminateResult = await this._manager.ShouldTerminate(this._chat, messageContext.CancellationToken).ConfigureAwait(false); + this.Logger.LogChatManagerTerminate(this.Id, terminateResult.Value, terminateResult.Reason); + if (terminateResult.Value) + { + GroupChatManagerResult filterResult = await this._manager.FilterResults(this._chat, messageContext.CancellationToken).ConfigureAwait(false); + this.Logger.LogChatManagerResult(this.Id, filterResult.Value, filterResult.Reason); + await this.SendMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false); + return; + } + + GroupChatManagerResult selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, messageContext.CancellationToken).ConfigureAwait(false); + AgentType selectionType = this._team[selectionResult.Value].Type; + this.Logger.LogChatManagerSelect(this.Id, selectionType); + await this.SendMessageAsync(new GroupChatMessages.Speak(), selectionType, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs similarity index 60% rename from dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs rename to dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs index bdef421968b0..aaf084b700c9 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatMessages.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using System.Collections.Generic; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// Common messages used for agent chat patterns. /// -public static class ChatMessages +public static class GroupChatMessages { /// /// An empty message instance as a default. @@ -13,18 +16,18 @@ public static class ChatMessages internal static readonly ChatMessageContent Empty = new(); /// - /// Broadcast a message to all . + /// Broadcast a message to all . /// public sealed class Group { /// /// The chat message being broadcast. /// - public ChatMessageContent Message { get; init; } = Empty; + public IEnumerable Messages { get; init; } = []; } /// - /// Reset/clear the conversation history for all . + /// Reset/clear the conversation history for all . /// public sealed class Reset; @@ -40,7 +43,7 @@ public sealed class Result } /// - /// Signal a to respond. + /// Signal a to respond. /// public sealed class Speak; @@ -57,21 +60,26 @@ public sealed class InputTask /// /// The input that defines the task goal. /// - public ChatMessageContent Message { get; init; } = Empty; + public IEnumerable Messages { get; init; } = []; } /// /// Extension method to convert a to a . /// - public static Group ToGroup(this ChatMessageContent message) => new() { Message = message }; + public static Group AsGroupMessage(this ChatMessageContent message) => new() { Messages = [message] }; + + /// + /// Extension method to convert a to a . + /// + public static Group AsGroupMessage(this IEnumerable messages) => new() { Messages = messages }; /// /// Extension method to convert a to a . /// - public static Result ToResult(this ChatMessageContent message) => new() { Message = message }; + public static InputTask AsInputTaskMessage(this IEnumerable messages) => new() { Messages = messages }; /// /// Extension method to convert a to a . /// - public static InputTask ToInputTask(this ChatMessageContent message) => new() { Message = message }; + public static Result AsResultMessage(this string text) => new() { Message = new(AuthorRole.Assistant, text) }; } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs index c4730a133014..ca7dc7c9ff90 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.String.cs @@ -1,10 +1,5 @@ // 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.GroupChat; /// @@ -15,13 +10,10 @@ public sealed class GroupChatOrchestration : GroupChatOrchestration /// Initializes a new instance of the class. /// - /// The runtime associated with the orchestration. - /// The strategy that determines how the chat shall proceed. + /// The manages the flow of the group-chat. /// The agents to be orchestrated. - public GroupChatOrchestration(IAgentRuntime runtime, GroupChatStrategy strategy, params OrchestrationTarget[] members) - : base(runtime, strategy, members) + public GroupChatOrchestration(GroupChatManager manager, params Agent[] members) + : base(manager, 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 726477c7c755..0f19517385e1 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Linq; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; @@ -13,91 +14,74 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// An orchestration that coordinates a group-chat. /// public class GroupChatOrchestration : - AgentOrchestration + AgentOrchestration { internal const string DefaultAgentDescription = "A helpful agent."; - internal static readonly string OrchestrationName = typeof(GroupChatOrchestration<,>).Name.Split('`').First(); + internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(GroupChatOrchestration<,>)); - private readonly GroupChatStrategy _strategy; + private readonly GroupChatManager _manager; /// /// Initializes a new instance of the class. /// - /// The runtime associated with the orchestration. - /// The strategy that determines how the chat shall proceed. + /// The manages the flow of the group-chat. /// The agents participating in the orchestration. - public GroupChatOrchestration(IAgentRuntime runtime, GroupChatStrategy strategy, params OrchestrationTarget[] agents) - : base(OrchestrationName, runtime, agents) + public GroupChatOrchestration(GroupChatManager manager, params Agent[] agents) + : base(OrchestrationName, agents) { - Verify.NotNull(strategy, nameof(strategy)); + Verify.NotNull(manager, nameof(manager)); - this._strategy = strategy; + this._manager = manager; } - /// - /// 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) + protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) { - return this.Runtime.SendMessageAsync(input, entryAgent!.Value); + if (!entryAgent.HasValue) + { + throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); + } + return runtime.SendMessageAsync(input.AsInputTaskMessage(), entryAgent.Value); } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType managerType = this.FormatAgentType(topic, "Manager"); + AgentType outputType = await registrar.RegisterResultTypeAsync(response => response.Message).ConfigureAwait(false); int agentCount = 0; ChatGroup team = []; - foreach (OrchestrationTarget member in this.Members) + foreach (Agent agent in this.Members) { ++agentCount; + AgentType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false); + string name = agent.Name ?? agent.Id ?? agentType; + string? description = agent.Description; - 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] = (name ?? memberType, description ?? DefaultAgentDescription); - - logger.LogRegisterActor(OrchestrationName, memberType, "MEMBER", agentCount); - - await this.SubscribeAsync(memberType, topic).ConfigureAwait(false); + team[name] = (agentType, description ?? DefaultAgentDescription); + + logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", agentCount); + + await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); } - await this.Runtime.RegisterAgentFactoryAsync( - managerType, - (agentId, runtime) => - ValueTask.FromResult( - new GroupChatManagerActor(agentId, runtime, team, orchestrationType, topic, this._strategy, this.Handoff, loggerFactory.CreateLogger()))).ConfigureAwait(false); + AgentType managerType = + await runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(context.Topic, "Manager"), + (agentId, runtime) => + ValueTask.FromResult( + new GroupChatManagerActor(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, managerType, "MANAGER"); - await this.SubscribeAsync(managerType, topic).ConfigureAwait(false); + await runtime.SubscribeAsync(managerType, context.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()))); - } + ValueTask RegisterAgentAsync(Agent agent, int agentCount) => + runtime.RegisterAgentFactoryAsync( + this.FormatAgentType(context.Topic, $"Agent_{agentCount}"), + (agentId, runtime) => + ValueTask.FromResult(new GroupChatAgentActor(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger()))); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs deleted file mode 100644 index 96a9b9a87489..000000000000 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatStrategy.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs new file mode 100644 index 000000000000..2010a3f3dcaf --- /dev/null +++ b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; + +/// +/// A that selects agents in a round-robin fashion. +/// +/// +/// Subclass this class to customize filter and user interaction behavior. +/// +public class RoundRobinGroupChatManager : GroupChatManager +{ + private int _currentAgentIndex; + + /// + public override ValueTask> FilterResults(ChatHistory history, CancellationToken cancellationToken = default) => + ValueTask.FromResult(new GroupChatManagerResult(history.LastOrDefault()?.Content ?? string.Empty) { Reason = "Default result filter provides the final chat message." }); + + /// + public override ValueTask> SelectNextAgent(ChatHistory history, ChatGroup team, CancellationToken cancellationToken = default) + { + string nextAgent = team.Skip(this._currentAgentIndex).First().Key; + this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count; + return ValueTask.FromResult(new GroupChatManagerResult(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" }); + } + + /// + public override ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default) => + ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "The default round-robin group chat manager does not request user input." }); +} diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index a322fee08b07..853380857f9e 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -16,12 +16,12 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// internal sealed class HandoffActor : AgentActor, + IHandle, IHandle, IHandle { private readonly HandoffLookup _handoffs; private readonly AgentType _resultHandoff; - private readonly TopicId _groupTopic; private readonly List _cache; /// @@ -29,16 +29,15 @@ internal sealed class HandoffActor : /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// The orchestration context. /// 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, logger) + public HandoffActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, HandoffLookup handoffs, AgentType resultHandoff, ILogger? logger = null) + : base(id, runtime, context, agent, logger) { this._cache = []; - this._groupTopic = groupTopic; this._handoffs = handoffs; this._resultHandoff = resultHandoff; } @@ -63,18 +62,16 @@ public HandoffActor(AgentId id, IAgentRuntime runtime, Agent agent, HandoffLooku } /// - public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext) + public ValueTask HandleAsync(HandoffMessages.InputTask 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); + this._cache.AddRange(item.Messages); - await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this._groupTopic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false); + return this.HandleAsync(messageContext.CancellationToken); } + /// + public ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext) => this.HandleAsync(messageContext.CancellationToken); + /// public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext) { @@ -83,6 +80,18 @@ public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messa return ValueTask.CompletedTask; } + private async ValueTask HandleAsync(CancellationToken cancellationToken) + { + this.Logger.LogHandoffAgentInvoke(this.Id); + + ChatMessageContent response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false); + this._cache.Clear(); + + this.Logger.LogHandoffAgentResult(this.Id, response.Content); + + await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); + } + private KernelPlugin CreateHandoffPlugin() { return KernelPluginFactory.CreateFromFunctions(HandoffInvocationFilter.HandoffPlugin, CreateHandoffFunctions()); diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs index 14eec13bef2b..805bfcd08dac 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffMessages.cs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; + namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// /// A message that describes the input task and captures results for a . /// -public sealed class HandoffMessages +internal static class HandoffMessages { /// /// An empty message instance as a default. @@ -18,9 +20,9 @@ public sealed class HandoffMessages public sealed class InputTask { /// - /// The orchestration input message. + /// The orchestration input messages. /// - public ChatMessageContent Message { get; init; } = Empty; + public IList Messages { get; init; } = []; } /// @@ -49,4 +51,14 @@ public sealed class Response /// public ChatMessageContent Message { get; init; } = Empty; } + + /// + /// Extension method to convert a to a . + /// + public static InputTask AsInputTaskMessage(this IEnumerable messages) => new() { Messages = [.. messages] }; + + /// + /// Extension method to convert a to a . + /// + public static Result AsResultMessage(this ChatMessageContent message) => new() { Message = message }; } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs index 6ac4f14bdfdd..e5c79a477961 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs @@ -1,9 +1,6 @@ // 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; @@ -16,13 +13,10 @@ 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) + public HandoffOrchestration(Dictionary handoffs, params Agent[] members) + : base(handoffs, members) { - this.InputTransform = (string input) => ValueTask.FromResult(new HandoffMessages.InputTask { 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 index 18a11fa92762..993ebac01629 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -14,7 +14,7 @@ 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 +public class HandoffOrchestration : AgentOrchestration { internal static readonly string OrchestrationName = typeof(HandoffOrchestration<,>).Name.Split('`').First(); @@ -23,52 +23,48 @@ public class HandoffOrchestration : AgentOrchestration /// 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) + public HandoffOrchestration(Dictionary handoffs, params Agent[] agents) + : base(OrchestrationName, agents) { this._handoffs = handoffs; } /// - protected override async ValueTask StartAsync(TopicId topic, HandoffMessages.InputTask input, AgentType? entryAgent) + protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable 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); + await runtime.PublishMessageAsync(input.AsInputTaskMessage(), topic).ConfigureAwait(false); + await runtime.SendMessageAsync(new HandoffMessages.Request(), entryAgent.Value).ConfigureAwait(false); } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { + AgentType outputType = await registrar.RegisterResultTypeAsync(response => response.Message).ConfigureAwait(false); + // Each agent handsoff its result to the next agent. Dictionary agentMap = []; Dictionary handoffMap = []; - AgentType nextAgent = orchestrationType; + AgentType agentType = outputType; 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)) // TODO: IS POSSIBLE ? - //{ - // nextAgent = await orchestration.RegisterAsync(topic, nextAgent, loggerFactory).ConfigureAwait(false); - //} + Agent agent = this.Members[index]; + HandoffLookup map = []; + handoffMap[agent.Name ?? agent.Id] = map; + agentType = + await runtime.RegisterAgentFactoryAsync( + this.GetAgentType(context.Topic, index), + (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, context, agent, map, outputType, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + agentMap[agent.Name ?? agent.Id] = agentType; - await this.SubscribeAsync(nextAgent, topic).ConfigureAwait(false); + await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, nextAgent, "MEMBER", index + 1); + logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", index + 1); } // Complete the handoff model @@ -83,15 +79,7 @@ protected override async ValueTask StartAsync(TopicId topic, HandoffMessages.Inp } } - 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()))); - } + return agentType; } private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); diff --git a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs index 922e15f56198..043d24934b12 100644 --- a/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/AgentOrchestrationLogMessages.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// Extensions for logging . +/// Extensions for logging . /// /// /// This extension uses the to diff --git a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs index 8a27196136d9..0f24fa1c2939 100644 --- a/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/ConcurrentOrchestrationLogMessages.cs @@ -20,11 +20,10 @@ internal static partial class ConcurrentOrchestrationLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "REQUEST Concurrent agent [{AgentId}]: {Message}")] + Message = "REQUEST Concurrent agent [{AgentId}]")] public static partial void LogConcurrentAgentInvoke( this ILogger logger, - AgentId agentId, - string? message); + AgentId agentId); [LoggerMessage( EventId = 0, diff --git a/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs similarity index 65% rename from dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs rename to dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs index f87ed6bd9aac..82c86ddc5cff 100644 --- a/dotnet/src/Agents/Orchestration/Logging/ChatOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/GroupChatOrchestrationLogMessages.cs @@ -15,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// generate logging code at compile time to achieve optimized code. /// [ExcludeFromCodeCoverage] -internal static partial class ChatOrchestrationLogMessages +internal static partial class GroupChatOrchestrationLogMessages { [LoggerMessage( EventId = 0, @@ -36,7 +36,7 @@ public static partial void LogChatAgentResult( [LoggerMessage( EventId = 0, - Level = LogLevel.Trace, + Level = LogLevel.Debug, Message = "CHAT MANAGER initialized [{AgentId}]")] public static partial void LogChatManagerInit( this ILogger logger, @@ -44,7 +44,7 @@ public static partial void LogChatManagerInit( [LoggerMessage( EventId = 0, - Level = LogLevel.Trace, + Level = LogLevel.Debug, Message = "CHAT MANAGER invoked [{AgentId}]")] public static partial void LogChatManagerInvoke( this ILogger logger, @@ -52,26 +52,49 @@ public static partial void LogChatManagerInvoke( [LoggerMessage( EventId = 0, - Level = LogLevel.Trace, - Message = "CHAT MANAGER terminating [{AgentId}]")] + Level = LogLevel.Debug, + Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")] public static partial void LogChatManagerTerminate( this ILogger logger, - AgentId agentId); + AgentId agentId, + bool result, + string reason); [LoggerMessage( EventId = 0, - Level = LogLevel.Trace, - Message = "CHAT MANAGER answer [{AgentId}]")] + Level = LogLevel.Debug, + Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")] + public static partial void LogChatManagerSelect( + this ILogger logger, + AgentId agentId, + AgentType nextAgent); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")] public static partial void LogChatManagerResult( this ILogger logger, - AgentId agentId); + AgentId agentId, + string result, + string reason); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")] + public static partial void LogChatManagerInput( + this ILogger logger, + AgentId agentId, + bool result, + string reason); [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")] - public static partial void LogChatManagerSelect( + Message = "CHAT AGENT user-input [{AgentId}]: {Message}")] + public static partial void LogChatManagerUserInput( this ILogger logger, AgentId agentId, - AgentType nextAgent); + string? message); } diff --git a/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs index dc58167c4b3a..fcd902b5aaf1 100644 --- a/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/OrchestrationResultLogMessages.cs @@ -29,7 +29,7 @@ public static partial void LogOrchestrationResultAwait( TopicId topic); /// - /// Logs awaiting the orchestration. + /// Logs timeout while awaiting the orchestration. /// [LoggerMessage( EventId = 0, @@ -41,7 +41,19 @@ public static partial void LogOrchestrationResultTimeout( TopicId topic); /// - /// Logs awaiting the orchestration. + /// Logs cancelled the orchestration. + /// + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "CANCELLED {Orchestration}: {Topic}")] + public static partial void LogOrchestrationResultCancelled( + this ILogger logger, + string orchestration, + TopicId topic); + + /// + /// Logs the awaited the orchestration has completed. /// [LoggerMessage( EventId = 0, diff --git a/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs index abd85de884d3..c82d6dab4186 100644 --- a/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/SequentialOrchestrationLogMessages.cs @@ -20,11 +20,10 @@ internal static partial class SequentialOrchestrationLogMessages [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "REQUEST Sequential agent [{AgentId}]: {Message}")] + Message = "REQUEST Sequential agent [{AgentId}]")] public static partial void LogSequentialAgentInvoke( this ILogger logger, - AgentId agentId, - string? message); + AgentId agentId); [LoggerMessage( EventId = 0, diff --git a/dotnet/src/Agents/Orchestration/Orchestratable.cs b/dotnet/src/Agents/Orchestration/Orchestratable.cs deleted file mode 100644 index 4c9039abde14..000000000000 --- a/dotnet/src/Agents/Orchestration/Orchestratable.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Runtime; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// Common protocol for so it -/// can be utlized by an another 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. - /// - /// The topic identifier to be used for 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? handoff, ILoggerFactory loggerFactory); -} diff --git a/dotnet/src/Agents/Orchestration/PatternActor.cs b/dotnet/src/Agents/Orchestration/OrchestrationActor.cs similarity index 71% rename from dotnet/src/Agents/Orchestration/PatternActor.cs rename to dotnet/src/Agents/Orchestration/OrchestrationActor.cs index 2b5eb3b2deb9..fa3ee1f15c23 100644 --- a/dotnet/src/Agents/Orchestration/PatternActor.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationActor.cs @@ -9,18 +9,24 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// An actor that represents an . +/// An actor that participates in an orchestration. /// -public abstract class PatternActor : BaseAgent +public abstract class OrchestrationActor : BaseAgent { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected PatternActor(AgentId id, IAgentRuntime runtime, string description, ILogger? logger = null) + protected OrchestrationActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, string description, ILogger? logger = null) : base(id, runtime, description, logger) { + this.Context = context; } + /// + /// The orchestration context. + /// + protected OrchestrationContext Context { get; } + /// /// Sends a message to a specified recipient agent-type through the runtime. /// diff --git a/dotnet/src/Agents/Orchestration/OrchestrationContext.cs b/dotnet/src/Agents/Orchestration/OrchestrationContext.cs new file mode 100644 index 000000000000..354ae12840fd --- /dev/null +++ b/dotnet/src/Agents/Orchestration/OrchestrationContext.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.Runtime; + +namespace Microsoft.SemanticKernel.Agents.Orchestration; + +/// +/// Provides contextual information for an orchestration operation, including topic, cancellation, logging, and response callback. +/// +public sealed class OrchestrationContext +{ + internal OrchestrationContext( + string orchestration, + TopicId topic, + OrchestrationResponseCallback? responseCallback, + ILoggerFactory loggerFactory, + CancellationToken cancellation) + { + this.Orchestration = orchestration; + this.Topic = topic; + this.ResponseCallback = responseCallback; + this.LoggerFactory = loggerFactory; + this.Cancellation = cancellation; + } + + /// + /// Gets the name or identifier of the orchestration. + /// + public string Orchestration { get; } + + /// + /// Gets the identifier associated with orchestration topic. + /// + /// + /// All orchestration actors are subscribed to this topic. + /// + public TopicId Topic { get; } + + /// + /// Gets the cancellation token that can be used to observe cancellation requests for the orchestration. + /// + public CancellationToken Cancellation { get; } + + /// + /// Gets the associated logger factory for creating loggers within the orchestration context. + /// + public ILoggerFactory LoggerFactory { get; } + + /// + /// Optional callback that is invoked for every agent response. + /// + public OrchestrationResponseCallback? ResponseCallback { get; } +} diff --git a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs index 41fa09cc22c5..d25ddc8bf953 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Runtime; @@ -12,24 +13,40 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// This class encapsulates the asynchronous completion of an orchestration process. /// /// The type of the value produced by the orchestration. -public sealed class OrchestrationResult +public sealed class OrchestrationResult : IDisposable { - private readonly string _orchestration; + private readonly OrchestrationContext _context; + private readonly CancellationTokenSource _cancelSource; private readonly TaskCompletionSource _completion; private readonly ILogger _logger; + private bool _isDisposed; - internal OrchestrationResult(string orchestration, TopicId topic, TaskCompletionSource completion, ILogger logger) + internal OrchestrationResult(OrchestrationContext context, TaskCompletionSource completion, CancellationTokenSource orchestrationCancelSource, ILogger logger) { - this._orchestration = orchestration; - this.Topic = topic; + this._cancelSource = orchestrationCancelSource; + this._context = context; this._completion = completion; this._logger = logger; } + /// + /// Releases all resources used by the instance. + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Gets the orchestration name associated with this orchestration result. + /// + public string Orchestration => this._context.Orchestration; + /// /// Gets the topic identifier associated with this orchestration result. /// - public TopicId Topic { get; } + public TopicId Topic => this._context.Topic; /// /// Asynchronously retrieves the orchestration result value. @@ -37,24 +54,61 @@ internal OrchestrationResult(string orchestration, TopicId topic, TaskCompletion /// if the orchestration does not complete within the allotted time. /// /// An optional representing the maximum wait duration. + /// A cancellation token that can be used to cancel the operation. /// A representing the result of the orchestration. + /// Thrown if this instance has been disposed. /// Thrown if the orchestration does not complete within the specified timeout period. - public async ValueTask GetValueAsync(TimeSpan? timeout = null) + public async ValueTask GetValueAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - this._logger.LogOrchestrationResultAwait(this._orchestration, this.Topic); + ObjectDisposedException.ThrowIf(this._isDisposed, this); + + this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic); if (timeout.HasValue) { Task[] tasks = { this._completion.Task }; if (!Task.WaitAll(tasks, timeout.Value)) { - this._logger.LogOrchestrationResultTimeout(this._orchestration, this.Topic); + this._logger.LogOrchestrationResultTimeout(this.Orchestration, this.Topic); throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout})."); } } - this._logger.LogOrchestrationResultComplete(this._orchestration, this.Topic); + this._logger.LogOrchestrationResultComplete(this.Orchestration, this.Topic); return await this._completion.Task.ConfigureAwait(false); } + + /// + /// Cancel the orchestration associated with this result. + /// + /// A cancellation token that can be used to cancel the operation. + /// Thrown if this instance has been disposed. + /// + /// Cancellation is not expected to immediately halt the orchestration. Messages that + /// are already in-flight may still be processed. + /// + public ValueTask CancelAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this._isDisposed, this); + + this._logger.LogOrchestrationResultCancelled(this.Orchestration, this.Topic); + this._cancelSource.Cancel(); + this._completion.SetCanceled(cancellationToken); + + return ValueTask.CompletedTask; + } + + private void Dispose(bool disposing) + { + if (!this._isDisposed) + { + if (disposing) + { + this._cancelSource.Dispose(); + } + + this._isDisposed = true; + } + } } diff --git a/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs b/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs deleted file mode 100644 index f9b549944c81..000000000000 --- a/dotnet/src/Agents/Orchestration/OrchestrationTarget.cs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Agents.Orchestration; - -/// -/// Represents a target for orchestration operations. This target can be either an Agent or an Orchestratable object. -/// -public enum OrchestrationTargetType -{ - /// - /// Target is an . - /// - Agent, - - /// - /// Target is an object. - /// - Orchestratable, -} - -/// -/// Encapsulates the target entity for orchestration, which may be an Agent or an Orchestratable object. -/// -public readonly struct OrchestrationTarget : IEquatable -{ - /// - /// Creates an orchestration target from the specified . - /// - /// The agent to convert to an orchestration target. - public static implicit operator OrchestrationTarget(Agent target) => new(target); - - /// - /// 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; - this.TargetType = OrchestrationTargetType.Orchestratable; - } - - /// - /// Gets the associated if this target represents an agent; otherwise, null. - /// - public Agent? Agent { get; } - - /// - /// Gets the associated object if this target represents an orchestratable entity; otherwise, null. - /// - public Orchestratable? Orchestration { get; } - - /// - /// 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 && obj is OrchestrationTarget target && this.Equals(target); - } - - /// - /// 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; - } - - /// - public override readonly int GetHashCode() - { - return HashCode.Combine(this.Agent?.GetHashCode() ?? 0, this.Orchestration?.GetHashCode() ?? 0); - } - - /// - /// 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); - } - - /// - /// 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/Sequential/SequentialActor.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs index 214754e7287b..8af67d287176 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialActor.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Runtime; @@ -10,7 +11,10 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// /// An actor used with the . /// -internal sealed class SequentialActor : AgentActor, IHandle +internal sealed class SequentialActor : + AgentActor, + IHandle, + IHandle { private readonly AgentType _nextAgent; @@ -19,24 +23,39 @@ internal sealed class SequentialActor : AgentActor, IHandle /// /// The unique identifier of the agent. /// The runtime associated with the agent. + /// The orchestration context. /// 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) - : base(id, runtime, agent, noThread: true, logger) + public SequentialActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType nextAgent, ILogger? logger = null) + : base(id, runtime, context, agent, logger) { + logger?.LogInformation("ACTOR {ActorId} {NextAgent}", this.Id, nextAgent); this._nextAgent = nextAgent; } /// - public async ValueTask HandleAsync(SequentialMessage item, MessageContext messageContext) + public async ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext) { - this.Logger.LogSequentialAgentInvoke(this.Id, item.Message.Content); + await this.InvokeAgentAsync(item.Messages, messageContext).ConfigureAwait(false); + } + + /// + public async ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext) + { + await this.InvokeAgentAsync([item.Message], messageContext).ConfigureAwait(false); + } + + private async ValueTask InvokeAgentAsync(IList input, MessageContext messageContext) + { + this.Logger.LogInformation("INVOKE {ActorId} {NextAgent}", this.Id, this._nextAgent); + + this.Logger.LogSequentialAgentInvoke(this.Id); - ChatMessageContent response = await this.InvokeAsync(item.Message, messageContext.CancellationToken).ConfigureAwait(false); + ChatMessageContent response = await this.InvokeAsync(input, messageContext.CancellationToken).ConfigureAwait(false); this.Logger.LogSequentialAgentResult(this.Id, response.Content); - await this.SendMessageAsync(SequentialMessage.FromChat(response), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); + await this.SendMessageAsync(response.AsResponseMessage(), this._nextAgent, messageContext.CancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs deleted file mode 100644 index fcce86311843..000000000000 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialMessage.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; - -/// -/// A message that describes the input task and captures results for a . -/// -public sealed class SequentialMessage -{ - /// - /// The input task. - /// - public ChatMessageContent Message { get; init; } = new(); - - /// - /// Extension method to convert a to a . - /// - public static SequentialMessage FromChat(ChatMessageContent content) => new() { Message = content }; -} diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialMessages.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessages.cs new file mode 100644 index 000000000000..c06f30f8a046 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialMessages.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; + +/// +/// A message that describes the input task and captures results for a . +/// +internal static class SequentialMessages +{ + /// + /// An empty message instance as a default. + /// + public static readonly ChatMessageContent Empty = new(); + + /// + /// Represents a request containing a sequence of chat messages to be processed by the sequential orchestration. + /// + public sealed class Request + { + /// + /// The request input. + /// + public IList Messages { get; init; } = []; + } + + /// + /// Represents a response containing the result message from the sequential orchestration. + /// + public sealed class Response + { + /// + /// The response message. + /// + public ChatMessageContent Message { get; init; } = Empty; + } + + /// + /// Extension method to convert a to a . + /// + /// The chat message to include in the request. + /// A containing the provided messages. + public static Request AsRequestMessage(this ChatMessageContent message) => new() { Messages = [message] }; + + /// + /// Extension method to convert a collection of to a . + /// + /// The collection of chat messages to include in the request. + /// A containing the provided messages. + public static Request AsRequestMessage(this IEnumerable messages) => new() { Messages = [.. messages] }; + + /// + /// Extension method to convert a to a . + /// + /// The chat message to include in the response. + /// A containing the provided message. + public static Response AsResponseMessage(this ChatMessageContent message) => new() { Message = message }; +} diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs index 9442a11d5292..29bb1ee362ca 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.String.cs @@ -1,9 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.Runtime; -using Microsoft.SemanticKernel.ChatCompletion; - namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// @@ -15,12 +11,9 @@ public sealed 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) + public SequentialOrchestration(params Agent[] members) + : base(members) { - this.InputTransform = (string input) => ValueTask.FromResult(SequentialMessage.FromChat(new ChatMessageContent(AuthorRole.User, input))); - 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 0869916b5d03..c43a525bd74a 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -13,59 +14,50 @@ 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 SequentialOrchestration : AgentOrchestration +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(OrchestrationName, runtime, agents) + public SequentialOrchestration(params Agent[] agents) + : base(OrchestrationName, agents) { } /// - protected override async ValueTask StartAsync(TopicId topic, SequentialMessage input, AgentType? entryAgent) + protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) { if (!entryAgent.HasValue) { throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent)); } - await this.Runtime.SendMessageAsync(input, entryAgent.Value).ConfigureAwait(false); + await runtime.SendMessageAsync(input.AsRequestMessage(), entryAgent.Value).ConfigureAwait(false); } /// - protected override async ValueTask RegisterMembersAsync(TopicId topic, AgentType orchestrationType, ILoggerFactory loggerFactory, ILogger logger) + protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { + AgentType outputType = await registrar.RegisterResultTypeAsync(response => response.Message).ConfigureAwait(false); + // Each agent handsoff its result to the next agent. - AgentType nextAgent = orchestrationType; + AgentType nextAgent = outputType; for (int index = this.Members.Count - 1; index >= 0; --index) { - OrchestrationTarget member = this.Members[index]; + Agent agent = this.Members[index]; + nextAgent = await RegisterAgentAsync(agent, index, nextAgent).ConfigureAwait(false); - if (member.IsAgent(out Agent? agent)) - { - nextAgent = await RegisterAgentAsync(topic, nextAgent, index, agent).ConfigureAwait(false); - } - else if (member.IsOrchestration(out Orchestratable? orchestration)) - { - nextAgent = await orchestration.RegisterAsync(topic, nextAgent, loggerFactory).ConfigureAwait(false); - } logger.LogRegisterActor(OrchestrationName, nextAgent, "MEMBER", index + 1); } return nextAgent; - ValueTask RegisterAgentAsync(TopicId topic, AgentType nextAgent, int index, Agent agent) - { - return - this.Runtime.RegisterAgentFactoryAsync( - this.GetAgentType(topic, index), - (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, agent, nextAgent, loggerFactory.CreateLogger()))); - } + ValueTask RegisterAgentAsync(Agent agent, int index, AgentType nextAgent) => + runtime.RegisterAgentFactoryAsync( + this.GetAgentType(context.Topic, index), + (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger()))); } private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); diff --git a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs new file mode 100644 index 000000000000..1615dbd3c2d8 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs @@ -0,0 +1,71 @@ +// 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.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Transforms; + +internal static class DefaultTransforms +{ + public static ValueTask> FromInput(TInput input, CancellationToken cancellationToken = default) + { + if (input is IEnumerable messages) + { + return new ValueTask>(messages); + } + + if (input is ChatMessageContent message) + { + return new ValueTask>([message]); + } + + if (input is not string text) + { + text = JsonSerializer.Serialize(input); + } + + return new ValueTask>([new ChatMessageContent(AuthorRole.User, text)]); + } + + public static ValueTask ToOutput(ChatMessageContent result, CancellationToken cancellationToken = default) + { + TOutput? output = + GetDefaultOutput() ?? + GetObjectOutput() ?? + throw new InvalidOperationException($"Unable to transform output message of type {typeof(ChatMessageContent)} to {typeof(TOutput)}."); + + return new ValueTask(output); + + TOutput? GetObjectOutput() + { + try + { + return JsonSerializer.Deserialize(result.Content ?? string.Empty); + } + catch (JsonException) + { + return default; + } + } + + TOutput? GetDefaultOutput() + { + object? output = null; + if (typeof(ChatMessageContent) == typeof(TOutput)) + { + output = (object)result; + } + + if (typeof(string) == typeof(TOutput)) + { + output = result.Content ?? string.Empty; + } + + return (TOutput?)output; + } + } +} diff --git a/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs b/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs new file mode 100644 index 000000000000..93971220bd64 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Transforms; + +/// +/// Delegate for transforming an input of type into a collection of . +/// This is typically used to convert user or system input into a format suitable for chat orchestration. +/// +/// The input object to transform. +/// A cancellation token that can be used to cancel the operation. +/// A containing an enumerable of representing the transformed input. +public delegate ValueTask> OrchestrationInputTransform(TInput input, CancellationToken cancellationToken = default); + +/// +/// Delegate for transforming a into an output of type . +/// This is typically used to convert a chat response into a desired output format. +/// +/// The result message to transform. +/// A cancellation token that can be used to cancel the operation. +/// A containing the transformed output of type . +public delegate ValueTask OrchestrationOutputTransform(ChatMessageContent result, CancellationToken cancellationToken = default); + +/// +/// Delegate for transforming the internal result message for an orchestration into a . +/// +/// The result message type +/// The result message +/// The orchestration result as a . +public delegate ChatMessageContent OrchestrationResultTransform(TResult result); diff --git a/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs b/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs new file mode 100644 index 000000000000..5c49b96274b9 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Transforms; + +/// +/// Populates the target result type into a structured output. +/// +/// The type of the structured output to deserialize to. +public sealed class StructuredOutputTransform +{ + internal const string DefaultInstructions = "Please respond with a JSON object that contains the."; + + private readonly IChatCompletionService _service; + private readonly PromptExecutionSettings _executionSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The chat completion service to use for generating responses. + /// The prompt execution settings to use for the chat completion service. + public StructuredOutputTransform(IChatCompletionService service, PromptExecutionSettings executionSettings) + { + Verify.NotNull(service, nameof(service)); + Verify.NotNull(executionSettings, nameof(executionSettings)); + + this._service = service; + this._executionSettings = executionSettings; + } + + /// + /// Gets or sets the instructions to be used as the system message for the chat completion. + /// + public string Instructions { get; init; } = DefaultInstructions; + + /// + /// Transforms the provided into a strongly-typed structured output by invoking the chat completion service and deserializing the response. + /// + /// The chat message content to process. + /// A cancellation token to observe while waiting for the task to complete. + /// The structured output of type . + /// Thrown if the response cannot be deserialized into . + public async ValueTask TransformAsync(ChatMessageContent message, CancellationToken cancellationToken = default) + { + ChatHistory history = + [ + new ChatMessageContent(AuthorRole.System, this.Instructions), + message, + ]; + ChatMessageContent response = await this._service.GetChatMessageContentAsync(history, this._executionSettings, kernel: null, cancellationToken).ConfigureAwait(false); + return + JsonSerializer.Deserialize(response.Content ?? string.Empty) ?? + throw new InvalidOperationException($"Unable to transform result into {typeof(TOutput).Name}"); + } +} diff --git a/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs index 0b7a89120d5f..96a33f38c2d9 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; @@ -53,35 +54,15 @@ public async Task ConcurrentOrchestrationWithMultipleAgentsAsync() 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) + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params Agent[] mockAgents) { // Act await runtime.StartAsync(); - ConcurrentOrchestration orchestration = new(runtime, mockAgents); + ConcurrentOrchestration orchestration = new(mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); // Assert Assert.NotNull(result); @@ -102,13 +83,4 @@ private static MockAgent CreateMockAgent(int index, string response) 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 index 568e2bc3fbdf..ccc30cdc444e 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -53,34 +54,15 @@ public async Task GroupChatOrchestrationWithMultipleAgentsAsync() 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) + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params Agent[] mockAgents) { // Act await runtime.StartAsync(); - GroupChatOrchestration orchestration = new(runtime, new SimpleGroupChatStrategy(), mockAgents); + GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocations = mockAgents.Length }, mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); // Assert Assert.NotNull(result); @@ -101,35 +83,4 @@ private static MockAgent CreateMockAgent(int index, string response) 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/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index ed007d5f1171..ea2e4b504817 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; @@ -80,15 +81,15 @@ public async Task HandoffOrchestrationWithMultipleAgentsAsync() Assert.Equal(1, mockAgent3.InvokeCount); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, Dictionary? handoffs, params OrchestrationTarget[] mockAgents) + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, Dictionary? handoffs, params Agent[] mockAgents) { // Act await runtime.StartAsync(); - HandoffOrchestration orchestration = new(runtime, handoffs ?? [], mockAgents); + HandoffOrchestration orchestration = new(handoffs ?? [], mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); // Assert Assert.NotNull(result); diff --git a/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs index 4e872ef2b436..fd8721482ceb 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationResultTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Orchestration; @@ -15,25 +16,26 @@ public class OrchestrationResultTests public void Constructor_InitializesPropertiesCorrectly() { // Arrange - string orchestrationName = "TestOrchestration"; - TopicId topic = new("testTopic"); + OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); // Act - OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + using CancellationTokenSource cancelSource = new(); + using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); // Assert - Assert.Equal(topic, result.Topic); + Assert.Equal("TestOrchestration", result.Orchestration); + Assert.Equal(new TopicId("testTopic"), result.Topic); } [Fact] public async Task GetValueAsync_ReturnsCompletedValue_WhenTaskIsCompletedAsync() { // Arrange - string orchestrationName = "TestOrchestration"; - TopicId topic = new("testTopic"); + OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); - OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + using CancellationTokenSource cancelSource = new(); + using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); string expectedValue = "Result value"; // Act @@ -48,10 +50,10 @@ public async Task GetValueAsync_ReturnsCompletedValue_WhenTaskIsCompletedAsync() public async Task GetValueAsync_WithTimeout_ReturnsCompletedValue_WhenTaskCompletesWithinTimeoutAsync() { // Arrange - string orchestrationName = "TestOrchestration"; - TopicId topic = new("testTopic"); + OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); - OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + using CancellationTokenSource cancelSource = new(); + using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); string expectedValue = "Result value"; TimeSpan timeout = TimeSpan.FromSeconds(1); @@ -67,10 +69,10 @@ public async Task GetValueAsync_WithTimeout_ReturnsCompletedValue_WhenTaskComple public async Task GetValueAsync_WithTimeout_ThrowsTimeoutException_WhenTaskDoesNotCompleteWithinTimeoutAsync() { // Arrange - string orchestrationName = "TestOrchestration"; - TopicId topic = new("testTopic"); + OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); - OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + using CancellationTokenSource cancelSource = new(); + using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); TimeSpan timeout = TimeSpan.FromMilliseconds(50); // Act & Assert @@ -82,10 +84,10 @@ public async Task GetValueAsync_WithTimeout_ThrowsTimeoutException_WhenTaskDoesN public async Task GetValueAsync_ReturnsCompletedValue_WhenCompletionIsDelayedAsync() { // Arrange - string orchestrationName = "TestOrchestration"; - TopicId topic = new("testTopic"); + OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, NullLoggerFactory.Instance, CancellationToken.None); TaskCompletionSource tcs = new(); - OrchestrationResult result = new(orchestrationName, topic, tcs, NullLogger.Instance); + using CancellationTokenSource cancelSource = new(); + using OrchestrationResult result = new(context, tcs, cancelSource, NullLogger.Instance); int expectedValue = 42; // Act diff --git a/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs deleted file mode 100644 index 05ed3f283a75..000000000000 --- a/dotnet/src/Agents/UnitTests/Orchestration/OrchestrationTargetTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -// 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 index 884b402400f3..3840365a69d6 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/SequentialOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/SequentialOrchestrationTests.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; @@ -50,34 +51,15 @@ public async Task SequentialOrchestrationWithMultipleAgentsAsync() 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) + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, params Agent[] mockAgents) { // Act await runtime.StartAsync(); - SequentialOrchestration orchestration = new(runtime, mockAgents); + SequentialOrchestration orchestration = new(mockAgents); const string InitialInput = "123"; - OrchestrationResult result = await orchestration.InvokeAsync(InitialInput); + OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); // Assert Assert.NotNull(result); @@ -98,13 +80,4 @@ private static MockAgent CreateMockAgent(int index, string response) 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 eaa4b45691913b24c1654d1374ed8c889a8db43e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 19:32:49 -0700 Subject: [PATCH 62/98] Handoff Checkpoint --- .../Orchestration/Step01_Concurrent.cs | 17 +++- .../Step01a_ConcurrentWithStructuredOutput.cs | 2 +- .../Orchestration/Step02_Sequential.cs | 27 ++++++- .../Orchestration/Step03_GroupChat.cs | 23 +++++- .../Step03a_GroupChatWithHumanInTheLoop.cs | 2 +- .../Step03b_GroupChatWithAIManager.cs | 2 +- .../Orchestration/Step04_Handoff.cs | 16 +++- .../Step04b_HandoffWithStructuredInput.cs | 4 +- dotnet/src/Agents/Orchestration/AgentActor.cs | 9 ++- .../Concurrent/ConcurrentOrchestration.cs | 1 - .../GroupChat/GroupChatManager.cs | 2 +- .../Orchestration/Handoff/HandoffActor.cs | 81 ++++++++++++------- .../Handoff/HandoffInvocationFilter.cs | 23 ++++++ .../Handoff/HandoffOrchestration.cs | 12 ++- .../AgentUtilities/BaseOrchestrationTest.cs | 13 +++ 15 files changed, 182 insertions(+), 52 deletions(-) create mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffInvocationFilter.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index 0a29a23f1264..045c588de75f 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -13,7 +14,7 @@ namespace GettingStarted.Orchestration; public class Step01_Concurrent(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task ConcurrentTaskAsync() // %%% TODO + public async Task ConcurrentTaskAsync() { // Define the agents ChatCompletionAgent physicist = @@ -26,7 +27,13 @@ public async Task ConcurrentTaskAsync() // %%% TODO description: "An expert in chemistry"); // Define the orchestration - ConcurrentOrchestration orchestration = new(physicist, chemist) { LoggerFactory = this.LoggerFactory }; + OrchestrationMonitor monitor = new(); + ConcurrentOrchestration orchestration = + new(physicist, chemist) + { + ResponseCallback = monitor.ResponseCallback, + LoggerFactory = this.LoggerFactory + }; // Start the runtime InProcessRuntime runtime = new(); @@ -41,5 +48,11 @@ public async Task ConcurrentTaskAsync() // %%% TODO Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); await runtime.RunUntilIdleAsync(); + + Console.WriteLine("\n\nORCHESTRATION HISTORY"); + foreach (ChatMessageContent message in monitor.History) + { + this.WriteAgentChatMessage(message); + } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs index a0c5c5a625a8..64e9f40efba3 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs @@ -21,7 +21,7 @@ public class Step01a_ConcurrentWithStructuredOutput(ITestOutputHelper output) : private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; [Fact] - public async Task ConcurrentStructuredOutputAsync() // %%% TODO + public async Task ConcurrentStructuredOutputAsync() { // Define the agents ChatCompletionAgent agent1 = diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index d6361996ca59..863df18da822 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Sequential; @@ -16,8 +17,10 @@ public class Step02_Sequential(ITestOutputHelper output) : BaseOrchestrationTest public async Task SequentialTaskAsync() { // Define the agents - ChatCompletionAgent agent1 = + ChatCompletionAgent analystAgent = this.CreateAgent( + name: "Analyst", + instructions: """ You are a marketing analyst. Given a product description, identify: - Key features @@ -25,16 +28,20 @@ public async Task SequentialTaskAsync() - Unique selling points """, description: "A agent that extracts key concepts from a product description."); - ChatCompletionAgent agent2 = + ChatCompletionAgent writerAgent = this.CreateAgent( + name: "copywriter", + instructions: """ You are a marketing copywriter. Given a block of text describing features, audience, and USPs, compose a compelling marketing copy (like a newsletter section) that highlights these points. Output should be short (around 150 words), output just the copy as a single text block. """, description: "An agent that writes a marketing copy based on the extracted concepts."); - ChatCompletionAgent agent3 = + ChatCompletionAgent editorAgent = this.CreateAgent( + name: "editor", + instructions: """ You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone, give format and make it polished. Output the final improved copy as a single text block. @@ -42,7 +49,13 @@ give format and make it polished. Output the final improved copy as a single tex description: "An agent that formats and proofreads the marketing copy."); // Define the orchestration - SequentialOrchestration orchestration = new(agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; + OrchestrationMonitor monitor = new(); + SequentialOrchestration orchestration = + new(analystAgent, writerAgent, editorAgent) + { + ResponseCallback = monitor.ResponseCallback, + LoggerFactory = this.LoggerFactory + }; // Start the runtime InProcessRuntime runtime = new(); @@ -56,5 +69,11 @@ give format and make it polished. Output the final improved copy as a single tex Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); + + Console.WriteLine("\n\nORCHESTRATION HISTORY"); + foreach (ChatMessageContent message in monitor.History) + { + this.WriteAgentChatMessage(message); + } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 29925be5963d..0a14f91085bc 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -8,7 +9,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest(output) { @@ -42,7 +43,19 @@ Consider suggestions when refining an idea. """); // Define the orchestration - GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocations = 5 }, writer, editor) { LoggerFactory = this.LoggerFactory }; + OrchestrationMonitor monitor = new(); + GroupChatOrchestration orchestration = + new(new RoundRobinGroupChatManager() + { + MaximumInvocations = 5 + }, + writer, + editor) + { + ResponseCallback = monitor.ResponseCallback, + LoggerFactory = this.LoggerFactory, + }; + // Start the runtime InProcessRuntime runtime = new(); @@ -55,5 +68,11 @@ Consider suggestions when refining an idea. Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); + + Console.WriteLine("\n\nORCHESTRATION HISTORY"); + foreach (ChatMessageContent message in monitor.History) + { + this.WriteAgentChatMessage(message); + } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs index c02b574a2c20..3608ebdc48d3 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs @@ -10,7 +10,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step03a_GroupChatWithHumanInTheLoop(ITestOutputHelper output) : BaseOrchestrationTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs index 8e7bca1b3362..ff63d4915ae1 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs @@ -13,7 +13,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step03b_GroupChatWithAIManager(ITestOutputHelper output) : BaseOrchestrationTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index d940bc57bfc4..22bb3064fa81 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -9,12 +9,12 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step04_Handoff(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task SimpleHandoffAsync() + public async Task TriageAsync() { // Initialize plugin GithubPlugin githubPlugin = new(); @@ -40,6 +40,7 @@ public async Task SimpleHandoffAsync() dotnetAgent.Kernel.Plugins.Add(plugin); // Define the orchestration + OrchestrationMonitor monitor = new(); HandoffOrchestration orchestration = new(handoffs: new() @@ -57,6 +58,7 @@ public async Task SimpleHandoffAsync() pythonAgent, dotnetAgent) { + ResponseCallback = monitor.ResponseCallback, LoggerFactory = this.LoggerFactory }; @@ -79,13 +81,19 @@ public async Task SimpleHandoffAsync() OrchestrationResult result = await orchestration.InvokeAsync(InputJson, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); - Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}"); + Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); await runtime.RunUntilIdleAsync(); + + Console.WriteLine("\n\nORCHESTRATION HISTORY"); + foreach (ChatMessageContent message in monitor.History) + { + this.WriteAgentChatMessage(message); + } } [Fact] - public async Task SingleHandoffAsync() + public async Task NoHandoffAsync() { // Define the agents ChatCompletionAgent agent1 = diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs index e156d39f1ee0..6e5ffedda97d 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs @@ -10,7 +10,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the . /// public class Step04b_HandoffWithStructuredInput(ITestOutputHelper output) : BaseOrchestrationTest(output) { @@ -95,7 +95,7 @@ Normal DBContext logging shows only normal context queries. Queries run by Vecto OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); - Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}"); + Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); await runtime.RunUntilIdleAsync(); } diff --git a/dotnet/src/Agents/Orchestration/AgentActor.cs b/dotnet/src/Agents/Orchestration/AgentActor.cs index 75d2bb129059..a67be5f056d1 100644 --- a/dotnet/src/Agents/Orchestration/AgentActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentActor.cs @@ -56,6 +56,13 @@ protected AgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext con return null; } + /// + /// Optionally overridden to introduce customer filtering logic for the response callback. + /// + /// The agent response + /// true if the response should be filtered (hidden) + protected virtual bool ResponseCallbackFilter(ChatMessageContent response) => false; + /// /// Deletes the agent thread. /// @@ -108,7 +115,7 @@ await this.Agent.InvokeAsync( AuthorName = firstResponse?.Message.AuthorName, }; - if (this.Context.ResponseCallback is not null) + if (this.Context.ResponseCallback is not null && !this.ResponseCallbackFilter(response)) { await this.Context.ResponseCallback.Invoke(response).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 8dd15a08d72f..fa5f9055c3c8 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -24,7 +24,6 @@ public class ConcurrentOrchestration public ConcurrentOrchestration(params Agent[] agents) : base(OrchestrationName, agents) { - //this.OutputTransform= (message) => // %%% ??? } /// diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index c3b3b3663fb3..bdaf5c4c69d4 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -48,7 +48,7 @@ protected GroupChatManager() { } public int MaximumInvocations { get; init; } = int.MaxValue; /// - /// Gets or sets the callback to be invoked for interactive operations. + /// Gets or sets the callback to be invoked for interactive input. /// public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index 853380857f9e..a9f5b149200c 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -24,6 +23,9 @@ internal sealed class HandoffActor : private readonly AgentType _resultHandoff; private readonly List _cache; + private AgentType? _handoffAgentType; + private string? _taskSummary; + /// /// Initializes a new instance of the class. /// @@ -42,6 +44,14 @@ public HandoffActor(AgentId id, IAgentRuntime runtime, OrchestrationContext cont this._resultHandoff = resultHandoff; } + /// + /// Gets or sets the callback to be invoked for interactive input. + /// + public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } + + /// + protected override bool ResponseCallbackFilter(ChatMessageContent response) => response.Role == AuthorRole.Tool; + /// protected override AgentInvokeOptions? CreateInvokeOptions() { @@ -64,14 +74,11 @@ public HandoffActor(AgentId id, IAgentRuntime runtime, OrchestrationContext cont /// public ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext) { + this._taskSummary = null; this._cache.AddRange(item.Messages); - - return this.HandleAsync(messageContext.CancellationToken); + return ValueTask.CompletedTask; } - /// - public ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext) => this.HandleAsync(messageContext.CancellationToken); - /// public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext) { @@ -80,16 +87,43 @@ public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messa return ValueTask.CompletedTask; } - private async ValueTask HandleAsync(CancellationToken cancellationToken) + /// + public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext) { this.Logger.LogHandoffAgentInvoke(this.Id); - ChatMessageContent response = await this.InvokeAsync(this._cache, cancellationToken).ConfigureAwait(false); - this._cache.Clear(); + while (this._taskSummary == null) + { + ChatMessageContent response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false); + this._cache.Clear(); + + this.Logger.LogHandoffAgentResult(this.Id, response.Content); - this.Logger.LogHandoffAgentResult(this.Id, response.Content); + // The response can potentially be a TOOL message from the Handoff plugin since we have added + // a filter which will terminate the conversation when a function from the handoff plugin is called + // nd we don't want to publish that message, so we only publish if the response is an ASSISTANT message. + if (response.Role == AuthorRole.Assistant) + { + await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false); + } + + if (this._handoffAgentType != null) + { + await this.SendMessageAsync(new HandoffMessages.Request(), this._handoffAgentType.Value, messageContext.CancellationToken).ConfigureAwait(false); - await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, cancellationToken).ConfigureAwait(false); + this._handoffAgentType = null; + break; + } + + if (this.InteractiveCallback != null) + { + ChatMessageContent input = await this.InteractiveCallback().ConfigureAwait(false); + this._cache.Add(input); + continue; + } + + await this.EndAsync(response.Content ?? "No handoff or human response function requested. Ending task.", messageContext.CancellationToken).ConfigureAwait(false); + } } private KernelPlugin CreateHandoffPlugin() @@ -116,32 +150,17 @@ IEnumerable CreateHandoffFunctions() } } - private async ValueTask HandoffAsync(AgentType agentType, CancellationToken cancellationToken = default) + private ValueTask HandoffAsync(AgentType agentType, CancellationToken cancellationToken = default) { this.Logger.LogHandoffFunctionCall(this.Id, agentType); - await this.SendMessageAsync(new HandoffMessages.Request(), agentType, cancellationToken).ConfigureAwait(false); + this._handoffAgentType = agentType; + return ValueTask.CompletedTask; } 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; - } + this._taskSummary = summary; + await this.SendMessageAsync(new HandoffMessages.Result { Message = new ChatMessageContent(AuthorRole.Assistant, summary) }, this._resultHandoff, cancellationToken).ConfigureAwait(false); } } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffInvocationFilter.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffInvocationFilter.cs new file mode 100644 index 000000000000..7c67f637bda2 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffInvocationFilter.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; + +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/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index 993ebac01629..ebf95176d3f6 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -31,6 +31,11 @@ public HandoffOrchestration(Dictionary handoffs, par this._handoffs = handoffs; } + /// + /// Gets or sets the callback to be invoked for interactive input. + /// + public OrchestrationInteractiveCallback? InteractiveCallback { get; init; } + /// protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable input, AgentType? entryAgent) { @@ -59,7 +64,12 @@ protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId top agentType = await runtime.RegisterAgentFactoryAsync( this.GetAgentType(context.Topic, index), - (agentId, runtime) => ValueTask.FromResult(new HandoffActor(agentId, runtime, context, agent, map, outputType, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + (agentId, runtime) => + ValueTask.FromResult( + new HandoffActor(agentId, runtime, context, agent, map, outputType, context.LoggerFactory.CreateLogger()) + { + InteractiveCallback = this.InteractiveCallback + })).ConfigureAwait(false); agentMap[agent.Name ?? agent.Id] = agentType; await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs index 515fb079885b..afaada4f009f 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; /// /// Base class for samples that demonstrate the usage of host agents @@ -21,4 +23,15 @@ protected ChatCompletionAgent CreateAgent(string instructions, string? name = nu Kernel = this.CreateKernelWithChatCompletion(), }; } + + protected sealed class OrchestrationMonitor + { + public ChatHistory History { get; } = []; + + public ValueTask ResponseCallback(ChatMessageContent response) + { + this.History.Add(response); + return ValueTask.CompletedTask; + } + } } From a51b2cf5bc5f7197fad0bd270379db23a82e97de Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 19:34:48 -0700 Subject: [PATCH 63/98] Typo --- dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index a9f5b149200c..ba4be9046ebf 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -99,9 +99,9 @@ public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext this.Logger.LogHandoffAgentResult(this.Id, response.Content); - // The response can potentially be a TOOL message from the Handoff plugin since we have added - // a filter which will terminate the conversation when a function from the handoff plugin is called - // nd we don't want to publish that message, so we only publish if the response is an ASSISTANT message. + // The response can potentially be a TOOL message from the Handoff plugin due to the filter + // which will terminate the conversation when a function from the handoff plugin is called. + // Since we don't want to publish that message, so we only publish if the response is an ASSISTANT message. if (response.Role == AuthorRole.Assistant) { await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false); From e2c3b6ae110cb194ff292fd361cba013c4ab6785 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 19:56:39 -0700 Subject: [PATCH 64/98] Whitespace --- .../GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 0a14f91085bc..67c40a32c3c1 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -56,7 +56,6 @@ Consider suggestions when refining an idea. LoggerFactory = this.LoggerFactory, }; - // Start the runtime InProcessRuntime runtime = new(); await runtime.StartAsync(); From d501df8e55338a40bdfb83a1f4a1e1ae3965967d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 20:17:48 -0700 Subject: [PATCH 65/98] Namespace --- .../UnitTests/Orchestration/GroupChatOrchestrationTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs index ccc30cdc444e..a904123f9e26 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Linq; -using System.Threading; using System.Threading.Tasks; 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; using Microsoft.SemanticKernel.ChatCompletion; From 2ee2f3200fdfaa36311a0e743392c2a32ae3e78a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 20:24:19 -0700 Subject: [PATCH 66/98] Orchestration name & namespace --- dotnet/SK-dotnet.sln | 16 ++++++++-------- .../Concurrent/ConcurrentOrchestration.cs | 3 +-- .../Handoff/HandoffOrchestration.cs | 3 +-- .../Sequential/SequentialOrchestration.cs | 3 +-- .../ConcurrentOrchestrationTests.cs | 1 - 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index b6098eb8f11e..1d260a6a33bf 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -1507,18 +1507,18 @@ Global {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 - {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 {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Debug|Any CPU.Build.0 = Debug|Any CPU {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Publish|Any CPU.ActiveCfg = Debug|Any CPU {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Publish|Any CPU.Build.0 = Debug|Any CPU {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Release|Any CPU.ActiveCfg = Release|Any CPU {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.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 @@ -1544,7 +1544,7 @@ Global {AFA81EB7-F869-467D-8A90-744305D80AAC} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {627742DB-1E52-468A-99BD-6FF1A542D25B} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} {E3299033-EB81-4C4C-BCD9-E8DC40937969} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} - {078F96B4-09E1-4E0E-B214-F71A4F4BF633} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {078F96B4-09E1-4E0E-B214-F71A4F4BF633} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974} {F51017A9-15C8-472D-893C-080046D710A6} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633} {EC3BB6D1-2FB2-4702-84C6-F791DE533ED4} = {24503383-A8C4-4255-9998-28D70FE8E99A} {4D226C2F-AE9F-4EFB-AF2D-45C8FE5CB34E} = {24503383-A8C4-4255-9998-28D70FE8E99A} @@ -1723,8 +1723,8 @@ 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} - {D1A02387-FA60-22F8-C2ED-4676568B6CC3} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} {AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84} = {5A7028A7-4DDF-4E4F-84A9-37CE8F8D7E89} + {D1A02387-FA60-22F8-C2ED-4676568B6CC3} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index fa5f9055c3c8..61bae368597b 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; @@ -15,7 +14,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; public class ConcurrentOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = typeof(ConcurrentOrchestration<,>).Name.Split('`').First(); + internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(ConcurrentOrchestration<,>)); /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index ebf95176d3f6..85611a467064 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; @@ -16,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// public class HandoffOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = typeof(HandoffOrchestration<,>).Name.Split('`').First(); + internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(HandoffOrchestration<,>)); private readonly Dictionary _handoffs; diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index c43a525bd74a..3bc5ab433739 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; @@ -16,7 +15,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// public class SequentialOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = typeof(SequentialOrchestration<,>).Name.Split('`').First(); + internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(SequentialOrchestration<,>)); /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs index 96a33f38c2d9..fcf232f3153c 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/ConcurrentOrchestrationTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; From 72d50ba4273c0f01553492c3c806b2d100066b97 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 20:36:16 -0700 Subject: [PATCH 67/98] Cleanup --- .../Orchestration/Transforms/StructuredOutputTransform.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs b/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs index 5c49b96274b9..6b8850157240 100644 --- a/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs +++ b/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs @@ -11,10 +11,10 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Transforms; /// /// Populates the target result type into a structured output. /// -/// The type of the structured output to deserialize to. +/// The .NET type of the structured-output to deserialization target. public sealed class StructuredOutputTransform { - internal const string DefaultInstructions = "Please respond with a JSON object that contains the."; + internal const string DefaultInstructions = "Respond with JSON that is populated by using the information in this conversation."; private readonly IChatCompletionService _service; private readonly PromptExecutionSettings _executionSettings; From 76a211c249d9d69cdbc31e019e3a458881d81bb3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 20:41:12 -0700 Subject: [PATCH 68/98] Update test --- .../Orchestration/ChatGroupExtensionsTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs index 3d4d46d1e6cf..8a5f3ed7b88c 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs @@ -13,16 +13,16 @@ 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") } + { "AgentOne", ("agent1", "First agent description") }, + { "AgentTwo", ("agent2", "Second agent description") }, + { "AgentThree", ("agent3", "Third agent description") } }; // Act string result = group.FormatNames(); // Assert - Assert.Equal("Agent One,Agent Two,Agent Three", result); + Assert.Equal("AgentOne,AgentTwo,AgentThree", result); } [Fact] @@ -31,14 +31,14 @@ public void FormatNames_WithSingleAgent_ReturnsSingleName() // Arrange ChatGroup group = new() { - { "agent1", ("Agent One", "First agent description") } + { "AgentOne", ("agent1", "First agent description") }, }; // Act string result = group.FormatNames(); // Assert - Assert.Equal("Agent One", result); + Assert.Equal("AgentOne", result); } [Fact] @@ -60,9 +60,9 @@ 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") } + { "AgentOne", ("agent1", "First agent description") }, + { "AgentTwo", ("agent2", "Second agent description") }, + { "AgentThree", ("agent3", "Third agent description") } }; // Act @@ -71,9 +71,9 @@ public void FormatList_WithMultipleAgents_ReturnsMarkdownList() // Assert const string Expected = """ - - Agent One: First agent description - - Agent Two: Second agent description - - Agent Three: Third agent description + - AgentOne: First agent description + - AgentTwo: Second agent description + - AgentThree: Third agent description """; Assert.Equal(Expected, result); } From 843be9ca4297597a1d69a74852470a2c1e288c6f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 22:00:34 -0700 Subject: [PATCH 69/98] Complete --- .../Orchestration/Step01_Concurrent.cs | 2 +- .../AgentOrchestration.ResultActor.cs | 3 +- .../Orchestration/AgentOrchestration.cs | 2 + .../ConcurrentOrchestration.String.cs | 4 + .../Concurrent/ConcurrentOrchestration.cs | 6 +- .../GroupChat/GroupChatOrchestration.cs | 2 +- .../Handoff/HandoffOrchestration.cs | 2 +- .../Sequential/SequentialOrchestration.cs | 2 +- .../Transforms/DefaultTransforms.cs | 26 ++- .../Transforms/OrchestrationTransforms.cs | 8 +- .../Transforms/StructuredOutputTransform.cs | 7 +- .../Orchestration/DefaultTransformsTests.cs | 203 ++++++++++++++++++ 12 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index 045c588de75f..cb77e71d2882 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -45,7 +45,7 @@ public async Task ConcurrentTaskAsync() OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT:\n{string.Join("\n", output.Select(text => $"\t{text}"))}"); + Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", output.Select(text => $"{text}"))}"); await runtime.RunUntilIdleAsync(); diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index 25c91ee3ce7b..87ba333949b7 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Transforms; @@ -64,7 +65,7 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) try { - ChatMessageContent result = this._transformResult.Invoke(item); + IList result = this._transformResult.Invoke(item); TOutput output = await this._transform.Invoke(result).ConfigureAwait(false); if (this.CompletionTarget.HasValue) diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index 4d644e4c9737..eea8a7f69c0f 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -27,6 +27,8 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// /// Base class for multi-agent agent orchestration patterns. /// +/// The type of the input to the orchestration. +/// The type of the result output by the orchestration. public abstract partial class AgentOrchestration { private readonly string _orchestrationRoot; diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs index ed724e5881a1..f18ca767c2c1 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; +using System.Threading.Tasks; + namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// @@ -14,5 +17,6 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration ValueTask.FromResult([.. response.Select(r => r.Content ?? string.Empty)]); } } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 61bae368597b..5a580cb51786 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; @@ -11,6 +12,9 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; /// /// An orchestration that broadcasts the input message to each agent. /// +/// +/// TOutput must be an array type for . +/// public class ConcurrentOrchestration : AgentOrchestration { @@ -34,7 +38,7 @@ protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IE /// protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => response[0].Message).ConfigureAwait(false); // %%% HACK + AgentType outputType = await registrar.RegisterResultTypeAsync(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false); // Register result actor AgentType resultType = this.FormatAgentType(context.Topic, "Results"); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 0f19517385e1..eb64675ff662 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -48,7 +48,7 @@ protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IE /// protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => response.Message).ConfigureAwait(false); + AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); int agentCount = 0; ChatGroup team = []; diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index 85611a467064..8ab4a20e7323 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -49,7 +49,7 @@ protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId top /// protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => response.Message).ConfigureAwait(false); + AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); // Each agent handsoff its result to the next agent. Dictionary agentMap = []; diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 3bc5ab433739..9ff4559b8576 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -39,7 +39,7 @@ protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId top /// protected override async ValueTask RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger) { - AgentType outputType = await registrar.RegisterResultTypeAsync(response => response.Message).ConfigureAwait(false); + AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); // Each agent handsoff its result to the next agent. AgentType nextAgent = outputType; diff --git a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs index 1615dbd3c2d8..5b6c3b9e6d24 100644 --- a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs +++ b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs @@ -31,20 +31,27 @@ public static ValueTask> FromInput(TInpu return new ValueTask>([new ChatMessageContent(AuthorRole.User, text)]); } - public static ValueTask ToOutput(ChatMessageContent result, CancellationToken cancellationToken = default) + public static ValueTask ToOutput(IList result, CancellationToken cancellationToken = default) { - TOutput? output = + bool isSingleResult = result.Count == 1; + + TOutput output = GetDefaultOutput() ?? GetObjectOutput() ?? - throw new InvalidOperationException($"Unable to transform output message of type {typeof(ChatMessageContent)} to {typeof(TOutput)}."); + throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}."); return new ValueTask(output); TOutput? GetObjectOutput() { + if (!isSingleResult) + { + return default; + } + try { - return JsonSerializer.Deserialize(result.Content ?? string.Empty); + return JsonSerializer.Deserialize(result[0].Content ?? string.Empty); } catch (JsonException) { @@ -55,14 +62,17 @@ public static ValueTask ToOutput(ChatMessageContent result, Ca TOutput? GetDefaultOutput() { object? output = null; - if (typeof(ChatMessageContent) == typeof(TOutput)) + if (typeof(TOutput).IsAssignableFrom(result.GetType())) { output = (object)result; } - - if (typeof(string) == typeof(TOutput)) + else if (isSingleResult && typeof(TOutput).IsAssignableFrom(typeof(ChatMessageContent))) + { + output = (object)result[0]; + } + else if (isSingleResult && typeof(string) == typeof(TOutput)) { - output = result.Content ?? string.Empty; + output = result[0].Content ?? string.Empty; } return (TOutput?)output; diff --git a/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs b/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs index 93971220bd64..5b691b310d60 100644 --- a/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs +++ b/dotnet/src/Agents/Orchestration/Transforms/OrchestrationTransforms.cs @@ -19,15 +19,15 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Transforms; /// Delegate for transforming a into an output of type . /// This is typically used to convert a chat response into a desired output format. /// -/// The result message to transform. +/// The result messages to transform. /// A cancellation token that can be used to cancel the operation. /// A containing the transformed output of type . -public delegate ValueTask OrchestrationOutputTransform(ChatMessageContent result, CancellationToken cancellationToken = default); +public delegate ValueTask OrchestrationOutputTransform(IList result, CancellationToken cancellationToken = default); /// /// Delegate for transforming the internal result message for an orchestration into a . /// /// The result message type -/// The result message +/// The result messages /// The orchestration result as a . -public delegate ChatMessageContent OrchestrationResultTransform(TResult result); +public delegate IList OrchestrationResultTransform(TResult result); diff --git a/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs b/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs index 6b8850157240..d6dc8494a287 100644 --- a/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs +++ b/dotnet/src/Agents/Orchestration/Transforms/StructuredOutputTransform.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -41,16 +42,16 @@ public StructuredOutputTransform(IChatCompletionService service, PromptExecution /// /// Transforms the provided into a strongly-typed structured output by invoking the chat completion service and deserializing the response. /// - /// The chat message content to process. + /// The chat messages to process. /// A cancellation token to observe while waiting for the task to complete. /// The structured output of type . /// Thrown if the response cannot be deserialized into . - public async ValueTask TransformAsync(ChatMessageContent message, CancellationToken cancellationToken = default) + public async ValueTask TransformAsync(IList messages, CancellationToken cancellationToken = default) { ChatHistory history = [ new ChatMessageContent(AuthorRole.System, this.Instructions), - message, + .. messages, ]; ChatMessageContent response = await this._service.GetChatMessageContentAsync(history, this._executionSettings, kernel: null, cancellationToken).ConfigureAwait(false); return diff --git a/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs new file mode 100644 index 000000000000..bf21ae4c2fcd --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.Orchestration.Transforms; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +public class DefaultTransformsTests +{ + [Fact] + public async Task FromInputAsync_WithEnumerableOfChatMessageContent_ReturnsInputAsync() + { + // Arrange + IEnumerable input = new List + { + new(AuthorRole.User, "Hello"), + new(AuthorRole.Assistant, "Hi there") + }; + + // Act + IEnumerable result = await DefaultTransforms.FromInput(input); + + // Assert + Assert.Equal(input, result); + } + + [Fact] + public async Task FromInputAsync_WithChatMessageContent_ReturnsInputAsListAsync() + { + // Arrange + ChatMessageContent input = new(AuthorRole.User, "Hello"); + + // Act + IEnumerable result = await DefaultTransforms.FromInput(input); + + // Assert + Assert.Single(result); + Assert.Equal(input, result.First()); + } + + [Fact] + public async Task FromInputAsync_WithStringInput_ReturnsUserChatMessageAsync() + { + // Arrange + string input = "Hello, world!"; + + // Act + IEnumerable result = await DefaultTransforms.FromInput(input); + + // Assert + Assert.Single(result); + ChatMessageContent message = result.First(); + Assert.Equal(AuthorRole.User, message.Role); + Assert.Equal(input, message.Content); + } + + [Fact] + public async Task FromInputAsync_WithObjectInput_SerializesAsJsonAsync() + { + // Arrange + TestObject input = new() { Id = 1, Name = "Test" }; + + // Act + IEnumerable result = await DefaultTransforms.FromInput(input); + + // Assert + Assert.Single(result); + ChatMessageContent message = result.First(); + Assert.Equal(AuthorRole.User, message.Role); + + string expectedJson = JsonSerializer.Serialize(input); + Assert.Equal(expectedJson, message.Content); + } + + [Fact] + public async Task ToOutputAsync_WithOutputTypeMatchingInputList_ReturnsSameListAsync() + { + // Arrange + IList input = + [ + new(AuthorRole.User, "Hello"), + new(AuthorRole.Assistant, "Hi there") + ]; + + // Act + IList result = await DefaultTransforms.ToOutput>(input); + + // Assert + Assert.Same(input, result); + } + + [Fact] + public async Task ToOutputAsync_WithOutputTypeChatMessageContent_ReturnsSingleMessageAsync() + { + // Arrange + IList input = + [ + new(AuthorRole.User, "Hello") + ]; + + // Act + ChatMessageContent result = await DefaultTransforms.ToOutput(input); + + // Assert + Assert.Same(input[0], result); + } + + [Fact] + public async Task ToOutputAsync_WithOutputTypeString_ReturnsContentOfSingleMessageAsync() + { + // Arrange + string expected = "Hello, world!"; + IList input = + [ + new(AuthorRole.User, expected) + ]; + + // Act + string result = await DefaultTransforms.ToOutput(input); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public async Task ToOutputAsync_WithOutputTypeDeserializable_DeserializesFromContentAsync() + { + // Arrange + TestObject expected = new() { Id = 42, Name = "TestName" }; + string json = JsonSerializer.Serialize(expected); + IList input = + [ + new(AuthorRole.User, json) + ]; + + // Act + TestObject result = await DefaultTransforms.ToOutput(input); + + // Assert + Assert.Equal(expected.Id, result.Id); + Assert.Equal(expected.Name, result.Name); + } + + [Fact] + public async Task ToOutputAsync_WithInvalidJson_ThrowsExceptionAsync() + { + // Arrange + IList input = + [ + new(AuthorRole.User, "Not valid JSON") + ]; + + // Act & Assert + await Assert.ThrowsAsync(async () => + await DefaultTransforms.ToOutput(input) + ); + } + + [Fact] + public async Task ToOutputAsync_WithMultipleMessagesAndNonMatchingType_ThrowsExceptionAsync() + { + // Arrange + IList input = + [ + new(AuthorRole.User, "Hello"), + new(AuthorRole.Assistant, "Hi there") + ]; + + // Act & Assert + await Assert.ThrowsAsync(async () => + await DefaultTransforms.ToOutput(input) + ); + } + + [Fact] + public async Task ToOutputAsync_WithNullContent_HandlesGracefullyAsync() + { + // Arrange + IList input = + [ + new(AuthorRole.User, (string?)null) + ]; + + // Act + string result = await DefaultTransforms.ToOutput(input); + + // Assert + Assert.Equal(string.Empty, result); + } + + private class TestObject + { + public int Id { get; set; } + public string? Name { get; set; } + } +} From fae03ff896b0d8afe98b5e0010eb7a0ebadc0f72 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 22:10:39 -0700 Subject: [PATCH 70/98] Formatting --- .../UnitTests/Orchestration/DefaultTransformsTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs index bf21ae4c2fcd..a223c3d14590 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/DefaultTransformsTests.cs @@ -18,11 +18,11 @@ public class DefaultTransformsTests public async Task FromInputAsync_WithEnumerableOfChatMessageContent_ReturnsInputAsync() { // Arrange - IEnumerable input = new List - { + IEnumerable input = + [ new(AuthorRole.User, "Hello"), new(AuthorRole.Assistant, "Hi there") - }; + ]; // Act IEnumerable result = await DefaultTransforms.FromInput(input); @@ -158,7 +158,7 @@ public async Task ToOutputAsync_WithInvalidJson_ThrowsExceptionAsync() ]; // Act & Assert - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await DefaultTransforms.ToOutput(input) ); } @@ -195,7 +195,7 @@ public async Task ToOutputAsync_WithNullContent_HandlesGracefullyAsync() Assert.Equal(string.Empty, result); } - private class TestObject + private sealed class TestObject { public int Id { get; set; } public string? Name { get; set; } From ffcf05ca2338b63ab98190890ddaabb6f28e39f0 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 22:33:17 -0700 Subject: [PATCH 71/98] Readme --- dotnet/samples/GettingStartedWithAgents/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dotnet/samples/GettingStartedWithAgents/README.md b/dotnet/samples/GettingStartedWithAgents/README.md index abe365fbe665..eb4aa3bbbff3 100644 --- a/dotnet/samples/GettingStartedWithAgents/README.md +++ b/dotnet/samples/GettingStartedWithAgents/README.md @@ -62,6 +62,20 @@ Example|Description [Step05_BedrockAgent_FileSearch](./BedrockAgent/Step05_BedrockAgent_FileSearch.cs)|How to use file search with a Bedrock agent (i.e. Bedrock knowledge base). [Step06_BedrockAgent_AgentChat](./BedrockAgent/Step06_BedrockAgent_AgentChat.cs)|How to create a conversation between two agents and one of them in a Bedrock agent. +### Orchestration + +Example|Description +---|--- +[Step01_Concurrent](./Orchestration/Step01_Concurrent.cs)|How to use a concurrent orchestration.. +[Step01a_ConcurrentWithStructuredOutput](./Orchestration/Step01a_ConcurrentWithStructuredOutput.cs)|How to use structured output (with concurrent orchestration). +[Step02_Sequential](./Orchestration/Step02_Sequential.cs)|How to use sequential orchestration. +[Step02a_Sequential](./Orchestration/Step02a_Sequential.cs)|How to cancel an orchestration (with sequential orchestration). +[Step03_GroupChat](./Orchestration/Step03_GroupChat.cs)|How to use group-chat orchestration. +[Step03a_GroupChatWithHumanInTheLoop](./Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs)|How to use group-chat orchestration with human in the loop. +[Step03b_GroupChatWithAIManager](./Orchestration/Step03b_GroupChatWithAIManager.cs)|How to use group-chat orchestration with a AI powered group-manager. +[Step04_Handoff](./Orchestration/Step04_Handoff.cs)|How to use handoff orchestration. +[Step04b_HandoffWithStructuredInput](./Orchestration/Step04b_HandoffWithStructuredInput.cs)|How to use structured input (with handoff orchestration). + ## Legacy Agents Support for the OpenAI Assistant API was originally published in `Microsoft.SemanticKernel.Experimental.Agents` package: From 8d7975eca3cdd59d083f8332a7a5950fd2c85f6e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 23:01:54 -0700 Subject: [PATCH 72/98] Straggler --- .../Orchestration/Step03b_GroupChatWithAIManager.cs | 3 +-- .../Orchestration/GroupChat/GroupChatManager.cs | 3 +-- .../Orchestration/GroupChat/GroupChatManagerActor.cs | 5 ++--- .../GroupChat/GroupChatOrchestration.cs | 3 +-- .../ChatGroup.cs => GroupChat/GroupChatTeam.cs} | 10 +++++----- .../GroupChat/RoundRobinGroupChatManager.cs | 3 +-- .../Orchestration/ChatGroupExtensionsTests.cs | 12 ++++++------ 7 files changed, 17 insertions(+), 22 deletions(-) rename dotnet/src/Agents/Orchestration/{Chat/ChatGroup.cs => GroupChat/GroupChatTeam.cs} (62%) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs index ff63d4915ae1..cd50166a44ea 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs @@ -4,7 +4,6 @@ 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.Agents.Runtime.InProcess; using Microsoft.SemanticKernel.ChatCompletion; @@ -175,7 +174,7 @@ public override ValueTask> FilterResults(ChatHist this.GetResponseAsync(history, Prompts.Filter(topic), cancellationToken); /// - public override ValueTask> SelectNextAgent(ChatHistory history, ChatGroup team, CancellationToken cancellationToken = default) => + public override ValueTask> SelectNextAgent(ChatHistory history, GroupChatTeam team, CancellationToken cancellationToken = default) => this.GetResponseAsync(history, Prompts.Selection(topic, team.FormatList()), cancellationToken); /// diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index bdaf5c4c69d4..f5dbad9de45a 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -67,7 +66,7 @@ protected GroupChatManager() { } /// The group of agents participating in the chat. /// A cancellation token that can be used to cancel the operation. /// A containing the identifier of the next agent as a string. - public abstract ValueTask> SelectNextAgent(ChatHistory history, ChatGroup team, CancellationToken cancellationToken = default); + public abstract ValueTask> SelectNextAgent(ChatHistory history, GroupChatTeam team, CancellationToken cancellationToken = default); /// /// Determines whether user input should be requested based on the provided chat history. diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs index 12d7f126bb0c..cff379ded649 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManagerActor.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Runtime; using Microsoft.SemanticKernel.Agents.Runtime.Core; using Microsoft.SemanticKernel.ChatCompletion; @@ -25,7 +24,7 @@ internal sealed class GroupChatManagerActor : private readonly AgentType _orchestrationType; private readonly GroupChatManager _manager; private readonly ChatHistory _chat; - private readonly ChatGroup _team; + private readonly GroupChatTeam _team; /// /// Initializes a new instance of the class. @@ -37,7 +36,7 @@ internal sealed class GroupChatManagerActor : /// The team of agents being orchestrated /// Identifies the orchestration agent. /// The logger to use for the actor - public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, ChatGroup team, AgentType orchestrationType, ILogger? logger = null) + public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, AgentType orchestrationType, ILogger? logger = null) : base(id, runtime, context, DefaultDescription, logger) { this._chat = []; diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index eb64675ff662..4d10fa8f20fa 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; using Microsoft.SemanticKernel.Agents.Runtime; @@ -51,7 +50,7 @@ protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IE AgentType outputType = await registrar.RegisterResultTypeAsync(response => [response.Message]).ConfigureAwait(false); int agentCount = 0; - ChatGroup team = []; + GroupChatTeam team = []; foreach (Agent agent in this.Members) { ++agentCount; diff --git a/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatTeam.cs similarity index 62% rename from dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs rename to dotnet/src/Agents/Orchestration/GroupChat/GroupChatTeam.cs index 3c362259b05c..1870d68ce489 100644 --- a/dotnet/src/Agents/Orchestration/Chat/ChatGroup.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatTeam.cs @@ -3,15 +3,15 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.SemanticKernel.Agents.Orchestration.Chat; +namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; /// /// Describes a team of agents participating in a group chat. /// -public class ChatGroup : Dictionary; +public class GroupChatTeam : Dictionary; /// -/// Extensions for . +/// Extensions for . /// public static class ChatGroupExtensions { @@ -20,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 GroupChatTeam 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 ChatGroup team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); + public static string FormatList(this GroupChatTeam team) => string.Join("\n", team.Select(t => $"- {t.Key}: {t.Value.Description}")); } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs index 2010a3f3dcaf..56b789a5486a 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; @@ -23,7 +22,7 @@ public override ValueTask> FilterResults(ChatHist ValueTask.FromResult(new GroupChatManagerResult(history.LastOrDefault()?.Content ?? string.Empty) { Reason = "Default result filter provides the final chat message." }); /// - public override ValueTask> SelectNextAgent(ChatHistory history, ChatGroup team, CancellationToken cancellationToken = default) + public override ValueTask> SelectNextAgent(ChatHistory history, GroupChatTeam team, CancellationToken cancellationToken = default) { string nextAgent = team.Skip(this._currentAgentIndex).First().Key; this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count; diff --git a/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs index 8a5f3ed7b88c..211bf716df72 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/ChatGroupExtensionsTests.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel.Agents.Orchestration.Chat; +using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat; using Xunit; namespace SemanticKernel.Agents.UnitTests.Orchestration; @@ -11,7 +11,7 @@ public class ChatGroupExtensionsTests public void FormatNames_WithMultipleAgents_ReturnsCommaSeparatedList() { // Arrange - ChatGroup group = new() + GroupChatTeam group = new() { { "AgentOne", ("agent1", "First agent description") }, { "AgentTwo", ("agent2", "Second agent description") }, @@ -29,7 +29,7 @@ public void FormatNames_WithMultipleAgents_ReturnsCommaSeparatedList() public void FormatNames_WithSingleAgent_ReturnsSingleName() { // Arrange - ChatGroup group = new() + GroupChatTeam group = new() { { "AgentOne", ("agent1", "First agent description") }, }; @@ -45,7 +45,7 @@ public void FormatNames_WithSingleAgent_ReturnsSingleName() public void FormatNames_WithEmptyGroup_ReturnsEmptyString() { // Arrange - ChatGroup group = []; + GroupChatTeam group = []; // Act string result = group.FormatNames(); @@ -58,7 +58,7 @@ public void FormatNames_WithEmptyGroup_ReturnsEmptyString() public void FormatList_WithMultipleAgents_ReturnsMarkdownList() { // Arrange - ChatGroup group = new() + GroupChatTeam group = new() { { "AgentOne", ("agent1", "First agent description") }, { "AgentTwo", ("agent2", "Second agent description") }, @@ -82,7 +82,7 @@ public void FormatList_WithMultipleAgents_ReturnsMarkdownList() public void FormatList_WithEmptyGroup_ReturnsEmptyString() { // Arrange - ChatGroup group = []; + GroupChatTeam group = []; // Act & Assert Assert.Equal(string.Empty, group.FormatNames()); From f711984114317aba08b6fbd5edad9a7d857f8cd5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 May 2025 23:07:24 -0700 Subject: [PATCH 73/98] Test input --- .../GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index cb77e71d2882..037b8b816d30 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -40,7 +40,7 @@ public async Task ConcurrentTaskAsync() await runtime.StartAsync(); // Run the orchestration - string input = "The quick brown fox jumps over the lazy dog"; + string input = "What is temperature?"; Console.WriteLine($"\n# INPUT: {input}\n"); OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); From 958d3341e4426028199761739e06b36c480592b8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 May 2025 12:47:15 -0700 Subject: [PATCH 74/98] Handoff update --- .../Orchestration/Step03_GroupChat.cs | 4 +- .../Step03a_GroupChatWithHumanInTheLoop.cs | 2 +- .../Step03b_GroupChatWithAIManager.cs | 2 +- .../Orchestration/Step04_Handoff.cs | 17 +-- ... => Step04a_HandoffWithStructuredInput.cs} | 2 +- .../AgentOrchestration.RequestActor.cs | 2 +- .../AgentOrchestration.ResultActor.cs | 23 +--- .../Orchestration/Handoff/HandoffActor.cs | 6 + .../Handoff/HandoffConnection.cs | 16 --- .../Handoff/HandoffOrchestration.String.cs | 4 +- .../Handoff/HandoffOrchestration.cs | 14 +- .../Agents/Orchestration/Handoff/Handoffs.cs | 125 ++++++++++++++++++ .../HandoffOrchestrationTests.cs | 2 +- .../AgentUtilities/BaseOrchestrationTest.cs | 2 +- 14 files changed, 161 insertions(+), 60 deletions(-) rename dotnet/samples/GettingStartedWithAgents/Orchestration/{Step04b_HandoffWithStructuredInput.cs => Step04a_HandoffWithStructuredInput.cs} (98%) delete mode 100644 dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs create mode 100644 dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 67c40a32c3c1..25fce8d70301 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -14,7 +14,7 @@ namespace GettingStarted.Orchestration; public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task AuthorCriticAsync() + public async Task GroupChatAsync() { // Define the agents ChatCompletionAgent writer = @@ -63,7 +63,7 @@ Consider suggestions when refining an idea. string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; Console.WriteLine($"\n# INPUT: {input}\n"); OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 4)); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3)); Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs index 3608ebdc48d3..6305915192c1 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs @@ -70,7 +70,7 @@ Consider suggestions when refining an idea. string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; Console.WriteLine($"\n# INPUT: {input}\n"); OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 4)); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3)); Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs index cd50166a44ea..39bb9446d6c4 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs @@ -135,7 +135,7 @@ You are in a debate. Feel free to challenge the other participants with respect. // Run the orchestration Console.WriteLine($"\n# INPUT: {topic}\n"); OrchestrationResult result = await orchestration.InvokeAsync(topic, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 4)); + string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3)); Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index 22bb3064fa81..f3f049aecd68 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -42,18 +42,9 @@ public async Task TriageAsync() // Define the orchestration OrchestrationMonitor monitor = new(); HandoffOrchestration orchestration = - new(handoffs: - new() - { - { - triageAgent.Name!, - new() - { - { pythonAgent.Name!, pythonAgent.Description! }, - { dotnetAgent.Name!, dotnetAgent.Description! }, - } - } - }, + new(new OrchestrationHandoffs() + .Add(triageAgent, dotnetAgent) + .Add(triageAgent, pythonAgent), triageAgent, pythonAgent, dotnetAgent) @@ -109,6 +100,8 @@ Analyze the previous message to determine count of words. // Define the pattern InProcessRuntime runtime = new(); + //Dictionary test = []; + //Dictionary> test = []; HandoffOrchestration orchestration = new(handoffs: [], agent1) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs similarity index 98% rename from dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs rename to dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs index 6e5ffedda97d..2e1ccc34d041 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04b_HandoffWithStructuredInput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs @@ -12,7 +12,7 @@ namespace GettingStarted.Orchestration; /// /// Demonstrates how to use the . /// -public class Step04b_HandoffWithStructuredInput(ITestOutputHelper output) : BaseOrchestrationTest(output) +public class Step04a_HandoffWithStructuredInput(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] public async Task HandoffStructuredInputAsync() diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs index e3ad0952f790..d1a65b1b3b0b 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.RequestActor.cs @@ -62,7 +62,7 @@ public async ValueTask HandleAsync(TInput item, MessageContext messageContext) this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id); await task.ConfigureAwait(false); } - catch (Exception exception) + catch (Exception exception) when (!exception.IsCriticalException()) { // Log exception details and allow orchestration to fail this.Logger.LogOrchestrationRequestFailure(this.Context.Orchestration, this.Id, exception); diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs index 87ba333949b7..2d7e8bbf37a8 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.ResultActor.cs @@ -46,11 +46,6 @@ public ResultActor( this._transform = transformOutput; } - /// - /// Gets or sets the optional target agent type to which the output message is forwarded. - /// - public AgentType? CompletionTarget { get; init; } - /// /// 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. @@ -65,27 +60,19 @@ public async ValueTask HandleAsync(TResult item, MessageContext messageContext) try { - IList result = this._transformResult.Invoke(item); - TOutput output = await this._transform.Invoke(result).ConfigureAwait(false); - - if (this.CompletionTarget.HasValue) + if (!this._completionSource.Task.IsCompleted) { - await this.SendMessageAsync(output!, this.CompletionTarget.Value, messageContext.CancellationToken).ConfigureAwait(false); + IList result = this._transformResult.Invoke(item); + TOutput output = await this._transform.Invoke(result).ConfigureAwait(false); + this._completionSource.TrySetResult(output); } - - this._completionSource?.SetResult(output); } catch (Exception exception) { // Log exception details and fail orchestration as per design. this.Logger.LogOrchestrationResultFailure(this.Context.Orchestration, this.Id, exception); - - if (this._completionSource == null) - { - throw; - } - this._completionSource.SetException(exception); + throw; } } } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index ba4be9046ebf..bc9944631c9d 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -39,6 +40,11 @@ internal sealed class HandoffActor : public HandoffActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, HandoffLookup handoffs, AgentType resultHandoff, ILogger? logger = null) : base(id, runtime, context, agent, logger) { + if (handoffs.ContainsKey(agent.Name ?? agent.Id)) + { + throw new ArgumentException($"The agent {agent.Name ?? agent.Id} cannot have a handoff to itself.", nameof(handoffs)); + } + this._cache = []; this._handoffs = handoffs; this._resultHandoff = resultHandoff; diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs deleted file mode 100644 index eeffca284c66..000000000000 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffConnection.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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/HandoffOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs index e5c79a477961..21f5fb3c5eca 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.String.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; - namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// @@ -15,7 +13,7 @@ public sealed class HandoffOrchestration : HandoffOrchestration /// /// Defines the handoff connections for each agent. /// The agents to be orchestrated. - public HandoffOrchestration(Dictionary handoffs, params Agent[] members) + public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] members) : base(handoffs, members) { } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index 8ab4a20e7323..433dae83b768 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.Orchestration.Extensions; @@ -17,16 +18,23 @@ public class HandoffOrchestration : AgentOrchestration)); - private readonly Dictionary _handoffs; + private readonly OrchestrationHandoffs _handoffs; /// /// Initializes a new instance of the class. /// /// Defines the handoff connections for each agent. /// The agents participating in the orchestration. - public HandoffOrchestration(Dictionary handoffs, params Agent[] agents) + public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agents) : base(OrchestrationName, agents) { + HashSet agentNames = agents.Select(a => a.Name ?? a.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); + string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))]; + if (badNames.Length > 0) + { + throw new ArgumentException($"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}", nameof(handoffs)); + } + this._handoffs = handoffs; } @@ -77,7 +85,7 @@ await runtime.RegisterAgentFactoryAsync( } // Complete the handoff model - foreach ((string agentName, HandoffConnections handoffs) in this._handoffs) + foreach ((string agentName, AgentHandoffs handoffs) in this._handoffs) { // Retrieve the map for the agent (every agent had an empty map created) HandoffLookup agentHandoffs = handoffMap[agentName]; diff --git a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs new file mode 100644 index 000000000000..f6c35524f082 --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs @@ -0,0 +1,125 @@ +// 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. +/// Maps target agent names/IDs to handoff descriptions. +/// +public sealed class AgentHandoffs : Dictionary +{ + /// + /// Initializes a new instance of the class with no handoff relationships. + /// + public AgentHandoffs() { } + + /// + /// Initializes a new instance of the class with the specified handoff relationships. + /// + /// A dictionary mapping target agent names/IDs to handoff descriptions. + public AgentHandoffs(Dictionary handoffs) : base(handoffs) { } +} + +/// +/// Defines the orchestration handoff relationships for all agents in the system. +/// Maps source agent names/IDs to their . +/// +public sealed class OrchestrationHandoffs : Dictionary +{ + /// + /// Initializes a new instance of the class with no handoff relationships. + /// + public OrchestrationHandoffs() { } + + /// + /// Initializes a new instance of the class with the specified handoff relationships. + /// + /// A dictionary mapping source agent names/IDs to their . + public OrchestrationHandoffs(Dictionary handoffs) : base(handoffs) { } +} + +/// +/// Extension methods for building and modifying relationships. +/// +public static class OrchestrationHandoffsExtensions +{ + /// + /// Adds handoff relationships from a source agent to one or more target agents. + /// Each target agent's name or ID is mapped to its description. + /// + /// The orchestration handoffs collection to update. + /// The source agent. + /// The target agents to add as handoff targets for the source agent. + /// The updated instance. + public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, params Agent[] targets) + { + string key = source.Name ?? source.Id; + + AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(key); + + foreach (Agent target in targets) + { + agentHandoffs[target.Name ?? target.Id] = target.Description ?? string.Empty; + } + + return handoffs; + } + + /// + /// Adds a handoff relationship from a source agent to a target agent with a custom description. + /// + /// The orchestration handoffs collection to update. + /// The source agent. + /// The target agent. + /// The handoff description. + /// The updated instance. + public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, Agent target, string description) + => handoffs.Add(source.Name ?? source.Id, target.Name ?? target.Id, description); + + /// + /// Adds a handoff relationship from a source agent to a target agent name/ID with a custom description. + /// + /// The orchestration handoffs collection to update. + /// The source agent. + /// The target agent's name or ID. + /// The handoff description. + /// The updated instance. + public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, string targetName, string description) + => handoffs.Add(source.Name ?? source.Id, targetName, description); + + /// + /// Adds a handoff relationship from a source agent name/ID to a target agent name/ID with a custom description. + /// + /// The orchestration handoffs collection to update. + /// The source agent's name or ID. + /// The target agent's name or ID. + /// The handoff description. + /// The updated instance. + public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, string sourceName, string targetName, string description) + { + AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(sourceName); + agentHandoffs[targetName] = description; + + return handoffs; + } + + private static AgentHandoffs GetAgentHandoffs(this OrchestrationHandoffs handoffs, string key) + { + if (!handoffs.TryGetValue(key, out AgentHandoffs? agentHandoffs)) + { + agentHandoffs = []; + handoffs[key] = agentHandoffs; + } + + return agentHandoffs; + } +} + +/// +/// Handoff relationships post-processed into a name-based lookup table that includes the agent type and handoff description. +/// Maps agent names/IDs to a tuple of and handoff description. +/// +internal sealed class HandoffLookup : Dictionary; diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index ea2e4b504817..859aada5cf22 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -81,7 +81,7 @@ public async Task HandoffOrchestrationWithMultipleAgentsAsync() Assert.Equal(1, mockAgent3.InvokeCount); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, Dictionary? handoffs, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, Dictionary? handoffs, params Agent[] mockAgents) { // Act await runtime.StartAsync(); diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs index afaada4f009f..0065c68e5934 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseOrchestrationTest.cs @@ -10,7 +10,7 @@ /// public abstract class BaseOrchestrationTest(ITestOutputHelper output) : BaseAgentsTest(output) { - protected const int ResultTimeoutInSeconds = 15; + protected const int ResultTimeoutInSeconds = 30; protected ChatCompletionAgent CreateAgent(string instructions, string? name = null, string? description = null) { From 968ee0f09461f1e2ef4303852c8540971de1f92e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 May 2025 12:53:15 -0700 Subject: [PATCH 75/98] Handoff cleanup --- .../Orchestration/Step04_Handoff.cs | 4 +--- .../Step04a_HandoffWithStructuredInput.cs | 14 ++------------ .../src/Agents/Orchestration/Handoff/Handoffs.cs | 11 ++++++++++- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index f3f049aecd68..451132893ae0 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -42,9 +42,7 @@ public async Task TriageAsync() // Define the orchestration OrchestrationMonitor monitor = new(); HandoffOrchestration orchestration = - new(new OrchestrationHandoffs() - .Add(triageAgent, dotnetAgent) - .Add(triageAgent, pythonAgent), + new(OrchestrationHandoffs.Add(triageAgent, dotnetAgent, pythonAgent), triageAgent, pythonAgent, dotnetAgent) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs index 2e1ccc34d041..715b24a83cd3 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; +using Amazon.Runtime.Internal.Transform; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; @@ -42,18 +43,7 @@ public async Task HandoffStructuredInputAsync() // Define the orchestration HandoffOrchestration orchestration = - new(handoffs: - new() - { - { - triageAgent.Name!, - new() - { - { pythonAgent.Name!, pythonAgent.Description! }, - { dotnetAgent.Name!, dotnetAgent.Description! }, - } - } - }, + new(OrchestrationHandoffs.Add(triageAgent, dotnetAgent, pythonAgent), triageAgent, pythonAgent, dotnetAgent) diff --git a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs index f6c35524f082..3a1a83efe238 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs @@ -39,11 +39,20 @@ public OrchestrationHandoffs() { } /// /// A dictionary mapping source agent names/IDs to their . public OrchestrationHandoffs(Dictionary handoffs) : base(handoffs) { } + + /// + /// Adds handoff relationships from a source agent to one or more target agents. + /// Each target agent's name or ID is mapped to its description. + /// + /// The source agent. + /// The target agents to add as handoff targets for the source agent. + /// The updated instance. + public static OrchestrationHandoffs Add(Agent source, params Agent[] targets) + => new OrchestrationHandoffs().Add(source, targets); } /// /// Extension methods for building and modifying relationships. -/// public static class OrchestrationHandoffsExtensions { /// From 2e763cbd2cdcc80dc70b9e3067efe8f2a5ac72bb Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 May 2025 13:12:49 -0700 Subject: [PATCH 76/98] Namespace --- .../Orchestration/Step04a_HandoffWithStructuredInput.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs index 715b24a83cd3..1a72ae846889 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; -using Amazon.Runtime.Internal.Transform; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; From 301ec29785d0fe98a3ce5ae62eb84973c76f7762 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 May 2025 13:28:26 -0700 Subject: [PATCH 77/98] Final refactoring --- dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs | 1 + .../UnitTests/Orchestration/HandoffOrchestrationTests.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs index 3a1a83efe238..2420a2893637 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs @@ -53,6 +53,7 @@ public static OrchestrationHandoffs Add(Agent source, params Agent[] targets) /// /// Extension methods for building and modifying relationships. +/// public static class OrchestrationHandoffsExtensions { /// diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index 859aada5cf22..330903001bbc 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration; @@ -81,7 +80,7 @@ public async Task HandoffOrchestrationWithMultipleAgentsAsync() Assert.Equal(1, mockAgent3.InvokeCount); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, Dictionary? handoffs, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, OrchestrationHandoffs? handoffs, params Agent[] mockAgents) { // Act await runtime.StartAsync(); From eeb42a3c9165d0590bb00ecbd1de0ac00e60e878 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 12 May 2025 16:38:45 -0700 Subject: [PATCH 78/98] Fix test --- .../AzureAIAgent/Step08_AzureAIAgent_Declarative.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs index bc0922498775..9a388a2e19cc 100644 --- a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs +++ b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs @@ -32,7 +32,7 @@ public async Task AzureAIAgentWithConfiguration() model: id: ${AzureAI:ChatModelId} connection: - connection_string: ${AzureAI:ConnectionString} + endpoint: ${AzureAI:Endpoint} """; AzureAIAgentFactory factory = new(); From 7c939bd5900404818ec1a051c7f3818ec59acc07 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 12 May 2025 16:41:30 -0700 Subject: [PATCH 79/98] Rollback --- .../AzureAIAgent/Step08_AzureAIAgent_Declarative.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs index 9a388a2e19cc..bc0922498775 100644 --- a/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs +++ b/dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step08_AzureAIAgent_Declarative.cs @@ -32,7 +32,7 @@ public async Task AzureAIAgentWithConfiguration() model: id: ${AzureAI:ChatModelId} connection: - endpoint: ${AzureAI:Endpoint} + connection_string: ${AzureAI:ConnectionString} """; AzureAIAgentFactory factory = new(); From 364446b5ea8ab8fc8cd0b4e8300a8faca91017d8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 12 May 2025 23:37:47 -0700 Subject: [PATCH 80/98] Handoff unit-tests --- .../Orchestration/Step04_Handoff.cs | 37 --- .../Orchestration/Handoff/HandoffActor.cs | 17 +- .../Handoff/HandoffOrchestration.cs | 3 + .../Agents/Orchestration/Handoff/Handoffs.cs | 2 +- .../HandoffOrchestrationLogMessages.cs | 4 +- .../HandoffOrchestrationTests.cs | 227 +++++++++++++---- .../UnitTests/Orchestration/HandoffsTests.cs | 238 ++++++++++++++++++ 7 files changed, 425 insertions(+), 103 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index 451132893ae0..9c323abc78ca 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -81,43 +81,6 @@ public async Task TriageAsync() } } - [Fact] - public async Task NoHandoffAsync() - { - // 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(); - //Dictionary test = []; - //Dictionary> test = []; - HandoffOrchestration orchestration = - new(handoffs: [], - agent1) - { - LoggerFactory = this.LoggerFactory - }; - - // Start the runtime - await runtime.StartAsync(); - string input = "Tell me the count of words in: The quick brown fox jumps over the lazy dog"; - Console.WriteLine($"\n# INPUT: {input}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); - Console.WriteLine($"\n# RESULT: {text}"); - - await runtime.RunUntilIdleAsync(); - } - private sealed class GithubPlugin { public Dictionary Labels { get; } = []; diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index bc9944631c9d..aa1f788b6466 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -24,7 +24,7 @@ internal sealed class HandoffActor : private readonly AgentType _resultHandoff; private readonly List _cache; - private AgentType? _handoffAgentType; + private string? _handoffAgent; private string? _taskSummary; /// @@ -113,11 +113,12 @@ public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false); } - if (this._handoffAgentType != null) + if (this._handoffAgent != null) { - await this.SendMessageAsync(new HandoffMessages.Request(), this._handoffAgentType.Value, messageContext.CancellationToken).ConfigureAwait(false); + AgentType handoffType = this._handoffs[this._handoffAgent].AgentType; + await this.SendMessageAsync(new HandoffMessages.Request(), handoffType, messageContext.CancellationToken).ConfigureAwait(false); - this._handoffAgentType = null; + this._handoffAgent = null; break; } @@ -147,7 +148,7 @@ IEnumerable CreateHandoffFunctions() { KernelFunction kernelFunction = KernelFunctionFactory.CreateFromMethod( - (CancellationToken cancellationToken) => this.HandoffAsync(type, cancellationToken), + (CancellationToken cancellationToken) => this.HandoffAsync(name, cancellationToken), functionName: $"transfer_to_{name}", description: description); @@ -156,10 +157,10 @@ IEnumerable CreateHandoffFunctions() } } - private ValueTask HandoffAsync(AgentType agentType, CancellationToken cancellationToken = default) + private ValueTask HandoffAsync(string agentName, CancellationToken cancellationToken = default) { - this.Logger.LogHandoffFunctionCall(this.Id, agentType); - this._handoffAgentType = agentType; + this.Logger.LogHandoffFunctionCall(this.Id, agentName); + this._handoffAgent = agentName; return ValueTask.CompletedTask; } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index 433dae83b768..0328e3b00785 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -28,8 +28,11 @@ public class HandoffOrchestration : AgentOrchestration agentNames = agents.Select(a => a.Name ?? a.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); + // Extract names from handoffs that don't align with a member agent. string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))]; + // Fail fast if invalid names are present. if (badNames.Length > 0) { throw new ArgumentException($"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}", nameof(handoffs)); diff --git a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs index 2420a2893637..4f55c4433678 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs @@ -132,4 +132,4 @@ private static AgentHandoffs GetAgentHandoffs(this OrchestrationHandoffs handoff /// Handoff relationships post-processed into a name-based lookup table that includes the agent type and handoff description. /// Maps agent names/IDs to a tuple of and handoff description. /// -internal sealed class HandoffLookup : Dictionary; +internal sealed class HandoffLookup : Dictionary; diff --git a/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs b/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs index f7865191d04d..59df59911f0c 100644 --- a/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs +++ b/dotnet/src/Agents/Orchestration/Logging/HandoffOrchestrationLogMessages.cs @@ -37,11 +37,11 @@ public static partial void LogHandoffAgentResult( [LoggerMessage( EventId = 0, Level = LogLevel.Trace, - Message = "TOOL Handoff [{AgentId}]: {Handoff}")] + Message = "TOOL Handoff [{AgentId}]: {Name}")] public static partial void LogHandoffFunctionCall( this ILogger logger, AgentId agentId, - AgentType handoff); + string name); [LoggerMessage( EventId = 0, diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index 330903001bbc..12db1e0cbb97 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; 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; @@ -14,79 +16,85 @@ namespace SemanticKernel.Agents.UnitTests.Orchestration; /// /// Tests for the class. /// -public class HandoffOrchestrationTests +public class HandoffOrchestrationTests : IDisposable { - [Fact(Skip = "Mock agent unable to provide expected function calls")] + private readonly List _disposables; + + /// + /// Initializes a new instance of the class. + /// + public HandoffOrchestrationTests() + { + this._disposables = []; + } + + /// + public void Dispose() + { + foreach (IDisposable disposable in this._disposables) + { + disposable.Dispose(); + } + GC.SuppressFinalize(this); + } + + [Fact] public async Task HandoffOrchestrationWithSingleAgentAsync() { // Arrange - await using InProcessRuntime runtime = new(); - MockAgent mockAgent1 = CreateMockAgent(2, "xyz"); + ChatCompletionAgent mockAgent1 = + this.CreateMockAgent( + "Agent1", + "Test Agent", + Responses.Message("Final response")); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(runtime, handoffs: null, mockAgent1); + string response = await ExecuteOrchestrationAsync(handoffs: null, mockAgent1); // Assert - Assert.Equal("xyz", response); - Assert.Equal(1, mockAgent1.InvokeCount); + Assert.Equal("Final response", response); } - [Fact(Skip = "Mock agent unable to provide expected function calls")] + [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"); + ChatCompletionAgent mockAgent1 = + this.CreateMockAgent( + "Agent1", + "Test Agent", + Responses.Handoff("Agent2")); + ChatCompletionAgent mockAgent2 = + this.CreateMockAgent( + "Agent2", + "Test Agent", + Responses.Result("Final response")); + ChatCompletionAgent mockAgent3 = + this.CreateMockAgent( + "Agent3", + "Test Agent", + Responses.Message("Wrong response")); // 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! }, - } - }, - }, + OrchestrationHandoffs.Add(mockAgent1, mockAgent2, mockAgent3), mockAgent1, mockAgent2, mockAgent3); // Assert - Assert.Equal("lmn", response); - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal(1, mockAgent2.InvokeCount); - Assert.Equal(1, mockAgent3.InvokeCount); + Assert.Equal("Final response", response); } - private static async Task ExecuteOrchestrationAsync(InProcessRuntime runtime, OrchestrationHandoffs? handoffs, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(OrchestrationHandoffs? handoffs, params Agent[] mockAgents) { - // Act + // Arrange + await using InProcessRuntime runtime = new(); await runtime.StartAsync(); HandoffOrchestration orchestration = new(handoffs ?? [], mockAgents); + // Act const string InitialInput = "123"; OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); @@ -94,20 +102,129 @@ private static async Task ExecuteOrchestrationAsync(InProcessRuntime run Assert.NotNull(result); // Act - string response = await result.GetValueAsync(TimeSpan.FromSeconds(20)); - + string response = await result.GetValueAsync(TimeSpan.FromSeconds(100)); await runtime.RunUntilIdleAsync(); return response; } - private static MockAgent CreateMockAgent(int index, string response) + private ChatCompletionAgent CreateMockAgent(string name, string description, string response) { - return new() - { - Name = $"agent{index}", - Description = "Provides a mock response", - Response = [new(AuthorRole.Assistant, response)] - }; + HttpMessageHandlerStub messageHandlerStub = + new() + { + ResponseToReturn = new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent(response), + }, + }; + HttpClient httpClient = new(messageHandlerStub, disposeHandler: false); + + this._disposables.Add(messageHandlerStub); + this._disposables.Add(httpClient); + + IKernelBuilder builder = Kernel.CreateBuilder(); + builder.AddOpenAIChatCompletion("gpt-test", "mykey", orgId: null, serviceId: null, httpClient); + Kernel kernel = builder.Build(); + + ChatCompletionAgent mockAgent1 = + new() + { + Name = name, + Description = description, + Kernel = kernel, + }; + + return mockAgent1; + } + + private static class Responses + { + public static string Message(string content) => + $$$""" + { + "id": "chat-123", + "object": "chat.completion", + "created": 1699482945, + "model": "gpt-4.1", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "{{{content}}}", + "tool_calls":[] + } + } + ], + "usage": { + "prompt_tokens": 52, + "completion_tokens": 1, + "total_tokens": 53 + } + } + """; + + public static string Handoff(string agentName) => + $$$""" + { + "id": "chat-123", + "object": "chat.completion", + "created": 1699482945, + "model": "gpt-4.1", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls":[{ + "id": "1", + "type": "function", + "function": { + "name": "{{{HandoffInvocationFilter.HandoffPlugin}}}-transfer_to_{{{agentName}}}", + "arguments": "{}" + } + } + ] + } + } + ], + "usage": { + "prompt_tokens": 52, + "completion_tokens": 1, + "total_tokens": 53 + } + } + """; + + public static string Result(string summary) => + $$$""" + { + "id": "chat-123", + "object": "chat.completion", + "created": 1699482945, + "model": "gpt-4.1", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls":[{ + "id": "1", + "type": "function", + "function": { + "name": "{{{HandoffInvocationFilter.HandoffPlugin}}}-end_task_with_summary", + "arguments": "{ \"summary\": \"{{{summary}}}\" }" + } + } + ] + } + } + ] + } + """; } } diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs new file mode 100644 index 000000000000..949b817ac851 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Orchestration; + +public class HandoffsTests +{ + [Fact] + public void EmptyConstructors_CreateEmptyCollections() + { + AgentHandoffs agentHandoffs = []; + Assert.Empty(agentHandoffs); + + OrchestrationHandoffs orchestrationHandoffs = []; + Assert.Empty(orchestrationHandoffs); + } + + [Fact] + public void DictionaryConstructors_CopyValues() + { + Dictionary handoffDict = new() + { + { "agent1", "description1" }, + { "agent2", "description2" } + }; + + AgentHandoffs agentHandoffs = new(handoffDict); + Assert.Equal(2, agentHandoffs.Count); + Assert.Equal("description1", agentHandoffs["agent1"]); + Assert.Equal("description2", agentHandoffs["agent2"]); + + Dictionary orchestrationDict = new() + { + { "source1", new AgentHandoffs(handoffDict) } + }; + + OrchestrationHandoffs orchestrationHandoffs = new(orchestrationDict); + Assert.Single(orchestrationHandoffs); + Assert.Equal(2, orchestrationHandoffs["source1"].Count); + } + + [Fact] + public void Add_WithAgentObjects_CreatesHandoffRelationships() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + Agent sourceAgent = CreateAgent("source", "Source Agent"); + Agent targetAgent1 = CreateAgent("target1", "Target Agent 1"); + Agent targetAgent2 = CreateAgent("target2", "Target Agent 2"); + + // Act + handoffs.Add(sourceAgent, targetAgent1, targetAgent2); + + // Assert + Assert.Single(handoffs); + Assert.True(handoffs.ContainsKey("source")); + + AgentHandoffs sourceHandoffs = handoffs["source"]; + Assert.Equal(2, sourceHandoffs.Count); + Assert.Equal("Target Agent 1", sourceHandoffs["target1"]); + Assert.Equal("Target Agent 2", sourceHandoffs["target2"]); + } + + [Fact] + public void Add_WithAgentAndCustomDescription_UsesCustomDescription() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + Agent sourceAgent = CreateAgent("source", "Source Agent"); + Agent targetAgent = CreateAgent("target", "Target Agent"); + string customDescription = "Custom handoff description"; + + // Act + handoffs.Add(sourceAgent, targetAgent, customDescription); + + // Assert + Assert.Single(handoffs); + AgentHandoffs sourceHandoffs = handoffs["source"]; + Assert.Single(sourceHandoffs); + Assert.Equal(customDescription, sourceHandoffs["target"]); + } + + [Fact] + public void Add_WithAgentAndTargetName_AddsHandoffWithDescription() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + Agent sourceAgent = CreateAgent("source", "Source Agent"); + string targetName = "targetName"; + string description = "Target description"; + + // Act + handoffs.Add(sourceAgent, targetName, description); + + // Assert + Assert.Single(handoffs); + AgentHandoffs sourceHandoffs = handoffs["source"]; + Assert.Single(sourceHandoffs); + Assert.Equal(description, sourceHandoffs[targetName]); + } + + [Fact] + public void Add_WithSourceNameAndTargetName_AddsHandoffWithDescription() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + string sourceName = "sourceName"; + string targetName = "targetName"; + string description = "Target description"; + + // Act + handoffs.Add(sourceName, targetName, description); + + // Assert + Assert.Single(handoffs); + AgentHandoffs sourceHandoffs = handoffs[sourceName]; + Assert.Single(sourceHandoffs); + Assert.Equal(description, sourceHandoffs[targetName]); + } + + [Fact] + public void Add_WithMultipleSourcesAndTargets_CreatesCorrectStructure() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + Agent source1 = CreateAgent("source1", "Source Agent 1"); + Agent source2 = CreateAgent("source2", "Source Agent 2"); + + Agent target1 = CreateAgent("target1", "Target Agent 1"); + Agent target2 = CreateAgent("target2", "Target Agent 2"); + Agent target3 = CreateAgent("target3", "Target Agent 3"); + + // Act + handoffs.Add(source1, target1, target2); + handoffs.Add(source2, target2, target3); + handoffs.Add(source1, target3, "Custom description"); + + // Assert + Assert.Equal(2, handoffs.Count); + + // Check source1's targets + AgentHandoffs source1Handoffs = handoffs["source1"]; + Assert.Equal(3, source1Handoffs.Count); + Assert.Equal("Target Agent 1", source1Handoffs["target1"]); + Assert.Equal("Target Agent 2", source1Handoffs["target2"]); + Assert.Equal("Custom description", source1Handoffs["target3"]); + + // Check source2's targets + AgentHandoffs source2Handoffs = handoffs["source2"]; + Assert.Equal(2, source2Handoffs.Count); + Assert.Equal("Target Agent 2", source2Handoffs["target2"]); + Assert.Equal("Target Agent 3", source2Handoffs["target3"]); + } + + [Fact] + public void StaticAdd_CreatesNewOrchestrationHandoffs() + { + // Arrange + Agent source = CreateAgent("source", "Source Agent"); + Agent target1 = CreateAgent("target1", "Target Agent 1"); + Agent target2 = CreateAgent("target2", "Target Agent 2"); + + // Act + OrchestrationHandoffs handoffs = OrchestrationHandoffs.Add(source, target1, target2); + + // Assert + Assert.NotNull(handoffs); + Assert.Single(handoffs); + Assert.True(handoffs.ContainsKey("source")); + + AgentHandoffs sourceHandoffs = handoffs["source"]; + Assert.Equal(2, sourceHandoffs.Count); + Assert.Equal("Target Agent 1", sourceHandoffs["target1"]); + Assert.Equal("Target Agent 2", sourceHandoffs["target2"]); + } + + [Fact] + public void Add_WithAgentsWithNoNameUsesId() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + Agent sourceAgent = CreateAgent(id: "source-id", name: null); + Agent targetAgent = CreateAgent(id: "target-id", name: null, description: "Target Description"); + + // Act + handoffs.Add(sourceAgent, targetAgent); + + // Assert + Assert.Single(handoffs); + Assert.True(handoffs.ContainsKey("source-id")); + + AgentHandoffs sourceHandoffs = handoffs["source-id"]; + Assert.Single(sourceHandoffs); + Assert.Equal("Target Description", sourceHandoffs["target-id"]); + } + + [Fact] + public void Add_WithTargetWithNoDescription_UsesEmptyString() + { + // Arrange + OrchestrationHandoffs handoffs = []; + + Agent sourceAgent = CreateAgent("source", "Source Agent"); + Agent targetAgent = CreateAgent("target", null); + + // Act + handoffs.Add(sourceAgent, targetAgent); + + // Assert + Assert.Single(handoffs); + AgentHandoffs sourceHandoffs = handoffs["source"]; + Assert.Single(sourceHandoffs); + Assert.Equal(string.Empty, sourceHandoffs["target"]); + } + + private static ChatCompletionAgent CreateAgent(string id, string? description = null, string? name = null) + { + ChatCompletionAgent mockAgent = + new() + { + Id = id, + Description = description, + Name = name, + }; + + return mockAgent; + } +} From a63e253581de8354ee56efe223caa392fbdabca4 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Tue, 13 May 2025 10:52:11 -0700 Subject: [PATCH 81/98] Update dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs Co-authored-by: Tao Chen --- .../Orchestration/Step01a_ConcurrentWithStructuredOutput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs index 64e9f40efba3..bcb08bb8a7ff 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs @@ -14,7 +14,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the with structured output. /// public class Step01a_ConcurrentWithStructuredOutput(ITestOutputHelper output) : BaseOrchestrationTest(output) { From 6a992c6552019c2d4093eb0da8dd3001f9caade1 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Tue, 13 May 2025 10:52:18 -0700 Subject: [PATCH 82/98] Update dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs Co-authored-by: Tao Chen --- .../Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs index 6305915192c1..70f6be325f0c 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs @@ -10,7 +10,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the with human in the loop. /// public class Step03a_GroupChatWithHumanInTheLoop(ITestOutputHelper output) : BaseOrchestrationTest(output) { From ff0f6f693389fdb4a6d9e943b6cb7ed9c68efd4c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 11:08:11 -0700 Subject: [PATCH 83/98] Comments --- .../Orchestration/Step01_Concurrent.cs | 3 ++- .../Orchestration/Step02_Sequential.cs | 4 +++- .../Orchestration/Step02a_Sequential.cs | 2 +- .../Orchestration/Step03_GroupChat.cs | 9 ++++++++- .../Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs | 9 ++++++++- .../Orchestration/Step03b_GroupChatWithAIManager.cs | 4 +++- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs index 037b8b816d30..9bf0afc24aea 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs @@ -9,7 +9,8 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the +/// for executing multiple agents on the same task in parallel. /// public class Step01_Concurrent(ITestOutputHelper output) : BaseOrchestrationTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs index 863df18da822..f0e08df86ebb 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs @@ -9,7 +9,9 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the for +/// executing multiple agents in sequence, i.e.the output of one agent is +/// the input to the next agent. /// public class Step02_Sequential(ITestOutputHelper output) : BaseOrchestrationTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs index a38b3394e970..56c6f79aac9f 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs @@ -8,7 +8,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use cancel a . +/// Demonstrates how to use cancel a while its running. /// public class Step02a_Sequential(ITestOutputHelper output) : BaseOrchestrationTest(output) { diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 25fce8d70301..71db32f03a73 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -9,8 +9,15 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the ith a default +/// round robin manager for controlling the flow of conversation in a round robin fashion. /// +/// +/// Think of the group chat manager as a state machine, with the following possible states: +/// - Request for user message +/// - Termination, after which the manager will try to filter a result from the conversation +/// - Continuation, at which the manager will select the next agent to speak. +/// public class Step03_GroupChat(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs index 70f6be325f0c..04bae9fa3332 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs @@ -10,7 +10,7 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the with human in the loop. +/// Demonstrates how to use the with human in the loop /// public class Step03a_GroupChatWithHumanInTheLoop(ITestOutputHelper output) : BaseOrchestrationTest(output) { @@ -76,6 +76,13 @@ Consider suggestions when refining an idea. await runtime.RunUntilIdleAsync(); } + /// + /// Define a custom group chat manager that enables user input. + /// + /// + /// User input is achieved by overriding the default round robin manager + /// to allow user input after the reviewer agent's message. + /// private sealed class CustomRoundRobinGroupChatManager : RoundRobinGroupChatManager { public override ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs index 39bb9446d6c4..5ca26e524fcb 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs @@ -12,7 +12,9 @@ namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the +/// with a group chat manager that uses a chat completion service to +/// control the flow of the conversation. /// public class Step03b_GroupChatWithAIManager(ITestOutputHelper output) : BaseOrchestrationTest(output) { From 01e73379faeb04f11a8858198290d60d74a4ef9a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 11:09:16 -0700 Subject: [PATCH 84/98] Rename sample --- ...{Step02a_Sequential.cs => Step02a_SequentialCancellation.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename dotnet/samples/GettingStartedWithAgents/Orchestration/{Step02a_Sequential.cs => Step02a_SequentialCancellation.cs} (94%) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs similarity index 94% rename from dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs rename to dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs index 56c6f79aac9f..52baa679c0f9 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_Sequential.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs @@ -10,7 +10,7 @@ namespace GettingStarted.Orchestration; /// /// Demonstrates how to use cancel a while its running. /// -public class Step02a_Sequential(ITestOutputHelper output) : BaseOrchestrationTest(output) +public class Step02a_SequentialCancellation(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] public async Task SequentialCancelledAsync() From 8eeaa1e0854b52aebeb495a1a4d67a61a5bfb1d2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 12:45:51 -0700 Subject: [PATCH 85/98] Update handoff sample --- .../Orchestration/Step04_Handoff.cs | 101 +++++++++++------- .../Orchestration/Handoff/HandoffActor.cs | 2 +- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index 9c323abc78ca..49d15e5d248a 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -5,72 +5,90 @@ using Microsoft.SemanticKernel.Agents.Orchestration; using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; using Microsoft.SemanticKernel.Agents.Runtime.InProcess; +using Microsoft.SemanticKernel.ChatCompletion; namespace GettingStarted.Orchestration; /// -/// Demonstrates how to use the . +/// Demonstrates how to use the that represents +/// a customer support triage system.The orchestration consists of 4 agents, each specialized +/// in a different area of customer support: triage, refunds, order status, and order returns. /// public class Step04_Handoff(ITestOutputHelper output) : BaseOrchestrationTest(output) { [Fact] - public async Task TriageAsync() + public async Task OrderSupportAsync() { // Initialize plugin - GithubPlugin githubPlugin = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(githubPlugin); + OrderStatusPlugin githubPlugin = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new OrderStatusPlugin()); // Define the agents ChatCompletionAgent triageAgent = this.CreateAgent( - instructions: "Given a GitHub issue, triage it.", + instructions: "A customer support agent that triages issues.", name: "TriageAgent", - description: "An agent that triages GitHub issues"); - ChatCompletionAgent pythonAgent = + description: "Handle customer requests."); + ChatCompletionAgent statusAgent = 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 = + name: "OrderStatusAgent", + instructions: "Handle order status requests.", + description: "A customer support agent that checks order status."); + statusAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderStatusPlugin())); + ChatCompletionAgent returnAgent = 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); + name: "OrderReturnAgent", + instructions: "Handle order return requests.", + description: "A customer support agent that handles order returns."); + returnAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderReturnPlugin())); + ChatCompletionAgent refundAgent = + this.CreateAgent( + name: "OrderRefundAgent", + instructions: "Handle order refund requests.", + description: "A customer support agent that handles order refund."); + refundAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderRefundPlugin())); // Define the orchestration OrchestrationMonitor monitor = new(); + Queue responses = new(); HandoffOrchestration orchestration = - new(OrchestrationHandoffs.Add(triageAgent, dotnetAgent, pythonAgent), + new(OrchestrationHandoffs + .Add(triageAgent, statusAgent, returnAgent, refundAgent) + .Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related") + .Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related") + .Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related"), triageAgent, - pythonAgent, - dotnetAgent) + statusAgent, + returnAgent, + refundAgent) { + InteractiveCallback = () => + { + string input = responses.Dequeue(); + Console.WriteLine($"\n# INPUT: {input}\n"); + return ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input)); + }, ResponseCallback = monitor.ResponseCallback, 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 InProcessRuntime runtime = new(); await runtime.StartAsync(); // Run the orchestration - Console.WriteLine($"\n# INPUT:\n{InputJson}\n"); - OrchestrationResult result = await orchestration.InvokeAsync(InputJson, runtime); + string task = "I am a customer that needs help with my orders"; + responses.Enqueue("I'd like to track the status of my order"); + responses.Enqueue("My order ID is 123"); + responses.Enqueue("I want to return another order of mine"); + responses.Enqueue("Order ID 321"); + responses.Enqueue("Broken item"); + responses.Enqueue("No, bye"); + Console.WriteLine($"\n# INPUT:\n{task}\n"); + OrchestrationResult result = await orchestration.InvokeAsync(task, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); - Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); + // %%% Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); await runtime.RunUntilIdleAsync(); @@ -81,14 +99,21 @@ public async Task TriageAsync() } } - private sealed class GithubPlugin + private sealed class OrderStatusPlugin + { + [KernelFunction] + public string CheckOrderStatus(string orderId) => $"Order {orderId} is shipped and will arrive in 2-3 days."; + } + + private sealed class OrderReturnPlugin { - public Dictionary Labels { get; } = []; + [KernelFunction] + public string ProcessReturn(string orderId, string reason) => $"Return for order {orderId} has been processed successfully."; + } + private sealed class OrderRefundPlugin + { [KernelFunction] - public void AddLabels(string issueId, params string[] labels) - { - this.Labels[issueId] = labels; - } + public string ProcessReturn(string orderId, string reason) => $"Refund for order {orderId} has been processed successfully."; } } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index aa1f788b6466..37bb5049dd69 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -122,7 +122,7 @@ public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext break; } - if (this.InteractiveCallback != null) + if (this.InteractiveCallback != null && this._taskSummary == null) { ChatMessageContent input = await this.InteractiveCallback().ConfigureAwait(false); this._cache.Add(input); From b1fa1c8762547a2900bee203bc5b3db8bc26479b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 14:15:42 -0700 Subject: [PATCH 86/98] Remove cruft --- .../Orchestration/Step04_Handoff.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index 49d15e5d248a..ecccc8b99e78 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -19,11 +19,7 @@ public class Step04_Handoff(ITestOutputHelper output) : BaseOrchestrationTest(ou [Fact] public async Task OrderSupportAsync() { - // Initialize plugin - OrderStatusPlugin githubPlugin = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new OrderStatusPlugin()); - - // Define the agents + // Define the agents & tools ChatCompletionAgent triageAgent = this.CreateAgent( instructions: "A customer support agent that triages issues.", @@ -88,7 +84,6 @@ public async Task OrderSupportAsync() OrchestrationResult result = await orchestration.InvokeAsync(task, runtime); string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); Console.WriteLine($"\n# RESULT: {text}"); - // %%% Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); await runtime.RunUntilIdleAsync(); From c2ba05e397af5188d8de0d6eb1cf3a78ac1e0446 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 15:03:15 -0700 Subject: [PATCH 87/98] Content fix --- .../Contents/StreamingAnnotationContent.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs index 60b2145d907d..c6e339f7e864 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs @@ -24,7 +24,6 @@ public class StreamingAnnotationContent : StreamingKernelContent public string FileId { get => this.ReferenceId; - init => this.ReferenceId = value; } /// @@ -40,11 +39,12 @@ public string FileId /// /// Provides context for using . /// - public AnnotationKind Kind { get; init; } + public AnnotationKind Kind { get; } /// /// The citation. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Label { get; init; } /// @@ -54,8 +54,7 @@ public string FileId /// A file is referenced for certain tools, such as file search, and also when /// and image or document is produced as part of the agent response. /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string ReferenceId { get; init; } + public string ReferenceId { get; } /// /// The title of the annotation reference (when == .. @@ -80,6 +79,7 @@ public string FileId /// /// Describes the kind of annotation /// Identifies the referenced resource. + [JsonConstructor] public StreamingAnnotationContent( AnnotationKind kind, string referenceId) From c72d70833d846501e1a994ad39c1ecb787cc6663 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 15:21:52 -0700 Subject: [PATCH 88/98] Suppression --- .../CompatibilitySuppressions.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml b/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml index 195237c69926..f925c94bf1c3 100644 --- a/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml +++ b/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml @@ -71,6 +71,13 @@ lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll true + + CP0002 + M:Microsoft.SemanticKernel.Agents.StreamingAnnotationContent.set_FileId(System.String) + lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll + lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll + true + CP0002 M:Microsoft.SemanticKernel.Agents.StreamingAnnotationContent.set_Quote(System.String) @@ -190,6 +197,13 @@ lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll true + + CP0002 + M:Microsoft.SemanticKernel.Agents.StreamingAnnotationContent.set_FileId(System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll + true + CP0002 M:Microsoft.SemanticKernel.Agents.StreamingAnnotationContent.set_Quote(System.String) From 71226ca408449f5992eb5b6c93848edb0dccb4fb Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 15:58:15 -0700 Subject: [PATCH 89/98] Revert timeout --- .../Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index 12db1e0cbb97..8a0596b5e5b1 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -102,7 +102,7 @@ private static async Task ExecuteOrchestrationAsync(OrchestrationHandoff Assert.NotNull(result); // Act - string response = await result.GetValueAsync(TimeSpan.FromSeconds(100)); + string response = await result.GetValueAsync(TimeSpan.FromSeconds(10)); await runtime.RunUntilIdleAsync(); return response; From cb08e41fb7c7c50ee68a0fe4edadcc2223faa968 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 21:38:38 -0700 Subject: [PATCH 90/98] netstandard2.0 --- .../Step02a_SequentialCancellation.cs | 2 +- .../Orchestration/Step04_Handoff.cs | 3 +- .../Orchestration/AgentOrchestration.cs | 34 ++++++++++++++----- .../Orchestration/Agents.Orchestration.csproj | 7 ++-- .../ConcurrentOrchestration.String.cs | 14 +++++++- .../Concurrent/ConcurrentOrchestration.cs | 19 +++++++++-- .../GroupChat/GroupChatAgentActor.cs | 4 +++ .../GroupChat/GroupChatManager.cs | 13 +++++-- .../GroupChat/GroupChatOrchestration.cs | 19 +++++++++-- .../GroupChat/RoundRobinGroupChatManager.cs | 29 +++++++++++++--- .../Orchestration/Handoff/HandoffActor.cs | 22 +++++++++--- .../Handoff/HandoffOrchestration.cs | 23 ++++++++----- .../Orchestration/OrchestrationResult.cs | 21 +++++++++--- .../Sequential/SequentialOrchestration.cs | 10 +++++- .../Agents/UnitTests/Agents.UnitTests.csproj | 2 +- 15 files changed, 175 insertions(+), 47 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs index 52baa679c0f9..0c55ae7e4299 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs @@ -36,7 +36,7 @@ public async Task SequentialCancelledAsync() OrchestrationResult result = await orchestration.InvokeAsync(input, runtime); - await result.CancelAsync(); + result.Cancel(); await Task.Delay(TimeSpan.FromSeconds(3)); try diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index ecccc8b99e78..02206de90e74 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -82,7 +82,8 @@ public async Task OrderSupportAsync() responses.Enqueue("No, bye"); Console.WriteLine($"\n# INPUT:\n{task}\n"); OrchestrationResult result = await orchestration.InvokeAsync(task, runtime); - string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds)); + + string text = await result.GetValueAsync(TimeSpan.FromSeconds(300)); Console.WriteLine($"\n# RESULT: {text}"); await runtime.RunUntilIdleAsync(); diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index eea8a7f69c0f..b55714909f1b 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -176,10 +176,22 @@ private async ValueTask RegisterAsync(IAgentRuntime runtime, Orchestr AgentType orchestrationEntry = await runtime.RegisterAgentFactoryAsync( this.FormatAgentType(context.Topic, "Boot"), - (agentId, runtime) => - ValueTask.FromResult( - new RequestActor(agentId, runtime, context, this.InputTransform, completion, StartAsync, context.LoggerFactory.CreateLogger())) - ).ConfigureAwait(false); + (agentId, runtime) => + { + RequestActor actor = + new(agentId, + runtime, + context, + this.InputTransform, + completion, + StartAsync, + context.LoggerFactory.CreateLogger()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }).ConfigureAwait(false); logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic); @@ -208,15 +220,21 @@ public async ValueTask RegisterResultTypeAsync(Orchestration await runtime.RegisterAgentFactoryAsync( agentType, (agentId, runtime) => - ValueTask.FromResult( - new ResultActor( - agentId, + { + ResultActor actor = + new(agentId, runtime, context, resultTransform, outputTransform, completion, - context.LoggerFactory.CreateLogger>()))).ConfigureAwait(false); + context.LoggerFactory.CreateLogger>()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }).ConfigureAwait(false); } } } diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index 5c7d3291ae93..dda5e3d0d9db 100644 --- a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj +++ b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj @@ -4,8 +4,8 @@ Microsoft.SemanticKernel.Agents.Orchestration Microsoft.SemanticKernel.Agents.Orchestration - net8.0 - + + net8.0;netstandard2.0 $(NoWarn);SKEXP0110;SKEXP0001 false preview @@ -21,8 +21,7 @@ - - + diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs index f18ca767c2c1..bfd3214fc105 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.String.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.Linq; + +#if NETCOREAPP using System.Threading.Tasks; +#endif namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; @@ -17,6 +20,15 @@ public sealed class ConcurrentOrchestration : ConcurrentOrchestration ValueTask.FromResult([.. response.Select(r => r.Content ?? string.Empty)]); + this.ResultTransform = + (response, cancellationToken) => + { + string[] result = [.. response.Select(r => r.Content ?? string.Empty)]; +#if !NETCOREAPP + return result.AsValueTask(); +#else + return ValueTask.FromResult(result); +#endif + }; } } diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 5a580cb51786..776ca11bd4db 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -45,8 +45,14 @@ protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IE await runtime.RegisterAgentFactoryAsync( resultType, (agentId, runtime) => - ValueTask.FromResult( - new ConcurrentResultActor(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + { + ConcurrentResultActor actor = new(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, resultType, "RESULTS"); // Register member actors - All agents respond to the same message. @@ -59,7 +65,14 @@ await runtime.RegisterAgentFactoryAsync( await runtime.RegisterAgentFactoryAsync( this.FormatAgentType(context.Topic, $"Agent_{agentCount}"), (agentId, runtime) => - ValueTask.FromResult(new ConcurrentActor(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + { + ConcurrentActor actor = new(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", agentCount); diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs index 320a9939d907..207702d82b4c 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatAgentActor.cs @@ -38,7 +38,11 @@ public ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messag { this._cache.AddRange(item.Messages); +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else return ValueTask.CompletedTask; +#endif } /// diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index f5dbad9de45a..b5d11efa4b86 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -86,11 +86,20 @@ public virtual ValueTask> ShouldTerminate(ChatHisto { Interlocked.Increment(ref this._invocationsCount); + bool resultValue = false; + string reason = "Maximum number of invocations has not been reached."; if (this.InvocationsCount > this.MaximumInvocations) { - return ValueTask.FromResult(new GroupChatManagerResult(true) { Reason = "Maximum number of invocations reached." }); + resultValue = true; + reason = "Maximum number of invocations reached."; } - return ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "Maximum number of invocations has not been reached." }); + GroupChatManagerResult result = new(resultValue) { Reason = reason }; + +#if !NETCOREAPP + return result.AsValueTask(); +#else + return ValueTask.FromResult(result); +#endif } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index 4d10fa8f20fa..a0d05236f8df 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -69,8 +69,14 @@ protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IE await runtime.RegisterAgentFactoryAsync( this.FormatAgentType(context.Topic, "Manager"), (agentId, runtime) => - ValueTask.FromResult( - new GroupChatManagerActor(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger()))).ConfigureAwait(false); + { + GroupChatManagerActor actor = new(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }).ConfigureAwait(false); logger.LogRegisterActor(OrchestrationName, managerType, "MANAGER"); await runtime.SubscribeAsync(managerType, context.Topic).ConfigureAwait(false); @@ -81,6 +87,13 @@ ValueTask RegisterAgentAsync(Agent agent, int agentCount) => runtime.RegisterAgentFactoryAsync( this.FormatAgentType(context.Topic, $"Agent_{agentCount}"), (agentId, runtime) => - ValueTask.FromResult(new GroupChatAgentActor(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger()))); + { + GroupChatAgentActor actor = new(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }); } } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs index 56b789a5486a..137a21345c44 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -18,18 +18,37 @@ public class RoundRobinGroupChatManager : GroupChatManager private int _currentAgentIndex; /// - public override ValueTask> FilterResults(ChatHistory history, CancellationToken cancellationToken = default) => - ValueTask.FromResult(new GroupChatManagerResult(history.LastOrDefault()?.Content ?? string.Empty) { Reason = "Default result filter provides the final chat message." }); + public override ValueTask> FilterResults(ChatHistory history, CancellationToken cancellationToken = default) + { + GroupChatManagerResult result = new (history.LastOrDefault()?.Content ?? string.Empty) { Reason = "Default result filter provides the final chat message." }; +#if !NETCOREAPP + return result.AsValueTask(); +#else + return ValueTask.FromResult(result); +#endif + } /// public override ValueTask> SelectNextAgent(ChatHistory history, GroupChatTeam team, CancellationToken cancellationToken = default) { string nextAgent = team.Skip(this._currentAgentIndex).First().Key; this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count; - return ValueTask.FromResult(new GroupChatManagerResult(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" }); + GroupChatManagerResult result = new(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" }; +#if !NETCOREAPP + return result.AsValueTask(); +#else + return ValueTask.FromResult(result); +#endif } /// - public override ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default) => - ValueTask.FromResult(new GroupChatManagerResult(false) { Reason = "The default round-robin group chat manager does not request user input." }); + public override ValueTask> ShouldRequestUserInput(ChatHistory history, CancellationToken cancellationToken = default) + { + GroupChatManagerResult result = new(false) { Reason = "The default round-robin group chat manager does not request user input." }; +#if !NETCOREAPP + return result.AsValueTask(); +#else + return ValueTask.FromResult(result); +#endif + } } diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs index 37bb5049dd69..5fd9e1ff268b 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffActor.cs @@ -82,7 +82,12 @@ public ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext mess { this._taskSummary = null; this._cache.AddRange(item.Messages); + +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else return ValueTask.CompletedTask; +#endif } /// @@ -90,7 +95,11 @@ public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messa { this._cache.Add(item.Message); +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else return ValueTask.CompletedTask; +#endif } /// @@ -144,13 +153,13 @@ IEnumerable CreateHandoffFunctions() 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 this._handoffs) + foreach (KeyValuePair handoff in this._handoffs) { KernelFunction kernelFunction = KernelFunctionFactory.CreateFromMethod( - (CancellationToken cancellationToken) => this.HandoffAsync(name, cancellationToken), - functionName: $"transfer_to_{name}", - description: description); + (CancellationToken cancellationToken) => this.HandoffAsync(handoff.Key, cancellationToken), + functionName: $"transfer_to_{handoff.Key}", + description: handoff.Value.Description); yield return kernelFunction; } @@ -161,7 +170,12 @@ private ValueTask HandoffAsync(string agentName, CancellationToken cancellationT { this.Logger.LogHandoffFunctionCall(this.Id, agentName); this._handoffAgent = agentName; + +#if !NETCOREAPP + return Task.CompletedTask.AsValueTask(); +#else return ValueTask.CompletedTask; +#endif } private async ValueTask EndAsync(string summary, CancellationToken cancellationToken) diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index 0328e3b00785..d29d247a9ae9 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -29,7 +29,7 @@ public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agent : base(OrchestrationName, agents) { // Extract agent names - HashSet agentNames = agents.Select(a => a.Name ?? a.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); + HashSet agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.OrdinalIgnoreCase); // Extract names from handoffs that don't align with a member agent. string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))]; // Fail fast if invalid names are present. @@ -75,11 +75,18 @@ protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId top await runtime.RegisterAgentFactoryAsync( this.GetAgentType(context.Topic, index), (agentId, runtime) => - ValueTask.FromResult( - new HandoffActor(agentId, runtime, context, agent, map, outputType, context.LoggerFactory.CreateLogger()) + { + HandoffActor actor = + new(agentId, runtime, context, agent, map, outputType, context.LoggerFactory.CreateLogger()) { InteractiveCallback = this.InteractiveCallback - })).ConfigureAwait(false); + }; +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }).ConfigureAwait(false); agentMap[agent.Name ?? agent.Id] = agentType; await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); @@ -88,14 +95,14 @@ await runtime.RegisterAgentFactoryAsync( } // Complete the handoff model - foreach ((string agentName, AgentHandoffs handoffs) in this._handoffs) + foreach (KeyValuePair 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) + HandoffLookup agentHandoffs = handoffMap[handoffs.Key]; + foreach (KeyValuePair handoff in handoffs.Value) { // name = (type,description) - agentHandoffs[handoffName] = (agentMap[handoffName], description); + agentHandoffs[handoff.Key] = (agentMap[handoff.Key], handoff.Value); } } diff --git a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs index d25ddc8bf953..978e7bcca74b 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationResult.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationResult.cs @@ -60,7 +60,14 @@ public void Dispose() /// Thrown if the orchestration does not complete within the specified timeout period. public async ValueTask GetValueAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) { +#if !NETCOREAPP + if (this._isDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } +#else ObjectDisposedException.ThrowIf(this._isDisposed, this); +#endif this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic); @@ -82,21 +89,25 @@ public async ValueTask GetValueAsync(TimeSpan? timeout = null, Cancellat /// /// Cancel the orchestration associated with this result. /// - /// A cancellation token that can be used to cancel the operation. /// Thrown if this instance has been disposed. /// /// Cancellation is not expected to immediately halt the orchestration. Messages that /// are already in-flight may still be processed. /// - public ValueTask CancelAsync(CancellationToken cancellationToken = default) + public void Cancel() { +#if !NETCOREAPP + if (this._isDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } +#else ObjectDisposedException.ThrowIf(this._isDisposed, this); +#endif this._logger.LogOrchestrationResultCancelled(this.Orchestration, this.Topic); this._cancelSource.Cancel(); - this._completion.SetCanceled(cancellationToken); - - return ValueTask.CompletedTask; + this._completion.SetCanceled(); } private void Dispose(bool disposing) diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 9ff4559b8576..4e44fa916e75 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -56,7 +56,15 @@ protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId top ValueTask RegisterAgentAsync(Agent agent, int index, AgentType nextAgent) => runtime.RegisterAgentFactoryAsync( this.GetAgentType(context.Topic, index), - (agentId, runtime) => ValueTask.FromResult(new SequentialActor(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger()))); + (agentId, runtime) => + { + SequentialActor actor = new(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger()); +#if !NETCOREAPP + return actor.AsValueTask(); +#else + return ValueTask.FromResult(actor); +#endif + }); } private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index 28e51bfd9105..d6700e4e4011 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -3,7 +3,7 @@ SemanticKernel.Agents.UnitTests SemanticKernel.Agents.UnitTests - net8.0 + net8.0 LatestMajor true false From 9989c7f170272900e4f22c5f5a56ea2f3737780a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 21:39:18 -0700 Subject: [PATCH 91/98] Project clean-up --- dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj b/dotnet/src/Agents/Orchestration/Agents.Orchestration.csproj index dda5e3d0d9db..d748b3d8457a 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;netstandard2.0 $(NoWarn);SKEXP0110;SKEXP0001 false From 950d100c5d80b8e43b6ab1ef8bce0fc38582ac4d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 13 May 2025 21:47:38 -0700 Subject: [PATCH 92/98] Format --- .../Orchestration/GroupChat/RoundRobinGroupChatManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs index 137a21345c44..bfd92a858449 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/RoundRobinGroupChatManager.cs @@ -20,7 +20,7 @@ public class RoundRobinGroupChatManager : GroupChatManager /// public override ValueTask> FilterResults(ChatHistory history, CancellationToken cancellationToken = default) { - GroupChatManagerResult result = new (history.LastOrDefault()?.Content ?? string.Empty) { Reason = "Default result filter provides the final chat message." }; + GroupChatManagerResult result = new(history.LastOrDefault()?.Content ?? string.Empty) { Reason = "Default result filter provides the final chat message." }; #if !NETCOREAPP return result.AsValueTask(); #else From 9b687127299e9e83d603b520026eea10e9510ffb Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 May 2025 08:59:47 -0700 Subject: [PATCH 93/98] Experimental --- dotnet/src/Agents/Orchestration/Properties/AssemblyInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 dotnet/src/Agents/Orchestration/Properties/AssemblyInfo.cs diff --git a/dotnet/src/Agents/Orchestration/Properties/AssemblyInfo.cs b/dotnet/src/Agents/Orchestration/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..bd1c0f58314e --- /dev/null +++ b/dotnet/src/Agents/Orchestration/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +// This assembly is currently experimental. +[assembly: Experimental("SKEXP0110")] From ab1bfe1674d1ffa960daf3a0e6a10cc4113e33a1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 May 2025 13:21:08 -0700 Subject: [PATCH 94/98] Clarify property names --- .../Orchestration/Step03_GroupChat.cs | 2 +- .../Step03a_GroupChatWithHumanInTheLoop.cs | 2 +- .../Orchestration/Step03b_GroupChatWithAIManager.cs | 2 +- .../Orchestration/GroupChat/GroupChatManager.cs | 11 ++++++----- .../Orchestration/GroupChatOrchestrationTests.cs | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs index 71db32f03a73..da078315b861 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs @@ -54,7 +54,7 @@ Consider suggestions when refining an idea. GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { - MaximumInvocations = 5 + MaximumInvocationCount = 5 }, writer, editor) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs index 04bae9fa3332..a10c5b8a25a7 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs @@ -48,7 +48,7 @@ Consider suggestions when refining an idea. new( new CustomRoundRobinGroupChatManager() { - MaximumInvocations = 5, + MaximumInvocationCount = 5, InteractiveCallback = () => { ChatMessageContent input = new(AuthorRole.User, "I like it"); diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs index 5ca26e524fcb..f0eef06f53bb 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs @@ -116,7 +116,7 @@ You are in a debate. Feel free to challenge the other participants with respect. topic, kernel.GetRequiredService()) { - MaximumInvocations = 5 + MaximumInvocationCount = 5 }, farmer, developer, diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index b5d11efa4b86..fbbe45dbf843 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -29,7 +29,7 @@ public sealed class GroupChatManagerResult(TValue value) /// public abstract class GroupChatManager { - private int _invocationsCount; + private int _invocationCount; /// /// Initializes a new instance of the class. @@ -39,12 +39,13 @@ protected GroupChatManager() { } /// /// Gets the number of times the group chat manager has been invoked. /// - public int InvocationsCount => this._invocationsCount; + public int InvocationCount => this._invocationCount + ; /// /// Gets or sets the maximum number of invocations allowed for the group chat manager. /// - public int MaximumInvocations { get; init; } = int.MaxValue; + public int MaximumInvocationCount { get; init; } = int.MaxValue; /// /// Gets or sets the callback to be invoked for interactive input. @@ -84,11 +85,11 @@ protected GroupChatManager() { } /// A indicating whether the chat should be terminated. public virtual ValueTask> ShouldTerminate(ChatHistory history, CancellationToken cancellationToken = default) { - Interlocked.Increment(ref this._invocationsCount); + Interlocked.Increment(ref this._invocationCount); bool resultValue = false; string reason = "Maximum number of invocations has not been reached."; - if (this.InvocationsCount > this.MaximumInvocations) + if (this.InvocationCount > this.MaximumInvocationCount) { resultValue = true; reason = "Maximum number of invocations reached."; diff --git a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs index a904123f9e26..e0c405eefa33 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/GroupChatOrchestrationTests.cs @@ -56,7 +56,7 @@ private static async Task ExecuteOrchestrationAsync(InProcessRuntime run // Act await runtime.StartAsync(); - GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocations = mockAgents.Length }, mockAgents); + GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocationCount = mockAgents.Length }, mockAgents); const string InitialInput = "123"; OrchestrationResult result = await orchestration.InvokeAsync(InitialInput, runtime); From 764f2ef22466693645af262786f60b9f84a66d43 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 May 2025 13:21:17 -0700 Subject: [PATCH 95/98] Format --- dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs index fbbe45dbf843..b65f05f48d61 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatManager.cs @@ -39,8 +39,7 @@ protected GroupChatManager() { } /// /// Gets the number of times the group chat manager has been invoked. /// - public int InvocationCount => this._invocationCount - ; + public int InvocationCount => this._invocationCount; /// /// Gets or sets the maximum number of invocations allowed for the group chat manager. From 50281845c575ebca1027cdd9908635a2e08d5975 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 15 May 2025 11:23:45 -0700 Subject: [PATCH 96/98] Updated --- .../Orchestration/Step04_Handoff.cs | 1 + .../Step04a_HandoffWithStructuredInput.cs | 4 +- .../Orchestration/AgentOrchestration.cs | 36 +++++------- .../Concurrent/ConcurrentOrchestration.cs | 8 +-- .../GroupChat/GroupChatOrchestration.cs | 8 +-- .../Handoff/HandoffOrchestration.cs | 13 ++--- .../Agents/Orchestration/Handoff/Handoffs.cs | 24 +++++--- .../Orchestration/OrchestrationActor.cs | 2 +- .../Sequential/SequentialOrchestration.cs | 6 +- .../Transforms/DefaultTransforms.cs | 2 +- .../Abstractions/Runtime.Abstractions.csproj | 4 +- .../Agents/Runtime/Core/Runtime.Core.csproj | 4 +- .../HandoffOrchestrationTests.cs | 10 ++-- .../UnitTests/Orchestration/HandoffsTests.cs | 56 +++++++++---------- 14 files changed, 86 insertions(+), 92 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs index 02206de90e74..2aa66ee23905 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs @@ -49,6 +49,7 @@ public async Task OrderSupportAsync() Queue responses = new(); HandoffOrchestration orchestration = new(OrchestrationHandoffs + .StartWith(triageAgent) .Add(triageAgent, statusAgent, returnAgent, refundAgent) .Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related") .Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related") diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs index 1a72ae846889..596a5431aefc 100644 --- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs +++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs @@ -42,7 +42,9 @@ public async Task HandoffStructuredInputAsync() // Define the orchestration HandoffOrchestration orchestration = - new(OrchestrationHandoffs.Add(triageAgent, dotnetAgent, pythonAgent), + new(OrchestrationHandoffs + .StartWith(triageAgent) + .Add(triageAgent, dotnetAgent, pythonAgent), triageAgent, pythonAgent, dotnetAgent) diff --git a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs index b55714909f1b..994b611a59ac 100644 --- a/dotnet/src/Agents/Orchestration/AgentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/AgentOrchestration.cs @@ -31,27 +31,15 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// The type of the result output by the orchestration. public abstract partial class AgentOrchestration { - private readonly string _orchestrationRoot; - - /// - /// Provides a properly formatted name based on the orchestration type (removes generic parameters). - /// - /// The orchestration type - /// - /// Need to respect naming restrictions around and . - /// - protected static string FormatOrchestrationName(Type orchestrationType) => orchestrationType.Name.Split('`').First(); - /// /// Initializes a new instance of the class. /// - /// A descriptive root label for the orchestration. /// Specifies the member agents or orchestrations participating in this orchestration. - protected AgentOrchestration(string orchestrationRoot, params Agent[] members) + protected AgentOrchestration(params Agent[] members) { - Verify.NotNullOrWhiteSpace(orchestrationRoot, nameof(orchestrationRoot)); - - this._orchestrationRoot = orchestrationRoot; + // Capture orchestration root name without generic parameters for use in + // agent type and topic formatting as well as logging. + this.OrchestrationLabel = this.GetType().Name.Split('`').First(); this.Members = members; } @@ -91,6 +79,12 @@ protected AgentOrchestration(string orchestrationRoot, params Agent[] members) /// protected IReadOnlyList Members { get; } + /// + /// Orchestration identifier without generic parameters for use in + /// agent type and topic formatting as well as logging. + /// + protected string OrchestrationLabel { get; } + /// /// Initiates processing of the orchestration. /// @@ -104,11 +98,11 @@ public async ValueTask> InvokeAsync( { Verify.NotNull(input, nameof(input)); - TopicId topic = new($"ID_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); + TopicId topic = new($"{this.OrchestrationLabel}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"); CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - OrchestrationContext context = new(this._orchestrationRoot, topic, this.ResponseCallback, this.LoggerFactory, cancellationToken); + OrchestrationContext context = new(this.OrchestrationLabel, topic, this.ResponseCallback, this.LoggerFactory, cancellationToken); ILogger logger = this.LoggerFactory.CreateLogger(this.GetType()); @@ -118,11 +112,11 @@ public async ValueTask> InvokeAsync( cancellationToken.ThrowIfCancellationRequested(); - logger.LogOrchestrationInvoke(this._orchestrationRoot, topic); + logger.LogOrchestrationInvoke(this.OrchestrationLabel, topic); Task task = runtime.SendMessageAsync(input, orchestrationType, cancellationToken).AsTask(); - logger.LogOrchestrationYield(this._orchestrationRoot, topic); + logger.LogOrchestrationYield(this.OrchestrationLabel, topic); return new OrchestrationResult(context, completion, orchestrationCancelSource, logger); } @@ -152,7 +146,7 @@ public async ValueTask> InvokeAsync( /// 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._orchestrationRoot}_{suffix}"); + protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{suffix}"); /// /// Registers the orchestration's root and boot agents, setting up completion and target routing. diff --git a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs index 776ca11bd4db..fead042e1a14 100644 --- a/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Concurrent/ConcurrentOrchestration.cs @@ -18,14 +18,12 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Concurrent; public class ConcurrentOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(ConcurrentOrchestration<,>)); - /// /// Initializes a new instance of the class. /// /// The agents participating in the orchestration. public ConcurrentOrchestration(params Agent[] agents) - : base(OrchestrationName, agents) + : base(agents) { } @@ -53,7 +51,7 @@ await runtime.RegisterAgentFactoryAsync( return ValueTask.FromResult(actor); #endif }).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, resultType, "RESULTS"); + logger.LogRegisterActor(this.OrchestrationLabel, resultType, "RESULTS"); // Register member actors - All agents respond to the same message. int agentCount = 0; @@ -74,7 +72,7 @@ await runtime.RegisterAgentFactoryAsync( #endif }).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", agentCount); + logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount); await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); } diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs index a0d05236f8df..d2ed007d62d1 100644 --- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatOrchestration.cs @@ -17,8 +17,6 @@ public class GroupChatOrchestration : { internal const string DefaultAgentDescription = "A helpful agent."; - internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(GroupChatOrchestration<,>)); - private readonly GroupChatManager _manager; /// @@ -27,7 +25,7 @@ public class GroupChatOrchestration : /// The manages the flow of the group-chat. /// The agents participating in the orchestration. public GroupChatOrchestration(GroupChatManager manager, params Agent[] agents) - : base(OrchestrationName, agents) + : base(agents) { Verify.NotNull(manager, nameof(manager)); @@ -60,7 +58,7 @@ protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IE team[name] = (agentType, description ?? DefaultAgentDescription); - logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", agentCount); + logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount); await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); } @@ -77,7 +75,7 @@ await runtime.RegisterAgentFactoryAsync( return ValueTask.FromResult(actor); #endif }).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, managerType, "MANAGER"); + logger.LogRegisterActor(this.OrchestrationLabel, managerType, "MANAGER"); await runtime.SubscribeAsync(managerType, context.Topic).ConfigureAwait(false); diff --git a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs index d29d247a9ae9..04d75cef719a 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/HandoffOrchestration.cs @@ -16,8 +16,6 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Handoff; /// public class HandoffOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(HandoffOrchestration<,>)); - private readonly OrchestrationHandoffs _handoffs; /// @@ -26,10 +24,11 @@ public class HandoffOrchestration : AgentOrchestrationDefines the handoff connections for each agent. /// The agents participating in the orchestration. public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agents) - : base(OrchestrationName, agents) + : base(agents) { - // Extract agent names - HashSet agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.OrdinalIgnoreCase); + // Create list of distinct agent names + HashSet agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal); + agentNames.Add(handoffs.FirstAgentName); // Extract names from handoffs that don't align with a member agent. string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))]; // Fail fast if invalid names are present. @@ -91,7 +90,7 @@ await runtime.RegisterAgentFactoryAsync( await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, agentType, "MEMBER", index + 1); + logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", index + 1); } // Complete the handoff model @@ -106,7 +105,7 @@ await runtime.RegisterAgentFactoryAsync( } } - return agentType; + return agentMap[this._handoffs.FirstAgentName]; } private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}"); diff --git a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs index 4f55c4433678..3e145e606496 100644 --- a/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs +++ b/dotnet/src/Agents/Orchestration/Handoff/Handoffs.cs @@ -32,23 +32,33 @@ public sealed class OrchestrationHandoffs : Dictionary /// /// Initializes a new instance of the class with no handoff relationships. /// - public OrchestrationHandoffs() { } + /// The first agent to be invoked (prior to any handoff). + public OrchestrationHandoffs(Agent firstAgent) + : this(firstAgent.Name ?? firstAgent.Id) + { } /// - /// Initializes a new instance of the class with the specified handoff relationships. + /// Initializes a new instance of the class with no handoff relationships. /// - /// A dictionary mapping source agent names/IDs to their . - public OrchestrationHandoffs(Dictionary handoffs) : base(handoffs) { } + /// The name of the first agent to be invoked (prior to any handoff). + public OrchestrationHandoffs(string firstAgentName) + { + Verify.NotNullOrWhiteSpace(firstAgentName, nameof(firstAgentName)); + this.FirstAgentName = firstAgentName; + } + + /// + /// The name of the first agent to be invoked (prior to any handoff). + /// + public string FirstAgentName { get; } /// /// Adds handoff relationships from a source agent to one or more target agents. /// Each target agent's name or ID is mapped to its description. /// /// The source agent. - /// The target agents to add as handoff targets for the source agent. /// The updated instance. - public static OrchestrationHandoffs Add(Agent source, params Agent[] targets) - => new OrchestrationHandoffs().Add(source, targets); + public static OrchestrationHandoffs StartWith(Agent source) => new(source); } /// diff --git a/dotnet/src/Agents/Orchestration/OrchestrationActor.cs b/dotnet/src/Agents/Orchestration/OrchestrationActor.cs index fa3ee1f15c23..5f0a3b133dc2 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationActor.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationActor.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration; /// -/// An actor that participates in an orchestration. +/// Base abstractions for any actor that participates in an orchestration. /// public abstract class OrchestrationActor : BaseAgent { diff --git a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs index 4e44fa916e75..be13c1f87fe3 100644 --- a/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs +++ b/dotnet/src/Agents/Orchestration/Sequential/SequentialOrchestration.cs @@ -15,14 +15,12 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.Sequential; /// public class SequentialOrchestration : AgentOrchestration { - internal static readonly string OrchestrationName = FormatOrchestrationName(typeof(SequentialOrchestration<,>)); - /// /// Initializes a new instance of the class. /// /// The agents participating in the orchestration. public SequentialOrchestration(params Agent[] agents) - : base(OrchestrationName, agents) + : base(agents) { } @@ -48,7 +46,7 @@ protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId top Agent agent = this.Members[index]; nextAgent = await RegisterAgentAsync(agent, index, nextAgent).ConfigureAwait(false); - logger.LogRegisterActor(OrchestrationName, nextAgent, "MEMBER", index + 1); + logger.LogRegisterActor(this.OrchestrationLabel, nextAgent, "MEMBER", index + 1); } return nextAgent; diff --git a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs index 5b6c3b9e6d24..b86993fe86a9 100644 --- a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs +++ b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs @@ -66,7 +66,7 @@ public static ValueTask ToOutput(IList res { output = (object)result; } - else if (isSingleResult && typeof(TOutput).IsAssignableFrom(typeof(ChatMessageContent))) + else if (isSingleResult && typeof(ChatMessageContent).IsAssignableFrom(typeof(TOutput))) { output = (object)result[0]; } diff --git a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj index 2e32ccad3c7a..107614183055 100644 --- a/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj +++ b/dotnet/src/Agents/Runtime/Abstractions/Runtime.Abstractions.csproj @@ -5,11 +5,9 @@ Microsoft.SemanticKernel.Agents.Runtime.Abstractions net8.0;netstandard2.0 $(NoWarn);IDE1006;IDE0130 - preview SKIPSKABSTRACTION + false - - diff --git a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj index 5607805fa2d3..7b90840b7fb9 100644 --- a/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj +++ b/dotnet/src/Agents/Runtime/Core/Runtime.Core.csproj @@ -4,12 +4,10 @@ Microsoft.SemanticKernel.Agents.Runtime.Core Microsoft.SemanticKernel.Agents.Runtime.Core net8.0;netstandard2.0 - preview SKIPSKABSTRACTION + false - - diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs index 8a0596b5e5b1..a5f27265b09a 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffOrchestrationTests.cs @@ -49,7 +49,7 @@ public async Task HandoffOrchestrationWithSingleAgentAsync() Responses.Message("Final response")); // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(handoffs: null, mockAgent1); + string response = await ExecuteOrchestrationAsync(OrchestrationHandoffs.StartWith(mockAgent1), mockAgent1); // Assert Assert.Equal("Final response", response); @@ -77,7 +77,9 @@ public async Task HandoffOrchestrationWithMultipleAgentsAsync() // Act: Create and execute the orchestration string response = await ExecuteOrchestrationAsync( - OrchestrationHandoffs.Add(mockAgent1, mockAgent2, mockAgent3), + OrchestrationHandoffs + .StartWith(mockAgent1) + .Add(mockAgent1, mockAgent2, mockAgent3), mockAgent1, mockAgent2, mockAgent3); @@ -86,13 +88,13 @@ public async Task HandoffOrchestrationWithMultipleAgentsAsync() Assert.Equal("Final response", response); } - private static async Task ExecuteOrchestrationAsync(OrchestrationHandoffs? handoffs, params Agent[] mockAgents) + private static async Task ExecuteOrchestrationAsync(OrchestrationHandoffs handoffs, params Agent[] mockAgents) { // Arrange await using InProcessRuntime runtime = new(); await runtime.StartAsync(); - HandoffOrchestration orchestration = new(handoffs ?? [], mockAgents); + HandoffOrchestration orchestration = new(handoffs, mockAgents); // Act const string InitialInput = "123"; diff --git a/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs b/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs index 949b817ac851..edb2643c2498 100644 --- a/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs +++ b/dotnet/src/Agents/UnitTests/Orchestration/HandoffsTests.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; +using System; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Orchestration.Handoff; using Xunit; @@ -15,39 +15,24 @@ public void EmptyConstructors_CreateEmptyCollections() AgentHandoffs agentHandoffs = []; Assert.Empty(agentHandoffs); - OrchestrationHandoffs orchestrationHandoffs = []; + OrchestrationHandoffs orchestrationHandoffs = new("first"); Assert.Empty(orchestrationHandoffs); + Assert.Equal("first", orchestrationHandoffs.FirstAgentName); } [Fact] - public void DictionaryConstructors_CopyValues() + public void DictionaryConstructors_InvalidFirstAgent() { - Dictionary handoffDict = new() - { - { "agent1", "description1" }, - { "agent2", "description2" } - }; - - AgentHandoffs agentHandoffs = new(handoffDict); - Assert.Equal(2, agentHandoffs.Count); - Assert.Equal("description1", agentHandoffs["agent1"]); - Assert.Equal("description2", agentHandoffs["agent2"]); - - Dictionary orchestrationDict = new() - { - { "source1", new AgentHandoffs(handoffDict) } - }; - - OrchestrationHandoffs orchestrationHandoffs = new(orchestrationDict); - Assert.Single(orchestrationHandoffs); - Assert.Equal(2, orchestrationHandoffs["source1"].Count); + Assert.Throws(() => new OrchestrationHandoffs((string)null!)); + Assert.Throws(() => new OrchestrationHandoffs(string.Empty)); + Assert.Throws(() => new OrchestrationHandoffs(" ")); } [Fact] public void Add_WithAgentObjects_CreatesHandoffRelationships() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("source"); Agent sourceAgent = CreateAgent("source", "Source Agent"); Agent targetAgent1 = CreateAgent("target1", "Target Agent 1"); @@ -58,6 +43,7 @@ public void Add_WithAgentObjects_CreatesHandoffRelationships() // Assert Assert.Single(handoffs); + Assert.Equal("source", handoffs.FirstAgentName); Assert.True(handoffs.ContainsKey("source")); AgentHandoffs sourceHandoffs = handoffs["source"]; @@ -70,7 +56,7 @@ public void Add_WithAgentObjects_CreatesHandoffRelationships() public void Add_WithAgentAndCustomDescription_UsesCustomDescription() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("source"); Agent sourceAgent = CreateAgent("source", "Source Agent"); Agent targetAgent = CreateAgent("target", "Target Agent"); @@ -81,6 +67,7 @@ public void Add_WithAgentAndCustomDescription_UsesCustomDescription() // Assert Assert.Single(handoffs); + Assert.Equal("source", handoffs.FirstAgentName); AgentHandoffs sourceHandoffs = handoffs["source"]; Assert.Single(sourceHandoffs); Assert.Equal(customDescription, sourceHandoffs["target"]); @@ -90,7 +77,7 @@ public void Add_WithAgentAndCustomDescription_UsesCustomDescription() public void Add_WithAgentAndTargetName_AddsHandoffWithDescription() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("source"); Agent sourceAgent = CreateAgent("source", "Source Agent"); string targetName = "targetName"; @@ -101,6 +88,7 @@ public void Add_WithAgentAndTargetName_AddsHandoffWithDescription() // Assert Assert.Single(handoffs); + Assert.Equal("source", handoffs.FirstAgentName); AgentHandoffs sourceHandoffs = handoffs["source"]; Assert.Single(sourceHandoffs); Assert.Equal(description, sourceHandoffs[targetName]); @@ -110,7 +98,7 @@ public void Add_WithAgentAndTargetName_AddsHandoffWithDescription() public void Add_WithSourceNameAndTargetName_AddsHandoffWithDescription() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("sourceName"); string sourceName = "sourceName"; string targetName = "targetName"; @@ -121,6 +109,7 @@ public void Add_WithSourceNameAndTargetName_AddsHandoffWithDescription() // Assert Assert.Single(handoffs); + Assert.Equal("sourceName", handoffs.FirstAgentName); AgentHandoffs sourceHandoffs = handoffs[sourceName]; Assert.Single(sourceHandoffs); Assert.Equal(description, sourceHandoffs[targetName]); @@ -130,7 +119,7 @@ public void Add_WithSourceNameAndTargetName_AddsHandoffWithDescription() public void Add_WithMultipleSourcesAndTargets_CreatesCorrectStructure() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("source1"); Agent source1 = CreateAgent("source1", "Source Agent 1"); Agent source2 = CreateAgent("source2", "Source Agent 2"); @@ -146,6 +135,7 @@ public void Add_WithMultipleSourcesAndTargets_CreatesCorrectStructure() // Assert Assert.Equal(2, handoffs.Count); + Assert.Equal("source1", handoffs.FirstAgentName); // Check source1's targets AgentHandoffs source1Handoffs = handoffs["source1"]; @@ -170,10 +160,14 @@ public void StaticAdd_CreatesNewOrchestrationHandoffs() Agent target2 = CreateAgent("target2", "Target Agent 2"); // Act - OrchestrationHandoffs handoffs = OrchestrationHandoffs.Add(source, target1, target2); + OrchestrationHandoffs handoffs = + OrchestrationHandoffs + .StartWith(source) + .Add(source, target1, target2); // Assert Assert.NotNull(handoffs); + Assert.Equal(source.Id, handoffs.FirstAgentName); Assert.Single(handoffs); Assert.True(handoffs.ContainsKey("source")); @@ -187,7 +181,7 @@ public void StaticAdd_CreatesNewOrchestrationHandoffs() public void Add_WithAgentsWithNoNameUsesId() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("source-id"); Agent sourceAgent = CreateAgent(id: "source-id", name: null); Agent targetAgent = CreateAgent(id: "target-id", name: null, description: "Target Description"); @@ -197,6 +191,7 @@ public void Add_WithAgentsWithNoNameUsesId() // Assert Assert.Single(handoffs); + Assert.Equal("source-id", handoffs.FirstAgentName); Assert.True(handoffs.ContainsKey("source-id")); AgentHandoffs sourceHandoffs = handoffs["source-id"]; @@ -208,7 +203,7 @@ public void Add_WithAgentsWithNoNameUsesId() public void Add_WithTargetWithNoDescription_UsesEmptyString() { // Arrange - OrchestrationHandoffs handoffs = []; + OrchestrationHandoffs handoffs = new("source"); Agent sourceAgent = CreateAgent("source", "Source Agent"); Agent targetAgent = CreateAgent("target", null); @@ -218,6 +213,7 @@ public void Add_WithTargetWithNoDescription_UsesEmptyString() // Assert Assert.Single(handoffs); + Assert.Equal("source", handoffs.FirstAgentName); AgentHandoffs sourceHandoffs = handoffs["source"]; Assert.Single(sourceHandoffs); Assert.Equal(string.Empty, sourceHandoffs["target"]); From 94079deec8ae337d67d3749bae87b93071e174f3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 15 May 2025 11:41:16 -0700 Subject: [PATCH 97/98] Address suggestion --- dotnet/src/Agents/Orchestration/OrchestrationActor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Orchestration/OrchestrationActor.cs b/dotnet/src/Agents/Orchestration/OrchestrationActor.cs index 5f0a3b133dc2..1e7866d2ca86 100644 --- a/dotnet/src/Agents/Orchestration/OrchestrationActor.cs +++ b/dotnet/src/Agents/Orchestration/OrchestrationActor.cs @@ -33,15 +33,19 @@ protected OrchestrationActor(AgentId id, IAgentRuntime runtime, OrchestrationCon /// The message object to send. /// The recipient agent's type. /// A token used to cancel the operation if needed. - protected async ValueTask SendMessageAsync( + /// The agent identifier, if it exists. + protected async ValueTask SendMessageAsync( object message, AgentType agentType, CancellationToken cancellationToken = default) { AgentId? agentId = await this.GetAgentAsync(agentType, cancellationToken).ConfigureAwait(false); + if (agentId.HasValue) { await this.SendMessageAsync(message, agentId.Value, messageId: null, cancellationToken).ConfigureAwait(false); } + + return agentId; } } From 049ce254c7a44b55f0c9c60d1ec2cc853da40bd9 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 15 May 2025 12:49:41 -0700 Subject: [PATCH 98/98] Update transform --- .../Transforms/DefaultTransforms.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs index b86993fe86a9..51d3c9c3a465 100644 --- a/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs +++ b/dotnet/src/Agents/Orchestration/Transforms/DefaultTransforms.cs @@ -13,22 +13,20 @@ internal static class DefaultTransforms { public static ValueTask> FromInput(TInput input, CancellationToken cancellationToken = default) { - if (input is IEnumerable messages) - { - return new ValueTask>(messages); - } - - if (input is ChatMessageContent message) - { - return new ValueTask>([message]); - } +#if !NETCOREAPP + return TransformInput().AsValueTask(); +#else + return ValueTask.FromResult(TransformInput()); +#endif - if (input is not string text) - { - text = JsonSerializer.Serialize(input); - } - - return new ValueTask>([new ChatMessageContent(AuthorRole.User, text)]); + IEnumerable TransformInput() => + input switch + { + IEnumerable messages => messages, + ChatMessageContent message => [message], + string text => [new ChatMessageContent(AuthorRole.User, text)], + _ => [new ChatMessageContent(AuthorRole.User, JsonSerializer.Serialize(input))] + }; } public static ValueTask ToOutput(IList result, CancellationToken cancellationToken = default)