From 1ed46e41a721128ee5bc677a5d08402f2241f51d Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:43:16 +0100 Subject: [PATCH] Further fixes for compatibility with Microsoft.Extensions repo. (#555) --- .../AgentThread.cs | 4 +- .../InMemoryChatMessageStore.cs | 4 +- .../ChatCompletion/ChatClientAgent.cs | 2 +- .../ChatClientAgentExtensions.cs | 4 +- .../ChatCompletion/ChatClientAgentOptions.cs | 5 +- .../ChatClientAgentRunOptions.cs | 10 ---- .../AIAgentTests.cs | 17 +++++- .../AgentRunResponseTests.cs | 2 +- .../AgentRunResponseUpdateExtensionsTests.cs | 40 +++++++------- .../AgentThreadTests.cs | 45 +++++++++------ .../InMemoryChatMessageStoreTests.cs | 16 ++++-- .../TestJsonSerializerContext.cs | 4 ++ .../ChatClientAgentRunOptionsTests.cs | 55 +------------------ .../ChatCompletion/ChatClientAgentTests.cs | 54 +++++++++++------- 14 files changed, 127 insertions(+), 135 deletions(-) diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs index a705ba6530..63b8f07f69 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs @@ -205,7 +205,7 @@ public class AgentThread } // If we don't have any IChatMessageStore state return here. - if (state?.StoreState is null || state?.StoreState?.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + if (state?.StoreState is null || state?.StoreState.Value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) { return; } @@ -219,7 +219,7 @@ public class AgentThread await this._messageStore.DeserializeStateAsync(state!.StoreState.Value, jsonSerializerOptions, cancellationToken).ConfigureAwait(false); } - internal class ThreadState + internal sealed class ThreadState { public string? ConversationId { get; set; } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs index b535f10081..086a94b9c8 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.AI.Agents; /// /// Represents an in-memory store for chat messages associated with a specific thread. /// -internal class InMemoryChatMessageStore : IList, IChatMessageStore +internal sealed class InMemoryChatMessageStore : IList, IChatMessageStore { private readonly List _messages = new(); @@ -114,7 +114,7 @@ internal class InMemoryChatMessageStore : IList, IChatMessageStore IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - internal class StoreState + internal sealed class StoreState { public IList Messages { get; set; } = new List(); } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs index 472ed7730d..76bb906a8d 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs @@ -231,7 +231,7 @@ public sealed class ChatClientAgent : AIAgent // If no request chat options were provided, use the agent's chat options clone. if (requestChatOptions is null) { - return this._agentOptions?.ChatOptions?.Clone(); + return this._agentOptions?.ChatOptions.Clone(); } // If both are present, we need to merge them. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs index d66a34f546..8dd36477a1 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs @@ -33,7 +33,7 @@ public static class ChatClientAgentExtensions Throw.IfNull(agent); Throw.IfNull(messages); - return agent.RunAsync(messages, thread, new ChatClientAgentRunOptions(agentRunOptions, chatOptions), cancellationToken); + return agent.RunAsync(messages, thread, new ChatClientAgentRunOptions(chatOptions), cancellationToken); } /// @@ -80,7 +80,7 @@ public static class ChatClientAgentExtensions Throw.IfNull(agent); Throw.IfNull(messages); - return agent.RunStreamingAsync(messages, thread, new ChatClientAgentRunOptions(agentRunOptions, chatOptions), cancellationToken); + return agent.RunStreamingAsync(messages, thread, new ChatClientAgentRunOptions(chatOptions), cancellationToken); } /// diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs index 2690539db1..2759523cc2 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentOptions.cs @@ -53,6 +53,7 @@ public class ChatClientAgentOptions /// Gets or sets the agent id. /// public string? Id { get; set; } + /// /// Gets or sets the agent name. /// @@ -77,7 +78,7 @@ public class ChatClientAgentOptions /// Gets or sets a factory function to create an instance of /// which will be used to store chat messages for this agent. /// - public Func? ChatMessageStoreFactory { get; set; } = null; + public Func? ChatMessageStoreFactory { get; set; } /// /// Gets or sets a value indicating whether to use the provided instance as is, @@ -91,7 +92,7 @@ public class ChatClientAgentOptions /// than the default ones. The provided instance should then already be decorated /// with the desired decorators. /// - public bool UseProvidedChatClientAsIs { get; set; } = false; + public bool UseProvidedChatClientAsIs { get; set; } /// /// Creates a new instance of with the same values as this instance. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentRunOptions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentRunOptions.cs index e7d13e9b70..7148ea8199 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentRunOptions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentRunOptions.cs @@ -12,16 +12,6 @@ public sealed class ChatClientAgentRunOptions : AgentRunOptions /// /// Optional chat options to pass to the agent's invocation. public ChatClientAgentRunOptions(ChatOptions? chatOptions = null) - : this(null, chatOptions) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Optional source to clone. - /// Optional chat options to pass to the agent's invocation. - internal ChatClientAgentRunOptions(AgentRunOptions? source, ChatOptions? chatOptions = null) { this.ChatOptions = chatOptions; } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs index 5456c1b548..411e0bfb7e 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable S3717 // Track use of "NotImplementedException" + using System; using System.Collections.Generic; using System.Linq; @@ -44,7 +46,7 @@ public class AIAgentTests this._agentThreadMock.Object, It.IsAny(), It.IsAny())) - .Returns(this._invokeStreamingResponses.ToAsyncEnumerable()); + .Returns(ToAsyncEnumerableAsync(this._invokeStreamingResponses)); } /// @@ -370,7 +372,7 @@ public class AIAgentTests AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override IAsyncEnumerable RunStreamingAsync( @@ -379,7 +381,16 @@ public class AIAgentTests AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); + } + } + + private static async IAsyncEnumerable ToAsyncEnumerableAsync(IEnumerable values) + { + await Task.Yield(); + foreach (var update in values) + { + yield return update; } } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseTests.cs index 46610d7138..b83050b620 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseTests.cs @@ -259,7 +259,7 @@ public class AgentRunResponseTests var response = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal))); // Act. - var animal = response.Deserialize(TestJsonSerializerContext.Default.Options); + response.TryDeserialize(TestJsonSerializerContext.Default.Options, out Animal? animal); // Assert. Assert.NotNull(animal); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs index 041bc201b2..a5e5cb097e 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateExtensionsTests.cs @@ -10,6 +10,26 @@ namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests; public class AgentRunResponseUpdateExtensionsTests { + public static IEnumerable ToAgentRunResponseCoalescesVariousSequenceAndGapLengthsMemberData() + { + foreach (bool useAsync in new[] { false, true }) + { + for (int numSequences = 1; numSequences <= 3; numSequences++) + { + for (int sequenceLength = 1; sequenceLength <= 3; sequenceLength++) + { + for (int gapLength = 1; gapLength <= 3; gapLength++) + { + foreach (bool gapBeginningEnd in new[] { false, true }) + { + yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false }; + } + } + } + } + } + } + [Fact] public void ToAgentRunResponseWithInvalidArgsThrows() { @@ -59,26 +79,6 @@ public class AgentRunResponseUpdateExtensionsTests Assert.Equal("Hello, world!", response.Text); } - public static IEnumerable ToAgentRunResponseCoalescesVariousSequenceAndGapLengthsMemberData() - { - foreach (bool useAsync in new[] { false, true }) - { - for (int numSequences = 1; numSequences <= 3; numSequences++) - { - for (int sequenceLength = 1; sequenceLength <= 3; sequenceLength++) - { - for (int gapLength = 1; gapLength <= 3; gapLength++) - { - foreach (bool gapBeginningEnd in new[] { false, true }) - { - yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false }; - } - } - } - } - } - } - [Theory] [MemberData(nameof(ToAgentRunResponseCoalescesVariousSequenceAndGapLengthsMemberData))] public async Task ToAgentRunResponseCoalescesVariousSequenceAndGapLengthsAsync(bool useAsync, int numSequences, int sequenceLength, int gapLength, bool gapBeginningEnd) diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs index 09c00a6c8d..157fb30fdc 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs @@ -97,7 +97,7 @@ public class AgentThreadTests var thread = new AgentThread(); // Act - var messages = await thread.GetMessagesAsync(CancellationToken.None).ToListAsync(); + var messages = await ToListAsync(thread.GetMessagesAsync(CancellationToken.None)); // Assert Assert.Empty(messages); @@ -107,10 +107,10 @@ public class AgentThreadTests public async Task GetMessagesAsyncReturnsEmptyListWhenAgentServiceIdAsync() { // Arrange - var thread = new AgentThread() { ConversationId = "thread-123" }; + var thread = new AgentThread { ConversationId = "thread-123" }; // Act - var messages = await thread.GetMessagesAsync(CancellationToken.None).ToListAsync(); + var messages = await ToListAsync(thread.GetMessagesAsync(CancellationToken.None)); // Assert Assert.Empty(messages); @@ -125,10 +125,10 @@ public class AgentThreadTests new ChatMessage(ChatRole.User, "Hello"), new ChatMessage(ChatRole.Assistant, "Hi there!") }; - var thread = new AgentThread() { MessageStore = store }; + var thread = new AgentThread { MessageStore = store }; // Act - var messages = await thread.GetMessagesAsync(CancellationToken.None).ToListAsync(); + var messages = await ToListAsync(thread.GetMessagesAsync(CancellationToken.None)); // Assert Assert.Equal(2, messages.Count); @@ -144,7 +144,7 @@ public class AgentThreadTests public async Task OnNewMessagesAsyncDoesNothingWhenAgentServiceIdAsync() { // Arrange - var thread = new AgentThread() { ConversationId = "thread-123" }; + var thread = new AgentThread { ConversationId = "thread-123" }; var messages = new List { new(ChatRole.User, "Hello"), @@ -162,7 +162,7 @@ public class AgentThreadTests { // Arrange var store = new InMemoryChatMessageStore(); - var thread = new AgentThread() { MessageStore = store }; + var thread = new AgentThread { MessageStore = store }; var messages = new List { new(ChatRole.User, "Hello"), @@ -187,12 +187,12 @@ public class AgentThreadTests { // Arrange var chatMessageStore = new InMemoryChatMessageStore(); - var json = JsonSerializer.Deserialize(""" + var json = JsonSerializer.Deserialize(""" { "storeState": { "messages": [{"authorName": "testAuthor"}] } } - """); - var thread = new AgentThread() { MessageStore = chatMessageStore }; + """, TestJsonSerializerContext.Default.JsonElement); + var thread = new AgentThread { MessageStore = chatMessageStore }; // Act. await thread.DeserializeAsync(json); @@ -208,11 +208,11 @@ public class AgentThreadTests public async Task VerifyDeserializeWithIdAsync() { // Arrange - var json = JsonSerializer.Deserialize(""" + var json = JsonSerializer.Deserialize(""" { "conversationId": "TestConvId" } - """); + """, TestJsonSerializerContext.Default.JsonElement); var thread = new AgentThread(); // Act @@ -227,7 +227,7 @@ public class AgentThreadTests public async Task DeserializeWithInvalidJsonThrowsAsync() { // Arrange - var invalidJson = JsonSerializer.Deserialize("[42]"); + var invalidJson = JsonSerializer.Deserialize("[42]", TestJsonSerializerContext.Default.JsonElement); var thread = new AgentThread(); // Act & Assert @@ -245,7 +245,7 @@ public class AgentThreadTests public async Task VerifyThreadSerializationWithIdAsync() { // Arrange - var thread = new AgentThread() { ConversationId = "TestConvId" }; + var thread = new AgentThread { ConversationId = "TestConvId" }; // Act var json = await thread.SerializeAsync(); @@ -268,7 +268,7 @@ public class AgentThreadTests // Arrange var store = new InMemoryChatMessageStore(); store.Add(new ChatMessage(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" }); - var thread = new AgentThread() { MessageStore = store }; + var thread = new AgentThread { MessageStore = store }; // Act var json = await thread.SerializeAsync(); @@ -306,7 +306,9 @@ public class AgentThreadTests JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); - var storeStateElement = JsonSerializer.SerializeToElement(new { Key = "TestValue" }); + var storeStateElement = JsonSerializer.SerializeToElement( + new Dictionary { ["Key"] = "TestValue" }, + TestJsonSerializerContext.Default.DictionaryStringObject); var messageStoreMock = new Mock(); messageStoreMock @@ -332,4 +334,15 @@ public class AgentThreadTests } #endregion Serialize Tests + + private static async Task> ToListAsync(IAsyncEnumerable values) + { + var result = new List(); + await foreach (var v in values) + { + result.Add(v); + } + + return result; + } } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs index e63f83ad23..d11174eb91 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/InMemoryChatMessageStoreTests.cs @@ -62,7 +62,7 @@ public class InMemoryChatMessageStoreTests { var newStore = new InMemoryChatMessageStore(); - var emptyObject = JsonSerializer.Deserialize("{}"); + var emptyObject = JsonSerializer.Deserialize("{}", TestJsonSerializerContext.Default.JsonElement); await newStore.DeserializeStateAsync(emptyObject); @@ -129,7 +129,9 @@ public class InMemoryChatMessageStoreTests { // Arrange var store = new InMemoryChatMessageStore(); - var stateWithEmptyMessages = JsonSerializer.SerializeToElement(new { Messages = new List() }); + var stateWithEmptyMessages = JsonSerializer.SerializeToElement( + new Dictionary { ["Messages"] = new List() }, + TestJsonSerializerContext.Default.IDictionaryStringObject); // Act await store.DeserializeStateAsync(stateWithEmptyMessages); @@ -143,7 +145,9 @@ public class InMemoryChatMessageStoreTests { // Arrange var store = new InMemoryChatMessageStore(); - var stateWithNullMessages = JsonSerializer.SerializeToElement(new { Messages = (List?)null }); + var stateWithNullMessages = JsonSerializer.SerializeToElement( + new Dictionary { ["Messages"] = null! }, + TestJsonSerializerContext.Default.DictionaryStringObject); // Act await store.DeserializeStateAsync(stateWithNullMessages); @@ -162,8 +166,10 @@ public class InMemoryChatMessageStoreTests new(ChatRole.User, "User message"), new(ChatRole.Assistant, "Assistant message") }; - var state = new { Messages = messages }; - var serializedState = JsonSerializer.SerializeToElement(state); + var state = new Dictionary { ["Messages"] = messages }; + var serializedState = JsonSerializer.SerializeToElement( + state, + TestJsonSerializerContext.Default.DictionaryStringObject); // Act await store.DeserializeStateAsync(serializedState); diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/TestJsonSerializerContext.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/TestJsonSerializerContext.cs index bf0a0ee62e..bd39c6638a 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/TestJsonSerializerContext.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/TestJsonSerializerContext.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.AI.Agents.Abstractions.UnitTests.Models; @@ -13,4 +15,6 @@ namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests; [JsonSerializable(typeof(AgentRunResponseUpdate))] [JsonSerializable(typeof(AgentRunOptions))] [JsonSerializable(typeof(Animal))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(Dictionary))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs index 12cbe53779..2951cdae07 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentRunOptionsTests.cs @@ -5,10 +5,10 @@ namespace Microsoft.Extensions.AI.Agents.UnitTests.ChatCompletion; public class ChatClientAgentRunOptionsTests { /// - /// Verify that ChatClientAgentRunOptions constructor works with null source and null chatOptions. + /// Verify that ChatClientAgentRunOptions constructor works with null chatOptions. /// [Fact] - public void ConstructorWorksWithNullSourceAndNullChatOptions() + public void ConstructorWorksWithNullChatOptions() { // Act var runOptions = new ChatClientAgentRunOptions(); @@ -17,55 +17,6 @@ public class ChatClientAgentRunOptionsTests Assert.Null(runOptions.ChatOptions); } - /// - /// Verify that ChatClientAgentRunOptions constructor works with null source and provided chatOptions. - /// - [Fact] - public void ConstructorWorksWithNullSourceAndProvidedChatOptions() - { - // Arrange - var chatOptions = new ChatOptions { MaxOutputTokens = 100 }; - - // Act - var runOptions = new ChatClientAgentRunOptions(null, chatOptions); - - // Assert - Assert.Same(chatOptions, runOptions.ChatOptions); - } - - /// - /// Verify that ChatClientAgentRunOptions constructor copies properties from source AgentRunOptions. - /// - [Fact] - public void ConstructorCopiesPropertiesFromSourceAgentRunOptions() - { - // Arrange - var sourceRunOptions = new AgentRunOptions(); - var chatOptions = new ChatOptions { MaxOutputTokens = 200 }; - - // Act - var runOptions = new ChatClientAgentRunOptions(sourceRunOptions, chatOptions); - - // Assert - Assert.Same(chatOptions, runOptions.ChatOptions); - } - - /// - /// Verify that ChatClientAgentRunOptions constructor works with source but null chatOptions. - /// - [Fact] - public void ConstructorWorksWithSourceButNullChatOptions() - { - // Arrange - var sourceRunOptions = new AgentRunOptions(); - - // Act - var runOptions = new ChatClientAgentRunOptions(sourceRunOptions, null); - - // Assert - Assert.Null(runOptions.ChatOptions); - } - /// /// Verify that ChatClientAgentRunOptions ChatOptions property is set and mutable. /// @@ -74,7 +25,7 @@ public class ChatClientAgentRunOptionsTests { // Arrange var chatOptions = new ChatOptions { MaxOutputTokens = 100 }; - var runOptions = new ChatClientAgentRunOptions(null, chatOptions); + var runOptions = new ChatClientAgentRunOptions(chatOptions); chatOptions.MaxOutputTokens = 200; // Change the property to verify mutability // Act & Assert diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs index 666777a9b6..46afaace36 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Moq; @@ -114,7 +113,7 @@ public class ChatClientAgentTests ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); // Act - await agent.RunAsync([new(ChatRole.User, "test")], chatOptions: chatOptions); + await agent.RunAsync([new(ChatRole.User, "test")], options: new ChatClientAgentRunOptions(chatOptions)); // Assert mockService.Verify( @@ -320,7 +319,7 @@ public class ChatClientAgentTests AgentThread thread = new() { ConversationId = "ConvId" }; // Act & Assert - var response = await agent.RunAsync([new(ChatRole.User, "test")], thread, chatOptions: chatOptions); + var response = await agent.RunAsync([new(ChatRole.User, "test")], thread, options: new ChatClientAgentRunOptions(chatOptions)); Assert.NotNull(response); } @@ -340,7 +339,7 @@ public class ChatClientAgentTests AgentThread thread = new() { ConversationId = "ThreadId" }; // Act & Assert - await Assert.ThrowsAsync(() => agent.RunAsync([new(ChatRole.User, "test")], thread, chatOptions: chatOptions)); + await Assert.ThrowsAsync(() => agent.RunAsync([new(ChatRole.User, "test")], thread, options: new ChatClientAgentRunOptions(chatOptions))); } /// @@ -363,7 +362,7 @@ public class ChatClientAgentTests AgentThread thread = new() { ConversationId = "ConvId" }; // Act - await agent.RunAsync([new(ChatRole.User, "test")], thread, chatOptions: chatOptions); + await agent.RunAsync([new(ChatRole.User, "test")], thread, options: new ChatClientAgentRunOptions(chatOptions)); // Assert Assert.Null(chatOptions.ConversationId); @@ -757,7 +756,7 @@ public class ChatClientAgentTests var messages = new List { new(ChatRole.User, "test") }; // Act - await agent.RunAsync(messages, chatOptions: requestChatOptions); + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions)); // Assert Assert.NotNull(capturedChatOptions); @@ -780,21 +779,21 @@ public class ChatClientAgentTests Temperature = 0.7f, TopP = 0.9f, ModelId = "agent-model", - AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "agent-value" } + AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "agent-value" } }; var requestChatOptions = new ChatOptions { + // TopP and ModelId not set, should use agent values MaxOutputTokens = 200, Temperature = 0.3f, - AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" }, + AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "request-value" }, Instructions = "request instructions" - // TopP and ModelId not set, should use agent values }; var expectedChatOptionsMerge = new ChatOptions { MaxOutputTokens = 200, // Request value takes priority Temperature = 0.3f, // Request value takes priority - AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" }, // Request value takes priority + AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "request-value" }, // Request value takes priority TopP = 0.9f, // Agent value used when request doesn't specify ModelId = "agent-model", // Agent value used when request doesn't specify Instructions = "test instructions\nrequest instructions" // Request is in addition to agent instructions @@ -819,7 +818,7 @@ public class ChatClientAgentTests var messages = new List { new(ChatRole.User, "test") }; // Act - await agent.RunAsync(messages, chatOptions: requestChatOptions); + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions)); // Assert Assert.NotNull(capturedChatOptions); @@ -898,12 +897,13 @@ public class ChatClientAgentTests var messages = new List { new(ChatRole.User, "test") }; // Act - await agent.RunAsync(messages, chatOptions: requestChatOptions); + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions)); // Assert Assert.NotNull(capturedChatOptions); Assert.NotNull(capturedChatOptions.Tools); Assert.Equal(2, capturedChatOptions.Tools.Count); + // Request tools should come first, then agent tools Assert.Contains(requestTool, capturedChatOptions.Tools); Assert.Contains(agentTool, capturedChatOptions.Tools); @@ -924,8 +924,8 @@ public class ChatClientAgentTests }; var requestChatOptions = new ChatOptions { - MaxOutputTokens = 100 // No Tools specified + MaxOutputTokens = 100 }; Mock mockService = new(); @@ -947,7 +947,7 @@ public class ChatClientAgentTests var messages = new List { new(ChatRole.User, "test") }; // Act - await agent.RunAsync(messages, chatOptions: requestChatOptions); + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions)); // Assert Assert.NotNull(capturedChatOptions); @@ -994,7 +994,7 @@ public class ChatClientAgentTests var messages = new List { new(ChatRole.User, "test") }; // Act - await agent.RunAsync(messages, chatOptions: requestChatOptions); + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions)); // Assert Assert.NotNull(capturedChatOptions); @@ -1029,6 +1029,7 @@ public class ChatClientAgentTests MaxOutputTokens = 200, Temperature = 0.3f, Instructions = "request instructions", + // Other properties not set, should use agent values StopSequences = ["request-stop"] }; @@ -1072,7 +1073,7 @@ public class ChatClientAgentTests var messages = new List { new(ChatRole.User, "test") }; // Act - await agent.RunAsync(messages, chatOptions: requestChatOptions); + await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions)); // Assert Assert.NotNull(capturedChatOptions); @@ -1148,6 +1149,7 @@ public class ChatClientAgentTests // Assert Assert.NotNull(result); Assert.IsAssignableFrom(result); + // Note: The result will be the AgentInvokedChatClient wrapper, not the original mock Assert.Equal("AgentInvokedChatClient", result.GetType().Name); } @@ -1490,7 +1492,7 @@ public class ChatClientAgentTests s => s.GetStreamingResponseAsync( It.IsAny>(), It.IsAny(), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); + It.IsAny())).Returns(ToAsyncEnumerableAsync(returnUpdates)); ChatClientAgent agent = new(mockService.Object, options: new() @@ -1499,10 +1501,15 @@ public class ChatClientAgentTests }); // Act - var result = await agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "Hello")]).ToArrayAsync(); + var updates = agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "Hello")]); + List result = []; + await foreach (var update in updates) + { + result.Add(update); + } // Assert - Assert.Equal(2, result.Length); + Assert.Equal(2, result.Count); Assert.Equal("wh", result[0].Text); Assert.Equal("at?", result[1].Text); @@ -1516,4 +1523,13 @@ public class ChatClientAgentTests } #endregion + + private static async IAsyncEnumerable ToAsyncEnumerableAsync(IEnumerable values) + { + await Task.Yield(); + foreach (var update in values) + { + yield return update; + } + } }