.NET: Add Run overloads to expose ChatClientAgentRunOptions in IntelliSense (#3115)

* Initial plan

* Add ChatClientAgentExtensions for improved discoverability of ChatClientAgentRunOptions

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Address code review feedback - use collection expression syntax

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

* Fix issues with Copilot implementation

* Add additional tests for structured output overloads.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-08 19:25:47 +00:00
committed by GitHub
Unverified
parent f6086e4ccd
commit 92435c6ab5
2 changed files with 709 additions and 0 deletions
@@ -0,0 +1,253 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides extension methods for <see cref="ChatClientAgent"/> to enable discoverability of <see cref="ChatClientAgentRunOptions"/>.
/// </summary>
public partial class ChatClientAgent
{
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
/// </summary>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
public Task<AgentRunResponse> RunAsync(
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunAsync(thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent with a text message from the user.
/// </summary>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
public Task<AgentRunResponse> RunAsync(
string message,
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent with a single chat message.
/// </summary>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
public Task<AgentRunResponse> RunAsync(
ChatMessage message,
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent with a collection of chat messages.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
public Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunAsync(messages, thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent in streaming mode without providing new input messages, relying on existing context and instructions.
/// </summary>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunStreamingAsync(thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent in streaming mode with a text message from the user.
/// </summary>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
string message,
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunStreamingAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent in streaming mode with a single chat message.
/// </summary>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
ChatMessage message,
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunStreamingAsync(message, thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Runs the agent in streaming mode with a collection of chat messages.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input messages and any response updates generated during invocation.
/// </param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread,
ChatClientAgentRunOptions? options,
CancellationToken cancellationToken = default) =>
this.RunStreamingAsync(messages, thread, (AgentRunOptions?)options, cancellationToken);
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread, and requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
AgentThread? thread,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
this.RunAsync<T>(thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
/// <summary>
/// Runs the agent with a text message from the user, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <param name="message">The user message to send to the agent.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
string message,
AgentThread? thread,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
this.RunAsync<T>(message, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
/// <summary>
/// Runs the agent with a single chat message, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <param name="message">The chat message to send to the agent.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input message and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
ChatMessage message,
AgentThread? thread,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
this.RunAsync<T>(message, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
/// <summary>
/// Runs the agent with a collection of chat messages, requesting a response of the specified type <typeparamref name="T"/>.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="serializerOptions">The JSON serialization options to use.</param>
/// <param name="options">Configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="useJsonSchemaResponseFormat">
/// <see langword="true" /> to set a JSON schema on the <see cref="ChatResponseFormat"/>; otherwise, <see langword="false" />. The default is <see langword="true" />.
/// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="ChatClientAgentRunResponse{T}"/> with the agent's output.</returns>
public Task<ChatClientAgentRunResponse<T>> RunAsync<T>(
IEnumerable<ChatMessage> messages,
AgentThread? thread,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
this.RunAsync<T>(messages, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
}
@@ -0,0 +1,456 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Tests for <see cref="ChatClientAgent"/> run methods with <see cref="ChatClientAgentRunOptions"/>.
/// </summary>
public sealed partial class ChatClientAgent_RunWithCustomOptionsTests
{
#region RunAsync Tests
[Fact]
public async Task RunAsync_WithThreadAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse result = await agent.RunAsync(thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithStringMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse result = await agent.RunAsync("Test message", thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithChatMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatMessage message = new(ChatRole.User, "Test message");
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse result = await agent.RunAsync(message, thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")];
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse result = await agent.RunAsync(messages, thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithChatOptionsInRunOptions_UsesChatOptionsAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentRunOptions options = new(new ChatOptions { Temperature = 0.5f });
// Act
AgentRunResponse result = await agent.RunAsync("Test", null, options);
// Assert
Assert.NotNull(result);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.Temperature == 0.5f),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
#region RunStreamingAsync Tests
[Fact]
public async Task RunStreamingAsync_WithThreadAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WithStringMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync("Test message", thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WithChatMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatMessage message = new(ChatRole.User, "Test message");
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(message, thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
IEnumerable<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Message 1"), new ChatMessage(ChatRole.User, "Message 2")];
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(messages, thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
#region Helper Methods
private static async IAsyncEnumerable<ChatResponseUpdate> GetAsyncUpdatesAsync()
{
yield return new ChatResponseUpdate { Contents = new[] { new TextContent("Hello") } };
yield return new ChatResponseUpdate { Contents = new[] { new TextContent(" World") } };
await Task.CompletedTask;
}
#endregion
#region RunAsync{T} Tests
[Fact]
public async Task RunAsyncOfT_WithThreadAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>(thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentRunResponse);
Assert.Single(agentRunResponse.Messages);
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsyncOfT_WithStringMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>("Test message", thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentRunResponse);
Assert.Single(agentRunResponse.Messages);
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsyncOfT_WithChatMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
ChatMessage message = new(ChatRole.User, "Test message");
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>(message, thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentRunResponse);
Assert.Single(agentRunResponse.Messages);
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsyncOfT_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = agent.GetNewThread();
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")];
ChatClientAgentRunOptions options = new();
// Act
AgentRunResponse<Animal> agentRunResponse = await agent.RunAsync<Animal>(messages, thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentRunResponse);
Assert.Single(agentRunResponse.Messages);
Assert.Equal("Tigger", agentRunResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
private sealed class Animal
{
public int Id { get; set; }
public string? FullName { get; set; }
public Species Species { get; set; }
}
private enum Species
{
Bear,
Tiger,
Walrus,
}
[JsonSourceGenerationOptions(UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Animal))]
private sealed partial class JsonContext_WithCustomRunOptions : JsonSerializerContext;
}