.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
@@ -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();
}
}