From 8d13809922fff7973c7be3f59d67960744e6425d Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:19:39 +0100 Subject: [PATCH] Remove the ambiguity by removing extensions for chat options (#620) --- .../ChatClientAgentExtensions.cs | 109 --- .../ChatClientAgentExtensionsTests.cs | 857 ------------------ 2 files changed, 966 deletions(-) delete mode 100644 dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs delete mode 100644 dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentExtensionsTests.cs diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs deleted file mode 100644 index 8dd36477a1..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgentExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.AI.Agents; - -/// -/// Extensions for agent types. -/// -public static class ChatClientAgentExtensions -{ - /// - /// Run the agent with the provided messages and an optional thread. - /// - /// Target agent to run. - /// The messages to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. - /// Optional parameters for agent invocation. - /// Optional chat options. - /// The to monitor for cancellation requests. The default is . - /// A containing the list of items. - public static Task RunAsync( - this ChatClientAgent agent, - IReadOnlyCollection messages, - AgentThread? thread = null, - AgentRunOptions? agentRunOptions = null, - ChatOptions? chatOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(agent); - Throw.IfNull(messages); - - return agent.RunAsync(messages, thread, new ChatClientAgentRunOptions(chatOptions), cancellationToken); - } - - /// - /// Run the agent with the provided prompt. - /// - /// Target agent to run. - /// The prompt to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. - /// Optional parameters for agent invocation. - /// Optional chat options. - /// The to monitor for cancellation requests. The default is . - /// A containing the list of items. - public static Task RunAsync( - this ChatClientAgent agent, - string prompt, - AgentThread? thread = null, - AgentRunOptions? agentRunOptions = null, - ChatOptions? chatOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(agent); - Throw.IfNullOrWhitespace(prompt); - - return agent.RunAsync([new ChatMessage(ChatRole.User, prompt)], thread, agentRunOptions, chatOptions, cancellationToken); - } - - /// - /// Run the agent with the provided message and arguments. - /// - /// Target agent to run. - /// The messages to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. - /// Optional parameters for agent invocation. - /// Optional chat options. - /// The to monitor for cancellation requests. The default is . - public static IAsyncEnumerable RunStreamingAsync( - this ChatClientAgent agent, - IReadOnlyCollection messages, - AgentThread? thread = null, - AgentRunOptions? agentRunOptions = null, - ChatOptions? chatOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(agent); - Throw.IfNull(messages); - - return agent.RunStreamingAsync(messages, thread, new ChatClientAgentRunOptions(chatOptions), cancellationToken); - } - - /// - /// Run the agent with the provided prompt in streaming mode. - /// - /// Target agent to run. - /// The prompt to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. - /// Optional parameters for agent invocation. - /// Optional chat options. - /// The to monitor for cancellation requests. The default is . - /// An async enumerable of items for streaming the response. - public static IAsyncEnumerable RunStreamingAsync( - this ChatClientAgent agent, - string prompt, - AgentThread? thread = null, - AgentRunOptions? agentRunOptions = null, - ChatOptions? chatOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(agent); - Throw.IfNullOrWhitespace(prompt); - - return agent.RunStreamingAsync([new ChatMessage(ChatRole.User, prompt)], thread, agentRunOptions, chatOptions, cancellationToken); - } -} diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentExtensionsTests.cs deleted file mode 100644 index 6302bdeaa6..0000000000 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentExtensionsTests.cs +++ /dev/null @@ -1,857 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Moq; - -#pragma warning disable RCS1196 // Call extension method as instance method - -namespace Microsoft.Extensions.AI.Agents.UnitTests.ChatCompletion; - -public class ChatClientAgentExtensionsTests -{ - #region RunAsync with IReadOnlyCollection Tests - - /// - /// Verify that RunAsync extension method with messages works with valid parameters. - /// - [Fact] - public async Task RunAsyncWithMessagesWorksWithValidParametersAsync() - { - // Arrange - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test message") }; - - // Act & Assert - Should not throw - var result = await ChatClientAgentExtensions.RunAsync(agent, messages); - Assert.NotNull(result); - } - - /// - /// Verify that RunAsync extension method with messages throws ArgumentNullException when agent is null. - /// - [Fact] - public async Task RunAsyncWithMessagesThrowsArgumentNullExceptionWhenAgentIsNullAsync() - { - // Arrange - ChatClientAgent agent = null!; - var messages = new List { new(ChatRole.User, "test") }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - ChatClientAgentExtensions.RunAsync(agent, messages)); - Assert.Equal("agent", exception.ParamName); - } - - /// - /// Verify that RunAsync extension method with messages throws ArgumentNullException when messages is null. - /// - [Fact] - public async Task RunAsyncWithMessagesThrowsArgumentNullExceptionWhenMessagesIsNullAsync() - { - // Arrange - var chatClient = new Mock().Object; - ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - ChatClientAgentExtensions.RunAsync(agent, (IReadOnlyCollection)null!)); - Assert.Equal("messages", exception.ParamName); - } - - /// - /// Verify that RunAsync extension method with messages works with ChatOptions parameter. - /// - [Fact] - public async Task RunAsyncWithMessagesWorksWithChatOptionsAsync() - { - // Arrange - var chatOptions = new ChatOptions { MaxOutputTokens = 100 }; - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - - // Act - Call extension method (should not throw) - var result = await ChatClientAgentExtensions.RunAsync(agent, messages, chatOptions: chatOptions); - - // Assert - Extension method completed successfully - Assert.NotNull(result); - Assert.Single(result.Messages); - } - - /// - /// Verify that RunAsync extension method with messages passes Instructions correctly. - /// - [Fact] - public async Task RunAsyncWithMessagesPassesInstructionsCorrectlyAsync() - { - // Arrange - Mock mockService = new(); - List capturedMessages = []; - List capturedChatOptions = []; - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => - { - capturedChatOptions.Add(opts); - capturedMessages.AddRange(msgs); - }) - .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - var runOptions = new AgentRunOptions(); - - // Act - await ChatClientAgentExtensions.RunAsync(agent, messages, agentRunOptions: runOptions); - - // Assert - Assert.Contains(capturedMessages, m => m.Text == "test" && m.Role == ChatRole.User); - Assert.Single(capturedChatOptions); - Assert.Equal("base instructions", capturedChatOptions[0].Instructions); - } - - /// - /// Verify that RunAsync extension method with messages works with thread parameter. - /// - [Fact] - public async Task RunAsyncWithMessagesWorksWithThreadParameterAsync() - { - // Arrange - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - var thread = agent.GetNewThread(); - - // Act - var result = await ChatClientAgentExtensions.RunAsync(agent, messages, thread: thread); - - // Assert - Assert.NotNull(result); - Assert.Single(result.Messages); - mockService.Verify( - x => x.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunAsync extension method with messages respects cancellation token. - /// - [Fact] - public async Task RunAsyncWithMessagesRespectsCancellationTokenAsync() - { - // Arrange - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ThrowsAsync(new OperationCanceledException()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - - // Act & Assert - await Assert.ThrowsAsync(() => ChatClientAgentExtensions.RunAsync(agent, messages, cancellationToken: cts.Token)); - } - - #endregion - - #region RunAsync with string prompt Tests - - /// - /// Verify that RunAsync extension method with prompt calls the underlying agent method correctly. - /// - [Fact] - public async Task RunAsyncWithPromptCallsUnderlyingAgentMethodAsync() - { - // Arrange - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act - var result = await ChatClientAgentExtensions.RunAsync(agent, TestPrompt); - - // Assert - Assert.NotNull(result); - Assert.Single(result.Messages); - Assert.Equal("response", result.Messages[0].Text); - mockService.Verify( - x => x.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunAsync extension method with prompt throws ArgumentNullException when agent is null. - /// - [Fact] - public async Task RunAsyncWithPromptThrowsArgumentNullExceptionWhenAgentIsNullAsync() - { - // Arrange - ChatClientAgent agent = null!; - const string TestPrompt = "test prompt"; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - ChatClientAgentExtensions.RunAsync(agent, TestPrompt)); - Assert.Equal("agent", exception.ParamName); - } - - /// - /// Verify that RunAsync extension method with prompt throws ArgumentNullException when prompt is null. - /// - [Fact] - public async Task RunAsyncWithPromptThrowsArgumentNullExceptionWhenPromptIsNullAsync() - { - // Arrange - var chatClient = new Mock().Object; - ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - ChatClientAgentExtensions.RunAsync(agent, (string)null!)); - Assert.Equal("prompt", exception.ParamName); - } - - /// - /// Verify that RunAsync extension method with prompt throws ArgumentException when prompt is whitespace. - /// - [Fact] - public async Task RunAsyncWithPromptThrowsArgumentExceptionWhenPromptIsWhitespaceAsync() - { - // Arrange - var chatClient = new Mock().Object; - ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - ChatClientAgentExtensions.RunAsync(agent, " ")); - Assert.Equal("prompt", exception.ParamName); - } - - /// - /// Verify that RunAsync extension method with prompt converts prompt to ChatMessage correctly. - /// - [Fact] - public async Task RunAsyncWithPromptConvertsPromptToChatMessageCorrectlyAsync() - { - // Arrange - Mock mockService = new(); - List capturedMessages = []; - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => - capturedMessages.AddRange(msgs)) - .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act - await ChatClientAgentExtensions.RunAsync(agent, TestPrompt); - - // Assert - Assert.Contains(capturedMessages, m => m.Text == "test prompt" && m.Role == ChatRole.User); - } - - /// - /// Verify that RunAsync extension method with prompt passes ChatOptions correctly. - /// - [Fact] - public async Task RunAsyncWithPromptPassesChatOptionsCorrectlyAsync() - { - // Arrange - var chatOptions = new ChatOptions { MaxOutputTokens = 200 }; - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.Is(opts => opts.MaxOutputTokens == 200), - It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act - await ChatClientAgentExtensions.RunAsync(agent, TestPrompt, chatOptions: chatOptions); - - // Assert - mockService.Verify( - x => x.GetResponseAsync( - It.IsAny>(), - It.Is(opts => opts.MaxOutputTokens == 200), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunAsync extension method with prompt passes AgentRunOptions correctly. - /// - [Fact] - public async Task RunAsyncWithPromptPassesAgentRunOptionsCorrectlyAsync() - { - // Arrange - Mock mockService = new(); - List capturedMessages = []; - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => - capturedMessages.AddRange(msgs)) - .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" }); - const string TestPrompt = "test prompt"; - var runOptions = new AgentRunOptions(); - - // Act - await ChatClientAgentExtensions.RunAsync(agent, TestPrompt, agentRunOptions: runOptions); - - // Assert - Assert.Contains(capturedMessages, m => m.Text == "test prompt" && m.Role == ChatRole.User); - } - - /// - /// Verify that RunAsync extension method with prompt works with thread parameter. - /// - [Fact] - public async Task RunAsyncWithPromptWorksWithThreadParameterAsync() - { - // Arrange - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - var thread = agent.GetNewThread(); - - // Act - var result = await ChatClientAgentExtensions.RunAsync(agent, TestPrompt, thread: thread); - - // Assert - Assert.NotNull(result); - Assert.Single(result.Messages); - mockService.Verify( - x => x.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunAsync extension method with prompt respects cancellation token. - /// - [Fact] - public async Task RunAsyncWithPromptRespectsCancellationTokenAsync() - { - // Arrange - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - Mock mockService = new(); - mockService.Setup( - s => s.GetResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).ThrowsAsync(new OperationCanceledException()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act & Assert - await Assert.ThrowsAsync(() => agent.RunAsync(TestPrompt, cancellationToken: cts.Token)); - } - - #endregion - - #region RunStreamingAsync with IReadOnlyCollection Tests - - /// - /// Verify that RunStreamingAsync extension method with messages calls the underlying agent method correctly. - /// - [Fact] - public async Task RunStreamingAsyncWithMessagesCallsUnderlyingAgentMethodAsync() - { - // Arrange - ChatResponseUpdate[] returnUpdates = - [ - new ChatResponseUpdate(role: ChatRole.Assistant, content: "Hello"), - new ChatResponseUpdate(role: ChatRole.Assistant, content: " World"), - ]; - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test message") }; - - // Act - var updates = new List(); - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages)) - { - updates.Add(update); - } - - // Assert - Assert.Equal(2, updates.Count); - Assert.Equal("Hello", updates[0].Text); - Assert.Equal(" World", updates[1].Text); - mockService.Verify( - x => x.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunStreamingAsync extension method with messages throws ArgumentNullException when agent is null. - /// - [Fact] - public async Task RunStreamingAsyncWithMessagesThrowsArgumentNullExceptionWhenAgentIsNullAsync() - { - // Arrange - ChatClientAgent agent = null!; - var messages = new List { new(ChatRole.User, "test") }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages)) - { - // Should not reach here - } - }); - Assert.Equal("agent", exception.ParamName); - } - - /// - /// Verify that RunStreamingAsync extension method with messages throws ArgumentNullException when messages is null. - /// - [Fact] - public async Task RunStreamingAsyncWithMessagesThrowsArgumentNullExceptionWhenMessagesIsNullAsync() - { - // Arrange - var chatClient = new Mock().Object; - ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, (IReadOnlyCollection)null!)) - { - // Should not reach here - } - }); - Assert.Equal("messages", exception.ParamName); - } - - /// - /// Verify that RunStreamingAsync extension method with messages passes ChatOptions correctly. - /// - [Fact] - public async Task RunStreamingAsyncWithMessagesPassesChatOptionsCorrectlyAsync() - { - // Arrange - var chatOptions = new ChatOptions { MaxOutputTokens = 100 }; - ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")]; - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.Is(opts => opts.MaxOutputTokens == 100), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - - // Act - var updates = new List(); - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages, chatOptions: chatOptions)) - { - updates.Add(update); - } - - // Assert - Assert.Single(updates); - mockService.Verify( - x => x.GetStreamingResponseAsync( - It.IsAny>(), - It.Is(opts => opts.MaxOutputTokens == 100), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunStreamingAsync extension method with messages works with thread parameter. - /// - [Fact] - public async Task RunStreamingAsyncWithMessagesWorksWithThreadParameterAsync() - { - // Arrange - ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")]; - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - var thread = agent.GetNewThread(); - - // Act - var updates = new List(); - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages, thread: thread)) - { - updates.Add(update); - } - - // Assert - Assert.Single(updates); - mockService.Verify( - x => x.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunStreamingAsync extension method with messages respects cancellation token. - /// - [Fact] - public async Task RunStreamingAsyncWithMessagesRespectsCancellationTokenAsync() - { - // Arrange - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).Throws(new OperationCanceledException()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - var messages = new List { new(ChatRole.User, "test") }; - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages, cancellationToken: cts.Token)) - { - // Should not reach here - } - }); - } - - #endregion - - #region RunStreamingAsync with string prompt Tests - - /// - /// Verify that RunStreamingAsync extension method with prompt calls the underlying agent method correctly. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptCallsUnderlyingAgentMethodAsync() - { - // Arrange - ChatResponseUpdate[] returnUpdates = - [ - new ChatResponseUpdate(role: ChatRole.Assistant, content: "Hello"), - new ChatResponseUpdate(role: ChatRole.Assistant, content: " World"), - ]; - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act - var updates = new List(); - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, TestPrompt)) - { - updates.Add(update); - } - - // Assert - Assert.Equal(2, updates.Count); - Assert.Equal("Hello", updates[0].Text); - Assert.Equal(" World", updates[1].Text); - mockService.Verify( - x => x.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt throws ArgumentNullException when agent is null. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptThrowsArgumentNullExceptionWhenAgentIsNullAsync() - { - // Arrange - ChatClientAgent agent = null!; - const string TestPrompt = "test prompt"; - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, TestPrompt)) - { - // Should not reach here - } - }); - Assert.Equal("agent", exception.ParamName); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt throws ArgumentNullException when prompt is null. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptThrowsArgumentNullExceptionWhenPromptIsNullAsync() - { - // Arrange - var chatClient = new Mock().Object; - ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, (string)null!)) - { - // Should not reach here - } - }); - Assert.Equal("prompt", exception.ParamName); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt throws ArgumentException when prompt is whitespace. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptThrowsArgumentExceptionWhenPromptIsWhitespaceAsync() - { - // Arrange - var chatClient = new Mock().Object; - ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => - { - await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, " ")) - { - // Should not reach here - } - }); - Assert.Equal("prompt", exception.ParamName); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt converts prompt to ChatMessage correctly. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptConvertsPromptToChatMessageCorrectlyAsync() - { - // Arrange - ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")]; - - Mock mockService = new(); - List capturedMessages = []; - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => - capturedMessages.AddRange(msgs)) - .Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act - var updates = new List(); - await foreach (var update in agent.RunStreamingAsync(TestPrompt)) - { - updates.Add(update); - } - - // Assert - Assert.Single(updates); - Assert.Contains(capturedMessages, m => m.Text == "test prompt" && m.Role == ChatRole.User); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt passes ChatOptions correctly. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptPassesChatOptionsCorrectlyAsync() - { - // Arrange - var chatOptions = new ChatOptions { MaxOutputTokens = 200 }; - ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")]; - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.Is(opts => opts.MaxOutputTokens == 200), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act - var updates = new List(); - await foreach (var update in agent.RunStreamingAsync(TestPrompt, chatOptions: chatOptions)) - { - updates.Add(update); - } - - // Assert - Assert.Single(updates); - mockService.Verify( - x => x.GetStreamingResponseAsync( - It.IsAny>(), - It.Is(opts => opts.MaxOutputTokens == 200), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt works with thread parameter. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptWorksWithThreadParameterAsync() - { - // Arrange - ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")]; - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(returnUpdates.ToAsyncEnumerable()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - var thread = agent.GetNewThread(); - - // Act - var updates = new List(); - await foreach (var update in agent.RunStreamingAsync(TestPrompt, thread: thread)) - { - updates.Add(update); - } - - // Assert - Assert.Single(updates); - mockService.Verify( - x => x.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Once); - } - - /// - /// Verify that RunStreamingAsync extension method with prompt respects cancellation token. - /// - [Fact] - public async Task RunStreamingAsyncWithPromptRespectsCancellationTokenAsync() - { - // Arrange - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - Mock mockService = new(); - mockService.Setup( - s => s.GetStreamingResponseAsync( - It.IsAny>(), - It.IsAny(), - It.IsAny())).Throws(new OperationCanceledException()); - - ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" }); - const string TestPrompt = "test prompt"; - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await foreach (var update in agent.RunStreamingAsync(TestPrompt, cancellationToken: cts.Token)) - { - // Should not reach here - } - }); - } - - #endregion -}