From 9486c76ef80225a1dca590eb606733a963b96719 Mon Sep 17 00:00:00 2001 From: MaciejWarchalowski Date: Tue, 9 Jun 2026 06:25:31 -0500 Subject: [PATCH] .NET: Add Reasoning to ChatClientAgent ChatOptions merging (#5463) * Add reasoning option to request chat options in ChatClientAgent * Add tests for ChatOptions reasoning merging in ChatClientAgent --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --- .../ChatClient/ChatClientAgent.cs | 1 + ...ChatClientAgent_ChatOptionsMergingTests.cs | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index ff6d27aa7c..9e79ac5b78 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -564,6 +564,7 @@ public sealed partial class ChatClientAgent : AIAgent requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId; requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty; requestChatOptions.ResponseFormat ??= this._agentOptions.ChatOptions.ResponseFormat; + requestChatOptions.Reasoning ??= this._agentOptions.ChatOptions.Reasoning; requestChatOptions.Seed ??= this._agentOptions.ChatOptions.Seed; requestChatOptions.Temperature ??= this._agentOptions.ChatOptions.Temperature; requestChatOptions.TopP ??= this._agentOptions.ChatOptions.TopP; diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs index e4df863ce0..c4766b83f0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs @@ -347,6 +347,115 @@ public class ChatClientAgent_ChatOptionsMergingTests Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!)); } + /// + /// Verify that from the request takes priority over the agent's. + /// + [Fact] + public async Task ChatOptionsMergingUsesRequestReasoningOverAgentReasoningAsync() + { + // Arrange + var agentReasoning = new ReasoningOptions { Effort = ReasoningEffort.Low, Output = ReasoningOutput.Full }; + var requestReasoning = new ReasoningOptions { Effort = ReasoningEffort.High, Output = ReasoningOutput.Full }; + + Mock mockService = new(); + ChatOptions? capturedChatOptions = null; + mockService.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => + capturedChatOptions = opts) + .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); + + ChatClientAgent agent = new(mockService.Object, options: new() + { + ChatOptions = new ChatOptions { Reasoning = agentReasoning } + }); + var messages = new List { new(ChatRole.User, "test") }; + + // Act + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(new ChatOptions { Reasoning = requestReasoning })); + + // Assert + Assert.NotNull(capturedChatOptions); + Assert.NotNull(capturedChatOptions.Reasoning); + Assert.Equal(requestReasoning.Effort, capturedChatOptions.Reasoning.Effort); + Assert.Equal(requestReasoning.Output, capturedChatOptions.Reasoning.Output); + } + + /// + /// Verify that falls back to the agent's when the request has none. + /// + [Fact] + public async Task ChatOptionsMergingFallsBackToAgentReasoningWhenRequestHasNoneAsync() + { + // Arrange + var agentReasoning = new ReasoningOptions { Effort = ReasoningEffort.Low, Output = ReasoningOutput.Full }; + + Mock mockService = new(); + ChatOptions? capturedChatOptions = null; + mockService.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => + capturedChatOptions = opts) + .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); + + ChatClientAgent agent = new(mockService.Object, options: new() + { + ChatOptions = new ChatOptions { Reasoning = agentReasoning } + }); + var messages = new List { new(ChatRole.User, "test") }; + + // Act + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(new ChatOptions())); + + // Assert + Assert.NotNull(capturedChatOptions); + Assert.NotNull(capturedChatOptions.Reasoning); + Assert.Equal(agentReasoning.Effort, capturedChatOptions.Reasoning.Effort); + Assert.Equal(agentReasoning.Output, capturedChatOptions.Reasoning.Output); + } + + /// + /// Verify that from the request is used when the agent has none. + /// + [Fact] + public async Task ChatOptionsMergingUsesRequestReasoningWhenAgentHasNoneAsync() + { + // Arrange + var requestReasoning = new ReasoningOptions { Effort = ReasoningEffort.High, Output = ReasoningOutput.Full }; + + Mock mockService = new(); + ChatOptions? capturedChatOptions = null; + mockService.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => + capturedChatOptions = opts) + .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); + + ChatClientAgent agent = new(mockService.Object, options: new() + { + ChatOptions = new ChatOptions() + }); + var messages = new List { new(ChatRole.User, "test") }; + + // Act + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(new ChatOptions { Reasoning = requestReasoning })); + + // Assert + Assert.NotNull(capturedChatOptions); + Assert.NotNull(capturedChatOptions.Reasoning); + Assert.Equal(requestReasoning.Effort, capturedChatOptions.Reasoning.Effort); + Assert.Equal(requestReasoning.Output, capturedChatOptions.Reasoning.Output); + } + /// /// Verify that ChatOptions merging handles all scalar properties correctly. ///