From 0d9f133e3b9942d7377f9af0d4578dadbb60670c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 28 Sep 2025 14:54:52 -0400 Subject: [PATCH] [BREAKING] Delete Microsoft.Agents.Orchestration (#949) Replaced by AgentWorkflowBuilder. --- dotnet/agent-framework-dotnet.slnx | 5 - .../AgentWebChat.AgentHost.csproj | 2 +- .../AgentWebChat.AgentHost/Program.cs | 7 +- .../AgentOrchestration.csproj | 65 -- .../AgentOrchestration/AgentSample.cs | 179 ------ .../InMemoryActorStateStorageExample.cs | 127 ---- .../ConcurrentOrchestration_Intro.cs | 52 -- ...rentOrchestration_With_StructuredOutput.cs | 54 -- .../GroupChatOrchestration_Intro.cs | 76 --- .../GroupChatOrchestration_With_AIManager.cs | 198 ------ ...upChatOrchestration_With_HumanInTheLoop.cs | 99 --- .../HandoffOrchestration_Intro.cs | 97 --- ...ndoffOrchestration_With_StructuredInput.cs | 110 ---- .../SequentialOrchestration_Foundry_Agents.cs | 87 --- .../SequentialOrchestration_Intro.cs | 73 --- .../SequentialOrchestration_Multi_Agent.cs | 83 --- ...quentialOrchestration_With_Cancellation.cs | 46 -- .../Resources/Hamlet_full_play_summary.txt | 13 - .../Resources/employees.pdf | Bin 43422 -> 0 bytes .../Resources/groceries.txt | 6 - .../AIAgentExtensions.cs | 93 --- .../ConcurrentOrchestration.cs | 111 ---- .../GroupChat/GroupChatManager.cs | 100 --- .../GroupChat/GroupChatOrchestration.cs | 127 ---- .../GroupChat/GroupChatTeam.cs | 25 - .../GroupChat/RoundRobinGroupChatManager.cs | 46 -- .../Handoffs/HandoffOrchestration.cs | 239 ------- .../Handoffs/Handoffs.cs | 187 ------ .../Microsoft.Agents.Orchestration.csproj | 33 - .../OrchestratingAgent.cs | 315 --------- .../OrchestratingAgentContext.cs | 54 -- .../OrchestratingAgentResponse.cs | 64 -- .../OrchestratingAgentThread.cs | 17 - .../OrchestrationJsonContext.cs | 13 - .../SequentialOrchestration.cs | 68 -- .../ChatGroupExtensionsTests.cs | 85 --- .../ConcurrentOrchestrationTests.cs | 62 -- .../GroupChatOrchestration2Tests.cs | 60 -- .../GroupChatOrchestrationTests.cs | 60 -- .../HandoffOrchestrationTests.cs | 230 ------- .../HandoffsTests.cs | 605 ------------------ .../HttpMessageHandlerStub.cs | 16 - ...soft.Agents.Orchestration.UnitTests.csproj | 18 - .../MockAgent.cs | 50 -- .../OrchestrationResultTests.cs | 101 --- .../SequentialOrchestrationTests.cs | 59 -- 46 files changed, 6 insertions(+), 4211 deletions(-) delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/AgentSample.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/InMemoryActorStateStorageExample.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_Intro.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_Intro.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_AIManager.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_Intro.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_With_StructuredInput.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Foundry_Agents.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Intro.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Multi_Agent.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_With_Cancellation.cs delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Resources/Hamlet_full_play_summary.txt delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Resources/employees.pdf delete mode 100644 dotnet/samples/GettingStarted/AgentOrchestration/Resources/groceries.txt delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/AIAgentExtensions.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/ConcurrentOrchestration.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/Handoffs/HandoffOrchestration.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/Handoffs/Handoffs.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgent.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentContext.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentResponse.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/OrchestrationJsonContext.cs delete mode 100644 dotnet/src/Microsoft.Agents.Orchestration/SequentialOrchestration.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestration2Tests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HttpMessageHandlerStub.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/MockAgent.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index eda84f1e77..7ae4f52a03 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -21,9 +21,6 @@ - - - @@ -271,7 +268,6 @@ - @@ -299,7 +295,6 @@ - diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj index ba584b1cfc..4b30220786 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj @@ -8,7 +8,7 @@ - + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index d2ec620844..ab71949751 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -7,7 +7,7 @@ using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.A2A.AspNetCore; using Microsoft.Agents.AI.Runtime.Storage.CosmosDB; -using Microsoft.Agents.Orchestration; +using Microsoft.Agents.Workflows; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.AI; @@ -74,7 +74,10 @@ builder.AddAIAgent("knights-and-knaves", (sp, key) => If the user asks a general question about their surrounding, make something up which is consistent with the scenario. """, "Narrator"); - return new ConcurrentOrchestration([knight, knave, narrator], name: key); + // TODO: How to avoid sync-over-async here? +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + return AgentWorkflowBuilder.BuildConcurrent([knight, knave, narrator]).AsAgentAsync(name: key).AsTask().GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 }); // Add CosmosDB state storage to override default storage diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj b/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj deleted file mode 100644 index 9b77e8de9b..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - GettingStarted - Library - $(NoWarn);CA1707;CA1716;IDE0009;IDE1006; OPENAI001; - enable - true - true - true - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - - diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/AgentSample.cs b/dotnet/samples/GettingStarted/AgentOrchestration/AgentSample.cs deleted file mode 100644 index b817a86a45..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/AgentSample.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel; -using Azure.AI.Agents.Persistent; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; -using Microsoft.Shared.Samples; -using OpenAI.Assistants; -using OpenAI.Chat; -using OpenAI.Responses; - -#pragma warning disable OPENAI001 - -namespace GettingStarted; - -public class AgentSample(ITestOutputHelper output) : BaseSample(output) -{ - /// - /// Represents the available providers for instances. - /// - public enum ChatClientProviders - { - AzureOpenAI, - OpenAIChatCompletion, - OpenAIAssistant, - OpenAIResponses, - OpenAIResponses_InMemoryMessageThread, - OpenAIResponses_ConversationIdThread, - AzureAIAgentsPersistent - } - - protected static IChatClient GetChatClient(ChatClientProviders provider, ChatClientAgentOptions? options = null) - => provider switch - { - ChatClientProviders.OpenAIChatCompletion => GetOpenAIChatClient(), - ChatClientProviders.OpenAIAssistant => GetOpenAIAssistantChatClient(Throw.IfNull(options)), - ChatClientProviders.AzureOpenAI => GetAzureOpenAIChatClient(), - ChatClientProviders.AzureAIAgentsPersistent => GetAzureAIAgentPersistentClient(Throw.IfNull(options)), - ChatClientProviders.OpenAIResponses or - ChatClientProviders.OpenAIResponses_InMemoryMessageThread or - ChatClientProviders.OpenAIResponses_ConversationIdThread - => GetOpenAIResponsesClient(), - _ => throw new NotSupportedException($"Provider {provider} is not supported.") - }; - - /// - /// For providers that store the agent and the thread on the server side, this will clean and delete - /// any sample agent and thread that was created during this execution. - /// - /// The chat client provider type that determines the cleanup process. - /// The agent instance to be cleaned up. - /// Optional thread associated with the agent that may also need to be cleaned up. - /// Cancellation token to monitor for cancellation requests. The default is . - /// - /// Ideally for faster execution and potential cost savings, server-side agents should be reused. - /// - protected static Task AgentCleanUpAsync(ChatClientProviders provider, AIAgent agent, AgentThread? thread = null, CancellationToken cancellationToken = default) - => provider switch - { - ChatClientProviders.AzureAIAgentsPersistent => AzureAIAgentsPersistentAgentCleanUpAsync(agent, thread, cancellationToken), - ChatClientProviders.OpenAIAssistant => OpenAIAssistantCleanUpAgentAsync(agent, thread, cancellationToken), - // For other remaining provider sample types, no cleanup is needed as they don't offer a server-side agent/thread clean-up API. - _ => Task.CompletedTask - }; - - /// - /// Creates a server-side agent identifier based on the specified provider and options. - /// - /// The provider to use for creating the agent. - /// The options to configure the agent. - /// The to monitor for cancellation requests. The default is . - /// The identifier of the created agent, or if the provider does not use server-side agents. - /// Some server-side agent providers require an agent id reference to be created before it can be invoked. - protected static Task AgentCreateAsync(ChatClientProviders provider, ChatClientAgentOptions options, CancellationToken cancellationToken = default) - => provider switch - { - ChatClientProviders.OpenAIAssistant => OpenAIAssistantCreateAgentAsync(options, cancellationToken), - ChatClientProviders.AzureAIAgentsPersistent => AzureAIAgentsPersistentCreateAgentAsync(options, cancellationToken), - _ => Task.FromResult(null) - }; - - #region Private GetChatClient - - private static IChatClient GetOpenAIChatClient() - => new ChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) - .AsIChatClient(); - - private static IChatClient GetAzureOpenAIChatClient() - => ((TestConfiguration.AzureOpenAI.ApiKey is null) - // Use Azure CLI credentials if API key is not provided. - ? new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new AzureCliCredential()) - : new AzureOpenAIClient(TestConfiguration.AzureOpenAI.Endpoint, new ApiKeyCredential(TestConfiguration.AzureOpenAI.ApiKey))) - .GetChatClient(TestConfiguration.AzureOpenAI.DeploymentName) - .AsIChatClient(); - - private static IChatClient GetOpenAIResponsesClient() - => new OpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) - .AsIChatClient(); - - private static IChatClient GetAzureAIAgentPersistentClient(ChatClientAgentOptions options) - => new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()).AsNewIChatClient(options.Id!); - - private static IChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options) - => new AssistantClient(TestConfiguration.OpenAI.ApiKey).AsIChatClient(options.Id!); - - #endregion - - #region Private AgentCreate - - private static async Task AzureAIAgentsPersistentCreateAgentAsync(ChatClientAgentOptions options, CancellationToken cancellationToken) - { - var persistentAgentsClient = new PersistentAgentsAdministrationClient( - TestConfiguration.AzureAI.Endpoint, - new AzureCliCredential()); - - // Create a server side agent to work with. - var result = await persistentAgentsClient.CreateAgentAsync( - model: TestConfiguration.AzureAI.DeploymentName, - name: options.Name, - instructions: options.Instructions, - cancellationToken: cancellationToken); - - return result?.Value.Id; - } - - private static async Task OpenAIAssistantCreateAgentAsync(ChatClientAgentOptions options, CancellationToken cancellationToken) - { - var assistantClient = new AssistantClient(TestConfiguration.OpenAI.ApiKey); - Assistant assistant = await assistantClient.CreateAssistantAsync( - TestConfiguration.OpenAI.ChatModelId, - new() - { - Name = options.Name, - Instructions = options.Instructions - }, - cancellationToken); - - return assistant.Id; - } - - #endregion - - #region Private AgentCleanUp - - private static async Task AzureAIAgentsPersistentAgentCleanUpAsync(AIAgent agent, AgentThread? thread, CancellationToken cancellationToken) - { - var persistentAgentsClient = (agent as ChatClientAgent)?.ChatClient.GetService() ?? - throw new InvalidOperationException("The provided chat client is not a Persistent Agents Chat Client"); - - await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id, cancellationToken); - - // If a thread is provided, delete it as well. - if (thread is ChatClientAgentThread chatThread) - { - await persistentAgentsClient.Threads.DeleteThreadAsync(chatThread.ConversationId, cancellationToken); - } - } - - private static async Task OpenAIAssistantCleanUpAgentAsync(AIAgent agent, AgentThread? thread, CancellationToken cancellationToken) - { - var assistantClient = (agent as ChatClientAgent)?.ChatClient - .GetService() - ?? throw new InvalidOperationException("The provided chat client is not an OpenAI Assistant Chat Client"); - - // Delete the agent. - await assistantClient.DeleteAssistantAsync(agent.Id, cancellationToken); - - // If a thread is provided, delete it as well. - if (thread is ChatClientAgentThread chatThread) - { - await assistantClient.DeleteThreadAsync(chatThread.ConversationId, cancellationToken); - } - } - - #endregion -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/InMemoryActorStateStorageExample.cs b/dotnet/samples/GettingStarted/AgentOrchestration/InMemoryActorStateStorageExample.cs deleted file mode 100644 index b650fed6b7..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/InMemoryActorStateStorageExample.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; - -namespace Microsoft.Agents.AI.Runtime.Samples; - -/// -/// Example demonstrating how to use the InMemoryActorStateStorage. -/// -public static class InMemoryActorStateStorageExample -{ - /// - /// Demonstrates the basic usage of InMemoryActorStateStorage. - /// - /// A task representing the asynchronous operation. - public static async Task RunAsync() - { - // Create an in-memory actor state storage - var storage = new InMemoryActorStateStorage(); - - // Create an actor ID - var actorId = new ActorId("ExampleActor", "instance1"); - - Console.WriteLine("=== InMemoryActorStateStorage Example ==="); - Console.WriteLine(); - - // 1. Write some initial state - Console.WriteLine("1. Writing initial state..."); - var initialOperations = new List - { - new SetValueOperation("name", JsonSerializer.SerializeToElement("John Doe")), - new SetValueOperation("age", JsonSerializer.SerializeToElement(30)), - new SetValueOperation("city", JsonSerializer.SerializeToElement("Seattle")) - }; - - var writeResult = await storage.WriteStateAsync(actorId, initialOperations, "0").ConfigureAwait(false); - Console.WriteLine($" Write successful: {writeResult.Success}"); - Console.WriteLine($" New ETag: {writeResult.ETag}"); - Console.WriteLine($" Actor count: {storage.ActorCount}"); - Console.WriteLine($" Key count for actor: {storage.GetKeyCount(actorId)}"); - Console.WriteLine(); - - // 2. Read the state back - Console.WriteLine("2. Reading state back..."); - var readOperations = new List - { - new GetValueOperation("name"), - new GetValueOperation("age"), - new GetValueOperation("city"), - new GetValueOperation("nonexistent"), // This won't exist - new ListKeysOperation(continuationToken: null) // List all keys - }; - - var readResult = await storage.ReadStateAsync(actorId, readOperations).ConfigureAwait(false); - Console.WriteLine($" Current ETag: {readResult.ETag}"); - Console.WriteLine(" Results:"); - - foreach (var result in readResult.Results) - { - switch (result) - { - case GetValueResult getValue: - Console.WriteLine($" - Get value: {getValue.Value?.ToString() ?? "null"}"); - break; - - case ListKeysResult listKeys: - Console.WriteLine($" - Keys: [{string.Join(", ", listKeys.Keys)}]"); - break; - } - } - Console.WriteLine(); - - // 3. Update some values - Console.WriteLine("3. Updating state..."); - var updateOperations = new List - { - new SetValueOperation("age", JsonSerializer.SerializeToElement(31)), // Update age - new SetValueOperation("email", JsonSerializer.SerializeToElement("john@example.com")), // Add email - new RemoveKeyOperation("city") // Remove city - }; - - var updateResult = await storage.WriteStateAsync(actorId, updateOperations, writeResult.ETag).ConfigureAwait(false); - Console.WriteLine($" Update successful: {updateResult.Success}"); - Console.WriteLine($" New ETag: {updateResult.ETag}"); - Console.WriteLine($" Key count for actor: {storage.GetKeyCount(actorId)}"); - Console.WriteLine(); - - // 4. Try to update with wrong ETag (should fail) - Console.WriteLine("4. Trying to update with wrong ETag..."); - var failingOperations = new List - { - new SetValueOperation("shouldFail", JsonSerializer.SerializeToElement("this should fail")) - }; - - var failResult = await storage.WriteStateAsync(actorId, failingOperations, "wrong-etag").ConfigureAwait(false); - Console.WriteLine($" Update successful: {failResult.Success}"); - Console.WriteLine($" Current ETag: {failResult.ETag}"); - Console.WriteLine(); - - // 5. Read final state - Console.WriteLine("5. Reading final state..."); - var finalReadOperations = new List - { - new ListKeysOperation(continuationToken: null) - }; - - var finalReadResult = await storage.ReadStateAsync(actorId, finalReadOperations).ConfigureAwait(false); - var finalKeys = finalReadResult.Results.OfType().First(); - Console.WriteLine($" Final keys: [{string.Join(", ", finalKeys.Keys)}]"); - - // 6. Read each value - foreach (var key in finalKeys.Keys) - { - var valueReadOperations = new List - { - new GetValueOperation(key) - }; - - var valueReadResult = await storage.ReadStateAsync(actorId, valueReadOperations).ConfigureAwait(false); - var getValue = valueReadResult.Results.OfType().First(); - Console.WriteLine($" - {key}: {getValue.Value}"); - } - - Console.WriteLine(); - Console.WriteLine("=== Example Complete ==="); - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_Intro.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_Intro.cs deleted file mode 100644 index 923322a194..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_Intro.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; - -namespace Orchestration; - -/// -/// Demonstrates how to use the -/// for executing multiple agents on the same task in parallel. -/// -public class ConcurrentOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunOrchestrationAsync(bool streamedResponse) - { - // Define the agents - ChatClientAgent physicist = - CreateAgent( - instructions: "You are an expert in physics. You answer questions from a physics perspective.", - description: "An expert in physics"); - ChatClientAgent chemist = - CreateAgent( - instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective.", - description: "An expert in chemistry"); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - - // Define the orchestration - ConcurrentOrchestration orchestration = - new(physicist, chemist) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null, - }; - - // Run the orchestration - const string Input = "What is temperature?"; - Console.WriteLine($"\n# INPUT: {Input}\n"); - AgentRunResponse result = await orchestration.RunAsync(Input); - - Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", result.Messages.Select(r => $"{r.Text}"))}"); - - this.DisplayHistory(monitor.History); - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs deleted file mode 100644 index c0f0060a6a..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/ConcurrentOrchestration_With_StructuredOutput.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Shared.Samples; - -namespace Orchestration; - -/// -/// Demonstrates how to use the with structured output. -/// -public class ConcurrentOrchestration_With_StructuredOutput(ITestOutputHelper output) : OrchestrationSample(output) -{ - private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true }; - - [Fact] - public async Task RunOrchestrationAsync() - { - // Define the agents - ChatClientAgent agent1 = - 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"); - ChatClientAgent agent2 = - CreateAgent( - instructions: "You are an expert in sentiment analysis. Given an article, identify the sentiment.", - description: "An expert in sentiment analysis"); - ChatClientAgent agent3 = - 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 - ConcurrentOrchestration orchestration = new(agent1, agent2, agent3) { LoggerFactory = this.LoggerFactory }; - - // Run the orchestration - const string ResourceId = "Hamlet_full_play_summary.txt"; - string input = Resources.Read(ResourceId); - Console.WriteLine($"\n# INPUT: @{ResourceId}\n"); - - var output = await orchestration.RunAsync(CreateChatClient(), input); - Console.WriteLine($"\n# RESULT:\n{JsonSerializer.Serialize(output, s_options)}"); - } - -#pragma warning disable CA1812 // Avoid uninstantiated internal classes - private sealed class Analysis - { - public IList Themes { get; set; } = []; - public IList Sentiments { get; set; } = []; - public IList Entities { get; set; } = []; - } -#pragma warning restore CA1812 -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_Intro.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_Intro.cs deleted file mode 100644 index 6893655706..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_Intro.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; - -namespace Orchestration; - -/// -/// 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 GroupChatOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunOrchestrationAsync(bool streamedResponse) - { - // Define the agents - ChatClientAgent writer = - 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. - """); - ChatClientAgent editor = - 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. - """); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - // Define the orchestration - GroupChatOrchestration orchestration = - new(new RoundRobinGroupChatManager() - { - MaximumInvocationCount = 5 - }, - writer, - editor) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null, - }; - - const string Input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; - Console.WriteLine($"\n# INPUT: {Input}\n"); - AgentRunResponse result = await orchestration.RunAsync(Input); - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_AIManager.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_AIManager.cs deleted file mode 100644 index ff37ca66db..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_AIManager.cs +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Extensions.AI; - -namespace Orchestration; - -/// -/// 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 GroupChatOrchestration_With_AIManager(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Fact] - public async Task RunOrchestrationAsync() - { - // Define the agents - ChatClientAgent farmer = - 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. - """); - ChatClientAgent developer = - 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. - """); - ChatClientAgent teacher = - 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. - """); - ChatClientAgent activist = - 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. - """); - ChatClientAgent spiritual = - 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. - """); - ChatClientAgent artist = - 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. - """); - ChatClientAgent immigrant = - 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. - """); - ChatClientAgent doctor = - 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. - """); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - - // Define the orchestration - const string Topic = "What does a good life mean to you personally?"; - GroupChatOrchestration orchestration = - new( - new AIGroupChatManager( - Topic, - CreateChatClient()) - { - MaximumInvocationCount = 5 - }, - farmer, - developer, - teacher, - activist, - spiritual, - artist, - immigrant, - doctor) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - }; - - // Run the orchestration - Console.WriteLine($"\n# INPUT: {Topic}\n"); - AgentRunResponse result = await orchestration.RunAsync(Topic); - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - } - - private sealed class AIGroupChatManager(string topic, IChatClient chatClient) : 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. - """; - } - - /// - protected override ValueTask> FilterResultsAsync(IReadOnlyCollection history, CancellationToken cancellationToken = default) => - this.GetResponseAsync(history, Prompts.Filter(topic), cancellationToken); - - /// - protected override ValueTask> SelectNextAgentAsync(IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default) => - this.GetResponseAsync(history, Prompts.Selection(topic, team.FormatList()), cancellationToken); - - /// - protected override ValueTask> ShouldRequestUserInputAsync(IReadOnlyCollection history, CancellationToken cancellationToken = default) => - new(new GroupChatManagerResult(false) { Reason = "The AI group chat manager does not request user input." }); - - /// - protected override async ValueTask> ShouldTerminateAsync(IReadOnlyCollection history, CancellationToken cancellationToken = default) - { - GroupChatManagerResult result = await base.ShouldTerminateAsync(history, cancellationToken); - if (!result.Value) - { - result = await this.GetResponseAsync(history, Prompts.Termination(topic), cancellationToken); - } - return result; - } - - private async ValueTask> GetResponseAsync(IReadOnlyCollection history, string prompt, CancellationToken cancellationToken = default) - { - ChatResponse> response = await chatClient.GetResponseAsync>([.. history, new ChatMessage(ChatRole.System, prompt)], new ChatOptions { ToolMode = ChatToolMode.Auto }, useJsonSchemaResponseFormat: true, cancellationToken); - return response.Result; - } - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs deleted file mode 100644 index 655c992824..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/GroupChatOrchestration_With_HumanInTheLoop.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Extensions.AI; - -namespace Orchestration; - -/// -/// Demonstrates how to use the with human in the loop -/// -public class GroupChatOrchestration_With_HumanInTheLoop(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Fact] - public async Task RunOrchestrationAsync() - { - // Define the agents - ChatClientAgent writer = - 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. - """); - ChatClientAgent editor = - 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. - """); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - - // Define the orchestration - GroupChatOrchestration orchestration = - new( - new CustomRoundRobinGroupChatManager() - { - MaximumInvocationCount = 5, - InteractiveCallback = () => - { - ChatMessage input = new(ChatRole.User, "I like it"); - monitor.History.Add(input); - Console.WriteLine($"\n# INPUT: {input.Text}\n"); - return new ValueTask(input); - } - }, - writer, - editor) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - }; - - // Run the orchestration - const string Input = "Create a slogon for a new eletric SUV that is affordable and fun to drive."; - Console.WriteLine($"\n# INPUT: {Input}\n"); - AgentRunResponse result = await orchestration.RunAsync(Input); - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - } - - /// - /// 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 - { - protected override ValueTask> ShouldRequestUserInputAsync(IReadOnlyCollection history, CancellationToken cancellationToken = default) - { - string? lastAgent = history.LastOrDefault()?.AuthorName; - - GroupChatManagerResult result = - lastAgent is null ? new(false) { Reason = "No agents have spoken yet." } : - lastAgent is "Reviewer" ? new(true) { Reason = "User input is needed after the reviewer's message." } : - new(false) { Reason = "User input is not needed until the reviewer's message." }; - - return new(result); - } - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_Intro.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_Intro.cs deleted file mode 100644 index 9ac572635c..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_Intro.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Extensions.AI; - -namespace Orchestration; - -/// -/// 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 HandoffOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunOrchestrationAsync(bool streamedResponse) - { - // Define the agents & tools - ChatClientAgent triageAgent = - CreateAgent( - instructions: "A customer support agent that triages issues.", - name: "TriageAgent", - description: "Handle customer requests."); - ChatClientAgent statusAgent = - CreateAgent( - name: "OrderStatusAgent", - instructions: "Handle order status requests.", - description: "A customer support agent that checks order status.", - functions: AIFunctionFactory.Create(OrderFunctions.CheckOrderStatus)); - ChatClientAgent returnAgent = - CreateAgent( - name: "OrderReturnAgent", - instructions: "Handle order return requests.", - description: "A customer support agent that handles order returns.", - functions: AIFunctionFactory.Create(OrderFunctions.ProcessReturn)); - ChatClientAgent refundAgent = - CreateAgent( - name: "OrderRefundAgent", - instructions: "Handle order refund requests.", - description: "A customer support agent that handles order refund.", - functions: AIFunctionFactory.Create(OrderFunctions.ProcessRefund)); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - // Define user responses for InteractiveCallback (since sample is not interactive) - Queue responses = new(); - const 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"); - - // Define the orchestration - HandoffOrchestration orchestration = - new(Handoffs - .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") - .Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related")) - { - InteractiveCallback = () => - { - string text = responses.Dequeue(); - ChatMessage input = new(ChatRole.User, text); - monitor.History.Add(input); - Console.WriteLine($"\n# INPUT: {input.Text}\n"); - return new(input); - }, - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null, - }; - - // Run the orchestration - Console.WriteLine($"\n# INPUT:\n{Task}\n"); - AgentRunResponse result = await orchestration.RunAsync(Task); - - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - } - - private static class OrderFunctions - { - public static string CheckOrderStatus(string orderId) => $"Order {orderId} is shipped and will arrive in 2-3 days."; - public static string ProcessReturn(string orderId, string reason) => $"Return for order {orderId} has been processed successfully."; - public static string ProcessRefund(string orderId, string reason) => $"Refund for order {orderId} has been processed successfully."; - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_With_StructuredInput.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_With_StructuredInput.cs deleted file mode 100644 index 239bda7a91..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/HandoffOrchestration_With_StructuredInput.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Extensions.AI; - -namespace Orchestration; - -/// -/// Demonstrates how to use the . -/// -public class HandoffOrchestration_With_StructuredInput(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Fact] - public async Task RunOrchestrationAsync() - { - // Initialize plugin - GithubPlugin githubPlugin = new(); - AIFunction githubAddLabelFunction = AIFunctionFactory.Create(githubPlugin.AddLabels); - - // Define the agents - ChatClientAgent triageAgent = - CreateAgent( - instructions: "Given a GitHub issue, triage it.", - name: "TriageAgent", - description: "An agent that triages GitHub issues"); - ChatClientAgent pythonAgent = - CreateAgent( - instructions: "You are an agent that handles Python related GitHub issues.", - name: "PythonAgent", - description: "An agent that handles Python related issues", - functions: githubAddLabelFunction); - ChatClientAgent dotnetAgent = - CreateAgent( - instructions: "You are an agent that handles .NET related GitHub issues.", - name: "DotNetAgent", - description: "An agent that handles .NET related issues", - functions: githubAddLabelFunction); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - - // Define the orchestration - HandoffOrchestration orchestration = - new(Handoffs - .StartWith(triageAgent) - .Add(triageAgent, [dotnetAgent, pythonAgent])) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - }; - - 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 = [] - }; - - // Run the orchestration - Console.WriteLine($"\n# INPUT:\n{input.Id}: {input.Title}\n"); - AgentRunResponse result = await orchestration.RunAsync(JsonSerializer.Serialize(input)); - Console.WriteLine($"\n# RESULT: {result}"); - Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n"); - } - - 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; } = []; - - public void AddLabels(string issueId, params string[] labels) => this.Labels[issueId] = labels; - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Foundry_Agents.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Foundry_Agents.cs deleted file mode 100644 index 926ae53d20..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Foundry_Agents.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.AI.Agents.Persistent; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Shared.Samples; - -namespace Orchestration; - -/// -/// Demonstrates how to use the for -/// executing multiple Foundry agents in sequence. -/// -public class SequentialOrchestration_Foundry_Agents(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunOrchestrationAsync(bool streamedResponse) - { - // Get a client to create server side agents with. - var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()); - var model = TestConfiguration.OpenAI.ChatModelId; - - // Define the agents - AIAgent analystAgent = - await persistentAgentsClient.CreateAIAgentAsync( - model, - name: "Analyst", - instructions: - """ - You are a marketing analyst. Given a product description, identify: - - Key features - - Target audience - - Unique selling points - """, - description: "A agent that extracts key concepts from a product description."); - AIAgent writerAgent = - await persistentAgentsClient.CreateAIAgentAsync( - model, - 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."); - AIAgent editorAgent = - await persistentAgentsClient.CreateAIAgentAsync( - model, - 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. - """, - description: "An agent that formats and proofreads the marketing copy."); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - // Define the orchestration - SequentialOrchestration orchestration = - new(analystAgent, writerAgent, editorAgent) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null, - }; - - // Run the orchestration - const string Input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours"; - Console.WriteLine($"\n# INPUT: {Input}\n"); - AgentRunResponse result = await orchestration.RunAsync(Input); - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - - // Cleanup - await persistentAgentsClient.Administration.DeleteAgentAsync(editorAgent.Id); - await persistentAgentsClient.Administration.DeleteAgentAsync(writerAgent.Id); - await persistentAgentsClient.Administration.DeleteAgentAsync(analystAgent.Id); - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Intro.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Intro.cs deleted file mode 100644 index 211055270d..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Intro.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; - -namespace Orchestration; - -/// -/// 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 SequentialOrchestration_Intro(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunOrchestrationAsync(bool streamedResponse) - { - // Define the agents - ChatClientAgent analystAgent = - CreateAgent( - name: "Analyst", - instructions: - """ - You are a marketing analyst. Given a product description, identify: - - Key features - - Target audience - - Unique selling points - """, - description: "A agent that extracts key concepts from a product description."); - ChatClientAgent writerAgent = - 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."); - ChatClientAgent editorAgent = - 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. - """, - description: "An agent that formats and proofreads the marketing copy."); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - // Define the orchestration - SequentialOrchestration orchestration = - new(analystAgent, writerAgent, editorAgent) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null, - }; - - // Run the orchestration - const string Input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours"; - Console.WriteLine($"\n# INPUT: {Input}\n"); - AgentRunResponse result = await orchestration.RunAsync(Input); - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Multi_Agent.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Multi_Agent.cs deleted file mode 100644 index 7c6760f4ce..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_Multi_Agent.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Shared.Samples; -using OpenAI; - -namespace Orchestration; - -/// -/// Demonstrates how to use the for -/// executing multiple heterogeneous agents in sequence. -/// -public class SequentialOrchestration_Multi_Agent(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task RunOrchestrationAsync(bool streamedResponse) - { - var openAIClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey); - var model = TestConfiguration.OpenAI.ChatModelId; - - // Define the agents - AIAgent analystAgent = - openAIClient.GetChatClient(model).CreateAIAgent( - name: "Analyst", - instructions: - """ - You are a marketing analyst. Given a product description, identify: - - Key features - - Target audience - - Unique selling points - """, - description: "A agent that extracts key concepts from a product description."); - AIAgent writerAgent = - openAIClient.GetOpenAIResponseClient(model).CreateAIAgent( - 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."); - AIAgent editorAgent = - openAIClient.GetAssistantClient().CreateAIAgent( - model, - 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. - """, - description: "An agent that formats and proofreads the marketing copy."); - - // Create a monitor to capturing agent responses (via ResponseCallback) - // to display at the end of this sample. (optional) - // NOTE: Create your own callback to capture responses in your application or service. - OrchestrationMonitor monitor = new(); - // Define the orchestration - SequentialOrchestration orchestration = - new(analystAgent, writerAgent, editorAgent) - { - LoggerFactory = this.LoggerFactory, - ResponseCallback = monitor.ResponseCallbackAsync, - StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallbackAsync : null, - }; - - // Run the orchestration - const string Input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours"; - Console.WriteLine($"\n# INPUT: {Input}\n"); - AgentRunResponse result = await orchestration.RunAsync(Input); - Console.WriteLine($"\n# RESULT: {result}"); - - this.DisplayHistory(monitor.History); - - // Cleanup - var assistantClient = openAIClient.GetAssistantClient(); - await assistantClient.DeleteAssistantAsync(editorAgent.Id); - // Need to know how to get the assistant thread ID to delete the thread (issue #260) - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_With_Cancellation.cs b/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_With_Cancellation.cs deleted file mode 100644 index e19d7dce2c..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Orchestration/SequentialOrchestration_With_Cancellation.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Agents.Orchestration; -using Microsoft.Extensions.AI; - -namespace Orchestration; - -/// -/// Demonstrates how to use cancel a while its running. -/// -public class SequentialOrchestration_With_Cancellation(ITestOutputHelper output) : OrchestrationSample(output) -{ - [Fact] - public async Task RunOrchestrationAsync() - { - // Define the agents - ChatClientAgent agent = - 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 }; - - // Run the orchestration - const string Input = "42"; - Console.WriteLine($"\n# INPUT: {Input}\n"); - - OrchestratingAgentResponse result = await orchestration.RunAsync([new ChatMessage(ChatRole.User, Input)]); - - result.Cancel(); - await Task.Delay(TimeSpan.FromSeconds(3)); - - try - { - Console.WriteLine($"\n# RESULT: {await result}"); - } - catch (TimeoutException exception) - { - Console.WriteLine($"\n# CANCELED: {exception.Message}"); - } - } -} diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Resources/Hamlet_full_play_summary.txt b/dotnet/samples/GettingStarted/AgentOrchestration/Resources/Hamlet_full_play_summary.txt deleted file mode 100644 index 9050a46e66..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Resources/Hamlet_full_play_summary.txt +++ /dev/null @@ -1,13 +0,0 @@ -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/samples/GettingStarted/AgentOrchestration/Resources/employees.pdf b/dotnet/samples/GettingStarted/AgentOrchestration/Resources/employees.pdf deleted file mode 100644 index bba45f80a90b9e83dd627169bd2b35418fc7a182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43422 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 tt_Qna&aFsUb~*nK+dRE#YYMVVf=Qm-$*ez@Z! diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/Resources/groceries.txt b/dotnet/samples/GettingStarted/AgentOrchestration/Resources/groceries.txt deleted file mode 100644 index 350cfba875..0000000000 --- a/dotnet/samples/GettingStarted/AgentOrchestration/Resources/groceries.txt +++ /dev/null @@ -1,6 +0,0 @@ -Item Quantity -apple 3 -banana 2 -orange 5 -apple 1 -banana 3 diff --git a/dotnet/src/Microsoft.Agents.Orchestration/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.Orchestration/AIAgentExtensions.cs deleted file mode 100644 index 2eeeaa5e52..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/AIAgentExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.Orchestration; - -/// Provides extensions for orchestrating s. -public static class AIAgentExtensions -{ - private const string DefaultInstructions = "Respond with JSON that is populated by using the information in this conversation."; - - /// - /// Runs the agent with the messages, then uses the chat client to process the agent's output and return a structured response. - /// - /// The type of the result expected from the chat client response. - /// The AI agent to be run. - /// The chat client used to process the messages. - /// The message to be processed. - /// An optional thread context for the agent execution. - /// Optional settings that influence the agent's execution. - /// Optional serializer options to control how is deserialized. - /// A token to monitor for cancellation requests. - /// A task representing the asynchronous operation, with a result of type containing the - /// structured response. - public static ValueTask RunAsync( - this AIAgent agent, - IChatClient chatClient, - string message, - AgentThread? thread = null, - AgentRunOptions? runOptions = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(agent); - Throw.IfNull(chatClient); - Throw.IfNull(message); - - return RunAsync( - agent, - chatClient, - [new ChatMessage(ChatRole.User, message)], - thread, - runOptions, - serializerOptions, - cancellationToken); - } - - /// - /// Runs the agent with the messages, then uses the chat client to process the agent's output and return a structured response. - /// - /// The type of the result expected from the chat client response. - /// The AI agent to be run. - /// The chat client used to process the messages. - /// A collection of chat messages to be processed. - /// An optional thread context for the agent execution. - /// Optional settings that influence the agent's execution. - /// Optional serializer options to control how is deserialized. - /// A token to monitor for cancellation requests. - /// A task representing the asynchronous operation, with a result of type containing the - /// structured response. - public static async ValueTask RunAsync( - this AIAgent agent, - IChatClient chatClient, - IReadOnlyCollection messages, - AgentThread? thread = null, - AgentRunOptions? runOptions = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(agent); - Throw.IfNull(chatClient); - Throw.IfNull(messages); - - // Invoke the agent. - var response = await agent.RunAsync(messages, thread, runOptions, cancellationToken).ConfigureAwait(false); - - // Pass the output messages to the chat client to get a structured response. - var result = await chatClient.GetResponseAsync( - response.Messages, - serializerOptions: serializerOptions ?? AIJsonUtilities.DefaultOptions, - new ChatOptions() { Instructions = DefaultInstructions }, - cancellationToken: cancellationToken).ConfigureAwait(false); - - // Parse and return the results. - return result.Result; - } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/ConcurrentOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/ConcurrentOrchestration.cs deleted file mode 100644 index d53d8e46b0..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/ConcurrentOrchestration.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.Orchestration; - -/// Provides an orchestrating agent that broadcasts the input message to each agent and then aggregates the result into a single response. -public partial class ConcurrentOrchestration : OrchestratingAgent -{ - private Func>? _aggregationFunc; - - /// Initializes a new instance of the class. - /// The agents participating in the orchestration. - public ConcurrentOrchestration(params AIAgent[] subagents) : this(subagents, name: null) - { - } - - /// Initializes a new instance of the class. - /// The agents participating in the orchestration. - /// An optional name for this orchestrating agent. - public ConcurrentOrchestration(AIAgent[] subagents, string? name) : base(subagents, name) - { - } - - /// Gets or sets the function to use to aggregate an from each participating agent into a single . - /// The default function takes the last message from each response and puts those messages into a new response instance. - public Func> AggregationFunc - { - get - { - if (this._aggregationFunc is { } f) - { - return f; - } - - return (responses, cancellationToken) - => Task.FromResult( - new AgentRunResponse([.. responses - .Where(r => r.Messages.Count > 0) - .Select(r => - { - var messages = r.Messages; - return messages.Count > 0 ? messages[messages.Count - 1] : new(); - }) - ]) - ); - } - set => this._aggregationFunc = value; - } - - /// - protected override Task RunCoreAsync(IEnumerable messages, OrchestratingAgentContext context, CancellationToken cancellationToken) => - this.ResumeAsync(messages as IReadOnlyCollection ?? messages.ToList(), new AgentRunResponse?[this.Agents.Count], context, cancellationToken); - - /// - protected override Task ResumeCoreAsync(JsonElement checkpointState, IEnumerable newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.ConcurrentState) ?? throw new InvalidOperationException("The checkpoint state is invalid."); - - // Append the new messages to the checkpoint state - List allMessages = [.. state.Messages, .. newMessages]; - return this.ResumeAsync(allMessages, state.Completed, context, cancellationToken); - } - - /// - private async Task ResumeAsync( - IReadOnlyCollection input, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - List tasks = new(this.Agents.Count); - for (int i = 0; i < this.Agents.Count; i++) - { - if (completed[i] is null) - { - int localI = i; - tasks.Add(Task.Run(async () => - { - AIAgent agent = this.Agents[localI]; - LogOrchestrationSubagentRunning(context, agent); - - completed[localI] = await RunAsync(agent, context, input, options: null, cancellationToken).ConfigureAwait(false); - - LogOrchestrationSubagentCompleted(context, agent); - await CheckpointAsync(input, completed, context, cancellationToken).ConfigureAwait(false); - }, cancellationToken)); - } - } - - // TODO: What do we want to do if one of the agents fails? As written, this waits for all to complete, - // and then throws. And when resumption happens, it'll end up retrying failed agents. If we don't want that, - // which we probably don't, we should checkpoint that failures happened, too. - - await Task.WhenAll(tasks).ConfigureAwait(false); - - Debug.Assert(Array.TrueForAll(completed, r => r is not null), "Expected all agents to have produced a result"); - return await this.AggregationFunc(completed!, cancellationToken).ConfigureAwait(false); - } - - private static Task CheckpointAsync(IReadOnlyCollection messages, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken) => - context.Runtime is not null ? WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(messages, completed), OrchestrationJsonContext.Default.ConcurrentState), context, cancellationToken) : - Task.CompletedTask; - - internal sealed record ConcurrentState(IReadOnlyCollection Messages, AgentRunResponse?[] Completed); -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs deleted file mode 100644 index 7763995f8d..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatManager.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.Orchestration; - -/// -/// 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 _invocationCount; - - /// - /// Initializes a new instance of the class. - /// - protected GroupChatManager() { } - - /// - /// Gets the number of times the group chat manager has been invoked. - /// - public int InvocationCount => this._invocationCount; - - /// - /// Gets or sets the maximum number of invocations allowed for the group chat manager. - /// - public int MaximumInvocationCount { get; init; } = int.MaxValue; - - /// - /// Gets or sets the callback to be invoked for interactive input. - /// - public Func>? 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. - protected internal abstract ValueTask> FilterResultsAsync(IReadOnlyCollection 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. - protected internal abstract ValueTask> SelectNextAgentAsync(IReadOnlyCollection history, GroupChatTeam 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. - protected internal abstract ValueTask> ShouldRequestUserInputAsync(IReadOnlyCollection 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. - protected internal virtual ValueTask> ShouldTerminateAsync(IReadOnlyCollection history, CancellationToken cancellationToken = default) - { - bool resultValue = false; - string reason = "Maximum number of invocations has not been reached."; - if (Interlocked.Increment(ref this._invocationCount) > this.MaximumInvocationCount) - { - resultValue = true; - reason = "Maximum number of invocations reached."; - } - - GroupChatManagerResult result = new(resultValue) { Reason = reason }; - return new ValueTask>(result); - } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs deleted file mode 100644 index 1dc1953db2..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatOrchestration.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.Orchestration; - -/// -/// An orchestration that coordinates a group-chat using a manager to control conversation flow. -/// -public sealed partial class GroupChatOrchestration : OrchestratingAgent -{ - private readonly GroupChatManager _manager; - - /// - /// Initializes a new instance of the class. - /// - /// The manager that controls the flow of the group-chat. - /// The agents participating in the orchestration. - public GroupChatOrchestration(GroupChatManager manager, params AIAgent[] agents) : this(manager, agents, name: null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The manager that controls the flow of the group-chat. - /// The agents participating in the orchestration. - /// An optional name for this orchestrating agent. - public GroupChatOrchestration(GroupChatManager manager, AIAgent[] agents, string? name) : base(agents, name) - { - this._manager = Throw.IfNull(manager); - } - - /// Gets or sets a callback invoked when user input is requested. - public Func>? InteractiveCallback { get; set; } - - /// - protected override Task RunCoreAsync(IEnumerable messages, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - List allMessages = [.. messages]; - int originalMessageCount = allMessages.Count; - return this.ResumeAsync(allMessages, originalMessageCount, context, cancellationToken); - } - - /// - protected override Task ResumeCoreAsync(JsonElement checkpointState, IEnumerable newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.GroupChatState) ?? throw new InvalidOperationException("The checkpoint state is invalid."); - - // Append the new messages to the checkpoint state - List allMessages = [.. state.AllMessages, .. newMessages]; - - return this.ResumeAsync(allMessages, allMessages.Count, context, cancellationToken); - } - - private async Task ResumeAsync( - List allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - GroupChatTeam team = []; - foreach (AIAgent agent in this.Agents) - { - team[agent.DisplayName] = (agent.GetType().Name, agent.Description ?? agent.Name ?? "A helpful agent."); - } - - var interactiveCallback = this.InteractiveCallback ?? this._manager.InteractiveCallback; - while (true) - { - // First, check if we should request user input. - if (interactiveCallback is not null) - { - var userInputResult = await this._manager.ShouldRequestUserInputAsync(allMessages, cancellationToken).ConfigureAwait(false); - if (userInputResult.Value && interactiveCallback is not null) - { - ChatMessage userMessage = await interactiveCallback().ConfigureAwait(false); - allMessages.Add(userMessage); - - // Broadcast the user input - if (this.ResponseCallback is not null) - { - await this.ResponseCallback([userMessage]).ConfigureAwait(false); - } - - await CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false); - continue; - } - } - - // Check if we should terminate the conversation - var terminateResult = await this._manager.ShouldTerminateAsync(allMessages, cancellationToken).ConfigureAwait(false); - if (terminateResult.Value) - { - // Filter and return final results - var filterResult = await this._manager.FilterResultsAsync(allMessages, cancellationToken).ConfigureAwait(false); - return new AgentRunResponse([new ChatMessage(ChatRole.Assistant, filterResult.Value) { AuthorName = this.DisplayName }]); - } - - // Select the next agent to speak - var nextAgentResult = await this._manager.SelectNextAgentAsync(allMessages, team, cancellationToken).ConfigureAwait(false); - AIAgent nextAgent = this.FindAgentByName(nextAgentResult.Value) ?? - throw new InvalidOperationException($"AIAgent '{nextAgentResult.Value}' not found in the orchestration."); - - // Run the selected agent with all messages. - LogOrchestrationSubagentRunning(context, nextAgent); - AgentRunResponse response = await RunAsync(nextAgent, context, allMessages, options: null, cancellationToken).ConfigureAwait(false); - allMessages.AddRange(response.Messages); // Add the agent's response to the conversation. - LogOrchestrationSubagentCompleted(context, nextAgent); - - await CheckpointAsync(allMessages, originalMessageCount, context, cancellationToken).ConfigureAwait(false); - } - } - - private AIAgent? FindAgentByName(string name) => this.Agents.FirstOrDefault(a => a.DisplayName == name); - - private static Task CheckpointAsync(List allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken) => - context.Runtime is not null ? WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(allMessages, originalMessageCount), OrchestrationJsonContext.Default.GroupChatState), context, cancellationToken) : - Task.CompletedTask; - - internal sealed record GroupChatState(List AllMessages, int OriginalMessageCount); -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs deleted file mode 100644 index fcf844a18b..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/GroupChatTeam.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.Agents.Orchestration; - -/// -/// Describes a team of agents participating in a group chat. -/// -public sealed class GroupChatTeam : Dictionary -{ - /// - /// Format the names of the agents in the team as a comma delimimted list. - /// - /// A comma delimimted list of agent name. - public string FormatNames() => string.Join(",", this.Select(t => t.Key)); - - /// - /// Format the names and descriptions of the agents in the team as a markdown list. - /// - /// A markdown list of agent names and descriptions. - public string FormatList() => string.Join(Environment.NewLine, this.Select(t => $"- {t.Key}: {t.Value.Description}")); -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs b/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs deleted file mode 100644 index dd7d00e3da..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/GroupChat/RoundRobinGroupChatManager.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.Orchestration; - -/// -/// 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; - - /// - protected internal override ValueTask> FilterResultsAsync( - IReadOnlyCollection history, CancellationToken cancellationToken = default) - { - GroupChatManagerResult result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." }; - return new ValueTask>(result); - } - - /// - protected internal override ValueTask> SelectNextAgentAsync( - IReadOnlyCollection history, GroupChatTeam team, CancellationToken cancellationToken = default) - { - string nextAgent = team.Skip(this._currentAgentIndex).First().Key; - this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count; - GroupChatManagerResult result = new(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" }; - return new ValueTask>(result); - } - - /// - protected internal override ValueTask> ShouldRequestUserInputAsync( - IReadOnlyCollection history, CancellationToken cancellationToken = default) - { - GroupChatManagerResult result = new(false) { Reason = "The default round-robin group chat manager does not request user input." }; - return new ValueTask>(result); - } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoffs/HandoffOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoffs/HandoffOrchestration.cs deleted file mode 100644 index aeb4b607dd..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoffs/HandoffOrchestration.cs +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.Orchestration; - -/// -/// An orchestration that provides the input message to the first agent -/// and sequentially passes each agent result to the next agent. -/// -public sealed partial class HandoffOrchestration : OrchestratingAgent -{ - private readonly Handoffs _handoffs; - - /// - /// Initializes a new instance of the class. - /// - /// Defines the handoff connections for each agent. - public HandoffOrchestration(Handoffs handoffs) : this(handoffs, name: null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Defines the handoff connections for each agent. - /// An optional name for this orchestrating agent. - public HandoffOrchestration(Handoffs handoffs, string? name) : base(handoffs.Agents.ToArray(), name) - { - this._handoffs = handoffs; - } - - /// Gets or sets a callback invoked when no next handoff is selected in order to supply - public Func>? InteractiveCallback { get; set; } - - /// - protected override Task RunCoreAsync(IEnumerable messages, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - List allMessages = [.. messages]; - int originalMessageCount = allMessages.Count; - return this.ResumeAsync(this._handoffs.InitialAgent, allMessages, originalMessageCount, context, cancellationToken); - } - - /// - protected override Task ResumeCoreAsync(JsonElement checkpointState, IEnumerable newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.HandoffState) ?? throw new InvalidOperationException("The checkpoint state is invalid."); - - AIAgent? nextAgent = null; - if (state.NextAgent is null) - { - nextAgent = this._handoffs.InitialAgent; - } - else - { - nextAgent = this.Agents.FirstOrDefault(a => a.Id == state.NextAgent); - if (nextAgent is null) - { - Throw.InvalidOperationException($"The next agent '{state.NextAgent}' is not defined in the orchestration."); - } - } - - // Append the new messages to the checkpoint state - List allMessages = [.. state.AllMessages, .. newMessages]; - - return this.ResumeAsync(nextAgent, allMessages, allMessages.Count, context, cancellationToken); - } - - /// - private async Task ResumeAsync( - AIAgent? agent, List allMessages, int originalMessageCount, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - Debug.Assert(agent is not null); - AgentRunResponse? response = null; - - while (agent is not null) - { - LogOrchestrationSubagentRunning(context, agent); - - if (!this._handoffs.Targets.TryGetValue(agent, out var handoffs) || handoffs.Count == 0) - { - // If no handoff is available, we can run the agent directly and return its response. - response = await RunAsync(agent, context, allMessages, context.Options, cancellationToken).ConfigureAwait(false); - LogOrchestrationSubagentCompleted(context, agent); - allMessages.AddRange(response.Messages); - agent = null; - await CheckpointAsync().ConfigureAwait(false); - break; - } - - // Create the options for the next agent request, including handoff functions. - HandoffContext handoffCtx = new(handoffs); - ChatClientAgentRunOptions? options; - List handoffTools = handoffCtx.CreateHandoffFunctions(this.InteractiveCallback is not null); - if (context.Options is ChatClientAgentRunOptions contextOptions) - { - ChatOptions chatOptions = contextOptions.ChatOptions?.Clone() ?? new(); - chatOptions.Tools = chatOptions.Tools is { Count: > 0 } ? [.. chatOptions.Tools, .. handoffTools] : handoffTools; - options = new(chatOptions); - } - else - { - options = new(new() { Tools = handoffTools }); - } - - // Invoke the next agent with all of the messages collected so far. - response = await RunAsync(agent, context, allMessages, options, cancellationToken).ConfigureAwait(false); - LogOrchestrationSubagentCompleted(context, agent); - allMessages.AddRange(response.Messages); - agent = handoffCtx.TargetedAgent; - RemoveHandoffFunctionCalls(response, handoffTools); - - if (this.InteractiveCallback is not null) - { - if (handoffCtx.EndTaskInvoked) - { - break; - } - - allMessages.Add(await this.InteractiveCallback().ConfigureAwait(false)); - } - - await CheckpointAsync().ConfigureAwait(false); - } - - allMessages.RemoveRange(0, originalMessageCount); - response ??= new(); - response.Messages = allMessages; - return response; - - Task CheckpointAsync() => context.Runtime is not null ? - WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(agent?.Id, allMessages, originalMessageCount), OrchestrationJsonContext.Default.HandoffState), context, cancellationToken) : - Task.CompletedTask; - } - - private static void RemoveHandoffFunctionCalls(AgentRunResponse response, List handoffTools) - { - HashSet? removeToolNames = null; - HashSet? handoffCallIds = null; - - foreach (var message in response.Messages) - { - for (int i = message.Contents.Count - 1; i >= 0; i--) - { - if (message.Contents[i] is FunctionCallContent fcc) - { - removeToolNames ??= [.. handoffTools.Select(t => t.Name)]; - if (removeToolNames.Contains(fcc.Name)) - { - (handoffCallIds ??= []).Add(fcc.CallId); - message.Contents.RemoveAt(i); - } - } - } - } - - if (handoffCallIds is not null) - { - foreach (var message in response.Messages) - { - for (int i = message.Contents.Count - 1; i >= 0; i--) - { - if (message.Contents[i] is FunctionResultContent frc && handoffCallIds.Contains(frc.CallId)) - { - message.Contents.RemoveAt(i); - } - } - } - } - } - - private sealed class HandoffContext(HashSet handoffs) - { - public AIAgent? TargetedAgent { get; set; } - public bool EndTaskInvoked { get; set; } - - public List CreateHandoffFunctions(bool needsEndTask) - { - List functions = []; - - if (needsEndTask) - { - functions.Add(AIFunctionFactory.Create( - () => - { - this.EndTaskInvoked = true; - Terminate(); - }, - name: "end_task", - description: "Invoke this function when all work is completed and no further interactions are required.")); - } - - foreach (Handoffs.HandoffTarget handoff in handoffs) - { - functions.Add(AIFunctionFactory.Create( - () => - { - this.TargetedAgent = handoff.Target; - Terminate(); - }, - name: $"handoff_to_{InvalidNameCharsRegex().Replace(handoff.Target.DisplayName, "_")}", - description: handoff.Reason)); - } - - return functions; - - static void Terminate() - { - if (FunctionInvokingChatClient.CurrentContext is not { } ctx) - { - throw new NotSupportedException($"The agent is not configured with a {nameof(FunctionInvokingChatClient)}. Cease execution."); - } - - ctx.Terminate = true; - } - } - } - - internal sealed record HandoffState(string? NextAgent, List AllMessages, int OriginalMessageCount); - - /// Regex that flags any character other than ASCII digits or letters or the underscore. -#if NET - [GeneratedRegex("[^0-9A-Za-z_]+")] - private static partial Regex InvalidNameCharsRegex(); -#else - private static Regex InvalidNameCharsRegex() => s_invalidNameCharsRegex; - private static readonly Regex s_invalidNameCharsRegex = new("[^0-9A-Za-z_]+", RegexOptions.Compiled); -#endif -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Handoffs/Handoffs.cs b/dotnet/src/Microsoft.Agents.Orchestration/Handoffs/Handoffs.cs deleted file mode 100644 index bc314ff9b4..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Handoffs/Handoffs.cs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections; -using System.Collections.Generic; -using Microsoft.Agents.AI; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable CA1710 // Identifiers should have correct suffix - -namespace Microsoft.Agents.Orchestration; - -/// -/// Defines the orchestration handoff relationships for all agents in the system. -/// -public sealed class Handoffs : - IReadOnlyDictionary> -{ - /// - /// Initializes a new instance of the class with no handoff relationships. - /// - /// The first agent to be invoked (prior to any handoff). - private Handoffs(AIAgent initialAgent) - { - Throw.IfNull(initialAgent); - - this.Agents.Add(initialAgent); - this.InitialAgent = initialAgent; - } - - /// Gets the initial agent to which the first messages will be sent. - public AIAgent InitialAgent { get; } - - /// Gets a collection of all handoff targets, indexed by the source of the handoffs. - internal Dictionary> Targets { get; } = []; - - /// Gets a set of all agents involved in the handoffs, sources and targets. - internal HashSet Agents { get; } = []; - - /// - /// Creates a new collection of handoffs that start with the specified agent. - /// - /// The initial agent. - /// The new instance. - public static Handoffs StartWith(AIAgent initialAgent) => new(initialAgent); - - /// Creates a new from the described handoffs. - /// An optional name for this orchestrating agent. - /// The new . - public HandoffOrchestration Build(string? name = null) => new(this, name); - - /// - /// Adds handoff relationships from a source agent to one or more target agents. - /// - /// The source agent. - /// The target agents to add as handoff targets for the source agent. - /// The updated instance. - /// The handoff reason for each target is derived from its description or name. - public Handoffs Add(AIAgent source, AIAgent[] targets) - { - Throw.IfNull(source); - Throw.IfNull(targets); - if (Array.IndexOf(targets, null) >= 0) - { - Throw.ArgumentNullException(nameof(targets), "One or more target agents are null."); - } - - foreach (var target in targets) - { - this.Add(source, target); - } - - return this; - } - - /// - /// Adds a handoff relationship from a source agent to a target agent with a custom handoff reason. - /// - /// The source agent. - /// The target agent. - /// The reason the should hand off to the . - /// The updated instance. - public Handoffs Add(AIAgent source, AIAgent target, string? handoffReason = null) - { - Throw.IfNull(source); - Throw.IfNull(target); - - this.Agents.Add(source); - this.Agents.Add(target); - - if (!this.Targets.TryGetValue(source, out var handoffs)) - { - this.Targets[source] = handoffs = []; - } - - if (!handoffs.Add(new(target, handoffReason))) - { - Throw.InvalidOperationException($"A handoff from agent '{source.DisplayName}' to agent '{target.DisplayName}' has already been registered."); - } - - return this; - } - - /// - IEnumerable IReadOnlyDictionary>.this[AIAgent key] => this.Targets[key]; - - /// - IEnumerable IReadOnlyDictionary>.Keys => this.Targets.Keys; - - /// - IEnumerable> IReadOnlyDictionary>.Values => this.Targets.Values; - - /// - int IReadOnlyCollection>>.Count => this.Targets.Count; - - /// - bool IReadOnlyDictionary>.ContainsKey(AIAgent key) => this.Targets.ContainsKey(key); - - /// - IEnumerator>> IEnumerable>>.GetEnumerator() - { - foreach (var kvp in this.Targets) - { - yield return new(kvp.Key, kvp.Value); - } - } - - /// - IEnumerator IEnumerable.GetEnumerator() => - ((IReadOnlyDictionary>)this).GetEnumerator(); - - /// - bool IReadOnlyDictionary>.TryGetValue(AIAgent key, out IEnumerable value) - { - if (this.Targets.TryGetValue(key, out var handoffs)) - { - value = handoffs; - return true; - } - - value = []; - return false; - } - - /// Describes a handoff to a specific target . - public readonly struct HandoffTarget : IEquatable - { - internal HandoffTarget(AIAgent target, string? reason = null) - { - this.Target = Throw.IfNull(target); - - if (string.IsNullOrWhiteSpace(reason)) - { - reason = target.Description ?? target.Name; - if (string.IsNullOrWhiteSpace(reason)) - { - Throw.InvalidOperationException( - $"The provided target agent with Id '{target.Id}' has no description or name, and no handoff description has been provided. " + - "At least one of these are required to register a handoff so that the appropriate target agent can be chosen."); - } - } - - this.Reason = reason!; - } - - /// Gets the target of the handoff. - public AIAgent Target { get; } - - /// Gets the reason a handoff to should be performed. - public string Reason { get; } - - /// - public bool Equals(HandoffTarget other) => this.Target == other.Target; - - /// - public override bool Equals(object? obj) => obj is HandoffTarget other && this.Equals(other); - - /// - public override int GetHashCode() => this.Target.GetHashCode(); - - /// - public static bool operator ==(HandoffTarget left, HandoffTarget right) => left.Equals(right); - - /// - public static bool operator !=(HandoffTarget left, HandoffTarget right) => !left.Equals(right); - } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj b/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj deleted file mode 100644 index cc4ac79ce8..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - Microsoft.Agents.Orchestration - alpha - $(NoWarn);MEAI001 - - - - true - true - - - - - - - Microsoft Agent Orchestration Framework - Contains the Microsoft Agent Orchestration Framework. - - - - - - - - - - - - diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgent.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgent.cs deleted file mode 100644 index f7b9119a5a..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgent.cs +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.Orchestration; - -/// -/// Base class for multi-agent agent orchestration patterns. -/// -public abstract partial class OrchestratingAgent : AIAgent -{ - /// Key used to persist state with the runtime. - private const string StateKey = "State"; - - /// - /// Initializes a new instance of the class. - /// - /// Specifies the agents participating in this orchestration. - /// An optional name for this agent. - protected OrchestratingAgent(IReadOnlyList agents, string? name = null) - { - _ = Throw.IfNullOrEmpty(agents); - - this.Agents = agents; - this.Name = name; - } - - /// - public override string? Name { get; } - - /// - /// Gets the list of member targets involved in the orchestration. - /// - protected IReadOnlyList Agents { get; } - - /// Gets the serializer options to use by the orchestration. - public JsonSerializerOptions? SerializerOptions { get; set; } - - /// - /// Gets the associated logger. - /// - public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance; - - /// - /// Optional callback that is invoked for every agent response. - /// - public Func, ValueTask>? ResponseCallback { get; set; } - - /// - /// Optional callback that is invoked for every agent update. - /// - public Func? StreamingResponseCallback { get; set; } - - /// - public override AgentThread GetNewThread() - => new OrchestratingAgentThread(); - - /// - public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) - => new OrchestratingAgentThread(serializedThread, jsonSerializerOptions); - - /// - public sealed override async Task RunAsync( - IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(messages); - - if (thread is not null) - { - if (thread is not OrchestratingAgentThread typedThread) - { - throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used."); - } - - if (typedThread.MessageStore is null) - { - throw new InvalidOperationException("An agent service managed thread is not supported by this agent."); - } - - List messagesList = (await typedThread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false)).ToList(); - messagesList.AddRange(messages); - messages = messagesList; - } - - var orchestrationResult = await this.RunAsync(messages, options, runtime: null, cancellationToken).ConfigureAwait(false); - return await orchestrationResult.Task.ConfigureAwait(false); - } - - /// - public sealed override async IAsyncEnumerable RunStreamingAsync( - IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // TODO: There should be a RunAsync overload that returns an OrchestratingAgentStreamingResponse, which this then delegates to. - - var response = await this.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false); - foreach (var update in response.ToAgentRunResponseUpdates()) - { - yield return update; - } - } - - /// - /// Initiates processing of the orchestration. - /// - /// The input message. - /// Optional parameters for agent invocation. - /// The runtime associated with the orchestration. - /// The to monitor for cancellation requests. The default is . - public async ValueTask RunAsync( - IEnumerable messages, - AgentRunOptions? options = null, - IActorRuntimeContext? runtime = null, - CancellationToken cancellationToken = default) - { - var readonlyCollectionMessages = Throw.IfNull(messages) as IReadOnlyCollection ?? messages.ToList(); - cancellationToken.ThrowIfCancellationRequested(); - - ILogger logger = this.LoggerFactory.CreateLogger(this.GetType().Name); - - OrchestratingAgentContext context = new() - { - OrchestratingAgent = this, - Runtime = runtime, - Options = options, - Logger = logger, - }; - - LogOrchestrationInvoked(logger, this.DisplayName, context.Id); - - CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cancellationToken = cts.Token; - - JsonElement? checkpoint = await ReadCheckpointAsync(context, cancellationToken).ConfigureAwait(false); - Task completion = checkpoint is null ? - this.RunCoreAsync(readonlyCollectionMessages, context, cancellationToken) : - this.ResumeCoreAsync(checkpoint.Value, readonlyCollectionMessages, context, cancellationToken); - - if (logger.IsEnabled(LogLevel.Trace)) - { - _ = LogCompletionAsync(logger, context, completion); - } - - return new OrchestratingAgentResponse(context, completion, cts, logger); - } - - /// - /// Initiates processing of the orchestration. - /// - /// The input message. - /// The context for this operation. - /// A cancellation token that can be used to cancel the operation. - protected abstract Task RunCoreAsync(IEnumerable messages, OrchestratingAgentContext context, CancellationToken cancellationToken); - - /// - /// Resumes processing of the orchestration. - /// - /// The last checkpoint state available from which to resume the operation. - /// The new messages to be processed in addition to the checkpoint state. - /// The context for this operation. - /// A cancellation token that can be used to cancel the operation. - protected abstract Task ResumeCoreAsync(JsonElement checkpointState, IEnumerable newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken); - - /// - /// Runs the agent with input messages and respond with both streamed and regular messages. - /// - /// The agent being run - /// The associated orchestration context for this run. - /// The list of chat messages to send. - /// Options to use when invoking the agent. - /// A cancellation token that can be used to cancel the operation. - /// A task that returns the response . - protected static async ValueTask RunAsync(AIAgent agent, OrchestratingAgentContext context, IEnumerable input, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - { - // Utilize streaming iff a streaming callback is provided; otherwise, use the non-streaming API. - AgentRunResponse response; - if (context.OrchestratingAgent?.StreamingResponseCallback is { } streamingCallback) - { - // For streaming, enumerate all the updates, invoking the callback for each, and storing them all. - // Then convert them all into a single response instance. - List updates = []; - - await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, options: options ?? context.Options, cancellationToken: cancellationToken).ConfigureAwait(false)) - { - updates.Add(update); - await streamingCallback(update).ConfigureAwait(false); - } - - response = updates.ToAgentRunResponse(); - } - else - { - // For non-streaming, just invoke the non-streaming method and get back the response. - response = await agent.RunAsync(input, options: options ?? context.Options, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - // Regardless of whether we invoked streaming callbacks for individual updates, invoke the non-streaming callback with the final response instance. - // This can be used as an indication of completeness if someone otherwise only cares about the streaming updates. - if (context.OrchestratingAgent?.ResponseCallback is { } responseCallback) - { - await responseCallback.Invoke(response.Messages).ConfigureAwait(false); - } - - return response; - } - - /// Writes the specified checkpoint state to the runtime. - /// The state to persist. - /// The context for the orchestrating operation. - /// A cancellation token that can be used to cancel the operation. - /// A Task that completes when the asynchronous operation quiesces. - protected static async Task WriteCheckpointAsync(JsonElement state, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - _ = Throw.IfNull(context); - - if (context.Runtime is not null) - { - while (true) - { - var response = await context.Runtime.WriteAsync( - new ActorWriteOperationBatch(context.ETag ?? "", [new SetValueOperation(StateKey, state)]), - cancellationToken).ConfigureAwait(false); - - if (response.Success) - { - break; - } - - // If the write failed, there was a concurrency conflict where someone else updated the state. - // But we don't actually care about consistency between the previous checkpoint and the current one, - // so we just retry the write with the new etag. - context.ETag = response.ETag; - } - } - } - - /// Read checkpoint information, if it exists, for the specified context. - /// The context for the orchestrating operation. - /// A cancellation token that can be used to cancel the operation. - /// The loaded state, or null if it doesn't exist. - protected static async ValueTask ReadCheckpointAsync(OrchestratingAgentContext context, CancellationToken cancellationToken) - { - _ = Throw.IfNull(context); - - if (context.Runtime is not null) - { - ReadResponse response = await context.Runtime.ReadAsync( - new ActorReadOperationBatch([new GetValueOperation(StateKey)]), - cancellationToken).ConfigureAwait(false); - - context.ETag = response.ETag; - - if (response.Results is { } results && - results[results.Count - 1] is GetValueResult { Value: not null } getValueResult) - { - return getValueResult.Value.Value; - } - } - - return default; - } - - [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} started ('{Id}')")] - private static partial void LogOrchestrationInvoked(ILogger logger, string orchestration, string id); - - [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed ('{Id}'). Result: '{Result}'")] - private static partial void LogOrchestrationResult(ILogger logger, string orchestration, string id, string result); - - [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} cancellation requested ('{Id}')")] - internal static partial void LogOrchestrationCancellationRequested(ILogger logger, string orchestration, string id); - - [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} failed ('{Id}')")] - private static partial void LogOrchestrationFailure(ILogger logger, string orchestration, string id, Exception error); - - [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} invoking agent '{Agent}' ('{Id}')")] - private static partial void LogOrchestrationSubagentRunning(ILogger logger, string orchestration, string id, string agent); - - [LoggerMessage(Level = LogLevel.Trace, Message = "{Orchestration} completed agent '{Agent}' ('{Id}')")] - private static partial void LogOrchestrationSubagentCompleted(ILogger logger, string orchestration, string id, string agent); - - private protected static void LogOrchestrationSubagentRunning(OrchestratingAgentContext context, AIAgent agent) => - LogOrchestrationSubagentRunning(context.Logger, context.ToString(), context.Id, agent.DisplayName); - - private protected static void LogOrchestrationSubagentCompleted(OrchestratingAgentContext context, AIAgent agent) => - LogOrchestrationSubagentCompleted(context.Logger, context.ToString(), context.Id, agent.DisplayName); - - private static async Task LogCompletionAsync(ILogger logger, OrchestratingAgentContext context, Task completion) - { - try - { - AgentRunResponse result = await completion.ConfigureAwait(false); - - if (logger.IsEnabled(LogLevel.Trace)) - { - JsonSerializerOptions jso = context.OrchestratingAgent?.SerializerOptions ?? AgentAbstractionsJsonUtilities.DefaultOptions; - LogOrchestrationResult(logger, context.ToString(), context.Id, JsonSerializer.Serialize(result, jso.GetTypeInfo(typeof(AgentRunResponse)))); - } - } - catch (Exception ex) - { - LogOrchestrationFailure(logger, context.ToString(), context.Id, ex); - } - } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentContext.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentContext.cs deleted file mode 100644 index f17ae4e676..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Runtime; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.Agents.Orchestration; - -/// -/// Provides contextual information for an orchestration operation, including logging, and response callback. -/// -public sealed class OrchestratingAgentContext -{ - private ILogger? _logger; - private string? _id; - - /// Gets the orchestrating agent associated with this operation. - public OrchestratingAgent? OrchestratingAgent { get; set; } - - /// Gets the associated agent runtime, if one is being used. - public IActorRuntimeContext? Runtime { get; set; } - - /// Gets the options associated with the orchestration run. - public AgentRunOptions? Options { get; set; } - - /// Gets or sets the last version number provided by the runtime for checkpoint state. - public string? ETag { get; set; } - - /// Gets or sets an ID to use for the orchestration operation. - public string Id - { - get - { - this._id ??= this.Runtime?.ActorId.ToString() ?? Guid.NewGuid().ToString("N"); - return this._id; - } - } - - /// - /// Gets the associated logger for this operation. - /// - public ILogger Logger - { - get => this._logger ?? NullLogger.Instance; - set => this._logger = value; - } - - /// - public override string ToString() => - this.OrchestratingAgent?.DisplayName ?? - nameof(OrchestratingAgentContext); -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentResponse.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentResponse.cs deleted file mode 100644 index d50160de98..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentResponse.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Agents.Orchestration; - -/// -/// Represents the result of an orchestrating agent. -/// This class encapsulates the asynchronous completion of an orchestration process. -/// -public sealed partial class OrchestratingAgentResponse : IAsyncDisposable -{ - private readonly CancellationTokenSource _cancelSource; - private readonly ILogger _logger; - - internal OrchestratingAgentResponse( - OrchestratingAgentContext context, - Task completion, - CancellationTokenSource orchestrationCancelSource, - ILogger logger) - { - this.Context = context; - this._cancelSource = orchestrationCancelSource; - this.Task = completion; - this._logger = logger; - } - - /// Gets the associated with this response. - public OrchestratingAgentContext Context { get; } - - /// - /// Releases all resources used by the instance. - /// - public ValueTask DisposeAsync() - { - this._cancelSource.Dispose(); - return default; - } - - /// - /// Gets a task that represents the completion of the orchestration result. - /// - public Task Task { get; } - - /// - /// Requests cancellation of the orchestration associated with this result. - /// - /// Thrown if this instance has been disposed. - public void Cancel() - { - OrchestratingAgent.LogOrchestrationCancellationRequested(this._logger, this.Context.ToString(), this.Context.Id); - this._cancelSource.Cancel(); - } - - /// Enable directly awaiting an by using 's awaiter. - [EditorBrowsable(EditorBrowsableState.Never)] - public TaskAwaiter GetAwaiter() => this.Task.GetAwaiter(); -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs deleted file mode 100644 index 9518993a4f..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestratingAgentThread.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using Microsoft.Agents.AI; - -namespace Microsoft.Agents.Orchestration; - -/// -/// The thread implementation used by . -/// -internal sealed class OrchestratingAgentThread : InMemoryAgentThread -{ - internal OrchestratingAgentThread() { } - - internal OrchestratingAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null) - : base(serializedThreadState, jsonSerializerOptions) { } -} diff --git a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationJsonContext.cs b/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationJsonContext.cs deleted file mode 100644 index 5066f3e6dc..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/OrchestrationJsonContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.Agents.Orchestration; - -[JsonSerializable(typeof(SequentialOrchestration.SequentialState))] -[JsonSerializable(typeof(ConcurrentOrchestration.ConcurrentState))] -[JsonSerializable(typeof(GroupChatOrchestration.GroupChatState))] -[JsonSerializable(typeof(HandoffOrchestration.HandoffState))] -[JsonSerializable(typeof(JsonElement))] -internal sealed partial class OrchestrationJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.Orchestration/SequentialOrchestration.cs b/dotnet/src/Microsoft.Agents.Orchestration/SequentialOrchestration.cs deleted file mode 100644 index 26c7a66346..0000000000 --- a/dotnet/src/Microsoft.Agents.Orchestration/SequentialOrchestration.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.Orchestration; - -/// Provides an orchestration that passes messages sequentially through a series of agents. -public sealed partial class SequentialOrchestration : OrchestratingAgent -{ - /// Initializes a new instance of the class. - /// The agents participating in the orchestration. - public SequentialOrchestration(params AIAgent[] agents) : this(agents, name: null) - { - } - - /// Initializes a new instance of the class. - /// The agents participating in the orchestration. - /// An optional name for this orchestrating agent. - public SequentialOrchestration(AIAgent[] agents, string? name) : base(agents, name) - { - } - - /// - protected override Task RunCoreAsync(IEnumerable messages, OrchestratingAgentContext context, CancellationToken cancellationToken) => - this.ResumeAsync(0, messages as IReadOnlyCollection ?? messages.ToList(), context, cancellationToken); - - /// - protected override Task ResumeCoreAsync(JsonElement checkpointState, IEnumerable newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - var state = checkpointState.Deserialize(OrchestrationJsonContext.Default.SequentialState) ?? throw new InvalidOperationException("The checkpoint state is invalid."); - - // Append the new messages to the checkpoint state - List allMessages = [.. state.Messages, .. newMessages]; - return this.ResumeAsync(state.Index, allMessages, context, cancellationToken); - } - - /// - private async Task ResumeAsync(int i, IReadOnlyCollection input, OrchestratingAgentContext context, CancellationToken cancellationToken) - { - AgentRunResponse? response = null; - for (; i < this.Agents.Count; i++) - { - LogOrchestrationSubagentRunning(context, this.Agents[i]); - - response = await RunAsync(this.Agents[i], context, input, options: null, cancellationToken).ConfigureAwait(false); - input = response.Messages as IReadOnlyCollection ?? [.. response.Messages]; - - await CheckpointAsync(i + 1, input, context, cancellationToken).ConfigureAwait(false); - } - - Debug.Assert(response is not null, "Response should not be null after processing a positive number of agents."); - return response!; - } - - private static Task CheckpointAsync(int index, IReadOnlyCollection messages, OrchestratingAgentContext context, CancellationToken cancellationToken) => - context.Runtime is not null ? WriteCheckpointAsync(JsonSerializer.SerializeToElement(new(index, messages), OrchestrationJsonContext.Default.SequentialState), context, cancellationToken) : - Task.CompletedTask; - - internal sealed record SequentialState(int Index, IReadOnlyCollection Messages); -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs deleted file mode 100644 index 457109fe1c..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ChatGroupExtensionsTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -public class ChatGroupExtensionsTests -{ - [Fact] - public void FormatNamesWithMultipleAgentsReturnsCommaSeparatedList() - { - // Arrange - GroupChatTeam group = new() - { - { "AgentOne", ("agent1", "First agent description") }, - { "AgentTwo", ("agent2", "Second agent description") }, - { "AgentThree", ("agent3", "Third agent description") } - }; - - // Act - string result = group.FormatNames(); - - // Assert - Assert.Equal("AgentOne,AgentTwo,AgentThree", result); - } - - [Fact] - public void FormatNamesWithSingleAgentReturnsSingleName() - { - // Arrange - GroupChatTeam group = new() - { - { "AgentOne", ("agent1", "First agent description") }, - }; - - // Act - string result = group.FormatNames(); - - // Assert - Assert.Equal("AgentOne", result); - } - - [Fact] - public void FormatNamesWithEmptyGroupReturnsEmptyString() - { - // Arrange - GroupChatTeam group = []; - - // Act - string result = group.FormatNames(); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void FormatListWithMultipleAgentsReturnsMarkdownList() - { - // Arrange - GroupChatTeam group = new() - { - { "AgentOne", ("agent1", "First agent description") }, - { "AgentTwo", ("agent2", "Second agent description") }, - { "AgentThree", ("agent3", "Third agent description") } - }; - - // Act - string result = group.FormatList(); - - // Assert - string expected = $"- AgentOne: First agent description{Environment.NewLine}- AgentTwo: Second agent description{Environment.NewLine}- AgentThree: Third agent description"; - Assert.Equal(expected, result); - } - - [Fact] - public void FormatListWithEmptyGroupReturnsEmptyString() - { - // Arrange - GroupChatTeam group = []; - - // Act & Assert - Assert.Equal(string.Empty, group.FormatNames()); - Assert.Equal(string.Empty, group.FormatList()); - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs deleted file mode 100644 index 50b2665ff3..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/ConcurrentOrchestrationTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Agents.AI; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -/// -/// Tests for the class. -/// -public class ConcurrentOrchestrationTests -{ - [Fact] - public async Task ConcurrentOrchestrationWithSingleAgentAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "xyz"); - - // Act: Create and execute the orchestration - string[] response = await ExecuteOrchestrationAsync(mockAgent1); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Contains("xyz", response); - } - - [Fact] - public async Task ConcurrentOrchestrationWithMultipleAgentsAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); - MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); - MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); - - // Act: Create and execute the orchestration - string[] response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal(1, mockAgent2.InvokeCount); - Assert.Equal(1, mockAgent3.InvokeCount); - Assert.Contains("lmn", response); - Assert.Contains("xyz", response); - Assert.Contains("abc", response); - } - - private static async Task ExecuteOrchestrationAsync(params AIAgent[] mockAgents) - { - // Act - ConcurrentOrchestration orchestration = new(mockAgents); - - const string InitialInput = "123"; - AgentRunResponse result = await orchestration.RunAsync(InitialInput); - - // Assert - Assert.NotNull(result); - - // Act - return result.Messages.Select(m => m.Text).ToArray(); - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestration2Tests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestration2Tests.cs deleted file mode 100644 index 30a66ca853..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestration2Tests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Agents.AI; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -/// -/// Tests for the class. -/// -public class GroupChatOrchestration2Tests -{ - [Fact] - public async Task GroupChatOrchestration2WithSingleAgentAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz"); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(mockAgent1); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal("xyz", response); - } - - [Fact] - public async Task GroupChatOrchestration2WithMultipleAgentsAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); - MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); - MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal(1, mockAgent2.InvokeCount); - Assert.Equal(1, mockAgent3.InvokeCount); - Assert.Equal("lmn", response); - } - - private static async Task ExecuteOrchestrationAsync(params AIAgent[] mockAgents) - { - // Act - GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocationCount = mockAgents.Length }, mockAgents); - - const string InitialInput = "123"; - AgentRunResponse result = await orchestration.RunAsync(InitialInput); - - // Assert - Assert.NotNull(result); - - // Return the text from the last message - return result.Messages.LastOrDefault()?.Text ?? string.Empty; - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs deleted file mode 100644 index e150cc3cc4..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/GroupChatOrchestrationTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Agents.AI; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -/// -/// Tests for the class. -/// -public class GroupChatOrchestrationTests -{ - [Fact] - public async Task GroupChatOrchestrationWithSingleAgentAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz"); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(mockAgent1); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal("xyz", response); - } - - [Fact] - public async Task GroupChatOrchestrationWithMultipleAgentsAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); - MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); - MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal(1, mockAgent2.InvokeCount); - Assert.Equal(1, mockAgent3.InvokeCount); - Assert.Equal("lmn", response); - } - - private static async Task ExecuteOrchestrationAsync(params AIAgent[] mockAgents) - { - // Act - GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocationCount = mockAgents.Length }, mockAgents); - - const string InitialInput = "123"; - AgentRunResponse result = await orchestration.RunAsync(InitialInput); - - // Assert - Assert.NotNull(result); - - // Act - return result.Messages.Last().Text; - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs deleted file mode 100644 index 5eee2e0447..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using OpenAI; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -/// -/// Tests for the class. -/// -public sealed class HandoffOrchestrationTests : IDisposable -{ - 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 - AIAgent mockAgent1 = - this.CreateMockAgent( - "Agent1", - "Test Agent", - Responses.Message("Final response")); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(Handoffs.StartWith(mockAgent1)); - - // Assert - Assert.Equal("Final response", response); - } - - [Fact(Skip = "Incomplete mock responses")] - public async Task HandoffOrchestrationWithMultipleAgentsAsync() - { - // Arrange - AIAgent mockAgent1 = - this.CreateMockAgent( - "Agent1", - "Test Agent", - Responses.Handoff("Agent2")); - AIAgent mockAgent2 = - this.CreateMockAgent( - "Agent2", - "Test Agent", - Responses.Result("Final response")); - AIAgent mockAgent3 = - this.CreateMockAgent( - "Agent3", - "Test Agent", - Responses.Message("Wrong response")); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync( - Handoffs - .StartWith(mockAgent1) - .Add(mockAgent1, [mockAgent2, mockAgent3])); - - // Assert - Assert.Equal("Final response", response); - } - - private static async Task ExecuteOrchestrationAsync(Handoffs handoffs) - { - // Arrange - HandoffOrchestration orchestration = new(handoffs); - - // Act - const string InitialInput = "123"; - AgentRunResponse result = await orchestration.RunAsync(InitialInput); - - // Assert - Assert.NotNull(result); - - // Act - return result.Text; - } - - private ChatClientAgent CreateMockAgent(string name, string description, params string[] responses) - { - HttpMessageHandlerStub messageHandlerStub = new(); - foreach (string response in responses) - { - HttpResponseMessage responseMessage = - new() - { - StatusCode = System.Net.HttpStatusCode.OK, - Content = new StringContent(response), - }; - messageHandlerStub.ResponseQueue.Enqueue(responseMessage); - this._disposables.Add(responseMessage); - } - HttpClient httpClient = new(messageHandlerStub, disposeHandler: false); - - this._disposables.Add(messageHandlerStub); - this._disposables.Add(httpClient); - - OpenAIClientOptions clientOptions = - new() - { - Transport = new HttpClientPipelineTransport(httpClient), - RetryPolicy = new ClientRetryPolicy(maxRetries: 0), - NetworkTimeout = Timeout.InfiniteTimeSpan, - }; - IChatClient chatClient = - new OpenAIClient(new ApiKeyCredential("fake-key"), clientOptions) - .GetChatClient("Any Model") - .AsIChatClient() - .AsBuilder() - .UseFunctionInvocation() - .Build(); - - ChatClientAgentOptions agentOptions = new() { Name = name, Description = description }; - return new(chatClient, agentOptions); - } - - 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": "transfer_to_{{{agentName}}}", - "arguments": "{}" - } - } - ] - } - } - ], - "usage": { - "prompt_tokens": 52, - "completion_tokens": 1, - "total_tokens": 53 - } - } - """; - - public static string Result(string summary) => - $$$""" - { - "id": "chat-234", - "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": "end_task_with_summary", - "arguments": "{ \"summary\": \"{{{summary}}}\" }" - } - } - ] - } - } - ] - } - """; - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs deleted file mode 100644 index 4c902dcd90..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffsTests.cs +++ /dev/null @@ -1,605 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Moq; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -public class HandoffsTests -{ - [Fact] - public void StartWith_ValidAgent_ReturnsHandoffsInstance() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - - // Act - var handoffs = Handoffs.StartWith(agent); - - // Assert - Assert.NotNull(handoffs); - Assert.Equal(agent, handoffs.InitialAgent); - Assert.Contains(agent, handoffs.Agents); - Assert.Empty(handoffs.Targets); - } - - [Fact] - public void StartWith_NullAgent_ThrowsArgumentNullException() => - // Act & Assert - Assert.Throws("initialAgent", () => Handoffs.StartWith(null!)); - - [Fact] - public void Add_ValidSourceAndTargets_AddsHandoffRelationships() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent1 = CreateAgent("target1", "Target agent 1"); - var targetAgent2 = CreateAgent("target2", "Target agent 2"); - var handoffs = Handoffs.StartWith(sourceAgent); - - // Act - var result = handoffs.Add(sourceAgent, [targetAgent1, targetAgent2]); - - // Assert - Assert.Same(handoffs, result); // Should return the same instance for fluent API - Assert.Contains(sourceAgent, handoffs.Agents); - Assert.Contains(targetAgent1, handoffs.Agents); - Assert.Contains(targetAgent2, handoffs.Agents); - - Assert.True(handoffs.Targets.ContainsKey(sourceAgent)); - Assert.Equal(2, handoffs.Targets[sourceAgent].Count); - - var targetNames = handoffs.Targets[sourceAgent].Select(t => t.Target.Id).ToArray(); - Assert.Contains("target1", targetNames); - Assert.Contains("target2", targetNames); - } - - [Fact] - public void Add_NullSource_ThrowsArgumentNullException() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var handoffs = Handoffs.StartWith(agent); - - // Act & Assert - Assert.Throws("source", () => handoffs.Add(null!, agent)); - } - - [Fact] - public void Add_NullTargets_ThrowsArgumentNullException() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - - // Act & Assert - Assert.Throws("targets", () => handoffs.Add(sourceAgent, null!)); - } - - [Fact] - public void Add_SingleTargetWithCustomReason_AddsHandoffWithReason() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - const string CustomReason = "Custom handoff reason"; - - // Act - var result = handoffs.Add(sourceAgent, targetAgent, CustomReason); - - // Assert - Assert.Same(handoffs, result); - Assert.True(handoffs.Targets.ContainsKey(sourceAgent)); - var target = handoffs.Targets[sourceAgent].Single(); - Assert.Equal(targetAgent, target.Target); - Assert.Equal(CustomReason, target.Reason); - } - - [Fact] - public void Add_SingleTargetWithNullSource_ThrowsArgumentNullException() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var handoffs = Handoffs.StartWith(agent); - - // Act & Assert - Assert.Throws("source", () => handoffs.Add(null!, agent, "reason")); - } - - [Fact] - public void Add_SingleTargetWithNullTarget_ThrowsArgumentNullException() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - - // Act & Assert - Assert.Throws("target", () => handoffs.Add(sourceAgent, null!, "reason")); - } - - [Fact] - public void Add_DuplicateHandoff_ThrowsInvalidOperationException() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, targetAgent); - - // Act & Assert - Assert.Throws(() => handoffs.Add(sourceAgent, targetAgent)); - } - - [Fact] - public void Build_WithoutName_ReturnsHandoffOrchestration() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var handoffs = Handoffs.StartWith(agent); - - // Act - var orchestration = handoffs.Build(); - - // Assert - Assert.NotNull(orchestration); - Assert.IsType(orchestration); - } - - [Fact] - public void Build_WithName_ReturnsHandoffOrchestrationWithName() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var handoffs = Handoffs.StartWith(agent); - const string OrchestrationName = "Test Orchestration"; - - // Act - var orchestration = handoffs.Build(OrchestrationName); - - // Assert - Assert.NotNull(orchestration); - Assert.IsType(orchestration); - Assert.Equal(OrchestrationName, orchestration.Name); - } - - [Fact] - public void IReadOnlyDictionary_Indexer_ReturnsTargetsForAgent() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, targetAgent); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var targets = readOnlyDict[sourceAgent]; - - // Assert - Assert.NotNull(targets); - Assert.Single(targets); - Assert.Equal(targetAgent, targets.First().Target); - } - - [Fact] - public void IReadOnlyDictionary_Keys_ReturnsSourceAgents() - { - // Arrange - var sourceAgent1 = CreateAgent("source1", "Source agent 1"); - var sourceAgent2 = CreateAgent("source2", "Source agent 2"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent1) - .Add(sourceAgent1, targetAgent) - .Add(sourceAgent2, targetAgent); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var keys = readOnlyDict.Keys; - - // Assert - Assert.Equal(2, keys.Count()); - Assert.Contains(sourceAgent1, keys); - Assert.Contains(sourceAgent2, keys); - } - - [Fact] - public void IReadOnlyDictionary_Values_ReturnsAllTargetCollections() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent1 = CreateAgent("target1", "Target agent 1"); - var targetAgent2 = CreateAgent("target2", "Target agent 2"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, [targetAgent1, targetAgent2]); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var values = readOnlyDict.Values; - - // Assert - Assert.Single(values); - Assert.Equal(2, values.First().Count()); - } - - [Fact] - public void IReadOnlyDictionary_Count_ReturnsNumberOfSourceAgents() - { - // Arrange - var sourceAgent1 = CreateAgent("source1", "Source agent 1"); - var sourceAgent2 = CreateAgent("source2", "Source agent 2"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent1) - .Add(sourceAgent1, targetAgent) - .Add(sourceAgent2, targetAgent); - var readOnlyCollection = (IReadOnlyCollection>>)handoffs; - - // Act - var count = readOnlyCollection.Count; - - // Assert - Assert.Equal(2, count); - } - - [Fact] - public void IReadOnlyDictionary_ContainsKey_ExistingAgent_ReturnsTrue() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, targetAgent); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var contains = readOnlyDict.ContainsKey(sourceAgent); - - // Assert - Assert.True(contains); - } - - [Fact] - public void IReadOnlyDictionary_ContainsKey_NonExistingAgent_ReturnsFalse() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var otherAgent = CreateAgent("other", "Other agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var contains = readOnlyDict.ContainsKey(otherAgent); - - // Assert - Assert.False(contains); - } - - [Fact] - public void IReadOnlyDictionary_TryGetValue_ExistingAgent_ReturnsTrueAndValue() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, targetAgent); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var success = readOnlyDict.TryGetValue(sourceAgent, out var targets); - - // Assert - Assert.True(success); - Assert.NotNull(targets); - Assert.Single(targets); - Assert.Equal(targetAgent, targets.First().Target); - } - - [Fact] - public void IReadOnlyDictionary_TryGetValue_NonExistingAgent_ReturnsFalseAndEmptyCollection() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var otherAgent = CreateAgent("other", "Other agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - var readOnlyDict = (IReadOnlyDictionary>)handoffs; - - // Act - var success = readOnlyDict.TryGetValue(otherAgent, out var targets); - - // Assert - Assert.False(success); - Assert.NotNull(targets); - Assert.Empty(targets); - } - - [Fact] - public void IEnumerable_GetEnumerator_IteratesOverHandoffs() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, targetAgent); - var enumerable = (IEnumerable>>)handoffs; - - // Act - var items = enumerable.ToArray(); - - // Assert - Assert.Single(items); - Assert.Equal(sourceAgent, items[0].Key); - Assert.Single(items[0].Value); - Assert.Equal(targetAgent, items[0].Value.First().Target); - } - - [Fact] - public void IEnumerable_NonGeneric_GetEnumerator_IteratesOverHandoffs() - { - // Arrange - var sourceAgent = CreateAgent("source", "Source agent"); - var targetAgent = CreateAgent("target", "Target agent"); - var handoffs = Handoffs.StartWith(sourceAgent); - handoffs.Add(sourceAgent, targetAgent); - var enumerable = (IEnumerable)handoffs; - - // Act - var enumerator = enumerable.GetEnumerator(); - var items = new List>>(); - while (enumerator.MoveNext()) - { - items.Add((KeyValuePair>)enumerator.Current); - } - - // Assert - Assert.Single(items); - Assert.Equal(sourceAgent, items[0].Key); - Assert.Single(items[0].Value); - Assert.Equal(targetAgent, items[0].Value.First().Target); - } - - [Fact] - public void HandoffTarget_Constructor_WithValidTarget_CreatesTarget() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - - // Act - var target = new Handoffs.HandoffTarget(agent); - - // Assert - Assert.Equal(agent, target.Target); - Assert.Equal("Test agent", target.Reason); // Should use description as reason - } - - [Fact] - public void HandoffTarget_Constructor_WithValidTargetAndReason_CreatesTargetWithReason() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - const string Reason = "Custom reason"; - - // Act - var target = new Handoffs.HandoffTarget(agent, Reason); - - // Assert - Assert.Equal(agent, target.Target); - Assert.Equal(Reason, target.Reason); - } - - [Fact] - public void HandoffTarget_Constructor_WithNullTarget_ThrowsArgumentNullException() => - // Act & Assert - Assert.Throws(() => new Handoffs.HandoffTarget(null!)); - - [Fact] - public void HandoffTarget_Constructor_WithAgentWithoutDescriptionOrName_ThrowsInvalidOperationException() - { - // Arrange - var agent = CreateAgent("agent1"); // No description or name - - // Act & Assert - Assert.Throws(() => new Handoffs.HandoffTarget(agent)); - } - - [Fact] - public void HandoffTarget_Constructor_WithAgentWithNameButNoDescription_UsesName() - { - // Arrange - var agent = CreateAgent("agent1", description: null, name: "Agent Name"); - - // Act - var target = new Handoffs.HandoffTarget(agent); - - // Assert - Assert.Equal(agent, target.Target); - Assert.Equal("Agent Name", target.Reason); - } - - [Fact] - public void HandoffTarget_Constructor_WithEmptyReason_UsesAgentDescription() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - - // Act - var target = new Handoffs.HandoffTarget(agent, ""); - - // Assert - Assert.Equal(agent, target.Target); - Assert.Equal("Test agent", target.Reason); - } - - [Fact] - public void HandoffTarget_Constructor_WithWhitespaceReason_UsesAgentDescription() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - - // Act - var target = new Handoffs.HandoffTarget(agent, " "); - - // Assert - Assert.Equal(agent, target.Target); - Assert.Equal("Test agent", target.Reason); - } - - [Fact] - public void HandoffTarget_Equals_SameTarget_ReturnsTrue() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var target1 = new Handoffs.HandoffTarget(agent, "Reason 1"); - var target2 = new Handoffs.HandoffTarget(agent, "Reason 2"); // Different reason, same target - - // Act - var equals = target1.Equals(target2); - - // Assert - Assert.True(equals); - } - - [Fact] - public void HandoffTarget_Equals_DifferentTarget_ReturnsFalse() - { - // Arrange - var agent1 = CreateAgent("agent1", "Test agent 1"); - var agent2 = CreateAgent("agent2", "Test agent 2"); - var target1 = new Handoffs.HandoffTarget(agent1); - var target2 = new Handoffs.HandoffTarget(agent2); - - // Act - var equals = target1.Equals(target2); - - // Assert - Assert.False(equals); - } - - [Fact] - public void HandoffTarget_Equals_Object_SameTarget_ReturnsTrue() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var target1 = new Handoffs.HandoffTarget(agent); - object target2 = new Handoffs.HandoffTarget(agent); - - // Act - var equals = target1.Equals(target2); - - // Assert - Assert.True(equals); - } - - [Fact] - public void HandoffTarget_Equals_Object_DifferentType_ReturnsFalse() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var target = new Handoffs.HandoffTarget(agent); - object other = "not a HandoffTarget"; - - // Act - var equals = target.Equals(other); - - // Assert - Assert.False(equals); - } - - [Fact] - public void HandoffTarget_GetHashCode_SameTarget_ReturnsSameHashCode() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var target1 = new Handoffs.HandoffTarget(agent, "Reason 1"); - var target2 = new Handoffs.HandoffTarget(agent, "Reason 2"); - - // Act - var hashCode1 = target1.GetHashCode(); - var hashCode2 = target2.GetHashCode(); - - // Assert - Assert.Equal(hashCode1, hashCode2); - } - - [Fact] - public void HandoffTarget_EqualityOperator_SameTarget_ReturnsTrue() - { - // Arrange - var agent = CreateAgent("agent1", "Test agent"); - var target1 = new Handoffs.HandoffTarget(agent); - var target2 = new Handoffs.HandoffTarget(agent); - - // Act - var equals = target1 == target2; - - // Assert - Assert.True(equals); - } - - [Fact] - public void HandoffTarget_InequalityOperator_DifferentTarget_ReturnsTrue() - { - // Arrange - var agent1 = CreateAgent("agent1", "Test agent 1"); - var agent2 = CreateAgent("agent2", "Test agent 2"); - var target1 = new Handoffs.HandoffTarget(agent1); - var target2 = new Handoffs.HandoffTarget(agent2); - - // Act - var notEquals = target1 != target2; - - // Assert - Assert.True(notEquals); - } - - [Fact] - public void FluentAPI_ChainMultipleAdds_WorksCorrectly() - { - // Arrange - var agent1 = CreateAgent("agent1", "Agent 1"); - var agent2 = CreateAgent("agent2", "Agent 2"); - var agent3 = CreateAgent("agent3", "Agent 3"); - var agent4 = CreateAgent("agent4", "Agent 4"); - - // Act - var handoffs = Handoffs - .StartWith(agent1) - .Add(agent1, [agent2, agent3]) - .Add(agent2, agent4) - .Add(agent3, agent4, "Special handoff reason"); - - // Assert - Assert.Equal(agent1, handoffs.InitialAgent); - Assert.Equal(4, handoffs.Agents.Count); - Assert.Equal(3, handoffs.Targets.Count); - - // Verify agent1 handoffs - Assert.Equal(2, handoffs.Targets[agent1].Count); - - // Verify agent2 handoffs - Assert.Single(handoffs.Targets[agent2]); - Assert.Equal(agent4, handoffs.Targets[agent2].First().Target); - - // Verify agent3 handoffs - Assert.Single(handoffs.Targets[agent3]); - Assert.Equal(agent4, handoffs.Targets[agent3].First().Target); - Assert.Equal("Special handoff reason", handoffs.Targets[agent3].First().Reason); - } - - private static ChatClientAgent CreateAgent(string id, string? description = null, string? name = null) - { - Mock mockClient = new(MockBehavior.Loose); - ChatClientAgentOptions options = - new() - { - Id = id, - Name = name, - Description = description, - }; - return new(mockClient.Object, options); - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HttpMessageHandlerStub.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HttpMessageHandlerStub.cs deleted file mode 100644 index ff090a4501..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HttpMessageHandlerStub.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -internal sealed class HttpMessageHandlerStub : HttpMessageHandler -{ - public Queue ResponseQueue { get; } = new(); - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => - Task.FromResult(this.ResponseQueue.Dequeue()); -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj deleted file mode 100644 index 4cb04c3edc..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - - - - - - - - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/MockAgent.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/MockAgent.cs deleted file mode 100644 index 2b2ecb7228..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/MockAgent.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Moq; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -/// -/// Mock definition of . -/// -internal sealed class MockAgent(int index) : AIAgent -{ - public static MockAgent CreateWithResponse(int index, string response) => new(index) - { - Response = [new(ChatRole.Assistant, response)] - }; - - public int InvokeCount { get; private set; } - - public IReadOnlyList Response { get; set; } = []; - - public override string? Name => $"testagent{index}"; - - public override string? Description => $"test {index}"; - - public override AgentThread GetNewThread() - => new Mock().Object; - - public override AgentThread DeserializeThread(System.Text.Json.JsonElement serializedThread, System.Text.Json.JsonSerializerOptions? jsonSerializerOptions = null) - => new Mock().Object; - - public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - { - this.InvokeCount++; - - return Task.FromResult(new AgentRunResponse(messages: [.. this.Response])); - } - - public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - { - this.InvokeCount++; - - return this.Response.Select(message => new AgentRunResponseUpdate(message.Role, message.Text)).ToAsyncEnumerable(); - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs deleted file mode 100644 index cf0ed92631..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/OrchestrationResultTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// 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.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -public class OrchestrationResultTests -{ - [Fact] - public async Task ConstructorInitializesPropertiesCorrectlyAsync() - { - // Arrange - OrchestratingAgentContext context = new() - { - OrchestratingAgent = new MockOrchestratingAgent(), - }; - TaskCompletionSource tcs = new(); - - // Act - using CancellationTokenSource cancelSource = new(); - await using OrchestratingAgentResponse result = new(context, tcs.Task, cancelSource, NullLogger.Instance); - - // Assert - Assert.Same(context, result.Context); - Assert.Same(tcs.Task, result.Task); - } - - [Fact] - public async Task GetValueAsyncReturnsCompletedValueWhenTaskIsCompletedAsync() - { - // Arrange - OrchestratingAgentContext context = new() - { - OrchestratingAgent = new MockOrchestratingAgent(), - }; - TaskCompletionSource tcs = new(); - using CancellationTokenSource cancelSource = new(); - await using OrchestratingAgentResponse result = new(context, tcs.Task, cancelSource, NullLogger.Instance); - AgentRunResponse expectedValue = new(); - - // Act - tcs.SetResult(expectedValue); - - // Assert - Assert.Same(expectedValue, await result); - } - - [Fact] - public async Task GetValueAsyncReturnsCompletedValueWhenCompletionIsDelayedAsync() - { - // Arrange - OrchestratingAgentContext context = new() - { - OrchestratingAgent = new MockOrchestratingAgent(), - }; - - TaskCompletionSource tcs = new(); - using CancellationTokenSource cancelSource = new(); - await using OrchestratingAgentResponse result = new(context, tcs.Task, cancelSource, NullLogger.Instance); - AgentRunResponse expectedValue = new(); - - // Act - // Simulate delayed completion in a separate task - Task delayTask = Task.Run(async () => - { - await Task.Delay(100); - tcs.SetResult(expectedValue); - }); - - // Assert - Assert.Same(expectedValue, await result); - } - - private sealed class MockOrchestratingAgent() : OrchestratingAgent([new MockAgent()]) - { - protected override Task RunCoreAsync(IEnumerable messages, OrchestratingAgentContext context, CancellationToken cancellationToken) => - throw new NotSupportedException(); - - protected override Task ResumeCoreAsync(JsonElement checkpointState, IEnumerable newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken) => - throw new NotSupportedException(); - } - - private sealed class MockAgent : AIAgent - { - public override AgentThread GetNewThread() - => throw new NotSupportedException(); - public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) - => throw new NotSupportedException(); - public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => - throw new NotSupportedException(); - public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => - throw new NotSupportedException(); - } -} diff --git a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs deleted file mode 100644 index 668c15eaeb..0000000000 --- a/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/SequentialOrchestrationTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.Agents.AI; - -namespace Microsoft.Agents.Orchestration.UnitTest; - -/// -/// Tests for the class. -/// -public class SequentialOrchestrationTests -{ - [Fact] - public async Task SequentialOrchestrationWithSingleAgentAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz"); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(mockAgent1); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal("xyz", response); - } - - [Fact] - public async Task SequentialOrchestrationWithMultipleAgentsAsync() - { - // Arrange - MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "abc"); - MockAgent mockAgent2 = MockAgent.CreateWithResponse(2, "xyz"); - MockAgent mockAgent3 = MockAgent.CreateWithResponse(3, "lmn"); - - // Act: Create and execute the orchestration - string response = await ExecuteOrchestrationAsync(mockAgent1, mockAgent2, mockAgent3); - - // Assert - Assert.Equal(1, mockAgent1.InvokeCount); - Assert.Equal(1, mockAgent2.InvokeCount); - Assert.Equal(1, mockAgent3.InvokeCount); - Assert.Equal("lmn", response); - } - - private static async Task ExecuteOrchestrationAsync(params AIAgent[] mockAgents) - { - // Act - SequentialOrchestration orchestration = new(mockAgents); - - const string InitialInput = "123"; - AgentRunResponse result = await orchestration.RunAsync(InitialInput); - - // Assert - Assert.NotNull(result); - - // Act - return result.Text; - } -}