diff --git a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/AIAgentBuilderExtensions.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/AIAgentBuilderExtensions.cs
similarity index 90%
rename from dotnet/src/Microsoft.Agents.AI/StructuredOutput/AIAgentBuilderExtensions.cs
rename to dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/AIAgentBuilderExtensions.cs
index 8163f1d88f..987869e175 100644
--- a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/AIAgentBuilderExtensions.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/AIAgentBuilderExtensions.cs
@@ -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;
///
/// Provides extension methods for adding structured output capabilities to instances.
///
-public static class AIAgentBuilderExtensions
+internal static class AIAgentBuilderExtensions
{
///
/// 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? optionsFactory = null) =>
- Throw.IfNull(builder).Use((innerAgent, services) =>
+ Func? optionsFactory = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.Use((innerAgent, services) =>
{
chatClient ??= services?.GetService()
?? 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());
});
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgent.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgent.cs
similarity index 94%
rename from dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgent.cs
rename to dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgent.cs
index da22b3a6eb..641e0adfc4 100644
--- a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgent.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgent.cs
@@ -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;
///
/// 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;
}
diff --git a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgentOptions.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgentOptions.cs
similarity index 81%
rename from dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgentOptions.cs
rename to dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgentOptions.cs
index a03bba5a30..c5613d2015 100644
--- a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgentOptions.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgentOptions.cs
@@ -1,13 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
-namespace Microsoft.Agents.AI;
+namespace SampleApp;
///
/// Represents configuration options for a .
///
-public sealed class StructuredOutputAgentOptions
+#pragma warning disable CA1812 // Instantiated via AIAgentBuilderExtensions.UseStructuredOutput optionsFactory parameter
+internal sealed class StructuredOutputAgentOptions
+#pragma warning restore CA1812
{
///
/// Gets or sets the system message to use when invoking the chat client for structured output conversion.
diff --git a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgentResponse.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgentResponse.cs
similarity index 79%
rename from dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgentResponse.cs
rename to dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgentResponse.cs
index b23df080d7..c903b9f3ca 100644
--- a/dotnet/src/Microsoft.Agents.AI/StructuredOutput/StructuredOutputAgentResponse.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/StructuredOutputAgentResponse.cs
@@ -1,21 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
-namespace Microsoft.Agents.AI;
+namespace SampleApp;
///
/// Represents an agent response that contains structured output and
/// the original agent response from which the structured output was generated.
///
-public class StructuredOutputAgentResponse : AgentResponse
+internal sealed class StructuredOutputAgentResponse : AgentResponse
{
///
/// Initializes a new instance of the class.
///
/// The containing the structured output.
/// The original from the inner agent.
- internal StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
+ public StructuredOutputAgentResponse(ChatResponse chatResponse, AgentResponse agentResponse) : base(chatResponse)
{
this.OriginalResponse = agentResponse;
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/AIAgentBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/AIAgentBuilderExtensionsTests.cs
deleted file mode 100644
index 8d00cd1800..0000000000
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/AIAgentBuilderExtensionsTests.cs
+++ /dev/null
@@ -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;
-
-///
-/// Unit tests for the class.
-///
-public sealed class AIAgentBuilderExtensionsTests
-{
- private readonly Mock _chatClientMock;
- private readonly TestAIAgent _innerAgent;
-
- public AIAgentBuilderExtensionsTests()
- {
- this._chatClientMock = new Mock();
- this._innerAgent = new TestAIAgent();
-
- this._chatClientMock
- .Setup(c => c.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "{\"result\": \"test\"}")]));
- }
-
- [Fact]
- public void UseStructuredOutput_WithNullBuilder_ThrowsArgumentNullException()
- {
- // Arrange
- AIAgentBuilder builder = null!;
-
- // Act & Assert
- Assert.Throws("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(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(agent);
- }
-
- [Fact]
- public void UseStructuredOutput_WithNoChatClientAvailable_ThrowsInvalidOperationException()
- {
- // Arrange
- AIAgentBuilder builder = this._innerAgent.AsBuilder();
-
- // Act & Assert
- InvalidOperationException exception = Assert.Throws(() =>
- 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(agent);
- }
-}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/StructuredOutputAgentResponseTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/StructuredOutputAgentResponseTests.cs
deleted file mode 100644
index c38cf7de48..0000000000
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/StructuredOutputAgentResponseTests.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Extensions.AI;
-
-namespace Microsoft.Agents.AI.UnitTests;
-
-///
-/// Unit tests for the class.
-///
-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(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);
- }
-}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/StructuredOutputAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/StructuredOutputAgentTests.cs
deleted file mode 100644
index 088345e334..0000000000
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/StructuredOutput/StructuredOutputAgentTests.cs
+++ /dev/null
@@ -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;
-
-///
-/// Unit tests for the class.
-///
-public sealed class StructuredOutputAgentTests
-{
- private readonly Mock _chatClientMock;
- private readonly TestAIAgent _innerAgent;
- private readonly List _testMessages;
- private readonly AgentSession _testSession;
- private readonly AgentResponse _innerAgentResponse;
- private readonly ChatResponse _chatClientResponse;
-
- public StructuredOutputAgentTests()
- {
- this._chatClientMock = new Mock();
- this._innerAgent = new TestAIAgent();
- this._testMessages = [new ChatMessage(ChatRole.User, "Test message")];
- this._testSession = new Mock().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>(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(this._chatClientResponse);
- }
-
- #region Constructor Tests
-
- [Fact]
- public void Constructor_WithNullInnerAgent_ThrowsArgumentNullException()
- {
- // Act & Assert
- Assert.Throws("innerAgent", () =>
- new StructuredOutputAgent(null!, this._chatClientMock.Object));
- }
-
- [Fact]
- public void Constructor_WithNullChatClient_ThrowsArgumentNullException()
- {
- // Act & Assert
- Assert.Throws("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(
- () => 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(
- () => 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(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(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>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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? 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? capturedMessages = null;
-
- this._chatClientMock
- .Setup(c => c.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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 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? capturedMessages = null;
-
- this._chatClientMock
- .Setup(c => c.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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 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>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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(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(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(
- () => 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>(),
- It.IsAny(),
- It.IsAny()))
- .ThrowsAsync(expectedException);
-
- StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
- AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
-
- // Act & Assert
- InvalidOperationException actualException = await Assert.ThrowsAsync(
- () => 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>(),
- It.IsAny(),
- It.IsAny()))
- .ThrowsAsync(new OperationCanceledException());
-
- StructuredOutputAgent agent = new(this._innerAgent, this._chatClientMock.Object);
- AgentRunOptions runOptions = new() { ResponseFormat = CreateJsonResponseFormat() };
-
- // Act & Assert
- await Assert.ThrowsAsync(
- () => 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>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, 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");
- }
-}