Remove the ambiguity by removing extensions for chat options (#620)

This commit is contained in:
Roger Barreto
2025-09-05 15:19:39 +01:00
committed by GitHub
Unverified
parent dbc961c869
commit 8d13809922
2 changed files with 0 additions and 966 deletions
@@ -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;
/// <summary>
/// Extensions for <see cref="ChatClientAgent"/> agent types.
/// </summary>
public static class ChatClientAgentExtensions
{
/// <summary>
/// Run the agent with the provided messages and an optional thread.
/// </summary>
/// <param name="agent">Target agent to run.</param>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">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.</param>
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public static Task<AgentRunResponse> RunAsync(
this ChatClientAgent agent,
IReadOnlyCollection<ChatMessage> 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);
}
/// <summary>
/// Run the agent with the provided prompt.
/// </summary>
/// <param name="agent">Target agent to run.</param>
/// <param name="prompt">The prompt to pass to the agent.</param>
/// <param name="thread">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.</param>
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="AgentRunResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public static Task<AgentRunResponse> 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);
}
/// <summary>
/// Run the agent with the provided message and arguments.
/// </summary>
/// <param name="agent">Target agent to run.</param>
/// <param name="messages">The messages to pass to the agent.</param>
/// <param name="thread">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.</param>
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
public static IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
this ChatClientAgent agent,
IReadOnlyCollection<ChatMessage> 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);
}
/// <summary>
/// Run the agent with the provided prompt in streaming mode.
/// </summary>
/// <param name="agent">Target agent to run.</param>
/// <param name="prompt">The prompt to pass to the agent.</param>
/// <param name="thread">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.</param>
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
/// <param name="chatOptions">Optional chat options.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An async enumerable of <see cref="AgentRunResponseUpdate"/> items for streaming the response.</returns>
public static IAsyncEnumerable<AgentRunResponseUpdate> 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);
}
}
@@ -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<ChatMessage> Tests
/// <summary>
/// Verify that RunAsync extension method with messages works with valid parameters.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesWorksWithValidParametersAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test message") };
// Act & Assert - Should not throw
var result = await ChatClientAgentExtensions.RunAsync(agent, messages);
Assert.NotNull(result);
}
/// <summary>
/// Verify that RunAsync extension method with messages throws ArgumentNullException when agent is null.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesThrowsArgumentNullExceptionWhenAgentIsNullAsync()
{
// Arrange
ChatClientAgent agent = null!;
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
ChatClientAgentExtensions.RunAsync(agent, messages));
Assert.Equal("agent", exception.ParamName);
}
/// <summary>
/// Verify that RunAsync extension method with messages throws ArgumentNullException when messages is null.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesThrowsArgumentNullExceptionWhenMessagesIsNullAsync()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
ChatClientAgentExtensions.RunAsync(agent, (IReadOnlyCollection<ChatMessage>)null!));
Assert.Equal("messages", exception.ParamName);
}
/// <summary>
/// Verify that RunAsync extension method with messages works with ChatOptions parameter.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesWorksWithChatOptionsAsync()
{
// Arrange
var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { 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);
}
/// <summary>
/// Verify that RunAsync extension method with messages passes Instructions correctly.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesPassesInstructionsCorrectlyAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
List<ChatMessage> capturedMessages = [];
List<ChatOptions> capturedChatOptions = [];
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, 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<ChatMessage> { 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);
}
/// <summary>
/// Verify that RunAsync extension method with messages works with thread parameter.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesWorksWithThreadParameterAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { 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<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunAsync extension method with messages respects cancellation token.
/// </summary>
[Fact]
public async Task RunAsyncWithMessagesRespectsCancellationTokenAsync()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ThrowsAsync(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() => ChatClientAgentExtensions.RunAsync(agent, messages, cancellationToken: cts.Token));
}
#endregion
#region RunAsync with string prompt Tests
/// <summary>
/// Verify that RunAsync extension method with prompt calls the underlying agent method correctly.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptCallsUnderlyingAgentMethodAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).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<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunAsync extension method with prompt throws ArgumentNullException when agent is null.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptThrowsArgumentNullExceptionWhenAgentIsNullAsync()
{
// Arrange
ChatClientAgent agent = null!;
const string TestPrompt = "test prompt";
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
ChatClientAgentExtensions.RunAsync(agent, TestPrompt));
Assert.Equal("agent", exception.ParamName);
}
/// <summary>
/// Verify that RunAsync extension method with prompt throws ArgumentNullException when prompt is null.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptThrowsArgumentNullExceptionWhenPromptIsNullAsync()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
ChatClientAgentExtensions.RunAsync(agent, (string)null!));
Assert.Equal("prompt", exception.ParamName);
}
/// <summary>
/// Verify that RunAsync extension method with prompt throws ArgumentException when prompt is whitespace.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptThrowsArgumentExceptionWhenPromptIsWhitespaceAsync()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
ChatClientAgentExtensions.RunAsync(agent, " "));
Assert.Equal("prompt", exception.ParamName);
}
/// <summary>
/// Verify that RunAsync extension method with prompt converts prompt to ChatMessage correctly.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptConvertsPromptToChatMessageCorrectlyAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
List<ChatMessage> capturedMessages = [];
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, 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);
}
/// <summary>
/// Verify that RunAsync extension method with prompt passes ChatOptions correctly.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptPassesChatOptionsCorrectlyAsync()
{
// Arrange
var chatOptions = new ChatOptions { MaxOutputTokens = 200 };
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 200),
It.IsAny<CancellationToken>())).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<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 200),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunAsync extension method with prompt passes AgentRunOptions correctly.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptPassesAgentRunOptionsCorrectlyAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
List<ChatMessage> capturedMessages = [];
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, 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);
}
/// <summary>
/// Verify that RunAsync extension method with prompt works with thread parameter.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptWorksWithThreadParameterAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).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<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunAsync extension method with prompt respects cancellation token.
/// </summary>
[Fact]
public async Task RunAsyncWithPromptRespectsCancellationTokenAsync()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ThrowsAsync(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() => agent.RunAsync(TestPrompt, cancellationToken: cts.Token));
}
#endregion
#region RunStreamingAsync with IReadOnlyCollection<ChatMessage> Tests
/// <summary>
/// Verify that RunStreamingAsync extension method with messages calls the underlying agent method correctly.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithMessagesCallsUnderlyingAgentMethodAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "Hello"),
new ChatResponseUpdate(role: ChatRole.Assistant, content: " World"),
];
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test message") };
// Act
var updates = new List<AgentRunResponseUpdate>();
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<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with messages throws ArgumentNullException when agent is null.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithMessagesThrowsArgumentNullExceptionWhenAgentIsNullAsync()
{
// Arrange
ChatClientAgent agent = null!;
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
{
await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages))
{
// Should not reach here
}
});
Assert.Equal("agent", exception.ParamName);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with messages throws ArgumentNullException when messages is null.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithMessagesThrowsArgumentNullExceptionWhenMessagesIsNullAsync()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
{
await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, (IReadOnlyCollection<ChatMessage>)null!))
{
// Should not reach here
}
});
Assert.Equal("messages", exception.ParamName);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with messages passes ChatOptions correctly.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithMessagesPassesChatOptionsCorrectlyAsync()
{
// Arrange
var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")];
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
var updates = new List<AgentRunResponseUpdate>();
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<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with messages works with thread parameter.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithMessagesWorksWithThreadParameterAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")];
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
var thread = agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
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<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with messages respects cancellation token.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithMessagesRespectsCancellationTokenAsync()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Throws(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
{
await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, messages, cancellationToken: cts.Token))
{
// Should not reach here
}
});
}
#endregion
#region RunStreamingAsync with string prompt Tests
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt calls the underlying agent method correctly.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptCallsUnderlyingAgentMethodAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "Hello"),
new ChatResponseUpdate(role: ChatRole.Assistant, content: " World"),
];
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
var updates = new List<AgentRunResponseUpdate>();
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<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt throws ArgumentNullException when agent is null.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptThrowsArgumentNullExceptionWhenAgentIsNullAsync()
{
// Arrange
ChatClientAgent agent = null!;
const string TestPrompt = "test prompt";
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
{
await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, TestPrompt))
{
// Should not reach here
}
});
Assert.Equal("agent", exception.ParamName);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt throws ArgumentNullException when prompt is null.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptThrowsArgumentNullExceptionWhenPromptIsNullAsync()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
{
await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, (string)null!))
{
// Should not reach here
}
});
Assert.Equal("prompt", exception.ParamName);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt throws ArgumentException when prompt is whitespace.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptThrowsArgumentExceptionWhenPromptIsWhitespaceAsync()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await foreach (var update in ChatClientAgentExtensions.RunStreamingAsync(agent, " "))
{
// Should not reach here
}
});
Assert.Equal("prompt", exception.ParamName);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt converts prompt to ChatMessage correctly.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptConvertsPromptToChatMessageCorrectlyAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")];
Mock<IChatClient> mockService = new();
List<ChatMessage> capturedMessages = [];
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, 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<AgentRunResponseUpdate>();
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);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt passes ChatOptions correctly.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptPassesChatOptionsCorrectlyAsync()
{
// Arrange
var chatOptions = new ChatOptions { MaxOutputTokens = 200 };
ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")];
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 200),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(TestPrompt, chatOptions: chatOptions))
{
updates.Add(update);
}
// Assert
Assert.Single(updates);
mockService.Verify(
x => x.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 200),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt works with thread parameter.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptWorksWithThreadParameterAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates = [new ChatResponseUpdate(role: ChatRole.Assistant, content: "response")];
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).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<AgentRunResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(TestPrompt, thread: thread))
{
updates.Add(update);
}
// Assert
Assert.Single(updates);
mockService.Verify(
x => x.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
/// <summary>
/// Verify that RunStreamingAsync extension method with prompt respects cancellation token.
/// </summary>
[Fact]
public async Task RunStreamingAsyncWithPromptRespectsCancellationTokenAsync()
{
// Arrange
using var cts = new CancellationTokenSource();
cts.Cancel();
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Throws(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(TestPrompt, cancellationToken: cts.Token))
{
// Should not reach here
}
});
}
#endregion
}