.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 10:04:34 -07:00
committed by GitHub
Unverified
parent 35c938fb5b
commit 7c8ec5ec19
71 changed files with 5675 additions and 131 deletions
+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}");
}