.NET Port Agent Orchestration (#107)

* Checkpoint

* Checkpoint

* Namespaces

* Namespace

* Cleanup

* Namespace order

* Fix sync

* Formatting

* Formatting

* Namespace

* Namespace order

* Code convention

* Naming

* Naming

* Text handling

* Text handling

* Namespace

* Namespace order

* Namespace ordering

* Test

* ValueTask

* net472

* Test fix

* Fix namespace (net472)

* Namespace

* Fix conditional namespace

* Fix type expression

* Compatibility and cleanup

* Sample compatibility

* Sample compat

* Test compat

* modifier order

* Simply http-stub

* Formating fix for unit-test

* Fix test

* Real fix

* Test clean-up

* Update dotnet/src/Microsoft.Agents.Orchestration/Handoff/HandoffOrchestration.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix build errors after merging

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Chris
2025-07-08 13:04:34 -04:00
committed by GitHub
co-authored by Copilot Stephen Toub
parent 35c938fb5b
commit 7c8ec5ec19
71 changed files with 5675 additions and 131 deletions
+7
View File
@@ -23,8 +23,15 @@
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.6.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<!-- Microsoft.SemanticKernel.Agents.Runtime.* -->
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Runtime.Abstractions" Version="1.58.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Runtime.Core" Version="1.58.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" Version="1.58.0-preview" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.2.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Moq" Version="[4.18.4]" />
<PackageVersion Include="xunit" Version="2.9.2" />
+123 -120
View File
@@ -1,120 +1,123 @@
<Solution>
<Configurations>
<BuildType Name="Debug" />
<BuildType Name="Publish" />
<BuildType Name="Release" />
</Configurations>
<Folder Name="/ConformanceTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" Id="e91d4745-7ccd-4de8-9bc0-31081f540038">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" Id="a35b6971-6f27-4904-a168-8e12b229cced">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
</Folder>
<Folder Name="/Samples/">
<Project Path="samples/GettingStarted/GettingStarted.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
<File Path="Directory.Packages.props" />
<File Path="unittests.runsettings" />
</Folder>
<Folder Name="/Solution Items/nuget/">
<File Path="nuget/icon.png" />
<File Path="nuget/nuget-package.props" />
<File Path="nuget/NUGET.md" />
</Folder>
<Folder Name="/Solution Items/tests/">
<File Path="tests/.editorconfig" />
<File Path="tests/Directory.Build.props" />
</Folder>
<Folder Name="/Solution Items/.github/" />
<Folder Name="/Solution Items/.github/workflows/">
<File Path="../.github/workflows/dotnet-build-and-test.yml" />
<File Path="../.github/workflows/dotnet-check-coverage.ps1" />
<File Path="../.github/workflows/dotnet-format.yml" />
</Folder>
<Folder Name="/Solution Items/eng/" />
<Folder Name="/Solution Items/eng/MSBuild/">
<File Path="eng/MSBuild/LegacySupport.props" />
<File Path="eng/MSBuild/Shared.props" />
<File Path="eng/MSBuild/Shared.targets" />
</Folder>
<Folder Name="/Solution Items/src/Shared/" />
<Folder Name="/Solution Items/src/Shared/Throw/">
<File Path="src/Shared/Throw/README.md" />
<File Path="src/Shared/Throw/Throw.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/IntegrationTests/">
<File Path="src/Shared/IntegrationTests/AzureAIConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/README.md" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Samples/">
<File Path="src/Shared/Samples/README.md" />
<File Path="src/Shared/Samples/BaseSample.cs" />
<File Path="src/Shared/Samples/TestConfiguration.cs" />
<File Path="src/Shared/Samples/TextOutputHelperExtensions.cs" />
<File Path="src/Shared/Samples/XunitLogger.cs" />
</Folder>
<Folder Name="/Solution Items/samples/">
<File Path="samples/.editorconfig" />
<File Path="samples/Directory.Build.props" />
</Folder>
<Folder Name="/Solution Items/src/" />
<Folder Name="/Solution Items/src/LegacySupport/">
<File Path="src/LegacySupport/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/CallerAttributes/">
<File Path="src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs" />
<File Path="src/LegacySupport/CallerAttributes/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/DiagnosticAttributes/">
<File Path="src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs" />
<File Path="src/LegacySupport/DiagnosticAttributes/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/DiagnosticClasses/">
<File Path="src/LegacySupport/DiagnosticClasses/UnreachableException.cs" />
<File Path="src/LegacySupport/DiagnosticClasses/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/ExperimentalAttribute/">
<File Path="src/LegacySupport/ExperimentalAttribute/ExperimentalAttribute.cs" />
<File Path="src/LegacySupport/ExperimentalAttribute/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/IsExternalInit/">
<File Path="src/LegacySupport/IsExternalInit/IsExternalInit.cs" />
<File Path="src/LegacySupport/IsExternalInit/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/TrimAttributes/">
<File Path="src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs" />
<File Path="src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs" />
<File Path="src/LegacySupport/TrimAttributes/README.md" />
<File Path="src/LegacySupport/TrimAttributes/RequiresDynamicCodeAttribute.cs" />
<File Path="src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs" />
<File Path="src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs" />
</Folder>
<Project Path="src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<Project Path="tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
<Project Path="tests/Microsoft.Extensions.AI.Agents.UnitTests/Microsoft.Extensions.AI.Agents.UnitTests.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
</Solution>
<Solution>
<Configurations>
<BuildType Name="Debug" />
<BuildType Name="Publish" />
<BuildType Name="Release" />
</Configurations>
<Folder Name="/ConformanceTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" Id="e91d4745-7ccd-4de8-9bc0-31081f540038">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" Id="a35b6971-6f27-4904-a168-8e12b229cced">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj">
<BuildType Solution="Publish|*" Project="Release" />
</Project>
</Folder>
<Folder Name="/Samples/">
<Project Path="samples/GettingStarted/GettingStarted.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
<File Path="Directory.Packages.props" />
</Folder>
<Folder Name="/Solution Items/nuget/">
<File Path="nuget/icon.png" />
<File Path="nuget/nuget-package.props" />
<File Path="nuget/NUGET.md" />
</Folder>
<Folder Name="/Solution Items/tests/">
<File Path="tests/.editorconfig" />
<File Path="tests/Directory.Build.props" />
</Folder>
<Folder Name="/Solution Items/.github/" />
<Folder Name="/Solution Items/.github/workflows/">
<File Path="../.github/workflows/dotnet-build-and-test.yml" />
<File Path="../.github/workflows/dotnet-check-coverage.ps1" />
<File Path="../.github/workflows/dotnet-format.yml" />
</Folder>
<Folder Name="/Solution Items/eng/" />
<Folder Name="/Solution Items/eng/MSBuild/">
<File Path="eng/MSBuild/LegacySupport.props" />
<File Path="eng/MSBuild/Shared.props" />
<File Path="eng/MSBuild/Shared.targets" />
</Folder>
<Folder Name="/Solution Items/src/Shared/" />
<Folder Name="/Solution Items/src/Shared/Throw/">
<File Path="src/Shared/Throw/README.md" />
<File Path="src/Shared/Throw/Throw.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/IntegrationTests/">
<File Path="src/Shared/IntegrationTests/AzureAIConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
<File Path="src/Shared/IntegrationTests/README.md" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Samples/">
<File Path="src/Shared/Samples/README.md" />
<File Path="src/Shared/Samples/BaseSample.cs" />
<File Path="src/Shared/Samples/TestConfiguration.cs" />
<File Path="src/Shared/Samples/TextOutputHelperExtensions.cs" />
<File Path="src/Shared/Samples/XunitLogger.cs" />
</Folder>
<Folder Name="/Solution Items/samples/">
<File Path="samples/.editorconfig" />
<File Path="samples/Directory.Build.props" />
</Folder>
<Folder Name="/Solution Items/src/" />
<Folder Name="/Solution Items/src/LegacySupport/">
<File Path="src/LegacySupport/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/CallerAttributes/">
<File Path="src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs" />
<File Path="src/LegacySupport/CallerAttributes/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/DiagnosticAttributes/">
<File Path="src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs" />
<File Path="src/LegacySupport/DiagnosticAttributes/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/DiagnosticClasses/">
<File Path="src/LegacySupport/DiagnosticClasses/UnreachableException.cs" />
<File Path="src/LegacySupport/DiagnosticClasses/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/ExperimentalAttribute/">
<File Path="src/LegacySupport/ExperimentalAttribute/ExperimentalAttribute.cs" />
<File Path="src/LegacySupport/ExperimentalAttribute/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/IsExternalInit/">
<File Path="src/LegacySupport/IsExternalInit/IsExternalInit.cs" />
<File Path="src/LegacySupport/IsExternalInit/README.md" />
</Folder>
<Folder Name="/Solution Items/src/LegacySupport/TrimAttributes/">
<File Path="src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs" />
<File Path="src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs" />
<File Path="src/LegacySupport/TrimAttributes/README.md" />
<File Path="src/LegacySupport/TrimAttributes/RequiresDynamicCodeAttribute.cs" />
<File Path="src/LegacySupport/TrimAttributes/RequiresUnreferencedCodeAttribute.cs" />
<File Path="src/LegacySupport/TrimAttributes/UnconditionalSuppressMessageAttribute.cs" />
</Folder>
<Project Path="src/Microsoft.Agents.Orchestration/Microsoft.Agents.Orchestration.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents.Abstractions/Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
<Project Path="src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<Project Path="tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
<Project Path="tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
<Project Path="tests/Microsoft.Extensions.AI.Agents.UnitTests/Microsoft.Extensions.AI.Agents.UnitTests.csproj">
<BuildType Solution="Publish|*" Project="Debug" />
</Project>
</Solution>
@@ -26,6 +26,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
</ItemGroup>
@@ -36,8 +37,12 @@
</ItemGroup>
<ItemGroup>
<None Update="Resources\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Tools\Files\groceries.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.Concurrent;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
instructions: "You are an expert in physics. You answer questions from a physics perspective.",
description: "An expert in physics");
ChatClientAgent chemist =
this.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.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
string input = "What is temperature?";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult<string[]> result = await orchestration.InvokeAsync(input, runtime);
string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", output.Select(text => $"{text}"))}");
await runtime.RunUntilIdleAsync();
this.DisplayHistory(monitor.History);
}
}
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.Concurrent;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
instructions: "You are an expert in identifying themes in articles. Given an article, identify the main themes.",
description: "An expert in identifying themes in articles");
ChatClientAgent agent2 =
this.CreateAgent(
instructions: "You are an expert in sentiment analysis. Given an article, identify the sentiment.",
description: "An expert in sentiment analysis");
ChatClientAgent agent3 =
this.CreateAgent(
instructions: "You are an expert in entity recognition. Given an article, extract the entities.",
description: "An expert in entity recognition");
// Define the orchestration with transform
StructuredOutputTransform<Analysis> outputTransform = new(this.CreateChatClient());
ConcurrentOrchestration<string, Analysis> orchestration =
new(agent1, agent2, agent3)
{
LoggerFactory = this.LoggerFactory,
ResultTransform = outputTransform.TransformAsync,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
const string resourceId = "Hamlet_full_play_summary.txt";
string input = Resources.Read(resourceId);
Console.WriteLine($"\n# INPUT: @{resourceId}\n");
OrchestrationResult<Analysis> result = await orchestration.InvokeAsync(input, runtime);
Analysis output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 2));
Console.WriteLine($"\n# RESULT:\n{JsonSerializer.Serialize(output, s_options)}");
await runtime.RunUntilIdleAsync();
}
#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 // Avoid uninstantiated internal classes
}
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.GroupChat;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
"""
You are a copywriter with ten years of experience and are known for brevity and a dry humor.
The goal is to refine and decide on the single best copy as an expert in the field.
Only provide a single proposal per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Consider suggestions when refining an idea.
""");
ChatClientAgent editor =
this.CreateAgent(
name: "Reviewer",
description: "An editor.",
instructions:
"""
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
The goal is to determine if the given copy is acceptable to print.
If so, state that it is approved.
If not, provide insight on how to refine suggested copy without example.
""");
// 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.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
this.DisplayHistory(monitor.History);
}
}
@@ -0,0 +1,207 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.GroupChat;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
name: "Farmer",
description: "A rural farmer from Southeast Asia.",
instructions:
"""
You're a farmer from Southeast Asia.
Your life is deeply connected to land and family.
You value tradition and sustainability.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent developer =
this.CreateAgent(
name: "Developer",
description: "An urban software developer from the United States.",
instructions:
"""
You're a software developer from the United States.
Your life is fast-paced and technology-driven.
You value innovation, freedom, and work-life balance.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent teacher =
this.CreateAgent(
name: "Teacher",
description: "A retired history teacher from Eastern Europe",
instructions:
"""
You're a retired history teacher from Eastern Europe.
You bring historical and philosophical perspectives to discussions.
You value legacy, learning, and cultural continuity.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent activist =
this.CreateAgent(
name: "Activist",
description: "A young activist from South America.",
instructions:
"""
You're a young activist from South America.
You focus on social justice, environmental rights, and generational change.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent spiritual =
this.CreateAgent(
name: "SpiritualLeader",
description: "A spiritual leader from the Middle East.",
instructions:
"""
You're a spiritual leader from the Middle East.
You provide insights grounded in religion, morality, and community service.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent artist =
this.CreateAgent(
name: "Artist",
description: "An artist from Africa.",
instructions:
"""
You're an artist from Africa.
You view life through creative expression, storytelling, and collective memory.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent immigrant =
this.CreateAgent(
name: "Immigrant",
description: "An immigrant entrepreneur from Asia living in Canada.",
instructions:
"""
You're an immigrant entrepreneur from Asia living in Canada.
You balance trandition with adaption.
You focus on family success, risk, and opportunity.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatClientAgent doctor =
this.CreateAgent(
name: "Doctor",
description: "A doctor from Scandinavia.",
instructions:
"""
You're a doctor from Scandinavia.
Your perspective is shaped by public health, equity, and structured societal support.
You are in a debate. Feel free to challenge the other participants with respect.
""");
// 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,
this.CreateChatClient())
{
MaximumInvocationCount = 5
},
farmer,
developer,
teacher,
activist,
spiritual,
artist,
immigrant,
doctor)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
Console.WriteLine($"\n# INPUT: {topic}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(topic, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
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/>
public override ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
this.GetResponseAsync<string>(history, Prompts.Filter(topic), cancellationToken);
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<string>> SelectNextAgent(IReadOnlyCollection<ChatMessage> history, GroupChatTeam team, CancellationToken cancellationToken = default) =>
this.GetResponseAsync<string>(history, Prompts.Selection(topic, team.FormatList()), cancellationToken);
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default) =>
new(new GroupChatManagerResult<bool>(false) { Reason = "The AI group chat manager does not request user input." });
/// <inheritdoc/>
public override async ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = await base.ShouldTerminate(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;
}
}
}
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.GroupChat;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
"""
You are a copywriter with ten years of experience and are known for brevity and a dry humor.
The goal is to refine and decide on the single best copy as an expert in the field.
Only provide a single proposal per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Consider suggestions when refining an idea.
""");
ChatClientAgent editor =
this.CreateAgent(
name: "Reviewer",
description: "An editor.",
instructions:
"""
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
The goal is to determine if the given copy is acceptable to print.
If so, state that it is approved.
If not, provide insight on how to refine suggested copy without example.
""");
// 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.ResponseCallback,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
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
{
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
string? lastAgent = history.LastOrDefault()?.AuthorName;
GroupChatManagerResult<bool> result;
if (lastAgent is null)
{
result = new GroupChatManagerResult<bool>(false) { Reason = "No agents have spoken yet." };
}
if (lastAgent == "Reviewer")
{
result = new GroupChatManagerResult<bool>(true) { Reason = "User input is needed after the reviewer's message." };
}
else
{
result = new GroupChatManagerResult<bool>(false) { Reason = "User input is not needed until the reviewer's message." };
}
return new ValueTask<GroupChatManagerResult<bool>>(result);
}
}
}
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.Handoff;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
instructions: "A customer support agent that triages issues.",
name: "TriageAgent",
description: "Handle customer requests.");
ChatClientAgent statusAgent =
this.CreateAgent(
name: "OrderStatusAgent",
instructions: "Handle order status requests.",
description: "A customer support agent that checks order status.",
functions: AIFunctionFactory.Create(OrderFunctions.CheckOrderStatus));
ChatClientAgent returnAgent =
this.CreateAgent(
name: "OrderReturnAgent",
instructions: "Handle order return requests.",
description: "A customer support agent that handles order returns.",
functions: AIFunctionFactory.Create(OrderFunctions.ProcessReturn));
ChatClientAgent refundAgent =
this.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();
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(OrchestrationHandoffs
.StartWith(triageAgent)
.Add(triageAgent, statusAgent, returnAgent, refundAgent)
.Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related")
.Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related")
.Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related"),
triageAgent,
statusAgent,
returnAgent,
refundAgent)
{
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.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
Console.WriteLine($"\n# INPUT:\n{task}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(task, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(300));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
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.";
}
}
@@ -0,0 +1,124 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.Handoff;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
instructions: "Given a GitHub issue, triage it.",
name: "TriageAgent",
description: "An agent that triages GitHub issues");
ChatClientAgent pythonAgent =
this.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 =
this.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<GithubIssue, string> orchestration =
new(OrchestrationHandoffs
.StartWith(triageAgent)
.Add(triageAgent, dotnetAgent, pythonAgent),
triageAgent,
pythonAgent,
dotnetAgent)
{
LoggerFactory = this.LoggerFactory,
ResponseCallback = monitor.ResponseCallback,
};
GithubIssue input =
new()
{
Id = "12345",
Title = "Bug: SQLite Error 1: 'ambiguous column name:' when including VectorStoreRecordKey in VectorSearchOptions.Filter",
Body =
"""
Describe the bug
When using column names marked as [VectorStoreRecordData(IsFilterable = true)] in VectorSearchOptions.Filter, the query runs correctly.
However, using the column name marked as [VectorStoreRecordKey] in VectorSearchOptions.Filter, the query throws exception 'SQLite Error 1: ambiguous column name: StartUTC'.
To Reproduce
Add a filter for the column marked [VectorStoreRecordKey]. Since that same column exists in both the vec_TestTable and TestTable, the data for both columns cannot be returned.
Expected behavior
The query should explicitly list the vec_TestTable column names to retrieve and should omit the [VectorStoreRecordKey] column since it will be included in the primary TestTable columns.
Platform
Microsoft.SemanticKernel.Connectors.Sqlite v1.46.0-preview
Additional context
Normal DBContext logging shows only normal context queries. Queries run by VectorizedSearchAsync() don't appear in those logs and I could not find a way to enable logging in semantic search so that I could actually see the exact query that is failing. It would have been very useful to see the failing semantic query.
""",
Labels = []
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
Console.WriteLine($"\n# INPUT:\n{input.Id}: {input.Title}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
Console.WriteLine($"\n# RESULT: {text}");
Console.WriteLine($"\n# LABELS: {string.Join(",", githubPlugin.Labels["12345"])}\n");
await runtime.RunUntilIdleAsync();
}
private sealed class GithubIssue
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("body")]
public string Body { get; set; } = string.Empty;
[JsonPropertyName("labels")]
public string[] Labels { get; set; } = [];
}
private sealed class GithubPlugin
{
public Dictionary<string, string[]> Labels { get; } = [];
public void AddLabels(string issueId, params string[] labels)
{
this.Labels[issueId] = labels;
}
}
}
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.Sequential;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.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 =
this.CreateAgent(
name: "copywriter",
instructions:
"""
You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
compose a compelling marketing copy (like a newsletter section) that highlights these points.
Output should be short (around 150 words), output just the copy as a single text block.
""",
description: "An agent that writes a marketing copy based on the extracted concepts.");
ChatClientAgent editorAgent =
this.CreateAgent(
name: "editor",
instructions:
"""
You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
give format and make it polished. Output the final improved copy as a single text block.
""",
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.ResponseCallback,
StreamingResponseCallback = streamedResponse ? monitor.StreamingResultCallback : null,
};
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
this.DisplayHistory(monitor.History);
}
}
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.Orchestration;
using Microsoft.Agents.Orchestration.Sequential;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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 =
this.CreateAgent(
"""
If the input message is a number, return the number incremented by one.
""",
description: "A agent that increments numbers.");
// Define the orchestration
SequentialOrchestration orchestration = new(agent) { LoggerFactory = this.LoggerFactory };
// Start the runtime
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
// Run the orchestration
string input = "42";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult<string> result = await orchestration.InvokeAsync(input, runtime);
result.Cancel();
await Task.Delay(TimeSpan.FromSeconds(3));
try
{
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
Console.WriteLine($"\n# RESULT: {text}");
}
catch (AggregateException exception)
{
Console.WriteLine($"\n# CANCELLED: {exception.InnerException?.Message}");
}
await runtime.RunUntilIdleAsync();
}
}
@@ -0,0 +1,13 @@
On a dark winter night, a ghost walks the ramparts of Elsinore Castle in Denmark. Discovered first by a pair of watchmen, then by the scholar Horatio, the ghost resembles the recently deceased King Hamlet, whose brother Claudius has inherited the throne and married the 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.
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// An actor that represents an <see cref="Agent"/>.
/// </summary>
public abstract class AgentActor : OrchestrationActor
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="agent">An <see cref="Agent"/>.</param>
/// <param name="logger">The logger to use for the actor</param>
protected AgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger? logger = null)
: base(
id,
runtime,
context,
VerifyDescription(agent),
logger)
{
this.Agent = agent;
this.Thread = this.Agent.GetNewThread();
}
/// <summary>
/// Gets the associated agent.
/// </summary>
protected Agent Agent { get; }
/// <summary>
/// Gets the current conversation thread used during agent communication.
/// </summary>
protected AgentThread Thread { get; private set; }
/// <summary>
/// Reset the conversation thread.
/// </summary>
protected void ResetThread()
{
this.Thread = this.Agent.GetNewThread();
}
/// <summary>
/// Invokes the agent for a regular (not streamed) response.
/// </summary>
/// <param name="messages">The messages to send.</param>
/// <param name="options">The options for running the agent.</param>
/// <param name="cancellationToken">A cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// Override this method to customize the invocation of the agent.
/// </remarks>
protected virtual Task InvokeAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentRunOptions options,
CancellationToken cancellationToken = default) =>
this.Agent.RunAsync(
[.. messages],
this.Thread,
options,
cancellationToken);
/// <summary>
/// Invokes the agent for a streamed response.
/// </summary>
/// <param name="messages">The messages to send.</param>
/// <param name="options">The options for running the agent.</param>
/// <param name="cancellationToken">A cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks>
/// Override this method to customize the invocation of the agent.
/// </remarks>
protected virtual IAsyncEnumerable<ChatResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
this.Agent.RunStreamingAsync(
messages,
this.Thread,
options,
cancellationToken);
/// <summary>
/// Invokes the agent with a single chat message.
/// This method sets the message role to <see cref="ChatRole.User"/> and delegates to the overload accepting multiple messages.
/// </summary>
/// <param name="input">The chat message content to send.</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 ValueTask<ChatMessage> InvokeAsync(ChatMessage input, CancellationToken cancellationToken) =>
this.InvokeAsync([input], cancellationToken);
/// <summary>
/// Invokes the agent with input messages and respond with both streamed and regular messages.
/// </summary>
/// <param name="input">The list of chat messages to send.</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 async ValueTask<ChatMessage> InvokeAsync(IEnumerable<ChatMessage> input, CancellationToken cancellationToken)
{
this.Context.Cancellation.ThrowIfCancellationRequested();
List<ChatMessage>? responseMessages = [];
ChatResponse response = new(responseMessages);
AgentRunOptions options =
new()
{
OnIntermediateMessages = HandleMessage,
};
if (this.Context.StreamingResponseCallback == null)
{
// No need to utilize streaming if no callback is provided
await this.InvokeAsync([.. input], options, cancellationToken).ConfigureAwait(false);
}
else
{
IAsyncEnumerable<ChatResponseUpdate> streamedResponses = this.InvokeStreamingAsync([.. input], options, cancellationToken);
ChatResponseUpdate? lastStreamedResponse = null;
await foreach (ChatResponseUpdate streamedResponse in streamedResponses.ConfigureAwait(false))
{
this.Context.Cancellation.ThrowIfCancellationRequested();
await HandleStreamedMessage(lastStreamedResponse, isFinal: false).ConfigureAwait(false);
lastStreamedResponse = streamedResponse;
}
await HandleStreamedMessage(lastStreamedResponse, isFinal: true).ConfigureAwait(false);
}
return response.Messages.Last();
async Task HandleMessage(IReadOnlyCollection<ChatMessage> messages)
{
responseMessages?.AddRange(messages);
if (this.Context.ResponseCallback is not null)
{
await this.Context.ResponseCallback.Invoke(messages).ConfigureAwait(false);
}
}
async ValueTask HandleStreamedMessage(ChatResponseUpdate? streamedResponse, bool isFinal)
{
if (this.Context.StreamingResponseCallback != null && streamedResponse != null)
{
await this.Context.StreamingResponseCallback.Invoke(streamedResponse, isFinal).ConfigureAwait(false);
}
}
}
private static string VerifyDescription(Agent agent)
{
return agent.Description ?? throw new ArgumentException($"Missing agent description: {agent.Name ?? agent.Id}", nameof(agent));
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration;
public abstract partial class AgentOrchestration<TInput, TOutput>
{
/// <summary>
/// Actor responsible for receiving final message and transforming it into the output type.
/// </summary>
private sealed class RequestActor : OrchestrationActor, IHandle<TInput>
{
private readonly OrchestrationInputTransform<TInput> _transform;
private readonly Func<IEnumerable<ChatMessage>, ValueTask> _action;
private readonly TaskCompletionSource<TOutput> _completionSource;
/// <summary>
/// Initializes a new instance of the <see cref="AgentOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="transform">A function that transforms an input of type TInput into a source type TSource.</param>
/// <param name="completionSource">Optional TaskCompletionSource to signal orchestration completion.</param>
/// <param name="action">An asynchronous function that processes the resulting source.</param>
/// <param name="logger">The logger to use for the actor</param>
public RequestActor(
AgentId id,
IAgentRuntime runtime,
OrchestrationContext context,
OrchestrationInputTransform<TInput> transform,
TaskCompletionSource<TOutput> completionSource,
Func<IEnumerable<ChatMessage>, ValueTask> action,
ILogger<RequestActor>? logger = null)
: base(id, runtime, context, $"{id.Type}_Actor", logger)
{
this._transform = transform;
this._action = action;
this._completionSource = completionSource;
}
/// <summary>
/// Handles the incoming message by transforming the input and executing the corresponding action asynchronously.
/// </summary>
/// <param name="item">The input message of type TInput.</param>
/// <param name="messageContext">The context of the message, providing additional details.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
public async ValueTask HandleAsync(TInput item, MessageContext messageContext)
{
this.Logger.LogOrchestrationRequestInvoke(this.Context.Orchestration, this.Id);
try
{
IEnumerable<ChatMessage> input = await this._transform.Invoke(item).ConfigureAwait(false);
Task task = this._action.Invoke(input).AsTask();
this.Logger.LogOrchestrationStart(this.Context.Orchestration, this.Id);
await task.ConfigureAwait(false);
}
catch (Exception exception)
{
// Log exception details and allow orchestration to fail
this.Logger.LogOrchestrationRequestFailure(this.Context.Orchestration, this.Id, exception);
this._completionSource.SetException(exception);
throw;
}
}
}
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration;
public abstract partial class AgentOrchestration<TInput, TOutput>
{
/// <summary>
/// Actor responsible for receiving the resultant message, transforming it, and handling further orchestration.
/// </summary>
private sealed class ResultActor<TResult> : OrchestrationActor, IHandle<TResult>
{
private readonly TaskCompletionSource<TOutput> _completionSource;
private readonly OrchestrationResultTransform<TResult> _transformResult;
private readonly OrchestrationOutputTransform<TOutput> _transform;
/// <summary>
/// Initializes a new instance of the <see cref="AgentOrchestration{TInput, TOutput}.ResultActor{TResult}"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="transformResult">A delegate that transforms a TResult instance into a ChatMessage.</param>
/// <param name="transformOutput">A delegate that transforms a ChatMessage into a TOutput instance.</param>
/// <param name="completionSource">Optional TaskCompletionSource to signal orchestration completion.</param>
/// <param name="logger">The logger to use for the actor</param>
public ResultActor(
AgentId id,
IAgentRuntime runtime,
OrchestrationContext context,
OrchestrationResultTransform<TResult> transformResult,
OrchestrationOutputTransform<TOutput> transformOutput,
TaskCompletionSource<TOutput> completionSource,
ILogger<ResultActor<TResult>>? logger = null)
: base(id, runtime, context, $"{id.Type}_Actor", logger)
{
this._completionSource = completionSource;
this._transformResult = transformResult;
this._transform = transformOutput;
}
/// <summary>
/// Processes the received TResult message by transforming it into a TOutput message.
/// If a CompletionTarget is defined, it sends the transformed message to the corresponding agent.
/// Additionally, it signals completion via the provided TaskCompletionSource if available.
/// </summary>
/// <param name="item">The result item to process.</param>
/// <param name="messageContext">The context associated with the message.</param>
/// <returns>A ValueTask representing asynchronous operation.</returns>
public async ValueTask HandleAsync(TResult item, MessageContext messageContext)
{
this.Logger.LogOrchestrationResultInvoke(this.Context.Orchestration, this.Id);
try
{
if (!this._completionSource.Task.IsCompleted)
{
IList<ChatMessage> result = this._transformResult.Invoke(item);
TOutput output = await this._transform.Invoke(result).ConfigureAwait(false);
this._completionSource.TrySetResult(output);
}
}
catch (Exception exception)
{
// Log exception details and fail orchestration as per design.
this.Logger.LogOrchestrationResultFailure(this.Context.Orchestration, this.Id, exception);
this._completionSource.SetException(exception);
throw;
}
}
}
}
@@ -0,0 +1,257 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Called for every response is produced by any agent.
/// </summary>
/// <param name="response">The agent response</param>
public delegate ValueTask OrchestrationResponseCallback(IEnumerable<ChatMessage> response);
/// <summary>
/// Called to expose the streamed response produced by any agent.
/// </summary>
/// <param name="response">The agent response</param>
/// <param name="isFinal">Indicates if streamed content is final chunk of the message.</param>
public delegate ValueTask OrchestrationStreamingCallback(ChatResponseUpdate response, bool isFinal);
/// <summary>
/// Called when human interaction is requested.
/// </summary>
public delegate ValueTask<ChatMessage> OrchestrationInteractiveCallback();
/// <summary>
/// Base class for multi-agent agent orchestration patterns.
/// </summary>
/// <typeparam name="TInput">The type of the input to the orchestration.</typeparam>
/// <typeparam name="TOutput">The type of the result output by the orchestration.</typeparam>
public abstract partial class AgentOrchestration<TInput, TOutput>
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="members">Specifies the member agents or orchestrations participating in this orchestration.</param>
protected AgentOrchestration(params Agent[] members)
{
// Capture orchestration root name without generic parameters for use in
// agent type and topic formatting as well as logging.
this.OrchestrationLabel = this.GetType().Name.Split('`').First();
this.Members = members;
}
/// <summary>
/// Gets the description of the orchestration.
/// </summary>
public string Description { get; init; } = string.Empty;
/// <summary>
/// Gets the name of the orchestration.
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// Gets the associated logger.
/// </summary>
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
/// <summary>
/// Transforms the orchestration input into a source input suitable for processing.
/// </summary>
public OrchestrationInputTransform<TInput> InputTransform { get; init; } = DefaultTransforms.FromInput<TInput>;
/// <summary>
/// Transforms the processed result into the final output form.
/// </summary>
public OrchestrationOutputTransform<TOutput> ResultTransform { get; init; } = DefaultTransforms.ToOutput<TOutput>;
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationResponseCallback? ResponseCallback { get; init; }
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationStreamingCallback? StreamingResponseCallback { get; init; }
/// <summary>
/// Gets the list of member targets involved in the orchestration.
/// </summary>
protected IReadOnlyList<Agent> Members { get; }
/// <summary>
/// Orchestration identifier without generic parameters for use in
/// agent type and topic formatting as well as logging.
/// </summary>
protected string OrchestrationLabel { get; }
/// <summary>
/// Initiates processing of the orchestration.
/// </summary>
/// <param name="input">The input message.</param>
/// <param name="runtime">The runtime associated with the orchestration.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
public async ValueTask<OrchestrationResult<TOutput>> InvokeAsync(
TInput input,
IAgentRuntime runtime,
CancellationToken cancellationToken = default)
{
Throw.IfNull(input, nameof(input));
TopicId topic = new($"{this.OrchestrationLabel}_{Guid.NewGuid().ToString().Replace("-", string.Empty)}");
CancellationTokenSource orchestrationCancelSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
OrchestrationContext context =
new(this.OrchestrationLabel,
topic,
this.ResponseCallback,
this.StreamingResponseCallback,
this.LoggerFactory,
cancellationToken);
ILogger logger = this.LoggerFactory.CreateLogger(this.GetType());
TaskCompletionSource<TOutput> completion = new();
AgentType orchestrationType = await this.RegisterAsync(runtime, context, completion, handoff: null).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
logger.LogOrchestrationInvoke(this.OrchestrationLabel, topic);
Task task = runtime.PublishMessageAsync(input, orchestrationType, cancellationToken).AsTask();
logger.LogOrchestrationYield(this.OrchestrationLabel, topic);
return new OrchestrationResult<TOutput>(context, completion, orchestrationCancelSource, logger);
}
/// <summary>
/// Initiates processing according to the orchestration pattern.
/// </summary>
/// <param name="runtime">The runtime associated with the orchestration.</param>
/// <param name="topic">The unique identifier for the orchestration session.</param>
/// <param name="input">The input to be transformed and processed.</param>
/// <param name="entryAgent">The initial agent type used for starting the orchestration.</param>
protected abstract ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent);
/// <summary>
/// Orchestration specific registration, including members and returns an optional entry agent.
/// </summary>
/// <param name="runtime">The runtime targeted for registration.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="registrar">A registration context.</param>
/// <param name="logger">The logger to use during registration</param>
/// <returns>The entry AgentType for the orchestration, if any.</returns>
protected abstract ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger);
/// <summary>
/// Formats and returns a unique AgentType based on the provided topic and suffix.
/// </summary>
/// <param name="topic">The topic identifier used in formatting the agent type.</param>
/// <param name="suffix">A suffix to differentiate the agent type.</param>
/// <returns>A formatted AgentType object.</returns>
protected AgentType FormatAgentType(TopicId topic, string suffix) => new($"{topic.Type}_{suffix}");
/// <summary>
/// Registers the orchestration's root and boot agents, setting up completion and target routing.
/// </summary>
/// <param name="runtime">The runtime targeted for registration.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="completion">A TaskCompletionSource for the orchestration.</param>
/// <param name="handoff">The actor type used for handoff. Only defined for nested orchestrations.</param>
/// <returns>The AgentType representing the orchestration entry point.</returns>
private async ValueTask<AgentType> RegisterAsync(IAgentRuntime runtime, OrchestrationContext context, TaskCompletionSource<TOutput> completion, AgentType? handoff)
{
// Create a logger for the orchestration registration.
ILogger logger = context.LoggerFactory.CreateLogger(this.GetType());
logger.LogOrchestrationRegistrationStart(context.Orchestration, context.Topic);
// Register orchestration
RegistrationContext registrar = new(this.FormatAgentType(context.Topic, "Root"), runtime, context, completion, this.ResultTransform);
AgentType? entryAgent = await this.RegisterOrchestrationAsync(runtime, context, registrar, logger).ConfigureAwait(false);
// Register actor for orchestration entry-point
AgentType orchestrationEntry =
await runtime.RegisterOrchestrationAgentAsync(
this.FormatAgentType(context.Topic, "Boot"),
(agentId, runtime) =>
{
RequestActor actor =
new(agentId,
runtime,
context,
this.InputTransform,
completion,
StartAsync,
context.LoggerFactory.CreateLogger<RequestActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogOrchestrationRegistrationDone(context.Orchestration, context.Topic);
return orchestrationEntry;
ValueTask StartAsync(IEnumerable<ChatMessage> input) => this.StartAsync(runtime, context.Topic, input, entryAgent);
}
/// <summary>
/// A context used during registration (<see cref="RegisterAsync"/>).
/// </summary>
public sealed class RegistrationContext(
AgentType agentType,
IAgentRuntime runtime,
OrchestrationContext context,
TaskCompletionSource<TOutput> completion,
OrchestrationOutputTransform<TOutput> outputTransform)
{
/// <summary>
/// Register the final result type.
/// </summary>
public async ValueTask<AgentType> RegisterResultTypeAsync<TResult>(OrchestrationResultTransform<TResult> resultTransform)
{
// Register actor for final result
AgentType registeredType =
await runtime.RegisterOrchestrationAgentAsync(
agentType,
(agentId, runtime) =>
{
ResultActor<TResult> actor =
new(agentId,
runtime,
context,
resultTransform,
outputTransform,
completion,
context.LoggerFactory.CreateLogger<ResultActor<TResult>>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
return registeredType;
}
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// An <see cref="AgentActor"/> used with the <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
internal sealed class ConcurrentActor : AgentActor, IHandle<ConcurrentMessages.Request>
{
private readonly AgentType _handoffActor;
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="agent">An <see cref="Agent"/>.</param>
/// <param name="resultActor">Identifies the actor collecting results.</param>
/// <param name="logger">The logger to use for the actor</param>
public ConcurrentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType resultActor, ILogger<ConcurrentActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
this._handoffActor = resultActor;
}
/// <inheritdoc/>
public async ValueTask HandleAsync(ConcurrentMessages.Request item, MessageContext messageContext)
{
this.Logger.LogConcurrentAgentInvoke(this.Id);
ChatMessage response = await this.InvokeAsync(item.Messages, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogConcurrentAgentResult(this.Id, response.Text);
await this.PublishMessageAsync(response.AsResultMessage(), this._handoffActor, messageContext.CancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// Common messages used by the <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
internal static class ConcurrentMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
public static readonly ChatMessage Empty = new();
/// <summary>
/// The input task for a <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
public sealed class Request
{
/// <summary>
/// The request input.
/// </summary>
public IList<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// A result from a <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
public sealed class Result
{
/// <summary>
/// The result message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Extension method to convert a <see cref="string"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this string text, ChatRole? role = null) => new() { Message = new ChatMessage(role ?? ChatRole.Assistant, text) };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this ChatMessage message) => new() { Message = message };
/// <summary>
/// Extension method to convert a collection of <see cref="ChatMessage"/> to a <see cref="ConcurrentMessages.Request"/>.
/// </summary>
public static Request AsInputMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = [.. messages] };
}
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// An orchestration that broadcasts the input message to each agent.
/// </summary>
public sealed class ConcurrentOrchestration : ConcurrentOrchestration<string, string[]>
{
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentOrchestration"/> class.
/// </summary>
/// <param name="members">The agents to be orchestrated.</param>
public ConcurrentOrchestration(params Agent[] members)
: base(members)
{
this.ResultTransform =
(response, cancellationToken) =>
{
string[] result = [.. response.Select(r => r.Text)];
#if !NETCOREAPP
return new ValueTask<string[]>(result);
#else
return ValueTask.FromResult(result);
#endif
};
}
}
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// An orchestration that broadcasts the input message to each agent.
/// </summary>
/// <remarks>
/// <c>TOutput</c> must be an array type for <see cref="ConcurrentOrchestration"/>.
/// </remarks>
public class ConcurrentOrchestration<TInput, TOutput>
: AgentOrchestration<TInput, TOutput>
{
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="agents">The agents participating in the orchestration.</param>
public ConcurrentOrchestration(params Agent[] agents)
: base(agents)
{
}
/// <inheritdoc />
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
return runtime.PublishMessageAsync(input.AsInputMessage(), topic);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<ConcurrentMessages.Result[]>(response => [.. response.Select(r => r.Message)]).ConfigureAwait(false);
// Register result actor
AgentType resultType = this.FormatAgentType(context.Topic, "Results");
await runtime.RegisterOrchestrationAgentAsync(
resultType,
(agentId, runtime) =>
{
ConcurrentResultActor actor = new(agentId, runtime, context, outputType, this.Members.Count, context.LoggerFactory.CreateLogger<ConcurrentResultActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, resultType, "RESULTS");
// Register member actors - All agents respond to the same message.
int agentCount = 0;
foreach (Agent agent in this.Members)
{
++agentCount;
AgentType agentType =
await runtime.RegisterAgentFactoryAsync(
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
(agentId, runtime) =>
{
ConcurrentActor actor = new(agentId, runtime, context, agent, resultType, context.LoggerFactory.CreateLogger<ConcurrentActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
}
return null;
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Concurrent;
/// <summary>
/// Actor for capturing each <see cref="ConcurrentMessages.Result"/> message.
/// </summary>
internal sealed class ConcurrentResultActor :
OrchestrationActor,
IHandle<ConcurrentMessages.Result>
{
private readonly ConcurrentQueue<ConcurrentMessages.Result> _results;
private readonly AgentType _orchestrationType;
private readonly int _expectedCount;
private int _resultCount;
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentResultActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="orchestrationType">Identifies the orchestration agent.</param>
/// <param name="expectedCount">The expected number of messages to be received.</param>
/// <param name="logger">The logger to use for the actor</param>
public ConcurrentResultActor(
AgentId id,
IAgentRuntime runtime,
OrchestrationContext context,
AgentType orchestrationType,
int expectedCount,
ILogger logger)
: base(id, runtime, context, "Captures the results of the ConcurrentOrchestration", logger)
{
this._orchestrationType = orchestrationType;
this._expectedCount = expectedCount;
this._results = [];
}
/// <inheritdoc/>
public async ValueTask HandleAsync(ConcurrentMessages.Result item, MessageContext messageContext)
{
this.Logger.LogConcurrentResultCapture(this.Id, this._resultCount + 1, this._expectedCount);
this._results.Enqueue(item);
if (Interlocked.Increment(ref this._resultCount) == this._expectedCount)
{
await this.PublishMessageAsync(this._results.ToArray(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Extensions;
/// <summary>
/// Extension methods for <see cref="IAgentRuntime"/>.
/// </summary>
public static class RuntimeExtensions
{
/// <summary>
/// Sends a message to the specified agent.
/// </summary>
public static async ValueTask PublishMessageAsync(this IAgentRuntime runtime, object message, AgentType agentType, CancellationToken cancellationToken = default)
{
await runtime.PublishMessageAsync(message, new TopicId(agentType), sender: null, messageId: null, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Registers an agent factory for the specified agent type and associates it with the runtime.
/// </summary>
/// <param name="runtime">The runtime targeted for registration.</param>
/// <param name="agentType">The type of agent to register.</param>
/// <param name="factoryFunc">The factory function for creating the agent.</param>
/// <returns>The registered agent type.</returns>
public static async ValueTask<AgentType> RegisterOrchestrationAgentAsync(this IAgentRuntime runtime, AgentType agentType, Func<AgentId, IAgentRuntime, ValueTask<IHostableAgent>> factoryFunc)
{
AgentType registeredType = await runtime.RegisterAgentFactoryAsync(agentType, factoryFunc).ConfigureAwait(false);
// Subscribe agent to its own unique topic
await runtime.SubscribeAsync(registeredType).ConfigureAwait(false);
return registeredType;
}
/// <summary>
/// Subscribes the specified agent type to its own dedicated topic.
/// </summary>
/// <param name="runtime">The runtime for managing the subscription.</param>
/// <param name="agentType">The agent type to subscribe.</param>
public static async Task SubscribeAsync(this IAgentRuntime runtime, string agentType)
{
await runtime.AddSubscriptionAsync(new TypeSubscription(agentType, agentType)).ConfigureAwait(false);
}
/// <summary>
/// Subscribes the specified agent type to the provided topics.
/// </summary>
/// <param name="runtime">The runtime for managing the subscription.</param>
/// <param name="agentType">The agent type to subscribe.</param>
/// <param name="topics">A variable list of topics for subscription.</param>
public static async Task SubscribeAsync(this IAgentRuntime runtime, string agentType, params TopicId[] topics)
{
for (int index = 0; index < topics.Length; ++index)
{
await runtime.AddSubscriptionAsync(new TypeSubscription(topics[index].Type, agentType)).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An <see cref="AgentActor"/> used with the <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
/// </summary>
internal sealed class GroupChatAgentActor :
AgentActor,
IHandle<GroupChatMessages.Group>,
IHandle<GroupChatMessages.Reset>,
IHandle<GroupChatMessages.Speak>
{
private readonly List<ChatMessage> _cache;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatAgentActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="agent">An <see cref="Agent"/>.</param>
/// <param name="logger">The logger to use for the actor</param>
public GroupChatAgentActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, ILogger<GroupChatAgentActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
this._cache = [];
}
/// <inheritdoc/>
public ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext)
{
this._cache.AddRange(item.Messages);
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public ValueTask HandleAsync(GroupChatMessages.Reset item, MessageContext messageContext)
{
this.ResetThread();
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public async ValueTask HandleAsync(GroupChatMessages.Speak item, MessageContext messageContext)
{
this.Logger.LogChatAgentInvoke(this.Id);
ChatMessage response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatAgentResult(this.Id, response.Text);
this._cache.Clear();
await this.PublishMessageAsync(response.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
}
}
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <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 OrchestrationInteractiveCallback? 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>
public abstract ValueTask<GroupChatManagerResult<string>> FilterResults(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>
public abstract ValueTask<GroupChatManagerResult<string>> SelectNextAgent(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>
public abstract ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(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>
public virtual ValueTask<GroupChatManagerResult<bool>> ShouldTerminate(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref this._invocationCount);
bool resultValue = false;
string reason = "Maximum number of invocations has not been reached.";
if (this.InvocationCount > this.MaximumInvocationCount)
{
resultValue = true;
reason = "Maximum number of invocations reached.";
}
GroupChatManagerResult<bool> result = new(resultValue) { Reason = reason };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<bool>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
}
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An <see cref="OrchestrationActor"/> used to manage a <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
/// </summary>
internal sealed class GroupChatManagerActor :
OrchestrationActor,
IHandle<GroupChatMessages.InputTask>,
IHandle<GroupChatMessages.Group>
{
/// <summary>
/// A common description for the manager.
/// </summary>
public const string DefaultDescription = "Orchestrates a team of agents to accomplish a defined task.";
private readonly AgentType _orchestrationType;
private readonly GroupChatManager _manager;
private readonly List<ChatMessage> _chat;
private readonly GroupChatTeam _team;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatManagerActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="manager">The manages the flow of the group-chat.</param>
/// <param name="team">The team of agents being orchestrated</param>
/// <param name="orchestrationType">Identifies the orchestration agent.</param>
/// <param name="logger">The logger to use for the actor</param>
public GroupChatManagerActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, GroupChatManager manager, GroupChatTeam team, AgentType orchestrationType, ILogger? logger = null)
: base(id, runtime, context, DefaultDescription, logger)
{
this._chat = [];
this._manager = manager;
this._orchestrationType = orchestrationType;
this._team = team;
}
/// <inheritdoc/>
public async ValueTask HandleAsync(GroupChatMessages.InputTask item, MessageContext messageContext)
{
this.Logger.LogChatManagerInit(this.Id);
this._chat.AddRange(item.Messages);
await this.PublishMessageAsync(item.Messages.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
await this.ManageAsync(messageContext).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask HandleAsync(GroupChatMessages.Group item, MessageContext messageContext)
{
this.Logger.LogChatManagerInvoke(this.Id);
this._chat.AddRange(item.Messages);
await this.ManageAsync(messageContext).ConfigureAwait(false);
}
private async ValueTask ManageAsync(MessageContext messageContext)
{
if (this._manager.InteractiveCallback != null)
{
GroupChatManagerResult<bool> inputResult = await this._manager.ShouldRequestUserInput(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatManagerInput(this.Id, inputResult.Value, inputResult.Reason);
if (inputResult.Value)
{
ChatMessage input = await this._manager.InteractiveCallback.Invoke().ConfigureAwait(false);
this.Logger.LogChatManagerUserInput(this.Id, input.Text);
this._chat.Add(input);
await this.PublishMessageAsync(input.AsGroupMessage(), this.Context.Topic).ConfigureAwait(false);
}
}
GroupChatManagerResult<bool> terminateResult = await this._manager.ShouldTerminate(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatManagerTerminate(this.Id, terminateResult.Value, terminateResult.Reason);
if (terminateResult.Value)
{
GroupChatManagerResult<string> filterResult = await this._manager.FilterResults(this._chat, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogChatManagerResult(this.Id, filterResult.Value, filterResult.Reason);
await this.PublishMessageAsync(filterResult.Value.AsResultMessage(), this._orchestrationType, messageContext.CancellationToken).ConfigureAwait(false);
return;
}
GroupChatManagerResult<string> selectionResult = await this._manager.SelectNextAgent(this._chat, this._team, messageContext.CancellationToken).ConfigureAwait(false);
AgentType selectionType = this._team[selectionResult.Value].Type;
this.Logger.LogChatManagerSelect(this.Id, selectionType);
await this.PublishMessageAsync(new GroupChatMessages.Speak(), selectionType, messageContext.CancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// Common messages used for agent chat patterns.
/// </summary>
public static class GroupChatMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
internal static readonly ChatMessage Empty = new();
/// <summary>
/// Broadcast a message to all <see cref="GroupChatAgentActor"/>.
/// </summary>
public sealed class Group
{
/// <summary>
/// The chat message being broadcast.
/// </summary>
public IEnumerable<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// Reset/clear the conversation history for all <see cref="GroupChatAgentActor"/>.
/// </summary>
public sealed class Reset;
/// <summary>
/// The final result.
/// </summary>
public sealed class Result
{
/// <summary>
/// The chat response message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Signal a <see cref="GroupChatAgentActor"/> to respond.
/// </summary>
public sealed class Speak;
/// <summary>
/// The input task.
/// </summary>
public sealed class InputTask
{
/// <summary>
/// A task that does not require any action.
/// </summary>
public static readonly InputTask None = new();
/// <summary>
/// The input that defines the task goal.
/// </summary>
public IEnumerable<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Group"/>.
/// </summary>
public static Group AsGroupMessage(this ChatMessage message) => new() { Messages = [message] };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Group"/>.
/// </summary>
public static Group AsGroupMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = messages };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static InputTask AsInputTaskMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = messages };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this string text) => new() { Message = new(ChatRole.Assistant, text) };
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An orchestration that broadcasts the input message to each agent.
/// </summary>
public sealed class GroupChatOrchestration : GroupChatOrchestration<string, string>
{
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatOrchestration"/> class.
/// </summary>
/// <param name="manager">The manages the flow of the group-chat.</param>
/// <param name="members">The agents to be orchestrated.</param>
public GroupChatOrchestration(GroupChatManager manager, params Agent[] members)
: base(manager, members)
{
}
}
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// An orchestration that coordinates a group-chat.
/// </summary>
public class GroupChatOrchestration<TInput, TOutput> :
AgentOrchestration<TInput, TOutput>
{
internal const string DefaultAgentDescription = "A helpful agent.";
private readonly GroupChatManager _manager;
/// <summary>
/// Initializes a new instance of the <see cref="GroupChatOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="manager">The manages the flow of the group-chat.</param>
/// <param name="agents">The agents participating in the orchestration.</param>
public GroupChatOrchestration(GroupChatManager manager, params Agent[] agents)
: base(agents)
{
Throw.IfNull(manager, nameof(manager));
this._manager = manager;
}
/// <inheritdoc />
protected override ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
if (!entryAgent.HasValue)
{
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
}
return runtime.PublishMessageAsync(input.AsInputTaskMessage(), entryAgent.Value);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<GroupChatMessages.Result>(response => [response.Message]).ConfigureAwait(false);
int agentCount = 0;
GroupChatTeam team = [];
foreach (Agent agent in this.Members)
{
++agentCount;
AgentType agentType = await RegisterAgentAsync(agent, agentCount).ConfigureAwait(false);
string name = agent.Name ?? agent.Id ?? agentType;
string? description = agent.Description;
team[name] = (agentType, description ?? DefaultAgentDescription);
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", agentCount);
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
}
AgentType managerType =
await runtime.RegisterOrchestrationAgentAsync(
this.FormatAgentType(context.Topic, "Manager"),
(agentId, runtime) =>
{
GroupChatManagerActor actor = new(agentId, runtime, context, this._manager, team, outputType, context.LoggerFactory.CreateLogger<GroupChatManagerActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, managerType, "MANAGER");
await runtime.SubscribeAsync(managerType, context.Topic).ConfigureAwait(false);
return managerType;
ValueTask<AgentType> RegisterAgentAsync(Agent agent, int agentCount) =>
runtime.RegisterOrchestrationAgentAsync(
this.FormatAgentType(context.Topic, $"Agent_{agentCount}"),
(agentId, runtime) =>
{
GroupChatAgentActor actor = new(agentId, runtime, context, agent, context.LoggerFactory.CreateLogger<GroupChatAgentActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
});
}
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Agents.Orchestration.GroupChat;
/// <summary>
/// Describes a team of agents participating in a group chat.
/// </summary>
public class GroupChatTeam : Dictionary<string, (string Type, string Description)>;
/// <summary>
/// Extensions for <see cref="GroupChatTeam"/>.
/// </summary>
public static class ChatGroupExtensions
{
/// <summary>
/// Format the names of the agents in the team as a comma delimimted list.
/// </summary>
/// <param name="team">The agent team</param>
/// <returns>A comma delimimted list of agent name.</returns>
public static string FormatNames(this GroupChatTeam team) => string.Join(",", team.Select(t => t.Key));
/// <summary>
/// Format the names and descriptions of the agents in the team as a markdown list.
/// </summary>
/// <param name="team">The agent team</param>
/// <returns>A markdown list of agent names and descriptions.</returns>
public static string FormatList(this GroupChatTeam team) => string.Join(Environment.NewLine, team.Select(t => $"- {t.Key}: {t.Value.Description}"));
}
@@ -0,0 +1,55 @@
// 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.GroupChat;
/// <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/>
public override ValueTask<GroupChatManagerResult<string>> FilterResults(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<string> result = new(history.LastOrDefault()?.Text ?? string.Empty) { Reason = "Default result filter provides the final chat message." };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<string>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<string>> SelectNextAgent(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}" };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<string>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
/// <inheritdoc/>
public override ValueTask<GroupChatManagerResult<bool>> ShouldRequestUserInput(IReadOnlyCollection<ChatMessage> history, CancellationToken cancellationToken = default)
{
GroupChatManagerResult<bool> result = new(false) { Reason = "The default round-robin group chat manager does not request user input." };
#if !NETCOREAPP
return new ValueTask<GroupChatManagerResult<bool>>(result);
#else
return ValueTask.FromResult(result);
#endif
}
}
@@ -0,0 +1,211 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// An actor used with the <see cref="HandoffOrchestration{TInput,TOutput}"/>.
/// </summary>
internal sealed class HandoffActor :
AgentActor,
IHandle<HandoffMessages.InputTask>,
IHandle<HandoffMessages.Request>,
IHandle<HandoffMessages.Response>
{
private readonly ChatClientAgent _chatAgent;
private readonly HandoffLookup _handoffs;
private readonly AgentType _resultHandoff;
private readonly List<ChatMessage> _cache;
private readonly ChatOptions _options;
private string? _handoffAgent;
private string? _taskSummary;
/// <summary>
/// Initializes a new instance of the <see cref="HandoffActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="agent">An <see cref="Agent"/>.</param>>
/// <param name="handoffs">The handoffs available to this agent</param>
/// <param name="resultHandoff">The handoff agent for capturing the result.</param>
/// <param name="logger">The logger to use for the actor</param>
public HandoffActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, ChatClientAgent agent, HandoffLookup handoffs, AgentType resultHandoff, ILogger<HandoffActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
if (handoffs.ContainsKey(agent.Name ?? agent.Id))
{
throw new ArgumentException($"The agent {agent.Name ?? agent.Id} cannot have a handoff to itself.", nameof(handoffs));
}
this._cache = [];
this._chatAgent = agent;
this._handoffs = handoffs;
this._resultHandoff = resultHandoff;
this._options =
new ChatOptions
{
Tools = [.. this.CreateHandoffFunctions()],
ToolMode = ChatToolMode.Auto
};
}
/// <inheritdoc/>
protected override Task InvokeAsync(
IReadOnlyCollection<ChatMessage> messages,
AgentRunOptions options,
CancellationToken cancellationToken = default) =>
this._chatAgent.RunAsync(
[.. messages],
this.Thread,
options,
this._options,
cancellationToken);
/// <inheritdoc/>
protected override IAsyncEnumerable<ChatResponseUpdate> InvokeStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentRunOptions options, CancellationToken cancellationToken) =>
this._chatAgent.RunStreamingAsync(
messages,
this.Thread,
options,
this._options,
cancellationToken);
/// <summary>
/// Gets or sets the callback to be invoked for interactive input.
/// </summary>
public OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
/// <inheritdoc/>
public ValueTask HandleAsync(HandoffMessages.InputTask item, MessageContext messageContext)
{
this._taskSummary = null;
this._cache.AddRange(item.Messages);
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public ValueTask HandleAsync(HandoffMessages.Response item, MessageContext messageContext)
{
this._cache.Add(item.Message);
#if !NETCOREAPP
return new ValueTask();
#else
return ValueTask.CompletedTask;
#endif
}
/// <inheritdoc/>
public async ValueTask HandleAsync(HandoffMessages.Request item, MessageContext messageContext)
{
try
{
this.Logger.LogHandoffAgentInvoke(this.Id);
while (this._taskSummary == null)
{
ChatMessage response;
try
{
response = await this.InvokeAsync(this._cache, messageContext.CancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
this.Logger.LogError(exception, "Failure");
throw;
}
this._cache.Clear();
this.Logger.LogHandoffAgentResult(this.Id, response.Text);
// The response can potentially be a TOOL message from the Handoff plugin due to the filter
// which will terminate the conversation when a function from the handoff plugin is called.
// Since we don't want to publish that message, so we only publish if the response is an ASSISTANT message.
if (response.Role == ChatRole.Assistant)
{
await this.PublishMessageAsync(new HandoffMessages.Response { Message = response }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false);
}
if (this._handoffAgent != null)
{
AgentType handoffType = this._handoffs[this._handoffAgent].AgentType;
await this.PublishMessageAsync(new HandoffMessages.Request(), handoffType, messageContext.CancellationToken).ConfigureAwait(false);
this._handoffAgent = null;
break;
}
if (this.InteractiveCallback != null && this._taskSummary == null)
{
ChatMessage input = await this.InteractiveCallback().ConfigureAwait(false);
await this.PublishMessageAsync(new HandoffMessages.Response { Message = input }, this.Context.Topic, messageId: null, messageContext.CancellationToken).ConfigureAwait(false);
this._cache.Add(input);
continue;
}
await this.EndAsync(response.Text ?? "No handoff or human response function requested. Ending task.", messageContext.CancellationToken).ConfigureAwait(false);
}
}
catch (Exception exception)
{
this.Logger.LogError(exception, "Failure");
throw;
}
}
private IEnumerable<AIFunction> CreateHandoffFunctions()
{
yield return AIFunctionFactory.Create(
this.EndAsync,
name: "end_task",
description: "Complete the task with a summary when no further requests are given.");
foreach (KeyValuePair<string, (AgentType _, string Description)> handoff in this._handoffs)
{
AIFunction handoffFunction =
AIFunctionFactory.Create(
() => this.Handoff(handoff.Key),
name: $"transfer_to_{handoff.Key}",
description: handoff.Value.Description);
yield return handoffFunction;
}
}
private void Handoff(string agentName)
{
this.Logger.LogHandoffFunctionCall(this.Id, agentName);
this._handoffAgent = agentName;
FunctionInvokingChatClient.CurrentContext!.Terminate = true;
}
private async ValueTask EndAsync(string summary, CancellationToken cancellationToken)
{
this.Logger.LogHandoffSummary(this.Id, summary);
this._taskSummary = summary;
await this.PublishMessageAsync(new HandoffMessages.Result { Message = new ChatMessage(ChatRole.Assistant, summary) }, this._resultHandoff, cancellationToken).ConfigureAwait(false);
if (FunctionInvokingChatClient.CurrentContext is not null)
{
FunctionInvokingChatClient.CurrentContext.Terminate = true;
}
}
}
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// A message that describes the input task and captures results for a <see cref="HandoffOrchestration{TInput,TOutput}"/>.
/// </summary>
internal static class HandoffMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
internal static readonly ChatMessage Empty = new();
/// <summary>
/// The input message.
/// </summary>
public sealed class InputTask
{
/// <summary>
/// The orchestration input messages.
/// </summary>
public IList<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// The final result.
/// </summary>
public sealed class Result
{
/// <summary>
/// The orchestration result message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Signals the handoff to another agent.
/// </summary>
public sealed class Request;
/// <summary>
/// Broadcast an agent response to all actors in the orchestration.
/// </summary>
public sealed class Response
{
/// <summary>
/// The chat response message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static InputTask AsInputTaskMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = [.. messages] };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="Result"/>.
/// </summary>
public static Result AsResultMessage(this ChatMessage message) => new() { Message = message };
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// An orchestration that passes the input message to the first agent, and
/// then the subsequent result to the next agent, etc...
/// </summary>
public sealed class HandoffOrchestration : HandoffOrchestration<string, string>
{
/// <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="members">The agents to be orchestrated.</param>
public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] members)
: base(handoffs, members)
{
}
}
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// An orchestration that provides the input message to the first agent
/// and sequentially passes each agent result to the next agent.
/// </summary>
public class HandoffOrchestration<TInput, TOutput> : AgentOrchestration<TInput, TOutput>
{
private readonly OrchestrationHandoffs _handoffs;
/// <summary>
/// Initializes a new instance of the <see cref="HandoffOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="handoffs">Defines the handoff connections for each agent.</param>
/// <param name="agents">The agents participating in the orchestration.</param>
public HandoffOrchestration(OrchestrationHandoffs handoffs, params Agent[] agents)
: base(agents)
{
// Create list of distinct agent names
HashSet<string> agentNames = new(agents.Select(a => a.Name ?? a.Id), StringComparer.Ordinal);
agentNames.Add(handoffs.FirstAgentName);
// Extract names from handoffs that don't align with a member agent.
string[] badNames = [.. handoffs.Keys.Concat(handoffs.Values.SelectMany(h => h.Keys)).Where(name => !agentNames.Contains(name))];
// Fail fast if invalid names are present.
if (badNames.Length > 0)
{
throw new ArgumentException($"The following agents are not defined in the orchestration: {string.Join(", ", badNames)}", nameof(handoffs));
}
this._handoffs = handoffs;
}
/// <summary>
/// Gets or sets the callback to be invoked for interactive input.
/// </summary>
public OrchestrationInteractiveCallback? InteractiveCallback { get; init; }
/// <inheritdoc />
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
if (!entryAgent.HasValue)
{
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
}
await runtime.PublishMessageAsync(input.AsInputTaskMessage(), topic).ConfigureAwait(false);
await runtime.PublishMessageAsync(new HandoffMessages.Request(), entryAgent.Value).ConfigureAwait(false);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<HandoffMessages.Result>(response => [response.Message]).ConfigureAwait(false);
// Each agent handsoff its result to the next agent.
Dictionary<string, AgentType> agentMap = [];
Dictionary<string, HandoffLookup> handoffMap = [];
AgentType agentType = outputType;
for (int index = this.Members.Count - 1; index >= 0; --index)
{
Agent agent = this.Members[index];
HandoffLookup map = [];
handoffMap[agent.Name ?? agent.Id] = map;
agentType =
await runtime.RegisterOrchestrationAgentAsync(
this.GetAgentType(context.Topic, index),
(agentId, runtime) =>
{
HandoffActor actor =
new(agentId, runtime, context, (ChatClientAgent)agent, map, outputType, context.LoggerFactory.CreateLogger<HandoffActor>())
{
InteractiveCallback = this.InteractiveCallback
};
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
}).ConfigureAwait(false);
agentMap[agent.Name ?? agent.Id] = agentType;
await runtime.SubscribeAsync(agentType, context.Topic).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, agentType, "MEMBER", index + 1);
}
// Complete the handoff model
foreach (KeyValuePair<string, AgentHandoffs> handoffs in this._handoffs)
{
// Retrieve the map for the agent (every agent had an empty map created)
HandoffLookup agentHandoffs = handoffMap[handoffs.Key];
foreach (KeyValuePair<string, string> handoff in handoffs.Value)
{
// name = (type,description)
agentHandoffs[handoff.Key] = (agentMap[handoff.Key], handoff.Value);
}
}
return agentMap[this._handoffs.FirstAgentName];
}
private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
}
@@ -0,0 +1,147 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration.Handoff;
/// <summary>
/// Defines the handoff relationships for a given agent.
/// Maps target agent names/IDs to handoff descriptions.
/// </summary>
public sealed class AgentHandoffs : Dictionary<string, string>
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentHandoffs"/> class with no handoff relationships.
/// </summary>
public AgentHandoffs() { }
/// <summary>
/// Initializes a new instance of the <see cref="AgentHandoffs"/> class with the specified handoff relationships.
/// </summary>
/// <param name="handoffs">A dictionary mapping target agent names/IDs to handoff descriptions.</param>
public AgentHandoffs(Dictionary<string, string> handoffs) : base(handoffs) { }
}
/// <summary>
/// Defines the orchestration handoff relationships for all agents in the system.
/// Maps source agent names/IDs to their <see cref="AgentHandoffs"/>.
/// </summary>
public sealed class OrchestrationHandoffs : Dictionary<string, AgentHandoffs>
{
/// <summary>
/// Initializes a new instance of the <see cref="OrchestrationHandoffs"/> class with no handoff relationships.
/// </summary>
/// <param name="firstAgent">The first agent to be invoked (prior to any handoff).</param>
public OrchestrationHandoffs(Agent firstAgent)
: this(firstAgent.Name ?? firstAgent.Id)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="OrchestrationHandoffs"/> class with no handoff relationships.
/// </summary>
/// <param name="firstAgentName">The name of the first agent to be invoked (prior to any handoff).</param>
public OrchestrationHandoffs(string firstAgentName)
{
Throw.IfNullOrWhitespace(firstAgentName, nameof(firstAgentName));
this.FirstAgentName = firstAgentName;
}
/// <summary>
/// The name of the first agent to be invoked (prior to any handoff).
/// </summary>
public string FirstAgentName { get; }
/// <summary>
/// Adds handoff relationships from a source agent to one or more target agents.
/// Each target agent's name or ID is mapped to its description.
/// </summary>
/// <param name="source">The source agent.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs StartWith(Agent source) => new(source);
}
/// <summary>
/// Extension methods for building and modifying <see cref="OrchestrationHandoffs"/> relationships.
/// </summary>
public static class OrchestrationHandoffsExtensions
{
/// <summary>
/// Adds handoff relationships from a source agent to one or more target agents.
/// Each target agent's name or ID is mapped to its description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <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="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, params Agent[] targets)
{
string key = source.Name ?? source.Id;
AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(key);
foreach (Agent target in targets)
{
agentHandoffs[target.Name ?? target.Id] = target.Description ?? string.Empty;
}
return handoffs;
}
/// <summary>
/// Adds a handoff relationship from a source agent to a target agent with a custom description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="source">The source agent.</param>
/// <param name="target">The target agent.</param>
/// <param name="description">The handoff description.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, Agent target, string description)
=> handoffs.Add(source.Name ?? source.Id, target.Name ?? target.Id, description);
/// <summary>
/// Adds a handoff relationship from a source agent to a target agent name/ID with a custom description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="source">The source agent.</param>
/// <param name="targetName">The target agent's name or ID.</param>
/// <param name="description">The handoff description.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, Agent source, string targetName, string description)
=> handoffs.Add(source.Name ?? source.Id, targetName, description);
/// <summary>
/// Adds a handoff relationship from a source agent name/ID to a target agent name/ID with a custom description.
/// </summary>
/// <param name="handoffs">The orchestration handoffs collection to update.</param>
/// <param name="sourceName">The source agent's name or ID.</param>
/// <param name="targetName">The target agent's name or ID.</param>
/// <param name="description">The handoff description.</param>
/// <returns>The updated <see cref="OrchestrationHandoffs"/> instance.</returns>
public static OrchestrationHandoffs Add(this OrchestrationHandoffs handoffs, string sourceName, string targetName, string description)
{
AgentHandoffs agentHandoffs = handoffs.GetAgentHandoffs(sourceName);
agentHandoffs[targetName] = description;
return handoffs;
}
private static AgentHandoffs GetAgentHandoffs(this OrchestrationHandoffs handoffs, string key)
{
if (!handoffs.TryGetValue(key, out AgentHandoffs? agentHandoffs))
{
agentHandoffs = [];
handoffs[key] = agentHandoffs;
}
return agentHandoffs;
}
}
/// <summary>
/// Handoff relationships post-processed into a name-based lookup table that includes the agent type and handoff description.
/// Maps agent names/IDs to a tuple of <see cref="AgentType"/> and handoff description.
/// </summary>
internal sealed class HandoffLookup : Dictionary<string, (AgentType AgentType, string Description)>;
@@ -0,0 +1,157 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="AgentOrchestration{TInput, TOutput}"/>.
/// </summary>
/// <remarks>
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
/// generate logging code at compile time to achieve optimized code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static partial class AgentOrchestrationLogMessages
{
/// <summary>
/// Logs the start of the registration phase for an orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "REGISTER {Orchestration} Start: {Topic}")]
public static partial void LogOrchestrationRegistrationStart(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs pattern actor registration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "REGISTER ACTOR {Orchestration} {label}: {AgentType}")]
public static partial void LogRegisterActor(
this ILogger logger,
string orchestration,
AgentType agentType,
string label);
/// <summary>
/// Logs agent actor registration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "REGISTER ACTOR {Orchestration} {label} #{Count}: {AgentType}")]
public static partial void LogRegisterActor(
this ILogger logger,
string orchestration,
AgentType agentType,
string label,
int count);
/// <summary>
/// Logs the end of the registration phase for an orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "REGISTER {Orchestration} Complete: {Topic}")]
public static partial void LogOrchestrationRegistrationDone(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs an orchestration invocation
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "INVOKE {Orchestration}: {Topic}")]
public static partial void LogOrchestrationInvoke(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs that the orchestration has started successfully and
/// yielded control back to the caller.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "YIELD {Orchestration}: {Topic}")]
public static partial void LogOrchestrationYield(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs the start an orchestration (top/outer).
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "START {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationStart(
this ILogger logger,
string orchestration,
AgentId agentId);
/// <summary>
/// Logs that orchestration request actor is active
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "INIT {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationRequestInvoke(
this ILogger logger,
string orchestration,
AgentId agentId);
/// <summary>
/// Logs that orchestration request actor experienced an unexpected failure.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "FAILURE {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationRequestFailure(
this ILogger logger,
string orchestration,
AgentId agentId,
Exception exception);
/// <summary>
/// Logs that orchestration result actor is active
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "EXIT {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationResultInvoke(
this ILogger logger,
string orchestration,
AgentId agentId);
/// <summary>
/// Logs that orchestration result actor experienced an unexpected failure.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "FAILURE {Orchestration}: {AgentId}")]
public static partial void LogOrchestrationResultFailure(
this ILogger logger,
string orchestration,
AgentId agentId,
Exception exception);
}
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.Concurrent;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="ConcurrentOrchestration{TInput, TOutput}"/>.
/// </summary>
/// <remarks>
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
/// generate logging code at compile time to achieve optimized code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static partial class ConcurrentOrchestrationLogMessages
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "REQUEST Concurrent agent [{AgentId}]")]
public static partial void LogConcurrentAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Concurrent agent [{AgentId}]: {Message}")]
public static partial void LogConcurrentAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
/// <summary>
/// Logs result capture.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "COLLECT Concurrent result [{AgentId}]: #{ResultCount} / {ExpectedCount}")]
public static partial void LogConcurrentResultCapture(
this ILogger logger,
AgentId agentId,
int resultCount,
int expectedCount);
}
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.GroupChat;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="GroupChatOrchestration{TInput, TOutput}"/>.
/// </summary>
/// <remarks>
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
/// generate logging code at compile time to achieve optimized code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static partial class GroupChatOrchestrationLogMessages
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "CHAT AGENT invoked [{AgentId}]")]
public static partial void LogChatAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "CHAT AGENT result [{AgentId}]: {Message}")]
public static partial void LogChatAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER initialized [{AgentId}]")]
public static partial void LogChatManagerInit(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER invoked [{AgentId}]")]
public static partial void LogChatManagerInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER terminate? [{AgentId}]: {Result} ({Reason})")]
public static partial void LogChatManagerTerminate(
this ILogger logger,
AgentId agentId,
bool result,
string reason);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER select: {NextAgent} [{AgentId}]")]
public static partial void LogChatManagerSelect(
this ILogger logger,
AgentId agentId,
AgentType nextAgent);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER result [{AgentId}]: '{Result}' ({Reason})")]
public static partial void LogChatManagerResult(
this ILogger logger,
AgentId agentId,
string result,
string reason);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "CHAT MANAGER user-input? [{AgentId}]: {Result} ({Reason})")]
public static partial void LogChatManagerInput(
this ILogger logger,
AgentId agentId,
bool result,
string reason);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "CHAT AGENT user-input [{AgentId}]: {Message}")]
public static partial void LogChatManagerUserInput(
this ILogger logger,
AgentId agentId,
string? message);
}
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.Handoff;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="HandoffOrchestration{TInput, TOutput}"/>.
/// </summary>
/// <remarks>
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
/// generate logging code at compile time to achieve optimized code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static partial class HandoffOrchestrationLogMessages
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "REQUEST Handoff agent [{AgentId}]")]
public static partial void LogHandoffAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Handoff agent [{AgentId}]: {Message}")]
public static partial void LogHandoffAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "TOOL Handoff [{AgentId}]: {Name}")]
public static partial void LogHandoffFunctionCall(
this ILogger logger,
AgentId agentId,
string name);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Handoff summary [{AgentId}]: {Summary}")]
public static partial void LogHandoffSummary(
this ILogger logger,
AgentId agentId,
string? summary);
}
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="OrchestrationResult{TValue}"/>.
/// </summary>
/// <remarks>
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
/// generate logging code at compile time to achieve optimized code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static partial class OrchestrationResultLogMessages
{
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> awaiting the orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "AWAIT {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultAwait(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> timeout while awaiting the orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "TIMEOUT {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultTimeout(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> cancelled the orchestration.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Error,
Message = "CANCELLED {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultCancelled(
this ILogger logger,
string orchestration,
TopicId topic);
/// <summary>
/// Logs <see cref="OrchestrationResult{TValue}"/> the awaited the orchestration has completed.
/// </summary>
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "COMPLETE {Orchestration}: {Topic}")]
public static partial void LogOrchestrationResultComplete(
this ILogger logger,
string orchestration,
TopicId topic);
}
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.Orchestration.Sequential;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Extensions for logging <see cref="SequentialOrchestration{TInput, TOutput}"/>.
/// </summary>
/// <remarks>
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
/// generate logging code at compile time to achieve optimized code.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static partial class SequentialOrchestrationLogMessages
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "REQUEST Sequential agent [{AgentId}]")]
public static partial void LogSequentialAgentInvoke(
this ILogger logger,
AgentId agentId);
[LoggerMessage(
EventId = 0,
Level = LogLevel.Trace,
Message = "RESULT Sequential agent [{AgentId}]: {Message}")]
public static partial void LogSequentialAgentResult(
this ILogger logger,
AgentId agentId,
string? message);
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
#if !NET5_0_OR_GREATER
using System.ComponentModel;
namespace System.Runtime.CompilerServices;
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
#endif
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<RootNamespace>Microsoft.Agents.Orchestration</RootNamespace>
<VersionSuffix>alpha</VersionSuffix>
<IsAotCompatible>false</IsAotCompatible>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</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.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.Abstractions" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.Core" />
<PackageReference Include="Microsoft.SemanticKernel.Agents.Runtime.InProcess" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.Orchestration.UnitTests" />
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Base abstractions for any actor that participates in an orchestration.
/// </summary>
public abstract class OrchestrationActor : BaseAgent
{
/// <summary>
/// Initializes a new instance of the <see cref="OrchestrationActor"/> class.
/// </summary>
protected OrchestrationActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, string description, ILogger? logger = null)
: base(id, runtime, description, logger)
{
this.Context = context;
}
/// <summary>
/// The orchestration context.
/// </summary>
protected OrchestrationContext Context { get; }
/// <summary>
/// Sends a message to a specified recipient agent-type through the runtime.
/// </summary>
/// <param name="message">The message object to send.</param>
/// <param name="agentType">The recipient agent's type.</param>
/// <param name="cancellationToken">A token used to cancel the operation if needed.</param>
/// <returns>The agent identifier, if it exists.</returns>
protected async ValueTask PublishMessageAsync(
object message,
AgentType agentType,
CancellationToken cancellationToken = default)
{
await base.PublishMessageAsync(message, new TopicId(agentType), messageId: null, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Provides contextual information for an orchestration operation, including topic, cancellation, logging, and response callback.
/// </summary>
public sealed class OrchestrationContext
{
internal OrchestrationContext(
string orchestration,
TopicId topic,
OrchestrationResponseCallback? responseCallback,
OrchestrationStreamingCallback? streamingCallback,
ILoggerFactory loggerFactory,
CancellationToken cancellation)
{
this.Orchestration = orchestration;
this.Topic = topic;
this.ResponseCallback = responseCallback;
this.StreamingResponseCallback = streamingCallback;
this.LoggerFactory = loggerFactory;
this.Cancellation = cancellation;
}
/// <summary>
/// Gets the name or identifier of the orchestration.
/// </summary>
public string Orchestration { get; }
/// <summary>
/// Gets the identifier associated with orchestration topic.
/// </summary>
/// <remarks>
/// All orchestration actors are subscribed to this topic.
/// </remarks>
public TopicId Topic { get; }
/// <summary>
/// Gets the cancellation token that can be used to observe cancellation requests for the orchestration.
/// </summary>
public CancellationToken Cancellation { get; }
/// <summary>
/// Gets the associated logger factory for creating loggers within the orchestration context.
/// </summary>
public ILoggerFactory LoggerFactory { get; }
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationResponseCallback? ResponseCallback { get; }
/// <summary>
/// Optional callback that is invoked for every agent response.
/// </summary>
public OrchestrationStreamingCallback? StreamingResponseCallback { get; }
}
@@ -0,0 +1,125 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration;
/// <summary>
/// Represents the result of an orchestration operation that yields a value of type <typeparamref name="TValue"/>.
/// This class encapsulates the asynchronous completion of an orchestration process.
/// </summary>
/// <typeparam name="TValue">The type of the value produced by the orchestration.</typeparam>
public sealed class OrchestrationResult<TValue> : IDisposable
{
private readonly OrchestrationContext _context;
private readonly CancellationTokenSource _cancelSource;
private readonly TaskCompletionSource<TValue> _completion;
private readonly ILogger _logger;
private bool _isDisposed;
internal OrchestrationResult(OrchestrationContext context, TaskCompletionSource<TValue> completion, CancellationTokenSource orchestrationCancelSource, ILogger logger)
{
this._cancelSource = orchestrationCancelSource;
this._context = context;
this._completion = completion;
this._logger = logger;
}
/// <summary>
/// Releases all resources used by the <see cref="OrchestrationResult{TValue}"/> instance.
/// </summary>
public void Dispose()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Gets the orchestration name associated with this orchestration result.
/// </summary>
public string Orchestration => this._context.Orchestration;
/// <summary>
/// Gets the topic identifier associated with this orchestration result.
/// </summary>
public TopicId Topic => this._context.Topic;
/// <summary>
/// Asynchronously retrieves the orchestration result value.
/// If a timeout is specified, the method will throw a <see cref="TimeoutException"/>
/// if the orchestration does not complete within the allotted time.
/// </summary>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the maximum wait duration.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{TValue}"/> representing the result of the orchestration.</returns>
/// <exception cref="ObjectDisposedException">Thrown if this instance has been disposed.</exception>
/// <exception cref="TimeoutException">Thrown if the orchestration does not complete within the specified timeout period.</exception>
public async ValueTask<TValue> GetValueAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
{
#if !NETCOREAPP
if (this._isDisposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
#else
ObjectDisposedException.ThrowIf(this._isDisposed, this);
#endif
this._logger.LogOrchestrationResultAwait(this.Orchestration, this.Topic);
if (timeout.HasValue)
{
Task[] tasks = { this._completion.Task };
if (!Task.WaitAll(tasks, timeout.Value))
{
this._logger.LogOrchestrationResultTimeout(this.Orchestration, this.Topic);
throw new TimeoutException($"Orchestration did not complete within the allowed duration ({timeout}).");
}
}
this._logger.LogOrchestrationResultComplete(this.Orchestration, this.Topic);
return await this._completion.Task.ConfigureAwait(false);
}
/// <summary>
/// Cancel the orchestration associated with this result.
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown if this instance has been disposed.</exception>
/// <remarks>
/// Cancellation is not expected to immediately halt the orchestration. Messages that
/// are already in-flight may still be processed.
/// </remarks>
public void Cancel()
{
#if !NETCOREAPP
if (this._isDisposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
#else
ObjectDisposedException.ThrowIf(this._isDisposed, this);
#endif
this._logger.LogOrchestrationResultCancelled(this.Orchestration, this.Topic);
this._cancelSource.Cancel();
this._completion.SetCanceled();
}
private void Dispose(bool disposing)
{
if (!this._isDisposed)
{
if (disposing)
{
this._cancelSource.Dispose();
}
this._isDisposed = true;
}
}
}
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <summary>
/// An actor used with the <see cref="SequentialOrchestration{TInput,TOutput}"/>.
/// </summary>
internal sealed class SequentialActor :
AgentActor,
IHandle<SequentialMessages.Request>,
IHandle<SequentialMessages.Response>
{
private readonly AgentType _nextAgent;
/// <summary>
/// Initializes a new instance of the <see cref="SequentialActor"/> class.
/// </summary>
/// <param name="id">The unique identifier of the agent.</param>
/// <param name="runtime">The runtime associated with the agent.</param>
/// <param name="context">The orchestration context.</param>
/// <param name="agent">An <see cref="Agent"/>.</param>
/// <param name="nextAgent">The identifier of the next agent for which to handoff the result</param>
/// <param name="logger">The logger to use for the actor</param>
public SequentialActor(AgentId id, IAgentRuntime runtime, OrchestrationContext context, Agent agent, AgentType nextAgent, ILogger<SequentialActor>? logger = null)
: base(id, runtime, context, agent, logger)
{
logger?.LogInformation("ACTOR {ActorId} {NextAgent}", this.Id, nextAgent);
this._nextAgent = nextAgent;
}
/// <inheritdoc/>
public async ValueTask HandleAsync(SequentialMessages.Request item, MessageContext messageContext)
{
await this.InvokeAgentAsync(item.Messages, messageContext).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask HandleAsync(SequentialMessages.Response item, MessageContext messageContext)
{
await this.InvokeAgentAsync([item.Message], messageContext).ConfigureAwait(false);
}
private async ValueTask InvokeAgentAsync(IList<ChatMessage> input, MessageContext messageContext)
{
this.Logger.LogInformation("INVOKE {ActorId} {NextAgent}", this.Id, this._nextAgent);
this.Logger.LogSequentialAgentInvoke(this.Id);
ChatMessage response = await this.InvokeAsync(input, messageContext.CancellationToken).ConfigureAwait(false);
this.Logger.LogSequentialAgentResult(this.Id, response.Text);
await this.PublishMessageAsync(response.AsResponseMessage(), this._nextAgent).ConfigureAwait(false);
}
}
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <summary>
/// A message that describes the input task and captures results for a <see cref="SequentialOrchestration{TInput,TOutput}"/>.
/// </summary>
internal static class SequentialMessages
{
/// <summary>
/// An empty message instance as a default.
/// </summary>
public static readonly ChatMessage Empty = new();
/// <summary>
/// Represents a request containing a sequence of chat messages to be processed by the sequential orchestration.
/// </summary>
public sealed class Request
{
/// <summary>
/// The request input.
/// </summary>
public IList<ChatMessage> Messages { get; init; } = [];
}
/// <summary>
/// Represents a response containing the result message from the sequential orchestration.
/// </summary>
public sealed class Response
{
/// <summary>
/// The response message.
/// </summary>
public ChatMessage Message { get; init; } = Empty;
}
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="SequentialMessages.Request"/>.
/// </summary>
/// <param name="message">The chat message to include in the request.</param>
/// <returns>A <see cref="SequentialMessages.Request"/> containing the provided messages.</returns>
public static Request AsRequestMessage(this ChatMessage message) => new() { Messages = [message] };
/// <summary>
/// Extension method to convert a collection of <see cref="ChatMessage"/> to a <see cref="SequentialMessages.Request"/>.
/// </summary>
/// <param name="messages">The collection of chat messages to include in the request.</param>
/// <returns>A <see cref="SequentialMessages.Request"/> containing the provided messages.</returns>
public static Request AsRequestMessage(this IEnumerable<ChatMessage> messages) => new() { Messages = [.. messages] };
/// <summary>
/// Extension method to convert a <see cref="ChatMessage"/> to a <see cref="SequentialMessages.Response"/>.
/// </summary>
/// <param name="message">The chat message to include in the response.</param>
/// <returns>A <see cref="SequentialMessages.Response"/> containing the provided message.</returns>
public static Response AsResponseMessage(this ChatMessage message) => new() { Message = message };
}
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI.Agents;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <summary>
/// An orchestration that passes the input message to the first agent, and
/// then the subsequent result to the next agent, etc...
/// </summary>
public sealed class SequentialOrchestration : SequentialOrchestration<string, string>
{
/// <summary>
/// Initializes a new instance of the <see cref="SequentialOrchestration"/> class.
/// </summary>
/// <param name="members">The agents to be orchestrated.</param>
public SequentialOrchestration(params Agent[] members)
: base(members)
{
}
}
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Extensions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.Sequential;
/// <summary>
/// An orchestration that provides the input message to the first agent
/// and sequentially passes each agent result to the next agent.
/// </summary>
public class SequentialOrchestration<TInput, TOutput> : AgentOrchestration<TInput, TOutput>
{
/// <summary>
/// Initializes a new instance of the <see cref="SequentialOrchestration{TInput, TOutput}"/> class.
/// </summary>
/// <param name="agents">The agents participating in the orchestration.</param>
public SequentialOrchestration(params Agent[] agents)
: base(agents)
{
}
/// <inheritdoc />
protected override async ValueTask StartAsync(IAgentRuntime runtime, TopicId topic, IEnumerable<ChatMessage> input, AgentType? entryAgent)
{
if (!entryAgent.HasValue)
{
throw new ArgumentException("Entry agent is not defined.", nameof(entryAgent));
}
await runtime.PublishMessageAsync(input.AsRequestMessage(), entryAgent.Value).ConfigureAwait(false);
}
/// <inheritdoc />
protected override async ValueTask<AgentType?> RegisterOrchestrationAsync(IAgentRuntime runtime, OrchestrationContext context, RegistrationContext registrar, ILogger logger)
{
AgentType outputType = await registrar.RegisterResultTypeAsync<SequentialMessages.Response>(response => [response.Message]).ConfigureAwait(false);
// Each agent handsoff its result to the next agent.
AgentType nextAgent = outputType;
for (int index = this.Members.Count - 1; index >= 0; --index)
{
Agent agent = this.Members[index];
nextAgent = await RegisterAgentAsync(agent, index, nextAgent).ConfigureAwait(false);
logger.LogRegisterActor(this.OrchestrationLabel, nextAgent, "MEMBER", index + 1);
}
return nextAgent;
ValueTask<AgentType> RegisterAgentAsync(Agent agent, int index, AgentType nextAgent) =>
runtime.RegisterOrchestrationAgentAsync(
this.GetAgentType(context.Topic, index),
(agentId, runtime) =>
{
SequentialActor actor = new(agentId, runtime, context, agent, nextAgent, context.LoggerFactory.CreateLogger<SequentialActor>());
#if !NETCOREAPP
return new ValueTask<IHostableAgent>(actor);
#else
return ValueTask.FromResult<IHostableAgent>(actor);
#endif
});
}
private AgentType GetAgentType(TopicId topic, int index) => this.FormatAgentType(topic, $"Agent_{index + 1}");
}
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Transforms;
internal static class DefaultTransforms
{
public static ValueTask<IEnumerable<ChatMessage>> FromInput<TInput>(TInput input, CancellationToken cancellationToken = default)
{
#if !NETCOREAPP
return new ValueTask<IEnumerable<ChatMessage>>(TransformInput());
#else
return ValueTask.FromResult(TransformInput());
#endif
IEnumerable<ChatMessage> TransformInput() =>
input switch
{
IEnumerable<ChatMessage> messages => messages,
ChatMessage message => [message],
string text => [new ChatMessage(ChatRole.User, text)],
_ => [new ChatMessage(ChatRole.User, JsonSerializer.Serialize(input))]
};
}
public static ValueTask<TOutput> ToOutput<TOutput>(IList<ChatMessage> result, CancellationToken cancellationToken = default)
{
bool isSingleResult = result.Count == 1;
TOutput output =
GetDefaultOutput() ??
GetObjectOutput() ??
throw new InvalidOperationException($"Unable to transform output to {typeof(TOutput)}.");
return new ValueTask<TOutput>(output);
TOutput? GetObjectOutput()
{
if (!isSingleResult)
{
return default;
}
try
{
return JsonSerializer.Deserialize<TOutput>(result[0].Text);
}
catch (JsonException)
{
return default;
}
}
TOutput? GetDefaultOutput()
{
object? output = null;
if (typeof(TOutput).IsAssignableFrom(result.GetType()))
{
output = (object)result;
}
else if (isSingleResult && typeof(ChatMessage).IsAssignableFrom(typeof(TOutput)))
{
output = (object)result[0];
}
else if (isSingleResult && typeof(string) == typeof(TOutput))
{
output = result[0].Text ?? string.Empty;
}
return (TOutput?)output;
}
}
}
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.Transforms;
/// <summary>
/// Delegate for transforming an input of type <typeparamref name="TInput"/> into a collection of <see cref="ChatMessage"/>.
/// This is typically used to convert user or system input into a format suitable for chat orchestration.
/// </summary>
/// <param name="input">The input object to transform.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> containing an enumerable of <see cref="ChatMessage"/> representing the transformed input.</returns>
public delegate ValueTask<IEnumerable<ChatMessage>> OrchestrationInputTransform<TInput>(TInput input, CancellationToken cancellationToken = default);
/// <summary>
/// Delegate for transforming a <see cref="ChatMessage"/> into an output of type <typeparamref name="TOutput"/>.
/// This is typically used to convert a chat response into a desired output format.
/// </summary>
/// <param name="result">The result messages to transform.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> containing the transformed output of type <typeparamref name="TOutput"/>.</returns>
public delegate ValueTask<TOutput> OrchestrationOutputTransform<TOutput>(IList<ChatMessage> result, CancellationToken cancellationToken = default);
/// <summary>
/// Delegate for transforming the internal result message for an orchestration into a <see cref="ChatMessage"/>.
/// </summary>
/// <typeparam name="TResult">The result message type</typeparam>
/// <param name="result">The result messages</param>
/// <returns>The orchestration result as a <see cref="ChatMessage"/>.</returns>
public delegate IList<ChatMessage> OrchestrationResultTransform<TResult>(TResult result);
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Orchestration.Transforms;
/// <summary>
/// Populates the target result type <see cref="ChatMessage"/> into a structured output.
/// </summary>
/// <typeparam name="TOutput">The .NET type of the structured-output to deserialization target.</typeparam>
public sealed class StructuredOutputTransform<TOutput>
{
internal const string DefaultInstructions = "Respond with JSON that is populated by using the information in this conversation.";
private readonly IChatClient _client;
private readonly ChatOptions? _options;
/// <summary>
/// Initializes a new instance of the <see cref="StructuredOutputTransform{TOutput}"/> class.
/// </summary>
/// <param name="client">The chat completion service to use for generating responses.</param>
/// <param name="chatOptions">The prompt execution settings to use for the chat completion service.</param>
public StructuredOutputTransform(IChatClient client, ChatOptions? chatOptions = null)
{
Throw.IfNull(client, nameof(client));
this._client = client;
this._options = chatOptions;
}
/// <summary>
/// Gets or sets the instructions to be used as the system message for the chat completion.
/// </summary>
public string Instructions { get; init; } = DefaultInstructions;
/// <summary>
/// Transforms the provided <see cref="ChatMessage"/> into a strongly-typed structured output by invoking the chat completion service and deserializing the response.
/// </summary>
/// <param name="messages">The chat messages to process.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>The structured output of type <typeparamref name="TOutput"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if the response cannot be deserialized into <typeparamref name="TOutput"/>.</exception>
public async ValueTask<TOutput> TransformAsync(IList<ChatMessage> messages, CancellationToken cancellationToken = default)
{
IEnumerable<ChatMessage> input =
[
new ChatMessage(ChatRole.System, this.Instructions),
.. messages,
];
ChatResponse<TOutput> response = await this._client.GetResponseAsync<TOutput>(input, this._options, useJsonSchemaResponseFormat: true, cancellationToken).ConfigureAwait(false);
return response.Result;
}
}
+22 -11
View File
@@ -80,7 +80,7 @@ public abstract class BaseSample : TextWriter
/// <param name="message">The text of the message to be sent. Cannot be null or empty.</param>
protected void WriteUserMessage(string message)
{
this.WriteResponseOutput(new ChatResponse(new ChatMessage(ChatRole.User, message)), printUsage: false);
this.WriteMessageOutput(new ChatMessage(ChatRole.User, message));
}
/// <summary>
@@ -101,8 +101,28 @@ public abstract class BaseSample : TextWriter
}
var message = chatResponse.Messages.Last();
this.WriteMessageOutput(message);
WriteUsage();
void WriteUsage()
{
if (!(printUsage ?? true) || chatResponse.Usage is null) { return; }
UsageDetails usageDetails = chatResponse.Usage;
Console.WriteLine($" [Usage] Tokens: {usageDetails.TotalTokenCount}, Input: {usageDetails.InputTokenCount}, Output: {usageDetails.OutputTokenCount}");
}
}
/// <summary>
/// Writes the given chat message to the console.
/// </summary>
/// <param name="message">The specified message</param>
protected void WriteMessageOutput(ChatMessage message)
{
string authorExpression = message.Role == ChatRole.User ? string.Empty : FormatAuthor();
string contentExpression = string.IsNullOrWhiteSpace(chatResponse.Text) ? string.Empty : chatResponse.Text;
string contentExpression = message.Text.Trim();
bool isCode = false; //message.AdditionalProperties?.ContainsKey(OpenAIAssistantAgent.CodeInterpreterMetadataKey) ?? false;
string codeMarker = isCode ? "\n [CODE]\n" : " ";
Console.WriteLine($"\n# {message.Role}{authorExpression}:{codeMarker}{contentExpression}");
@@ -124,16 +144,7 @@ public abstract class BaseSample : TextWriter
}
}
WriteUsage(chatResponse.Usage);
string FormatAuthor() => message.AuthorName is not null ? $" - {message.AuthorName ?? " * "}" : string.Empty;
void WriteUsage(UsageDetails? usageDetails)
{
if (!(printUsage ?? true) || usageDetails is null) { return; }
Console.WriteLine($" [Usage] Tokens: {usageDetails.TotalTokenCount}, Input: {usageDetails.InputTokenCount}, Output: {usageDetails.OutputTokenCount}");
}
}
/// <summary>
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
using OpenAIClient = OpenAI.OpenAIClient;
namespace Microsoft.Shared.SampleUtilities;
/// <summary>
/// Provides a base class for orchestration samples that demonstrates agent orchestration scenarios.
/// Inherits from <see cref="BaseSample"/> and provides utility methods for creating agents, chat clients,
/// and writing responses to the console or test output.
/// </summary>
public abstract class OrchestrationSample : BaseSample
{
/// <summary>
/// This constant defines the timeout duration for result retrieval, measured in seconds.
/// </summary>
protected const int ResultTimeoutInSeconds = 30;
/// <summary>
/// Creates a new <see cref="ChatClientAgent"/> instance using the specified instructions, description, name, and functions.
/// </summary>
/// <param name="instructions">The instructions to provide to the agent.</param>
/// <param name="description">An optional description for the agent.</param>
/// <param name="name">An optional name for the agent.</param>
/// <param name="functions">A set of <see cref="AIFunction"/> instances to be used as tools by the agent.</param>
/// <returns>A new <see cref="ChatClientAgent"/> instance configured with the provided parameters.</returns>
protected ChatClientAgent CreateAgent(string instructions, string? description = null, string? name = null, params AIFunction[] functions)
{
// Get the chat client to use for the agent.
using IChatClient chatClient = CreateChatClient();
ChatClientAgentOptions options =
new()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = new() { Tools = functions, ToolMode = ChatToolMode.Auto }
};
return new ChatClientAgent(chatClient, options);
}
/// <summary>
/// Creates and configures a new <see cref="IChatClient"/> instance using the OpenAI client and test configuration.
/// </summary>
/// <returns>A configured <see cref="IChatClient"/> instance ready for use with agents.</returns>
protected IChatClient CreateChatClient()
{
return new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation()
.Build();
}
/// <summary>
/// Display the provided history.
/// </summary>
/// <param name="history">The history to display</param>
protected void DisplayHistory(IEnumerable<ChatMessage> history)
{
Console.WriteLine("\n\nORCHESTRATION HISTORY");
foreach (ChatMessage message in history)
{
this.WriteMessageOutput(message);
}
}
/// <summary>
/// Writes the provided chat response messages to the console or test output, including role and author information.
/// </summary>
/// <param name="response">An enumerable of <see cref="ChatMessage"/> objects to write.</param>
protected static void WriteResponse(IEnumerable<ChatMessage> response)
{
foreach (ChatMessage message in response)
{
if (!string.IsNullOrEmpty(message.Text))
{
System.Console.WriteLine($"\n# RESPONSE {message.Role}{(message.AuthorName is not null ? $" - {message.AuthorName}" : string.Empty)}: {message}");
}
}
}
/// <summary>
/// Writes the streamed chat response updates to the console or test output, including role and author information.
/// </summary>
/// <param name="streamedResponses">An enumerable of <see cref="ChatResponseUpdate"/> objects representing streamed responses.</param>
protected static void WriteStreamedResponse(IEnumerable<ChatResponseUpdate> streamedResponses)
{
string? authorName = null;
ChatRole? authorRole = null;
StringBuilder builder = new();
foreach (ChatResponseUpdate response in streamedResponses)
{
authorName ??= response.AuthorName;
authorRole ??= response.Role;
if (!string.IsNullOrEmpty(response.Text))
{
builder.Append($"({JsonSerializer.Serialize(response.Text)})");
}
}
if (builder.Length > 0)
{
System.Console.WriteLine($"\n# STREAMED {authorRole ?? ChatRole.Assistant}{(authorName is not null ? $" - {authorName}" : string.Empty)}: {builder}\n");
}
}
/// <summary>
/// Provides monitoring and callback functionality for orchestration scenarios, including tracking streamed responses and message history.
/// </summary>
protected sealed class OrchestrationMonitor
{
/// <summary>
/// Gets the list of streamed response updates received so far.
/// </summary>
public List<ChatResponseUpdate> StreamedResponses { get; } = [];
/// <summary>
/// Gets the list of chat messages representing the conversation history.
/// </summary>
public List<ChatMessage> History { get; } = [];
/// <summary>
/// Callback to handle a batch of chat messages, adding them to history and writing them to output.
/// </summary>
/// <param name="response">The collection of <see cref="ChatMessage"/> objects to process.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask ResponseCallback(IEnumerable<ChatMessage> response)
{
this.History.AddRange(response);
WriteResponse(response);
return new ValueTask();
}
/// <summary>
/// Callback to handle a streamed chat response update, adding it to the list and writing output if final.
/// </summary>
/// <param name="streamedResponse">The <see cref="ChatResponseUpdate"/> to process.</param>
/// <param name="isFinal">Indicates whether this is the final update in the stream.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask StreamingResultCallback(ChatResponseUpdate streamedResponse, bool isFinal)
{
this.StreamedResponses.Add(streamedResponse);
if (isFinal)
{
WriteStreamedResponse(this.StreamedResponses);
this.StreamedResponses.Clear();
}
return new ValueTask();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="BaseSample"/> class, setting up logging, configuration, and
/// optionally redirecting <see cref="System.Console"/> output to the test output.
/// </summary>
/// <remarks>This constructor initializes logging using an <see cref="XunitLogger"/> and sets up
/// configuration from multiple sources, including a JSON file, environment variables, and user secrets.
/// If <paramref name="redirectSystemConsoleOutput"/> is <see langword="true"/>, calls to <see cref="System.Console"/>
/// will be redirected to the test output provided by <paramref name="output"/>.
/// </remarks>
/// <param name="output">The <see cref="ITestOutputHelper"/> instance used to write test output.</param>
/// <param name="redirectSystemConsoleOutput">
/// A value indicating whether <see cref="System.Console"/> output should be redirected to the test output. <see langword="true"/> to redirect; otherwise, <see langword="false"/>.
/// </param>
protected OrchestrationSample(ITestOutputHelper output, bool redirectSystemConsoleOutput = true)
: base(output, redirectSystemConsoleOutput)
{
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Shared.Samples;
/// <summary>
/// Resource helper to load resources.
/// </summary>
internal static class Resources
{
private const string ResourceFolder = "Resources";
public static string Read(string fileName) => File.ReadAllText($"{ResourceFolder}/{fileName}");
}
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.Orchestration.GroupChat;
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());
}
}
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Concurrent;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
namespace Microsoft.Agents.Orchestration.UnitTest;
/// <summary>
/// Tests for the <see cref="ConcurrentOrchestration"/> class.
/// </summary>
public class ConcurrentOrchestrationTests
{
[Fact]
public async Task ConcurrentOrchestrationWithSingleAgentAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
MockAgent mockAgent1 = MockAgent.CreateWithResponse(1, "xyz");
// Act: Create and execute the orchestration
string[] response = await ExecuteOrchestrationAsync(runtime, mockAgent1);
// Assert
Assert.Equal(1, mockAgent1.InvokeCount);
Assert.Contains("xyz", response);
}
[Fact]
public async Task ConcurrentOrchestrationWithMultipleAgentsAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
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(runtime, 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(InProcessRuntime runtime, params Agent[] mockAgents)
{
// Act
await runtime.StartAsync();
ConcurrentOrchestration orchestration = new(mockAgents);
const string InitialInput = "123";
OrchestrationResult<string[]> result = await orchestration.InvokeAsync(InitialInput, runtime);
// Assert
Assert.NotNull(result);
// Act
string[] response = await result.GetValueAsync(TimeSpan.FromSeconds(20));
await runtime.RunUntilIdleAsync();
return response;
}
}
@@ -0,0 +1,201 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Transforms;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.Orchestration.UnitTest;
public class DefaultTransformsTests
{
[Fact]
public async Task FromInputAsyncWithEnumerableOfChatMessageReturnsInputAsync()
{
// Arrange
IEnumerable<ChatMessage> input =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there")
];
// Act
IEnumerable<ChatMessage> result = await DefaultTransforms.FromInput(input);
// Assert
Assert.Equal(input, result);
}
[Fact]
public async Task FromInputAsyncWithChatMessageReturnsInputAsListAsync()
{
// Arrange
ChatMessage input = new(ChatRole.User, "Hello");
// Act
IEnumerable<ChatMessage> result = await DefaultTransforms.FromInput(input);
// Assert
Assert.Single(result);
Assert.Equal(input, result.First());
}
[Fact]
public async Task FromInputAsyncWithStringInputReturnsUserChatMessageAsync()
{
// Arrange
string input = "Hello, world!";
// Act
IEnumerable<ChatMessage> result = await DefaultTransforms.FromInput(input);
// Assert
Assert.Single(result);
ChatMessage message = result.First();
Assert.Equal(ChatRole.User, message.Role);
Assert.Equal(input, message.Text);
}
[Fact]
public async Task FromInputAsyncWithObjectInputSerializesAsJsonAsync()
{
// Arrange
TestObject input = new() { Id = 1, Name = "Test" };
// Act
IEnumerable<ChatMessage> result = await DefaultTransforms.FromInput(input);
// Assert
Assert.Single(result);
ChatMessage message = result.First();
Assert.Equal(ChatRole.User, message.Role);
string expectedJson = JsonSerializer.Serialize(input);
Assert.Equal(expectedJson, message.Text);
}
[Fact]
public async Task ToOutputAsyncWithOutputTypeMatchingInputListReturnsSameListAsync()
{
// Arrange
IList<ChatMessage> input =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there")
];
// Act
IList<ChatMessage> result = await DefaultTransforms.ToOutput<IList<ChatMessage>>(input);
// Assert
Assert.Same(input, result);
}
[Fact]
public async Task ToOutputAsyncWithOutputTypeChatMessageReturnsSingleMessageAsync()
{
// Arrange
IList<ChatMessage> input =
[
new(ChatRole.User, "Hello")
];
// Act
ChatMessage result = await DefaultTransforms.ToOutput<ChatMessage>(input);
// Assert
Assert.Same(input[0], result);
}
[Fact]
public async Task ToOutputAsyncWithOutputTypeStringReturnsContentOfSingleMessageAsync()
{
// Arrange
string expected = "Hello, world!";
IList<ChatMessage> input =
[
new(ChatRole.User, expected)
];
// Act
string result = await DefaultTransforms.ToOutput<string>(input);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public async Task ToOutputAsyncWithOutputTypeDeserializableDeserializesFromContentAsync()
{
// Arrange
TestObject expected = new() { Id = 42, Name = "TestName" };
string json = JsonSerializer.Serialize(expected);
IList<ChatMessage> input =
[
new(ChatRole.User, json)
];
// Act
TestObject result = await DefaultTransforms.ToOutput<TestObject>(input);
// Assert
Assert.Equal(expected.Id, result.Id);
Assert.Equal(expected.Name, result.Name);
}
[Fact]
public async Task ToOutputAsyncWithInvalidJsonThrowsExceptionAsync()
{
// Arrange
IList<ChatMessage> input =
[
new(ChatRole.User, "Not valid JSON")
];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await DefaultTransforms.ToOutput<TestObject>(input)
);
}
[Fact]
public async Task ToOutputAsyncWithMultipleMessagesAndNonMatchingTypeThrowsExceptionAsync()
{
// Arrange
IList<ChatMessage> input =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there")
];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await DefaultTransforms.ToOutput<TestObject>(input)
);
}
[Fact]
public async Task ToOutputAsyncWithNullContentHandlesGracefullyAsync()
{
// Arrange
IList<ChatMessage> input =
[
new(ChatRole.User, (string?)null)
];
// Act
string result = await DefaultTransforms.ToOutput<string>(input);
// Assert
Assert.Equal(string.Empty, result);
}
private sealed class TestObject
{
public int Id { get; set; }
public string? Name { get; set; }
}
}
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.GroupChat;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
namespace Microsoft.Agents.Orchestration.UnitTest;
/// <summary>
/// Tests for the <see cref="GroupChatOrchestration"/> class.
/// </summary>
public class GroupChatOrchestrationTests
{
[Fact]
public async Task GroupChatOrchestrationWithSingleAgentAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz");
// Act: Create and execute the orchestration
string response = await ExecuteOrchestrationAsync(runtime, mockAgent1);
// Assert
Assert.Equal(1, mockAgent1.InvokeCount);
Assert.Equal("xyz", response);
}
[Fact]
public async Task GroupChatOrchestrationWithMultipleAgentsAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
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(runtime, 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(InProcessRuntime runtime, params Agent[] mockAgents)
{
// Act
await runtime.StartAsync();
GroupChatOrchestration orchestration = new(new RoundRobinGroupChatManager() { MaximumInvocationCount = mockAgents.Length }, mockAgents);
const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
// Assert
Assert.NotNull(result);
// Act
string response = await result.GetValueAsync(TimeSpan.FromSeconds(20));
await runtime.RunUntilIdleAsync();
return response;
}
}
@@ -0,0 +1,243 @@
// 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.Orchestration.Handoff;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
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
Agent mockAgent1 =
this.CreateMockAgent(
"Agent1",
"Test Agent",
Responses.Message("Final response"));
// Act: Create and execute the orchestration
string response = await ExecuteOrchestrationAsync(OrchestrationHandoffs.StartWith(mockAgent1), mockAgent1);
// Assert
Assert.Equal("Final response", response);
}
[Fact(Skip = "Incomplete mock responses")]
public async Task HandoffOrchestrationWithMultipleAgentsAsync()
{
// Arrange
Agent mockAgent1 =
this.CreateMockAgent(
"Agent1",
"Test Agent",
Responses.Handoff("Agent2"));
Agent mockAgent2 =
this.CreateMockAgent(
"Agent2",
"Test Agent",
Responses.Result("Final response"));
Agent mockAgent3 =
this.CreateMockAgent(
"Agent3",
"Test Agent",
Responses.Message("Wrong response"));
// Act: Create and execute the orchestration
string response = await ExecuteOrchestrationAsync(
OrchestrationHandoffs
.StartWith(mockAgent1)
.Add(mockAgent1, mockAgent2, mockAgent3),
mockAgent1,
mockAgent2,
mockAgent3);
// Assert
Assert.Equal("Final response", response);
}
private static async Task<string> ExecuteOrchestrationAsync(OrchestrationHandoffs handoffs, params Agent[] mockAgents)
{
// Arrange
await using InProcessRuntime runtime = new();
await runtime.StartAsync();
HandoffOrchestration orchestration = new(handoffs, mockAgents);
// Act
const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
// Assert
Assert.NotNull(result);
// Act
string response = await result.GetValueAsync(TimeSpan.FromSeconds(10));
await runtime.RunUntilIdleAsync();
return response;
}
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 };
ChatClientAgent mockAgent = new(chatClient, agentOptions);
return mockAgent;
}
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}}}\" }"
}
}
]
}
}
]
}
""";
}
}
@@ -0,0 +1,236 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.Orchestration.Handoff;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Moq;
namespace Microsoft.Agents.Orchestration.UnitTest;
public class HandoffsTests
{
[Fact]
public void EmptyConstructorsCreateEmptyCollections()
{
AgentHandoffs agentHandoffs = [];
Assert.Empty(agentHandoffs);
OrchestrationHandoffs orchestrationHandoffs = new("first");
Assert.Empty(orchestrationHandoffs);
Assert.Equal("first", orchestrationHandoffs.FirstAgentName);
}
[Fact]
public void DictionaryConstructorsInvalidFirstAgent()
{
Assert.Throws<ArgumentNullException>(() => new OrchestrationHandoffs((string)null!));
Assert.Throws<ArgumentException>(() => new OrchestrationHandoffs(string.Empty));
Assert.Throws<ArgumentException>(() => new OrchestrationHandoffs(" "));
}
[Fact]
public void AddWithAgentObjectsCreatesHandoffRelationships()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent1 = CreateAgent("target1", "Target Agent 1");
Agent targetAgent2 = CreateAgent("target2", "Target Agent 2");
// Act
handoffs.Add(sourceAgent, targetAgent1, targetAgent2);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
Assert.True(handoffs.ContainsKey("source"));
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Equal(2, sourceHandoffs.Count);
Assert.Equal("Target Agent 1", sourceHandoffs["target1"]);
Assert.Equal("Target Agent 2", sourceHandoffs["target2"]);
}
[Fact]
public void AddWithAgentAndCustomDescriptionUsesCustomDescription()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent = CreateAgent("target", "Target Agent");
string customDescription = "Custom handoff description";
// Act
handoffs.Add(sourceAgent, targetAgent, customDescription);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Single(sourceHandoffs);
Assert.Equal(customDescription, sourceHandoffs["target"]);
}
[Fact]
public void AddWithAgentAndTargetNameAddsHandoffWithDescription()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
string targetName = "targetName";
string description = "Target description";
// Act
handoffs.Add(sourceAgent, targetName, description);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Single(sourceHandoffs);
Assert.Equal(description, sourceHandoffs[targetName]);
}
[Fact]
public void AddWithSourceNameAndTargetNameAddsHandoffWithDescription()
{
// Arrange
OrchestrationHandoffs handoffs = new("sourceName");
string sourceName = "sourceName";
string targetName = "targetName";
string description = "Target description";
// Act
handoffs.Add(sourceName, targetName, description);
// Assert
Assert.Single(handoffs);
Assert.Equal("sourceName", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs[sourceName];
Assert.Single(sourceHandoffs);
Assert.Equal(description, sourceHandoffs[targetName]);
}
[Fact]
public void AddWithMultipleSourcesAndTargetsCreatesCorrectStructure()
{
// Arrange
OrchestrationHandoffs handoffs = new("source1");
Agent source1 = CreateAgent("source1", "Source Agent 1");
Agent source2 = CreateAgent("source2", "Source Agent 2");
Agent target1 = CreateAgent("target1", "Target Agent 1");
Agent target2 = CreateAgent("target2", "Target Agent 2");
Agent target3 = CreateAgent("target3", "Target Agent 3");
// Act
handoffs.Add(source1, target1, target2);
handoffs.Add(source2, target2, target3);
handoffs.Add(source1, target3, "Custom description");
// Assert
Assert.Equal(2, handoffs.Count);
Assert.Equal("source1", handoffs.FirstAgentName);
// Check source1's targets
AgentHandoffs source1Handoffs = handoffs["source1"];
Assert.Equal(3, source1Handoffs.Count);
Assert.Equal("Target Agent 1", source1Handoffs["target1"]);
Assert.Equal("Target Agent 2", source1Handoffs["target2"]);
Assert.Equal("Custom description", source1Handoffs["target3"]);
// Check source2's targets
AgentHandoffs source2Handoffs = handoffs["source2"];
Assert.Equal(2, source2Handoffs.Count);
Assert.Equal("Target Agent 2", source2Handoffs["target2"]);
Assert.Equal("Target Agent 3", source2Handoffs["target3"]);
}
[Fact]
public void StaticAddCreatesNewOrchestrationHandoffs()
{
// Arrange
Agent source = CreateAgent("source", "Source Agent");
Agent target1 = CreateAgent("target1", "Target Agent 1");
Agent target2 = CreateAgent("target2", "Target Agent 2");
// Act
OrchestrationHandoffs handoffs =
OrchestrationHandoffs
.StartWith(source)
.Add(source, target1, target2);
// Assert
Assert.NotNull(handoffs);
Assert.Equal(source.Id, handoffs.FirstAgentName);
Assert.Single(handoffs);
Assert.True(handoffs.ContainsKey("source"));
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Equal(2, sourceHandoffs.Count);
Assert.Equal("Target Agent 1", sourceHandoffs["target1"]);
Assert.Equal("Target Agent 2", sourceHandoffs["target2"]);
}
[Fact]
public void AddWithAgentsWithNoNameUsesId()
{
// Arrange
OrchestrationHandoffs handoffs = new("source-id");
Agent sourceAgent = CreateAgent(id: "source-id", name: null);
Agent targetAgent = CreateAgent(id: "target-id", name: null, description: "Target Description");
// Act
handoffs.Add(sourceAgent, targetAgent);
// Assert
Assert.Single(handoffs);
Assert.Equal("source-id", handoffs.FirstAgentName);
Assert.True(handoffs.ContainsKey("source-id"));
AgentHandoffs sourceHandoffs = handoffs["source-id"];
Assert.Single(sourceHandoffs);
Assert.Equal("Target Description", sourceHandoffs["target-id"]);
}
[Fact]
public void AddWithTargetWithNoDescriptionUsesEmptyString()
{
// Arrange
OrchestrationHandoffs handoffs = new("source");
Agent sourceAgent = CreateAgent("source", "Source Agent");
Agent targetAgent = CreateAgent("target", null);
// Act
handoffs.Add(sourceAgent, targetAgent);
// Assert
Assert.Single(handoffs);
Assert.Equal("source", handoffs.FirstAgentName);
AgentHandoffs sourceHandoffs = handoffs["source"];
Assert.Single(sourceHandoffs);
Assert.Equal(string.Empty, sourceHandoffs["target"]);
}
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,
};
ChatClientAgent mockAgent = new(mockClient.Object, options);
return mockAgent;
}
}
@@ -0,0 +1,18 @@
// 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)
{
return Task.FromResult(this.ResponseQueue.Dequeue());
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.Orchestration\Microsoft.Agents.Orchestration.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
</Project>
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Moq;
namespace Microsoft.Agents.Orchestration.UnitTest;
/// <summary>
/// Mock definition of <see cref="Agent"/>.
/// </summary>
internal sealed class MockAgent(int index) : Agent
{
public static MockAgent CreateWithResponse(int index, string response)
{
return 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()
{
return new AgentThread() { Id = Guid.NewGuid().ToString() };
}
public override async Task<ChatResponse> RunAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
this.InvokeCount++;
if (thread == null)
{
Mock<AgentThread> mockThread = new(MockBehavior.Strict);
thread = mockThread.Object;
}
await (options?.OnIntermediateMessages?.Invoke(this.Response) ?? Task.CompletedTask);
return new ChatResponse(messages: [.. this.Response]);
}
public override async IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(IReadOnlyCollection<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
this.InvokeCount++;
await (options?.OnIntermediateMessages?.Invoke(this.Response) ?? Task.CompletedTask);
foreach (ChatMessage message in this.Response)
{
yield return new ChatResponseUpdate(message.Role, message.Text);
}
}
}
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Agents.Runtime;
namespace Microsoft.Agents.Orchestration.UnitTest;
public class OrchestrationResultTests
{
[Fact]
public void ConstructorInitializesPropertiesCorrectly()
{
// Arrange
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
TaskCompletionSource<string> tcs = new();
// Act
using CancellationTokenSource cancelSource = new();
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
// Assert
Assert.Equal("TestOrchestration", result.Orchestration);
Assert.Equal(new TopicId("testTopic"), result.Topic);
}
[Fact]
public async Task GetValueAsyncReturnsCompletedValueWhenTaskIsCompletedAsync()
{
// Arrange
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
TaskCompletionSource<string> tcs = new();
using CancellationTokenSource cancelSource = new();
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
string expectedValue = "Result value";
// Act
tcs.SetResult(expectedValue);
string actualValue = await result.GetValueAsync();
// Assert
Assert.Equal(expectedValue, actualValue);
}
[Fact]
public async Task GetValueAsyncWithTimeoutReturnsCompletedValueWhenTaskCompletesWithinTimeoutAsync()
{
// Arrange
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
TaskCompletionSource<string> tcs = new();
using CancellationTokenSource cancelSource = new();
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
string expectedValue = "Result value";
TimeSpan timeout = TimeSpan.FromSeconds(1);
// Act
tcs.SetResult(expectedValue);
string actualValue = await result.GetValueAsync(timeout);
// Assert
Assert.Equal(expectedValue, actualValue);
}
[Fact]
public async Task GetValueAsyncWithTimeoutThrowsTimeoutExceptionWhenTaskDoesNotCompleteWithinTimeoutAsync()
{
// Arrange
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
TaskCompletionSource<string> tcs = new();
using CancellationTokenSource cancelSource = new();
using OrchestrationResult<string> result = new(context, tcs, cancelSource, NullLogger.Instance);
TimeSpan timeout = TimeSpan.FromMilliseconds(50);
// Act & Assert
TimeoutException exception = await Assert.ThrowsAsync<TimeoutException>(() => result.GetValueAsync(timeout).AsTask());
Assert.Contains("Orchestration did not complete within the allowed duration", exception.Message);
}
[Fact]
public async Task GetValueAsyncReturnsCompletedValueWhenCompletionIsDelayedAsync()
{
// Arrange
OrchestrationContext context = new("TestOrchestration", new TopicId("testTopic"), null, null, NullLoggerFactory.Instance, CancellationToken.None);
TaskCompletionSource<int> tcs = new();
using CancellationTokenSource cancelSource = new();
using OrchestrationResult<int> result = new(context, tcs, cancelSource, NullLogger.Instance);
int expectedValue = 42;
// Act
// Simulate delayed completion in a separate task
Task delayTask = Task.Run(async () =>
{
await Task.Delay(100);
tcs.SetResult(expectedValue);
});
int actualValue = await result.GetValueAsync();
// Assert
Assert.Equal(expectedValue, actualValue);
}
}
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading.Tasks;
using Microsoft.Agents.Orchestration.Sequential;
using Microsoft.Extensions.AI.Agents;
using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
namespace Microsoft.Agents.Orchestration.UnitTest;
/// <summary>
/// Tests for the <see cref="SequentialOrchestration"/> class.
/// </summary>
public class SequentialOrchestrationTests
{
[Fact]
public async Task SequentialOrchestrationWithSingleAgentAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
MockAgent mockAgent1 = MockAgent.CreateWithResponse(2, "xyz");
// Act: Create and execute the orchestration
string response = await ExecuteOrchestrationAsync(runtime, mockAgent1);
// Assert
Assert.Equal(1, mockAgent1.InvokeCount);
Assert.Equal("xyz", response);
}
[Fact]
public async Task SequentialOrchestrationWithMultipleAgentsAsync()
{
// Arrange
await using InProcessRuntime runtime = new();
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(runtime, 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(InProcessRuntime runtime, params Agent[] mockAgents)
{
// Act
await runtime.StartAsync();
SequentialOrchestration orchestration = new(mockAgents);
const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
// Assert
Assert.NotNull(result);
// Act
string response = await result.GetValueAsync(TimeSpan.FromSeconds(20));
await runtime.RunUntilIdleAsync();
return response;
}
}