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"); - } -}