.NET: Move SO agent to samples (#3820)

* move SO agent to samples

* change file encoding

* fix files encoding
This commit is contained in:
SergeyMenshykh
2026-02-11 11:11:30 +00:00
committed by GitHub
Unverified
parent 04abdbdc5d
commit 6dda25c499
7 changed files with 21 additions and 742 deletions
@@ -1,16 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
namespace SampleApp;
/// <summary>
/// Provides extension methods for adding structured output capabilities to <see cref="AIAgentBuilder"/> instances.
/// </summary>
public static class AIAgentBuilderExtensions
internal static class AIAgentBuilderExtensions
{
/// <summary>
/// Adds structured output capabilities to the agent pipeline, enabling conversion of text responses to structured JSON format.
@@ -35,12 +34,16 @@ public static class AIAgentBuilderExtensions
public static AIAgentBuilder UseStructuredOutput(
this AIAgentBuilder builder,
IChatClient? chatClient = null,
Func<StructuredOutputAgentOptions>? optionsFactory = null) =>
Throw.IfNull(builder).Use((innerAgent, services) =>
Func<StructuredOutputAgentOptions>? optionsFactory = null)
{
ArgumentNullException.ThrowIfNull(builder);
return builder.Use((innerAgent, services) =>
{
chatClient ??= services?.GetService<IChatClient>()
?? throw new InvalidOperationException($"No {nameof(IChatClient)} was provided and none could be resolved from the service provider. Either provide an {nameof(IChatClient)} explicitly or register one in the dependency injection container.");
return new StructuredOutputAgent(innerAgent, chatClient, optionsFactory?.Invoke());
});
}
}
@@ -1,13 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
namespace SampleApp;
/// <summary>
/// A delegating AI agent that converts text responses from an inner AI agent into structured output using a chat client.
@@ -37,7 +33,7 @@ internal sealed class StructuredOutputAgent : DelegatingAIAgent
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient, StructuredOutputAgentOptions? options = null)
: base(innerAgent)
{
this._chatClient = Throw.IfNull(chatClient);
this._chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient));
this._agentOptions = options;
}
@@ -1,13 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
namespace SampleApp;
/// <summary>
/// Represents configuration options for a <see cref="StructuredOutputAgent"/>.
/// </summary>
public sealed class StructuredOutputAgentOptions
#pragma warning disable CA1812 // Instantiated via AIAgentBuilderExtensions.UseStructuredOutput optionsFactory parameter
internal sealed class StructuredOutputAgentOptions
#pragma warning restore CA1812
{
/// <summary>
/// Gets or sets the system message to use when invoking the chat client for structured output conversion.
@@ -1,21 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
namespace SampleApp;
/// <summary>
/// Represents an agent response that contains structured output and
/// the original agent response from which the structured output was generated.
/// </summary>
public class StructuredOutputAgentResponse : AgentResponse
internal sealed class StructuredOutputAgentResponse : AgentResponse
{
/// <summary>
/// Initializes a new instance of the <see cref="StructuredOutputAgentResponse"/> class.
/// </summary>
/// <param name="chatResponse">The <see cref="ChatResponse"/> containing the structured output.</param>
/// <param name="agentResponse">The original <see cref="AgentResponse"/> from the inner agent.</param>
internal StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
public StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
{
this.OriginalResponse = agentResponse;
}
@@ -1,110 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AIAgentBuilderExtensions"/> class.
/// </summary>
public sealed class AIAgentBuilderExtensionsTests
{
private readonly Mock<IChatClient> _chatClientMock;
private readonly TestAIAgent _innerAgent;
public AIAgentBuilderExtensionsTests()
{
this._chatClientMock = new Mock<IChatClient>();
this._innerAgent = new TestAIAgent();
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "{\"result\": \"test\"}")]));
}
[Fact]
public void UseStructuredOutput_WithNullBuilder_ThrowsArgumentNullException()
{
// Arrange
AIAgentBuilder builder = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>("builder", () =>
builder.UseStructuredOutput(this._chatClientMock.Object));
}
[Fact]
public void UseStructuredOutput_WithExplicitChatClient_BuildsStructuredOutputAgent()
{
// Arrange
AIAgentBuilder builder = this._innerAgent.AsBuilder();
// Act
AIAgent agent = builder.UseStructuredOutput(this._chatClientMock.Object).Build();
// Assert
Assert.IsType<StructuredOutputAgent>(agent);
}
[Fact]
public void UseStructuredOutput_WithNoChatClientParameter_ResolvesChatClientFromServices()
{
// Arrange
ServiceCollection services = new();
services.AddSingleton(this._chatClientMock.Object);
ServiceProvider serviceProvider = services.BuildServiceProvider();
AIAgentBuilder builder = this._innerAgent.AsBuilder();
// Act
AIAgent agent = builder.UseStructuredOutput().Build(serviceProvider);
// Assert
Assert.IsType<StructuredOutputAgent>(agent);
}
[Fact]
public void UseStructuredOutput_WithNoChatClientAvailable_ThrowsInvalidOperationException()
{
// Arrange
AIAgentBuilder builder = this._innerAgent.AsBuilder();
// Act & Assert
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(() =>
builder.UseStructuredOutput().Build(services: null));
Assert.Contains("IChatClient", exception.Message);
}
[Fact]
public void UseStructuredOutput_WithOptionsFactory_AppliesConfiguration()
{
// Arrange
AIAgentBuilder builder = this._innerAgent.AsBuilder();
bool factoryInvoked = false;
// Act
AIAgent agent = builder.UseStructuredOutput(
this._chatClientMock.Object,
() =>
{
factoryInvoked = true;
return new StructuredOutputAgentOptions
{
ChatClientSystemMessage = "Custom system message"
};
}).Build();
// Assert
Assert.True(factoryInvoked);
Assert.IsType<StructuredOutputAgent>(agent);
}
}
@@ -1,74 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="StructuredOutputAgentResponse"/> class.
/// </summary>
public sealed class StructuredOutputAgentResponseTests
{
[Fact]
public void Constructor_WithValidParameters_SetsOriginalResponse()
{
// Arrange
ChatResponse chatResponse = new([new ChatMessage(ChatRole.Assistant, "Structured output")]);
AgentResponse originalResponse = new([new ChatMessage(ChatRole.Assistant, "Original response")]);
// Act
StructuredOutputAgentResponse structuredResponse = new(chatResponse, originalResponse);
// Assert
Assert.Same(originalResponse, structuredResponse.OriginalResponse);
}
[Fact]
public void Constructor_WithValidParameters_InheritsFromAgentResponse()
{
// Arrange
ChatResponse chatResponse = new([new ChatMessage(ChatRole.Assistant, "Structured output")]);
AgentResponse originalResponse = new([new ChatMessage(ChatRole.Assistant, "Original response")]);
// Act
StructuredOutputAgentResponse structuredResponse = new(chatResponse, originalResponse);
// Assert
Assert.IsAssignableFrom<AgentResponse>(structuredResponse);
}
[Fact]
public void OriginalResponse_ReturnsCorrectAgentResponse()
{
// Arrange
ChatResponse chatResponse = new([new ChatMessage(ChatRole.Assistant, "Structured output")]);
AgentResponse originalResponse = new([new ChatMessage(ChatRole.Assistant, "Original response")])
{
AgentId = "agent-1",
ResponseId = "original-response-123"
};
// Act
StructuredOutputAgentResponse structuredResponse = new(chatResponse, originalResponse);
// Assert
Assert.Same(originalResponse, structuredResponse.OriginalResponse);
Assert.Equal("agent-1", structuredResponse.OriginalResponse.AgentId);
Assert.Equal("original-response-123", structuredResponse.OriginalResponse.ResponseId);
}
[Fact]
public void Text_ReturnsStructuredOutputText()
{
// Arrange
const string StructuredJson = "{\"name\": \"Test\", \"value\": 42}";
ChatResponse chatResponse = new([new ChatMessage(ChatRole.Assistant, StructuredJson)]);
AgentResponse originalResponse = new([new ChatMessage(ChatRole.Assistant, "Original text response")]);
// Act
StructuredOutputAgentResponse structuredResponse = new(chatResponse, originalResponse);
// Assert
Assert.Equal(StructuredJson, structuredResponse.Text);
}
}
@@ -1,540 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="StructuredOutputAgent"/> class.
/// </summary>
public sealed class StructuredOutputAgentTests
{
private readonly Mock<IChatClient> _chatClientMock;
private readonly TestAIAgent _innerAgent;
private readonly List<ChatMessage> _testMessages;
private readonly AgentSession _testSession;
private readonly AgentResponse _innerAgentResponse;
private readonly ChatResponse _chatClientResponse;
public StructuredOutputAgentTests()
{
this._chatClientMock = new Mock<IChatClient>();
this._innerAgent = new TestAIAgent();
this._testMessages = [new ChatMessage(ChatRole.User, "Test message")];
this._testSession = new Mock<AgentSession>().Object;
this._innerAgentResponse = new AgentResponse([new ChatMessage(ChatRole.Assistant, "Inner agent response text")]);
this._chatClientResponse = new ChatResponse([new ChatMessage(ChatRole.Assistant, "{\"result\": \"structured output\"}")]);
this._innerAgent.RunAsyncFunc = (_, _, _, _) => Task.FromResult(this._innerAgentResponse);
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(this._chatClientResponse);
}
#region Constructor Tests
[Fact]
public void Constructor_WithNullInnerAgent_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>("innerAgent", () =>
new StructuredOutputAgent(null!, this._chatClientMock.Object));
}
[Fact]
public void Constructor_WithNullChatClient_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>("chatClient", () =>
new StructuredOutputAgent(this._innerAgent, null!));
}
[Fact]
public void Constructor_WithValidParameters_Succeeds()
{
// Act
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
// Assert
Assert.NotNull(agent);
}
[Fact]
public void Constructor_WithValidParametersAndOptions_Succeeds()
{
// Arrange
StructuredOutputAgentOptions options = new()
{
ChatClientSystemMessage = "Custom system message",
ChatOptions = new ChatOptions()
};
// Act
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, options);
// Assert
Assert.NotNull(agent);
}
#endregion
#region RunAsync Tests - Response Format Validation
[Fact]
public async Task RunAsync_WithNoResponseFormat_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
// Act & Assert
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => agent.RunAsync(this._testMessages, this._testSession, options: null));
Assert.Contains("ChatResponseFormatJson", exception.Message);
Assert.Contains("none was specified", exception.Message);
}
[Fact]
public async Task RunAsync_WithTextResponseFormat_ThrowsNotSupportedExceptionAsync()
{
// Arrange
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = ChatResponseFormat.Text };
// Act & Assert
NotSupportedException exception = await Assert.ThrowsAsync<NotSupportedException>(
() => agent.RunAsync(this._testMessages, this._testSession, runOptions));
Assert.Contains("ChatResponseFormatJson", exception.Message);
}
[Fact]
public async Task RunAsync_WithJsonResponseFormatInRunOptions_SucceedsAsync()
{
// Arrange
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act
AgentResponse result = await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.NotNull(result);
Assert.IsType<StructuredOutputAgentResponse>(result);
}
[Fact]
public async Task RunAsync_WithJsonResponseFormatInAgentOptions_SucceedsAsync()
{
// Arrange
StructuredOutputAgentOptions agentOptions = new()
{
ChatOptions = new ChatOptions { ResponseFormat = CreateJsonResponseFormat() }
};
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, agentOptions);
// Act
AgentResponse result = await agent.RunAsync(this._testMessages, this._testSession, options: null);
// Assert
Assert.NotNull(result);
Assert.IsType<StructuredOutputAgentResponse>(result);
}
[Fact]
public async Task RunAsync_RunOptionsResponseFormatTakesPrecedenceOverAgentOptions_UsesRunOptionsFormatAsync()
{
// Arrange
ChatResponseFormatJson agentOptionsFormat = CreateJsonResponseFormat();
ChatResponseFormatJson runOptionsFormat = CreateJsonResponseFormat();
StructuredOutputAgentOptions agentOptions = new()
{
ChatOptions = new ChatOptions { ResponseFormat = agentOptionsFormat }
};
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, agentOptions);
AgentRunOptions runOptions = new() { ResponseFormat = runOptionsFormat };
ChatOptions? capturedChatOptions = null;
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, options, _) =>
capturedChatOptions = options)
.ReturnsAsync(this._chatClientResponse);
// Act
await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Same(runOptionsFormat, capturedChatOptions.ResponseFormat);
}
#endregion
#region RunAsync Tests - Inner Agent Invocation
[Fact]
public async Task RunAsync_InvokesInnerAgentWithCorrectParametersAsync()
{
// Arrange
IEnumerable<ChatMessage>? capturedMessages = null;
AgentSession? capturedSession = null;
AgentRunOptions? capturedOptions = null;
CancellationToken capturedCancellationToken = default;
using CancellationTokenSource cts = new();
CancellationToken expectedToken = cts.Token;
this._innerAgent.RunAsyncFunc = (messages, session, options, cancellationToken) =>
{
capturedMessages = messages;
capturedSession = session;
capturedOptions = options;
capturedCancellationToken = cancellationToken;
return Task.FromResult(this._innerAgentResponse);
};
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
// Act
await agent.RunAsync(this._testMessages, this._testSession, runOptions, expectedToken);
// Assert
Assert.Same(this._testMessages, capturedMessages);
Assert.Same(this._testSession, capturedSession);
Assert.Same(runOptions, capturedOptions);
Assert.Equal(expectedToken, capturedCancellationToken);
}
#endregion
#region RunAsync Tests - Chat Client Invocation
[Fact]
public async Task RunAsync_WithoutSystemMessage_SendsOnlyUserMessageToChatClientAsync()
{
// Arrange
IEnumerable<ChatMessage>? capturedMessages = null;
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((messages, _, _) =>
capturedMessages = messages)
.ReturnsAsync(this._chatClientResponse);
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act
await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.NotNull(capturedMessages);
List<ChatMessage> messageList = [.. capturedMessages];
Assert.Single(messageList);
Assert.Equal(ChatRole.User, messageList[0].Role);
Assert.Equal(this._innerAgentResponse.Text, messageList[0].Text);
}
[Fact]
public async Task RunAsync_WithSystemMessage_SendsSystemAndUserMessagesToChatClientAsync()
{
// Arrange
const string CustomSystemMessage = "Custom conversion instruction";
IEnumerable<ChatMessage>? capturedMessages = null;
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((messages, _, _) =>
capturedMessages = messages)
.ReturnsAsync(this._chatClientResponse);
StructuredOutputAgentOptions agentOptions = new()
{
ChatClientSystemMessage = CustomSystemMessage,
ChatOptions = new ChatOptions { ResponseFormat = CreateJsonResponseFormat() }
};
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, agentOptions);
// Act
await agent.RunAsync(this._testMessages, this._testSession, options: null);
// Assert
Assert.NotNull(capturedMessages);
List<ChatMessage> messageList = [.. capturedMessages];
Assert.Equal(2, messageList.Count);
Assert.Equal(ChatRole.System, messageList[0].Role);
Assert.Equal(CustomSystemMessage, messageList[0].Text);
Assert.Equal(ChatRole.User, messageList[1].Role);
Assert.Equal(this._innerAgentResponse.Text, messageList[1].Text);
}
[Fact]
public async Task RunAsync_PassesCancellationTokenToChatClientAsync()
{
// Arrange
CancellationToken capturedToken = default;
using CancellationTokenSource cts = new();
CancellationToken expectedToken = cts.Token;
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, _, cancellationToken) =>
capturedToken = cancellationToken)
.ReturnsAsync(this._chatClientResponse);
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act
await agent.RunAsync(this._testMessages, this._testSession, runOptions, expectedToken);
// Assert
Assert.Equal(expectedToken, capturedToken);
}
[Fact]
public async Task RunAsync_ClonesChatOptionsFromAgentOptionsAsync()
{
// Arrange
const string ModelId = "test-model";
ChatOptions? capturedChatOptions = null;
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, options, _) =>
capturedChatOptions = options)
.ReturnsAsync(this._chatClientResponse);
ChatOptions originalChatOptions = new()
{
ResponseFormat = CreateJsonResponseFormat(),
ModelId = ModelId
};
StructuredOutputAgentOptions agentOptions = new()
{
ChatOptions = originalChatOptions
};
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, agentOptions);
// Act
await agent.RunAsync(this._testMessages, this._testSession, options: null);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotSame(originalChatOptions, capturedChatOptions);
Assert.Equal(ModelId, capturedChatOptions.ModelId);
}
#endregion
#region RunAsync Tests - Response
[Fact]
public async Task RunAsync_ReturnsStructuredOutputAgentResponseAsync()
{
// Arrange
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act
AgentResponse result = await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.IsType<StructuredOutputAgentResponse>(result);
}
[Fact]
public async Task RunAsync_StructuredOutputResponseContainsOriginalResponseAsync()
{
// Arrange
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act
AgentResponse result = await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
StructuredOutputAgentResponse structuredResponse = Assert.IsType<StructuredOutputAgentResponse>(result);
Assert.Same(this._innerAgentResponse, structuredResponse.OriginalResponse);
}
[Fact]
public async Task RunAsync_StructuredOutputResponseContainsChatClientResponseDataAsync()
{
// Arrange
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act
AgentResponse result = await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.Equal("{\"result\": \"structured output\"}", result.Text);
}
#endregion
#region Error Handling Tests
[Fact]
public async Task RunAsync_InnerAgentThrowsException_PropagatesExceptionAsync()
{
// Arrange
InvalidOperationException expectedException = new("Inner agent error");
this._innerAgent.RunAsyncFunc = (_, _, _, _) => throw expectedException;
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act & Assert
InvalidOperationException actualException = await Assert.ThrowsAsync<InvalidOperationException>(
() => agent.RunAsync(this._testMessages, this._testSession, runOptions));
Assert.Same(expectedException, actualException);
}
[Fact]
public async Task RunAsync_ChatClientThrowsException_PropagatesExceptionAsync()
{
// Arrange
InvalidOperationException expectedException = new("Chat client error");
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(expectedException);
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act & Assert
InvalidOperationException actualException = await Assert.ThrowsAsync<InvalidOperationException>(
() => agent.RunAsync(this._testMessages, this._testSession, runOptions));
Assert.Same(expectedException, actualException);
}
[Fact]
public async Task RunAsync_CancellationRequested_ThrowsOperationCanceledExceptionAsync()
{
// Arrange
using CancellationTokenSource cts = new();
cts.Cancel();
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new OperationCanceledException());
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => agent.RunAsync(this._testMessages, this._testSession, runOptions, cts.Token));
}
#endregion
#region ChatOptions Creation Tests
[Fact]
public async Task RunAsync_CreatesNewChatOptionsWhenAgentOptionsIsNullAsync()
{
// Arrange
ChatOptions? capturedChatOptions = null;
ChatResponseFormatJson expectedFormat = CreateJsonResponseFormat();
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, options, _) =>
capturedChatOptions = options)
.ReturnsAsync(this._chatClientResponse);
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, options: null);
AgentRunOptions runOptions = new() { ResponseFormat = expectedFormat };
// Act
await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Same(expectedFormat, capturedChatOptions.ResponseFormat);
}
[Fact]
public async Task RunAsync_CreatesNewChatOptionsWhenAgentOptionsChatOptionsIsNullAsync()
{
// Arrange
ChatOptions? capturedChatOptions = null;
ChatResponseFormatJson expectedFormat = CreateJsonResponseFormat();
this._chatClientMock
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, options, _) =>
capturedChatOptions = options)
.ReturnsAsync(this._chatClientResponse);
StructuredOutputAgentOptions agentOptions = new() { ChatOptions = null };
StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object, agentOptions);
AgentRunOptions runOptions = new() { ResponseFormat = expectedFormat };
// Act
await agent.RunAsync(this._testMessages, this._testSession, runOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Same(expectedFormat, capturedChatOptions.ResponseFormat);
}
#endregion
private static ChatResponseFormatJson CreateJsonResponseFormat()
{
// Create a simple JSON schema for testing
const string SchemaJson = """{"type":"object","properties":{"result":{"type":"string"}},"required":["result"]}""";
using JsonDocument doc = JsonDocument.Parse(SchemaJson);
return ChatResponseFormat.ForJsonSchema(
doc.RootElement.Clone(),
schemaName: "TestSchema",
schemaDescription: "Test schema for unit tests");
}
}