Files
agent-framework/dotnet/tests/Microsoft.Agents.Orchestration.UnitTests/HandoffOrchestrationTests.cs
T
Stephen ToubandGitHub 456e7d1b65 Round 2 of cleanup for agent runtime and orchestrations (#164)
- Moved InProcessRuntime type into abstractions package and deleted InProcess package.
- Moved several members of IAgentRuntime to be extension methods instead, e.g. multiple GetActorAsync overloads.
- Added synchronous RegisterMessageHandler overloads and used them to avoid unnecessary async usage at call sites.
- Removed unnecessary surface area from InProcessRuntime, e.g. StopAsync, RunUntilIdleAsync, etc.
- Fixed spin loop in InProcessRuntime that would consume an entire core for the duration of the orchestration's operation.
- Removed a bunch of allocation from InProcessRuntime.
- Made a runtime optional for orchestrations, defaulting to using a temporary InProcessRuntime if none is provided.
- Removed custom delegate types from orchestrations.
- Consolidated namespaces.
- Used records to simplify message classes.
- Tweaked naming on AgentActor to make purpose of protected methods more clear.
- Removed invocation in AgentActor.InvokeAsync of empty update / isFinal parameter.
- Changed OrchestrationHandoffs to avoid needing to pass in agents duplicatively.
- Made various extension methods, such as those on OrchestrationHandoffsExtensions, into instance methods.
2025-07-11 11:12:18 -04:00

233 lines
7.0 KiB
C#

// 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.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
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));
// 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));
// Assert
Assert.Equal("Final response", response);
}
private static async Task<string> ExecuteOrchestrationAsync(OrchestrationHandoffs handoffs)
{
// Arrange
HandoffOrchestration orchestration = new(handoffs);
// Act
const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput);
// Assert
Assert.NotNull(result);
// Act
return await result.Task;
}
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}}}\" }"
}
}
]
}
}
]
}
""";
}
}