[BREAKING] Delete Microsoft.Agents.Orchestration (#949)

Replaced by AgentWorkflowBuilder.
This commit is contained in:
Stephen Toub
2025-09-28 18:54:52 +00:00
committed by GitHub
parent 8ae15f10ed
commit 0d9f133e3b
46 changed files with 6 additions and 4211 deletions
-5
View File
@@ -21,9 +21,6 @@
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentOrchestration/">
<Project Path="samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/AgentProviders/">
<File Path="samples/GettingStarted/AgentProviders/README.md" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
@@ -271,7 +268,6 @@
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
<Project Path="src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj" />
<Project Path="src/Microsoft.Agents.Workflows.Declarative/Microsoft.Agents.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.Workflows/Microsoft.Agents.Workflows.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
@@ -299,7 +295,6 @@
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Microsoft.Agents.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.Workflows.UnitTests/Microsoft.Agents.Workflows.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Microsoft.Agents.AI.Hosting.A2A.Tests.csproj" Id="2a1c544d-237d-4436-8732-ba0c447ac06b" />
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.Workflows\Microsoft.Agents.Workflows.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Runtime.Abstractions\Microsoft.Agents.AI.Runtime.Abstractions.csproj" />
@@ -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
@@ -1,65 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>GettingStarted</RootNamespace>
<OutputType>Library</OutputType>
<NoWarn>$(NoWarn);CA1707;CA1716;IDE0009;IDE1006; OPENAI001;</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedSamples>true</InjectSharedSamples>
<InjectSharedThrow>true</InjectSharedThrow>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="GettingStarted" />
<Using Include="Microsoft.Shared.SampleUtilities" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="Xunit.Abstractions" />
</ItemGroup>
<ItemGroup>
<None Update="Resources\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -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)
{
/// <summary>
/// Represents the available providers for <see cref="IChatClient"/> instances.
/// </summary>
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.")
};
/// <summary>
/// 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.
/// </summary>
/// <param name="provider">The chat client provider type that determines the cleanup process.</param>
/// <param name="agent">The agent instance to be cleaned up.</param>
/// <param name="thread">Optional thread associated with the agent that may also need to be cleaned up.</param>
/// <param name="cancellationToken">Cancellation token to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <remarks>
/// Ideally for faster execution and potential cost savings, server-side agents should be reused.
/// </remarks>
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
};
/// <summary>
/// Creates a server-side agent identifier based on the specified provider and options.
/// </summary>
/// <param name="provider">The provider to use for creating the agent.</param>
/// <param name="options">The options to configure the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The identifier of the created agent, or <see langword="null"/> if the provider does not use server-side agents.</returns>
/// <remarks>Some server-side agent providers require an agent id reference to be created before it can be invoked.</remarks>
protected static Task<string?> AgentCreateAsync(ChatClientProviders provider, ChatClientAgentOptions options, CancellationToken cancellationToken = default)
=> provider switch
{
ChatClientProviders.OpenAIAssistant => OpenAIAssistantCreateAgentAsync(options, cancellationToken),
ChatClientProviders.AzureAIAgentsPersistent => AzureAIAgentsPersistentCreateAgentAsync(options, cancellationToken),
_ => Task.FromResult<string?>(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<string?> 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<string?> 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<PersistentAgentsClient>() ??
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<AssistantClient>()
?? 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
}
@@ -1,127 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
namespace Microsoft.Agents.AI.Runtime.Samples;
/// <summary>
/// Example demonstrating how to use the InMemoryActorStateStorage.
/// </summary>
public static class InMemoryActorStateStorageExample
{
/// <summary>
/// Demonstrates the basic usage of InMemoryActorStateStorage.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
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<ActorStateWriteOperation>
{
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<ActorStateReadOperation>
{
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<ActorStateWriteOperation>
{
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<ActorStateWriteOperation>
{
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<ActorStateReadOperation>
{
new ListKeysOperation(continuationToken: null)
};
var finalReadResult = await storage.ReadStateAsync(actorId, finalReadOperations).ConfigureAwait(false);
var finalKeys = finalReadResult.Results.OfType<ListKeysResult>().First();
Console.WriteLine($" Final keys: [{string.Join(", ", finalKeys.Keys)}]");
// 6. Read each value
foreach (var key in finalKeys.Keys)
{
var valueReadOperations = new List<ActorStateReadOperation>
{
new GetValueOperation(key)
};
var valueReadResult = await storage.ReadStateAsync(actorId, valueReadOperations).ConfigureAwait(false);
var getValue = valueReadResult.Results.OfType<GetValueResult>().First();
Console.WriteLine($" - {key}: {getValue.Value}");
}
Console.WriteLine();
Console.WriteLine("=== Example Complete ===");
}
}
@@ -1,52 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="ConcurrentOrchestration"/>
/// for executing multiple agents on the same task in parallel.
/// </summary>
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);
}
}
@@ -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;
/// <summary>
/// Demonstrates how to use the <see cref="ConcurrentOrchestration"/> with structured output.
/// </summary>
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<Analysis>(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<string> Themes { get; set; } = [];
public IList<string> Sentiments { get; set; } = [];
public IList<string> Entities { get; set; } = [];
}
#pragma warning restore CA1812
}
@@ -1,76 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/> ith a default
/// round robin manager for controlling the flow of conversation in a round robin fashion.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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);
}
}
@@ -1,198 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
using Microsoft.Extensions.AI;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/>
/// with a group chat manager that uses a chat completion service to
/// control the flow of the conversation.
/// </summary>
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.
""";
}
/// <inheritdoc/>
protected override ValueTask<GroupChatManagerResult<string>> FilterResultsAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
this.GetResponseAsync<string>(history, Prompts.Filter(topic), cancellationToken);
/// <inheritdoc/>
protected override ValueTask<GroupChatManagerResult<string>> SelectNextAgentAsync(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default) =>
this.GetResponseAsync<string>(history, Prompts.Selection(topic, team.FormatList()), cancellationToken);
/// <inheritdoc/>
protected override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
new(new GroupChatManagerResult<bool>(false) { Reason = "The AI group chat manager does not request user input." });
/// <inheritdoc/>
protected override async ValueTask<GroupChatManagerResult<bool>> ShouldTerminateAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = await base.ShouldTerminateAsync(history, cancellationToken);
if (!result.Value)
{
result = await this.GetResponseAsync<bool>(history, Prompts.Termination(topic), cancellationToken);
}
return result;
}
private async ValueTask<GroupChatManagerResult<TValue>> GetResponseAsync<TValue>(IReadOnlyCollection<ChatMessage> history, string prompt, CancellationToken cancellationToken = default)
{
ChatResponse<GroupChatManagerResult<TValue>> response = await chatClient.GetResponseAsync<GroupChatManagerResult<TValue>>([.. history, new ChatMessage(ChatRole.System, prompt)], new ChatOptions { ToolMode = ChatToolMode.Auto }, useJsonSchemaResponseFormat: true, cancellationToken);
return response.Result;
}
}
}
@@ -1,99 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
using Microsoft.Extensions.AI;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="GroupChatOrchestration"/> with human in the loop
/// </summary>
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<ChatMessage>(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);
}
/// <summary>
/// Define a custom group chat manager that enables user input.
/// </summary>
/// <remarks>
/// User input is achieved by overriding the default round robin manager
/// to allow user input after the reviewer agent's message.
/// </remarks>
private sealed class CustomRoundRobinGroupChatManager : RoundRobinGroupChatManager
{
protected override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
string? lastAgent = history.LastOrDefault()?.AuthorName;
GroupChatManagerResult<bool> 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);
}
}
}
@@ -1,97 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
using Microsoft.Extensions.AI;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="HandoffOrchestration"/> 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.
/// </summary>
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<string> 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.";
}
}
@@ -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;
/// <summary>
/// Demonstrates how to use the <see cref="HandoffOrchestration"/>.
/// </summary>
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<string, string[]> Labels { get; } = [];
public void AddLabels(string issueId, params string[] labels) => this.Labels[issueId] = labels;
}
}
@@ -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;
/// <summary>
/// Demonstrates how to use the <see cref="SequentialOrchestration"/> for
/// executing multiple Foundry agents in sequence.
/// </summary>
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);
}
}
@@ -1,73 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use the <see cref="SequentialOrchestration"/> for
/// executing multiple agents in sequence, i.e.the output of one agent is
/// the input to the next agent.
/// </summary>
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);
}
}
@@ -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;
/// <summary>
/// Demonstrates how to use the <see cref="SequentialOrchestration"/> for
/// executing multiple heterogeneous agents in sequence.
/// </summary>
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)
}
}
@@ -1,46 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Agents.Orchestration;
using Microsoft.Extensions.AI;
namespace Orchestration;
/// <summary>
/// Demonstrates how to use cancel a <see cref="SequentialOrchestration"/> while its running.
/// </summary>
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}");
}
}
}
@@ -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 kings 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 fathers 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 fathers 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 princes erratic behavior and attempt to discover its cause. They employ a pair of Hamlets 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 uncles 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 Claudiuss soul to heaven, Hamlet considers that it would be an inadequate revenge and decides to wait. Claudius, now frightened of Hamlets 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, Claudiuss 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 fathers death, Ophelia goes mad with grief and drowns in the river. Poloniuss son, Laertes, who has been staying in France, returns to Denmark in a rage. Claudius convinces him that Hamlet is to blame for his fathers and sisters 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 Hamlets 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 Ophelias 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 Claudiuss 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 kings 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 swords blade, and, after revealing to Hamlet that Claudius is responsible for the queens death, he dies from the blades 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 Hamlets last request, tells him Hamlets tragic story. Fortinbras orders that Hamlet be carried away in a manner befitting a fallen soldier.
@@ -1,6 +0,0 @@
Item Quantity
apple 3
banana 2
orange 5
apple 1
banana 3
@@ -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;
/// <summary>Provides extensions for orchestrating <see cref="AIAgent"/>s.</summary>
public static class AIAgentExtensions
{
private const string DefaultInstructions = "Respond with JSON that is populated by using the information in this conversation.";
/// <summary>
/// Runs the agent with the messages, then uses the chat client to process the agent's output and return a structured response.
/// </summary>
/// <typeparam name="T">The type of the result expected from the chat client response.</typeparam>
/// <param name="agent">The AI agent to be run.</param>
/// <param name="chatClient">The chat client used to process the messages.</param>
/// <param name="message">The message to be processed.</param>
/// <param name="thread">An optional thread context for the agent execution.</param>
/// <param name="runOptions">Optional settings that influence the agent's execution.</param>
/// <param name="serializerOptions">Optional serializer options to control how <typeparamref name="T"/> is deserialized.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation, with a result of type <typeparamref name="T"/> containing the
/// structured response.</returns>
public static ValueTask<T> RunAsync<T>(
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<T>(
agent,
chatClient,
[new ChatMessage(ChatRole.User, message)],
thread,
runOptions,
serializerOptions,
cancellationToken);
}
/// <summary>
/// Runs the agent with the messages, then uses the chat client to process the agent's output and return a structured response.
/// </summary>
/// <typeparam name="T">The type of the result expected from the chat client response.</typeparam>
/// <param name="agent">The AI agent to be run.</param>
/// <param name="chatClient">The chat client used to process the messages.</param>
/// <param name="messages">A collection of chat messages to be processed.</param>
/// <param name="thread">An optional thread context for the agent execution.</param>
/// <param name="runOptions">Optional settings that influence the agent's execution.</param>
/// <param name="serializerOptions">Optional serializer options to control how <typeparamref name="T"/> is deserialized.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation, with a result of type <typeparamref name="T"/> containing the
/// structured response.</returns>
public static async ValueTask<T> RunAsync<T>(
this AIAgent agent,
IChatClient chatClient,
IReadOnlyCollection<ChatMessage> 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<T>(
response.Messages,
serializerOptions: serializerOptions ?? AIJsonUtilities.DefaultOptions,
new ChatOptions() { Instructions = DefaultInstructions },
cancellationToken: cancellationToken).ConfigureAwait(false);
// Parse and return the results.
return result.Result;
}
}
@@ -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;
/// <summary>Provides an orchestrating agent that broadcasts the input message to each agent and then aggregates the result into a single response.</summary>
public partial class ConcurrentOrchestration : OrchestratingAgent
{
private Func<AgentRunResponse[], CancellationToken, Task<AgentRunResponse>>? _aggregationFunc;
/// <summary>Initializes a new instance of the <see cref="ConcurrentOrchestration"/> class.</summary>
/// <param name="subagents">The agents participating in the orchestration.</param>
public ConcurrentOrchestration(params AIAgent[] subagents) : this(subagents, name: null)
{
}
/// <summary>Initializes a new instance of the <see cref="ConcurrentOrchestration"/> class.</summary>
/// <param name="subagents">The agents participating in the orchestration.</param>
/// <param name="name">An optional name for this orchestrating agent.</param>
public ConcurrentOrchestration(AIAgent[] subagents, string? name) : base(subagents, name)
{
}
/// <summary>Gets or sets the function to use to aggregate an <see cref="AgentRunResponse"/> from each participating agent into a single <see cref="AgentRunResponse"/>.</summary>
/// <remarks>The default function takes the last message from each response and puts those messages into a new response instance.</remarks>
public Func<AgentRunResponse[], CancellationToken, Task<AgentRunResponse>> 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;
}
/// <inheritdoc />
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
this.ResumeAsync(messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), new AgentRunResponse?[this.Agents.Count], context, cancellationToken);
/// <inheritdoc />
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IEnumerable<ChatMessage> 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<ChatMessage> allMessages = [.. state.Messages, .. newMessages];
return this.ResumeAsync(allMessages, state.Completed, context, cancellationToken);
}
/// <inheritdoc />
private async Task<AgentRunResponse> ResumeAsync(
IReadOnlyCollection<ChatMessage> input, AgentRunResponse?[] completed, OrchestratingAgentContext context, CancellationToken cancellationToken)
{
List<Task> 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<ChatMessage> 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<ChatMessage> Messages, AgentRunResponse?[] Completed);
}
@@ -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;
/// <summary>
/// Represents the result of a group chat manager operation, including a value and a reason.
/// </summary>
/// <typeparam name="TValue">The type of the value returned by the operation.</typeparam>
/// <param name="value">The value returned by the operation.</param>
public sealed class GroupChatManagerResult<TValue>(TValue value)
{
/// <summary>
/// The reason for the result, providing additional context or explanation.
/// </summary>
public string Reason { get; init; } = string.Empty;
/// <summary>
/// The value returned by the group chat manager operation.
/// </summary>
public TValue Value { get; } = value;
}
/// <summary>
/// A manager that manages the flow of a group chat.
/// </summary>
public abstract class GroupChatManager
{
private int _invocationCount;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatManager"/> class.
/// </summary>
protected GroupChatManager() { }
/// <summary>
/// Gets the number of times the group chat manager has been invoked.
/// </summary>
public int InvocationCount => this._invocationCount;
/// <summary>
/// Gets or sets the maximum number of invocations allowed for the group chat manager.
/// </summary>
public int MaximumInvocationCount { get; init; } = int.MaxValue;
/// <summary>
/// Gets or sets the callback to be invoked for interactive input.
/// </summary>
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; init; }
/// <summary>
/// Filters the results of the group chat based on the provided chat history.
/// </summary>
/// <param name="history">The chat history to filter.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> containing the filtered result as a string.</returns>
protected internal abstract ValueTask<GroupChatManagerResult<string>> FilterResultsAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
/// <summary>
/// Selects the next agent to participate in the group chat based on the provided chat history and team.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="team">The group of agents participating in the chat.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> containing the identifier of the next agent as a string.</returns>
protected internal abstract ValueTask<GroupChatManagerResult<string>> SelectNextAgentAsync(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether user input should be requested based on the provided chat history.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> indicating whether user input should be requested.</returns>
protected internal abstract ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the group chat should be terminated based on the provided chat history and invocation count.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="GroupChatManagerResult{TValue}"/> indicating whether the chat should be terminated.</returns>
protected internal virtual ValueTask<GroupChatManagerResult<bool>> ShouldTerminateAsync(IReadOnlyCollection<ChatMessage> 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<bool> result = new(resultValue) { Reason = reason };
return new ValueTask<GroupChatManagerResult<bool>>(result);
}
}
@@ -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;
/// <summary>
/// An orchestration that coordinates a group-chat using a manager to control conversation flow.
/// </summary>
public sealed partial class GroupChatOrchestration : OrchestratingAgent
{
private readonly GroupChatManager _manager;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatOrchestration"/> class.
/// </summary>
/// <param name="manager">The manager that controls the flow of the group-chat.</param>
/// <param name="agents">The agents participating in the orchestration.</param>
public GroupChatOrchestration(GroupChatManager manager, params AIAgent[] agents) : this(manager, agents, name: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatOrchestration"/> class.
/// </summary>
/// <param name="manager">The manager that controls the flow of the group-chat.</param>
/// <param name="agents">The agents participating in the orchestration.</param>
/// <param name="name">An optional name for this orchestrating agent.</param>
public GroupChatOrchestration(GroupChatManager manager, AIAgent[] agents, string? name) : base(agents, name)
{
this._manager = Throw.IfNull(manager);
}
/// <summary>Gets or sets a callback invoked when user input is requested.</summary>
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; set; }
/// <inheritdoc />
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken)
{
List<ChatMessage> allMessages = [.. messages];
int originalMessageCount = allMessages.Count;
return this.ResumeAsync(allMessages, originalMessageCount, context, cancellationToken);
}
/// <inheritdoc />
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IEnumerable<ChatMessage> 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<ChatMessage> allMessages = [.. state.AllMessages, .. newMessages];
return this.ResumeAsync(allMessages, allMessages.Count, context, cancellationToken);
}
private async Task<AgentRunResponse> ResumeAsync(
List<ChatMessage> 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<ChatMessage> 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<ChatMessage> AllMessages, int OriginalMessageCount);
}
@@ -1,25 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Describes a team of agents participating in a group chat.
/// </summary>
public sealed class GroupChatTeam : Dictionary<string, (string Type, string Description)>
{
/// <summary>
/// Format the names of the agents in the team as a comma delimimted list.
/// </summary>
/// <returns>A comma delimimted list of agent name.</returns>
public string FormatNames() => string.Join(",", this.Select(t => t.Key));
/// <summary>
/// Format the names and descriptions of the agents in the team as a markdown list.
/// </summary>
/// <returns>A markdown list of agent names and descriptions.</returns>
public string FormatList() => string.Join(Environment.NewLine, this.Select(t => $"- {t.Key}: {t.Value.Description}"));
}
@@ -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;
/// <summary>
/// A <see cref="GroupChatManager"/> that selects agents in a round-robin fashion.
/// </summary>
/// <remarks>
/// Subclass this class to customize filter and user interaction behavior.
/// </remarks>
public class RoundRobinGroupChatManager : GroupChatManager
{
private int _currentAgentIndex;
/// <inheritdoc/>
protected internal override ValueTask<GroupChatManagerResult<string>> FilterResultsAsync(
IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<string> result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." };
return new ValueTask<GroupChatManagerResult<string>>(result);
}
/// <inheritdoc/>
protected internal override ValueTask<GroupChatManagerResult<string>> SelectNextAgentAsync(
IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default)
{
string nextAgent = team.Skip(this._currentAgentIndex).First().Key;
this._currentAgentIndex = (this._currentAgentIndex + 1) % team.Count;
GroupChatManagerResult<string> result = new(nextAgent) { Reason = $"Selected agent at index: {this._currentAgentIndex}" };
return new ValueTask<GroupChatManagerResult<string>>(result);
}
/// <inheritdoc/>
protected internal override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInputAsync(
IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = new(false) { Reason = "The default round-robin group chat manager does not request user input." };
return new ValueTask<GroupChatManagerResult<bool>>(result);
}
}
@@ -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;
/// <summary>
/// An orchestration that provides the input message to the first agent
/// and sequentially passes each agent result to the next agent.
/// </summary>
public sealed partial class HandoffOrchestration : OrchestratingAgent
{
private readonly Handoffs _handoffs;
/// <summary>
/// Initializes a new instance of the <see cref="HandoffOrchestration"/> class.
/// </summary>
/// <param name="handoffs">Defines the handoff connections for each agent.</param>
public HandoffOrchestration(Handoffs handoffs) : this(handoffs, name: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HandoffOrchestration"/> class.
/// </summary>
/// <param name="handoffs">Defines the handoff connections for each agent.</param>
/// <param name="name">An optional name for this orchestrating agent.</param>
public HandoffOrchestration(Handoffs handoffs, string? name) : base(handoffs.Agents.ToArray(), name)
{
this._handoffs = handoffs;
}
/// <summary>Gets or sets a callback invoked when no next handoff is selected in order to supply </summary>
public Func<ValueTask<ChatMessage>>? InteractiveCallback { get; set; }
/// <inheritdoc />
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken)
{
List<ChatMessage> allMessages = [.. messages];
int originalMessageCount = allMessages.Count;
return this.ResumeAsync(this._handoffs.InitialAgent, allMessages, originalMessageCount, context, cancellationToken);
}
/// <inheritdoc />
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IEnumerable<ChatMessage> 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<ChatMessage> allMessages = [.. state.AllMessages, .. newMessages];
return this.ResumeAsync(nextAgent, allMessages, allMessages.Count, context, cancellationToken);
}
/// <inheritdoc />
private async Task<AgentRunResponse> ResumeAsync(
AIAgent? agent, List<ChatMessage> 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<AITool> 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<AITool> handoffTools)
{
HashSet<string>? removeToolNames = null;
HashSet<string>? 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.HandoffTarget> handoffs)
{
public AIAgent? TargetedAgent { get; set; }
public bool EndTaskInvoked { get; set; }
public List<AITool> CreateHandoffFunctions(bool needsEndTask)
{
List<AITool> 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<ChatMessage> AllMessages, int OriginalMessageCount);
/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
#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
}
@@ -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;
/// <summary>
/// Defines the orchestration handoff relationships for all agents in the system.
/// </summary>
public sealed class Handoffs :
IReadOnlyDictionary<AIAgent, IEnumerable<Handoffs.HandoffTarget>>
{
/// <summary>
/// Initializes a new instance of the <see cref="Handoffs"/> class with no handoff relationships.
/// </summary>
/// <param name="initialAgent">The first agent to be invoked (prior to any handoff).</param>
private Handoffs(AIAgent initialAgent)
{
Throw.IfNull(initialAgent);
this.Agents.Add(initialAgent);
this.InitialAgent = initialAgent;
}
/// <summary>Gets the initial agent to which the first messages will be sent.</summary>
public AIAgent InitialAgent { get; }
/// <summary>Gets a collection of all handoff targets, indexed by the source of the handoffs.</summary>
internal Dictionary<AIAgent, HashSet<HandoffTarget>> Targets { get; } = [];
/// <summary>Gets a set of all agents involved in the handoffs, sources and targets.</summary>
internal HashSet<AIAgent> Agents { get; } = [];
/// <summary>
/// Creates a new collection of handoffs that start with the specified agent.
/// </summary>
/// <param name="initialAgent">The initial agent.</param>
/// <returns>The new <see cref="Handoffs"/> instance.</returns>
public static Handoffs StartWith(AIAgent initialAgent) => new(initialAgent);
/// <summary>Creates a new <see cref="HandoffOrchestration"/> from the described handoffs.</summary>
/// <param name="name">An optional name for this orchestrating agent.</param>
/// <returns>The new <see cref="HandoffOrchestration"/>.</returns>
public HandoffOrchestration Build(string? name = null) => new(this, name);
/// <summary>
/// Adds handoff relationships from a source agent to one or more target agents.
/// </summary>
/// <param name="source">The source agent.</param>
/// <param name="targets">The target agents to add as handoff targets for the source agent.</param>
/// <returns>The updated <see cref="Handoffs"/> instance.</returns>
/// <remarks>The handoff reason for each target is derived from its description or name.</remarks>
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;
}
/// <summary>
/// Adds a handoff relationship from a source agent to a target agent with a custom handoff reason.
/// </summary>
/// <param name="source">The source agent.</param>
/// <param name="target">The target agent.</param>
/// <param name="handoffReason">The reason the <paramref name="source"/> should hand off to the <paramref name="target"/>.</param>
/// <returns>The updated <see cref="Handoffs"/> instance.</returns>
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;
}
/// <inheritdoc />
IEnumerable<HandoffTarget> IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>.this[AIAgent key] => this.Targets[key];
/// <inheritdoc />
IEnumerable<AIAgent> IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>.Keys => this.Targets.Keys;
/// <inheritdoc />
IEnumerable<IEnumerable<HandoffTarget>> IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>.Values => this.Targets.Values;
/// <inheritdoc />
int IReadOnlyCollection<KeyValuePair<AIAgent, IEnumerable<HandoffTarget>>>.Count => this.Targets.Count;
/// <inheritdoc />
bool IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>.ContainsKey(AIAgent key) => this.Targets.ContainsKey(key);
/// <inheritdoc />
IEnumerator<KeyValuePair<AIAgent, IEnumerable<HandoffTarget>>> IEnumerable<KeyValuePair<AIAgent, IEnumerable<HandoffTarget>>>.GetEnumerator()
{
foreach (var kvp in this.Targets)
{
yield return new(kvp.Key, kvp.Value);
}
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() =>
((IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>)this).GetEnumerator();
/// <inheritdoc />
bool IReadOnlyDictionary<AIAgent, IEnumerable<HandoffTarget>>.TryGetValue(AIAgent key, out IEnumerable<HandoffTarget> value)
{
if (this.Targets.TryGetValue(key, out var handoffs))
{
value = handoffs;
return true;
}
value = [];
return false;
}
/// <summary>Describes a handoff to a specific target <see cref="AIAgent"/>.</summary>
public readonly struct HandoffTarget : IEquatable<HandoffTarget>
{
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!;
}
/// <summary>Gets the target <see cref="AIAgent"/> of the handoff.</summary>
public AIAgent Target { get; }
/// <summary>Gets the reason a handoff to <see cref="Target"/> should be performed.</summary>
public string Reason { get; }
/// <inheritdoc />
public bool Equals(HandoffTarget other) => this.Target == other.Target;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is HandoffTarget other && this.Equals(other);
/// <inheritdoc />
public override int GetHashCode() => this.Target.GetHashCode();
/// <inheritdoc />
public static bool operator ==(HandoffTarget left, HandoffTarget right) => left.Equals(right);
/// <inheritdoc />
public static bool operator !=(HandoffTarget left, HandoffTarget right) => !left.Equals(right);
}
}
@@ -1,33 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<RootNamespace>Microsoft.Agents.Orchestration</RootNamespace>
<VersionSuffix>alpha</VersionSuffix>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Orchestration Framework</Title>
<Description>Contains the Microsoft Agent Orchestration Framework.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\Microsoft.Agents.AI.Runtime.Abstractions\Microsoft.Agents.AI.Runtime.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.Orchestration.UnitTests" />
</ItemGroup>
</Project>
@@ -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;
/// <summary>
/// Base class for multi-agent agent orchestration patterns.
/// </summary>
public abstract partial class OrchestratingAgent : AIAgent
{
/// <summary>Key used to persist state with the runtime.</summary>
private const string StateKey = "State";
/// <summary>
/// Initializes a new instance of the <see cref="OrchestratingAgent"/> class.
/// </summary>
/// <param name="agents">Specifies the agents participating in this orchestration.</param>
/// <param name="name">An optional name for this agent.</param>
protected OrchestratingAgent(IReadOnlyList<AIAgent> agents, string? name = null)
{
_ = Throw.IfNullOrEmpty(agents);
this.Agents = agents;
this.Name = name;
}
/// <inheritdoc />
public override string? Name { get; }
/// <summary>
/// Gets the list of member targets involved in the orchestration.
/// </summary>
protected IReadOnlyList<AIAgent> Agents { get; }
/// <summary>Gets the serializer options to use by the orchestration.</summary>
public JsonSerializerOptions? SerializerOptions { get; set; }
/// <summary>
/// Gets the associated logger.
/// </summary>
public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance;
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public Func<IEnumerable<ChatMessage>, ValueTask>? ResponseCallback { get; set; }
/// <summary>
/// Optional callback that is invoked for every agent update.
/// </summary>
public Func<AgentRunResponseUpdate, ValueTask>? StreamingResponseCallback { get; set; }
/// <inheritdoc/>
public override AgentThread GetNewThread()
=> new OrchestratingAgentThread();
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new OrchestratingAgentThread(serializedThread, jsonSerializerOptions);
/// <inheritdoc />
public sealed override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> 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<ChatMessage> 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);
}
/// <inheritdoc />
public sealed override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> 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;
}
}
/// <summary>
/// Initiates processing of the orchestration.
/// </summary>
/// <param name="messages">The input message.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="runtime">The runtime associated with the orchestration.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
public async ValueTask<OrchestratingAgentResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentRunOptions? options = null,
IActorRuntimeContext? runtime = null,
CancellationToken cancellationToken = default)
{
var readonlyCollectionMessages = Throw.IfNull(messages) as IReadOnlyCollection<ChatMessage> ?? 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<AgentRunResponse> 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);
}
/// <summary>
/// Initiates processing of the orchestration.
/// </summary>
/// <param name="messages">The input message.</param>
/// <param name="context">The context for this operation.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
protected abstract Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken);
/// <summary>
/// Resumes processing of the orchestration.
/// </summary>
/// <param name="checkpointState">The last checkpoint state available from which to resume the operation.</param>
/// <param name="newMessages">The new messages to be processed in addition to the checkpoint state.</param>
/// <param name="context">The context for this operation.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
protected abstract Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IEnumerable<ChatMessage> newMessages, OrchestratingAgentContext context, CancellationToken cancellationToken);
/// <summary>
/// Runs the agent with input messages and respond with both streamed and regular messages.
/// </summary>
/// <param name="agent">The agent being run</param>
/// <param name="context">The associated orchestration context for this run.</param>
/// <param name="input">The list of chat messages to send.</param>
/// <param name="options">Options to use when invoking the agent.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task that returns the response <see cref="ChatMessage"/>.</returns>
protected static async ValueTask<AgentRunResponse> RunAsync(AIAgent agent, OrchestratingAgentContext context, IEnumerable<ChatMessage> 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<AgentRunResponseUpdate> 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;
}
/// <summary>Writes the specified checkpoint state to the runtime.</summary>
/// <param name="state">The state to persist.</param>
/// <param name="context">The context for the orchestrating operation.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A Task that completes when the asynchronous operation quiesces.</returns>
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;
}
}
}
/// <summary>Read checkpoint information, if it exists, for the specified context.</summary>
/// <param name="context">The context for the orchestrating operation.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The loaded state, or null if it doesn't exist.</returns>
protected static async ValueTask<JsonElement?> 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<AgentRunResponse> 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);
}
}
}
@@ -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;
/// <summary>
/// Provides contextual information for an orchestration operation, including logging, and response callback.
/// </summary>
public sealed class OrchestratingAgentContext
{
private ILogger? _logger;
private string? _id;
/// <summary>Gets the orchestrating agent associated with this operation.</summary>
public OrchestratingAgent? OrchestratingAgent { get; set; }
/// <summary>Gets the associated agent runtime, if one is being used.</summary>
public IActorRuntimeContext? Runtime { get; set; }
/// <summary>Gets the options associated with the orchestration run.</summary>
public AgentRunOptions? Options { get; set; }
/// <summary>Gets or sets the last version number provided by the runtime for checkpoint state.</summary>
public string? ETag { get; set; }
/// <summary>Gets or sets an ID to use for the orchestration operation.</summary>
public string Id
{
get
{
this._id ??= this.Runtime?.ActorId.ToString() ?? Guid.NewGuid().ToString("N");
return this._id;
}
}
/// <summary>
/// Gets the associated logger for this operation.
/// </summary>
public ILogger Logger
{
get => this._logger ?? NullLogger.Instance;
set => this._logger = value;
}
/// <inheritdoc />
public override string ToString() =>
this.OrchestratingAgent?.DisplayName ??
nameof(OrchestratingAgentContext);
}
@@ -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;
/// <summary>
/// Represents the result of an orchestrating agent.
/// This class encapsulates the asynchronous completion of an orchestration process.
/// </summary>
public sealed partial class OrchestratingAgentResponse : IAsyncDisposable
{
private readonly CancellationTokenSource _cancelSource;
private readonly ILogger _logger;
internal OrchestratingAgentResponse(
OrchestratingAgentContext context,
Task<AgentRunResponse> completion,
CancellationTokenSource orchestrationCancelSource,
ILogger logger)
{
this.Context = context;
this._cancelSource = orchestrationCancelSource;
this.Task = completion;
this._logger = logger;
}
/// <summary>Gets the <see cref="OrchestratingAgentContext"/> associated with this response.</summary>
public OrchestratingAgentContext Context { get; }
/// <summary>
/// Releases all resources used by the <see cref="OrchestratingAgentResponse"/> instance.
/// </summary>
public ValueTask DisposeAsync()
{
this._cancelSource.Dispose();
return default;
}
/// <summary>
/// Gets a task that represents the completion of the orchestration result.
/// </summary>
public Task<AgentRunResponse> Task { get; }
/// <summary>
/// Requests cancellation of the orchestration associated with this result.
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown if this instance has been disposed.</exception>
public void Cancel()
{
OrchestratingAgent.LogOrchestrationCancellationRequested(this._logger, this.Context.ToString(), this.Context.Id);
this._cancelSource.Cancel();
}
/// <summary>Enable directly awaiting an <see cref="OrchestratingAgentResponse"/> by using <see cref="Task"/>'s awaiter.</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public TaskAwaiter<AgentRunResponse> GetAwaiter() => this.Task.GetAwaiter();
}
@@ -1,17 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Agents.AI;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// The thread implementation used by <see cref="OrchestratingAgent"/>.
/// </summary>
internal sealed class OrchestratingAgentThread : InMemoryAgentThread
{
internal OrchestratingAgentThread() { }
internal OrchestratingAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
: base(serializedThreadState, jsonSerializerOptions) { }
}
@@ -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;
@@ -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;
/// <summary>Provides an orchestration that passes messages sequentially through a series of agents.</summary>
public sealed partial class SequentialOrchestration : OrchestratingAgent
{
/// <summary>Initializes a new instance of the <see cref="SequentialOrchestration"/> class.</summary>
/// <param name="agents">The agents participating in the orchestration.</param>
public SequentialOrchestration(params AIAgent[] agents) : this(agents, name: null)
{
}
/// <summary>Initializes a new instance of the <see cref="SequentialOrchestration"/> class.</summary>
/// <param name="agents">The agents participating in the orchestration.</param>
/// <param name="name">An optional name for this orchestrating agent.</param>
public SequentialOrchestration(AIAgent[] agents, string? name) : base(agents, name)
{
}
/// <inheritdoc />
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
this.ResumeAsync(0, messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList(), context, cancellationToken);
/// <inheritdoc />
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IEnumerable<ChatMessage> 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<ChatMessage> allMessages = [.. state.Messages, .. newMessages];
return this.ResumeAsync(state.Index, allMessages, context, cancellationToken);
}
/// <inheritdoc />
private async Task<AgentRunResponse> ResumeAsync(int i, IReadOnlyCollection<ChatMessage> 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<ChatMessage> ?? [.. 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<ChatMessage> 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<ChatMessage> Messages);
}
@@ -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());
}
}
@@ -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;
/// <summary>
/// Tests for the <see cref="ConcurrentOrchestration"/> class.
/// </summary>
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<string[]> 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();
}
}
@@ -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;
/// <summary>
/// Tests for the <see cref="GroupChatOrchestration"/> class.
/// </summary>
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<string> 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;
}
}
@@ -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;
/// <summary>
/// Tests for the <see cref="GroupChatOrchestration"/> class.
/// </summary>
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<string> 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;
}
}
@@ -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;
/// <summary>
/// Tests for the <see cref="HandoffOrchestration"/> class.
/// </summary>
public sealed class HandoffOrchestrationTests : IDisposable
{
private readonly List<IDisposable> _disposables;
/// <summary>
/// Initializes a new instance of the <see cref="HandoffOrchestrationTests"/> class.
/// </summary>
public HandoffOrchestrationTests()
{
this._disposables = [];
}
/// <inheritdoc/>
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<string> 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}}}\" }"
}
}
]
}
}
]
}
""";
}
}
@@ -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<ArgumentNullException>("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<ArgumentNullException>("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<ArgumentNullException>("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<ArgumentNullException>("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<ArgumentNullException>("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<InvalidOperationException>(() => 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<HandoffOrchestration>(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<HandoffOrchestration>(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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<KeyValuePair<AIAgent, IEnumerable<Handoffs.HandoffTarget>>>)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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<KeyValuePair<AIAgent, IEnumerable<Handoffs.HandoffTarget>>>)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<KeyValuePair<AIAgent, IEnumerable<Handoffs.HandoffTarget>>>();
while (enumerator.MoveNext())
{
items.Add((KeyValuePair<AIAgent, IEnumerable<Handoffs.HandoffTarget>>)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<ArgumentNullException>(() => new Handoffs.HandoffTarget(null!));
[Fact]
public void HandoffTarget_Constructor_WithAgentWithoutDescriptionOrName_ThrowsInvalidOperationException()
{
// Arrange
var agent = CreateAgent("agent1"); // No description or name
// Act & Assert
Assert.Throws<InvalidOperationException>(() => 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<IChatClient> mockClient = new(MockBehavior.Loose);
ChatClientAgentOptions options =
new()
{
Id = id,
Name = name,
Description = description,
};
return new(mockClient.Object, options);
}
}
@@ -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<HttpResponseMessage> ResponseQueue { get; } = new();
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
Task.FromResult(this.ResponseQueue.Dequeue());
}
@@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
</Project>
@@ -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;
/// <summary>
/// Mock definition of <see cref="AIAgent"/>.
/// </summary>
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<ChatMessage> Response { get; set; } = [];
public override string? Name => $"testagent{index}";
public override string? Description => $"test {index}";
public override AgentThread GetNewThread()
=> new Mock<AgentThread>().Object;
public override AgentThread DeserializeThread(System.Text.Json.JsonElement serializedThread, System.Text.Json.JsonSerializerOptions? jsonSerializerOptions = null)
=> new Mock<AgentThread>().Object;
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
this.InvokeCount++;
return Task.FromResult(new AgentRunResponse(messages: [.. this.Response]));
}
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
this.InvokeCount++;
return this.Response.Select(message => new AgentRunResponseUpdate(message.Role, message.Text)).ToAsyncEnumerable();
}
}
@@ -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<AgentRunResponse> 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<AgentRunResponse> 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<AgentRunResponse> 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<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, OrchestratingAgentContext context, CancellationToken cancellationToken) =>
throw new NotSupportedException();
protected override Task<AgentRunResponse> ResumeCoreAsync(JsonElement checkpointState, IEnumerable<ChatMessage> 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<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
}
}
@@ -1,59 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI;
namespace Microsoft.Agents.Orchestration.UnitTest;
/// <summary>
/// Tests for the <see cref="SequentialOrchestration"/> class.
/// </summary>
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<string> 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;
}
}