mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.Net: Adding ChatOptions configuration to the Agent level (#75)
* Adding ChatOptions configuration to the Agent level * fix warnings * Address Copilot Feedback * Add UT for new AgentExtensions * Add UnitTests for ChatOptions merging behavior * Fix warning * Address PR Feedback * Update dotnet/src/Microsoft.Agents/ChatCompletion/ChatClientAgent.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
293cdb1846
commit
16cbc44611
@@ -27,10 +27,6 @@
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents\Microsoft.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="GettingStarted" />
|
||||
<Using Include="Microsoft.Shared.SampleUtilities" />
|
||||
|
||||
@@ -15,23 +15,22 @@ public sealed class Step02_UsingTools(ITestOutputHelper output) : AgentSample(ou
|
||||
using var chatClient = base.GetOpenAIChatClient();
|
||||
|
||||
// Define the agent
|
||||
var menuTools = new MenuTools();
|
||||
ChatClientAgent agent =
|
||||
new(chatClient, new()
|
||||
{
|
||||
Name = "Host",
|
||||
Instructions = "Answer questions about the menu.",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Tools = [
|
||||
AIFunctionFactory.Create(menuTools.GetMenu),
|
||||
AIFunctionFactory.Create(menuTools.GetSpecials),
|
||||
AIFunctionFactory.Create(menuTools.GetItemPrice)
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var menuTools = new MenuTools();
|
||||
var chatOptions = new ChatOptions
|
||||
{
|
||||
Tools = [
|
||||
AIFunctionFactory.Create(menuTools.GetMenu),
|
||||
AIFunctionFactory.Create(menuTools.GetSpecials),
|
||||
AIFunctionFactory.Create(menuTools.GetItemPrice),
|
||||
],
|
||||
};
|
||||
|
||||
// Create the chat history thread to capture the agent interaction.
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
@@ -44,7 +43,7 @@ public sealed class Step02_UsingTools(ITestOutputHelper output) : AgentSample(ou
|
||||
async Task InvokeAgentAsync(string input)
|
||||
{
|
||||
this.WriteUserMessage(input);
|
||||
var response = await agent.RunAsync(input, thread, chatOptions: chatOptions);
|
||||
var response = await agent.RunAsync(input, thread);
|
||||
this.WriteResponseOutput(response);
|
||||
}
|
||||
}
|
||||
@@ -56,23 +55,22 @@ public sealed class Step02_UsingTools(ITestOutputHelper output) : AgentSample(ou
|
||||
using var chatClient = base.GetOpenAIChatClient();
|
||||
|
||||
// Define the agent
|
||||
var menuTools = new MenuTools();
|
||||
ChatClientAgent agent =
|
||||
new(chatClient, new()
|
||||
{
|
||||
Name = "Host",
|
||||
Instructions = "Answer questions about the menu.",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Tools = [
|
||||
AIFunctionFactory.Create(menuTools.GetMenu),
|
||||
AIFunctionFactory.Create(menuTools.GetSpecials),
|
||||
AIFunctionFactory.Create(menuTools.GetItemPrice)
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var menuTools = new MenuTools();
|
||||
var chatOptions = new ChatOptions
|
||||
{
|
||||
Tools = [
|
||||
AIFunctionFactory.Create(menuTools.GetMenu),
|
||||
AIFunctionFactory.Create(menuTools.GetSpecials),
|
||||
AIFunctionFactory.Create(menuTools.GetItemPrice),
|
||||
],
|
||||
};
|
||||
|
||||
// Create the chat history thread to capture the agent interaction.
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
@@ -85,7 +83,7 @@ public sealed class Step02_UsingTools(ITestOutputHelper output) : AgentSample(ou
|
||||
async Task InvokeAgentAsync(string input)
|
||||
{
|
||||
this.WriteUserMessage(input);
|
||||
await foreach (var update in agent.RunStreamingAsync(input, thread, chatOptions: chatOptions))
|
||||
await foreach (var update in agent.RunStreamingAsync(input, thread))
|
||||
{
|
||||
this.WriteAgentOutput(update);
|
||||
}
|
||||
|
||||
@@ -32,9 +32,14 @@ public sealed class ChatClientAgent : Agent
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
|
||||
// Options must be cloned since ChatClientAgentOptions is mutable.
|
||||
this._agentOptions = options?.Clone();
|
||||
|
||||
// Get the type of the chat client before wrapping it as an agent invoking chat client.
|
||||
this._chatClientType = chatClient.GetType();
|
||||
|
||||
this.ChatClient = chatClient.AsAgentInvokingChatClient();
|
||||
this._agentOptions = options;
|
||||
|
||||
this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>();
|
||||
}
|
||||
|
||||
@@ -55,6 +60,11 @@ public sealed class ChatClientAgent : Agent
|
||||
/// <inheritdoc/>
|
||||
public override string? Instructions => this._agentOptions?.Instructions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets of the default <see cref="Microsoft.Extensions.AI.ChatOptions"/> used by the agent.
|
||||
/// </summary>
|
||||
internal ChatOptions? ChatOptions => this._agentOptions?.ChatOptions;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
@@ -162,22 +172,119 @@ public sealed class ChatClientAgent : Agent
|
||||
|
||||
#region Private
|
||||
|
||||
/// <summary>
|
||||
/// Configures and returns chat options by merging the provided run options with the agent's default chat options.
|
||||
/// </summary>
|
||||
/// <remarks>This method prioritizes the chat options provided in <paramref name="runOptions"/> over the
|
||||
/// agent's default chat options. Any unset properties in the run options will be filled using the agent's chat
|
||||
/// options. If both are <see langword="null"/>, the method returns <see langword="null"/>.</remarks>
|
||||
/// <param name="runOptions">Optional run options that may include specific chat configuration settings.</param>
|
||||
/// <returns>A <see cref="ChatOptions"/> object representing the merged chat configuration, or <see langword="null"/> if
|
||||
/// neither the run options nor the agent's chat options are available.</returns>
|
||||
private ChatOptions? CreateConfiguredChatOptions(AgentRunOptions? runOptions)
|
||||
{
|
||||
ChatOptions? requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone();
|
||||
|
||||
// If no agent chat options were provided, return the request chat options as is.
|
||||
if (this._agentOptions?.ChatOptions is null)
|
||||
{
|
||||
return requestChatOptions;
|
||||
}
|
||||
|
||||
// If no request chat options were provided, use the agent's chat options clone.
|
||||
if (requestChatOptions is null)
|
||||
{
|
||||
return this._agentOptions?.ChatOptions?.Clone();
|
||||
}
|
||||
|
||||
// If both are present, we need to merge them.
|
||||
|
||||
// The merge strategy will prioritize the request options over the agent options,
|
||||
// and will fill the blanks with agent options where the request options were not set.
|
||||
|
||||
// Merge only the additional properties from the agent if they are not already set in the request options.
|
||||
if (requestChatOptions.AdditionalProperties is not null && this._agentOptions.ChatOptions.AdditionalProperties is not null)
|
||||
{
|
||||
foreach (var property in this._agentOptions.ChatOptions.AdditionalProperties.Keys)
|
||||
{
|
||||
requestChatOptions.AdditionalProperties.TryAdd(property, this._agentOptions.ChatOptions.AdditionalProperties[property]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
requestChatOptions.AdditionalProperties ??= this._agentOptions.ChatOptions.AdditionalProperties;
|
||||
}
|
||||
requestChatOptions.AllowMultipleToolCalls ??= this._agentOptions.ChatOptions.AllowMultipleToolCalls;
|
||||
requestChatOptions.ConversationId ??= this._agentOptions.ChatOptions.ConversationId;
|
||||
requestChatOptions.FrequencyPenalty ??= this._agentOptions.ChatOptions.FrequencyPenalty;
|
||||
requestChatOptions.MaxOutputTokens ??= this._agentOptions.ChatOptions.MaxOutputTokens;
|
||||
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
|
||||
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
|
||||
|
||||
// Chain the raw representation factory from the request options with the agent's factory if available.
|
||||
if (this._agentOptions.ChatOptions.RawRepresentationFactory is { } agentFactory)
|
||||
{
|
||||
requestChatOptions.RawRepresentationFactory = requestChatOptions.RawRepresentationFactory is { } requestFactory
|
||||
? chatClient => requestFactory(chatClient) ?? agentFactory(chatClient)
|
||||
: agentFactory;
|
||||
}
|
||||
|
||||
requestChatOptions.ResponseFormat ??= this._agentOptions.ChatOptions.ResponseFormat;
|
||||
requestChatOptions.Seed ??= this._agentOptions.ChatOptions.Seed;
|
||||
|
||||
// We concatenate the request stop sequences with the agent's stop sequences when available.
|
||||
if (this._agentOptions.ChatOptions.StopSequences is { Count: not 0 })
|
||||
{
|
||||
if (requestChatOptions.StopSequences is null || requestChatOptions.StopSequences.Count == 0)
|
||||
{
|
||||
// If the request stop sequences are not set or empty, we use the agent's stop sequences directly.
|
||||
requestChatOptions.StopSequences = this._agentOptions.ChatOptions.StopSequences.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If both agent's and request's stop sequences are set, we concatenate them.
|
||||
requestChatOptions.StopSequences = [.. requestChatOptions.StopSequences, .. this._agentOptions.ChatOptions.StopSequences];
|
||||
}
|
||||
}
|
||||
|
||||
requestChatOptions.Temperature ??= this._agentOptions.ChatOptions.Temperature;
|
||||
requestChatOptions.TopP ??= this._agentOptions.ChatOptions.TopP;
|
||||
requestChatOptions.TopK ??= this._agentOptions.ChatOptions.TopK;
|
||||
requestChatOptions.ToolMode ??= this._agentOptions.ChatOptions.ToolMode;
|
||||
|
||||
// We concatenate the request tools with the agent's tools when available.
|
||||
if (this._agentOptions.ChatOptions.Tools is { Count: not 0 })
|
||||
{
|
||||
if (requestChatOptions.Tools is not { Count: > 0 })
|
||||
{
|
||||
// If the request tools are not set or empty, we use the agent's tools directly.
|
||||
requestChatOptions.Tools = this._agentOptions.ChatOptions.Tools;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the both agent's and request's tools are set, we concatenate all tools.
|
||||
requestChatOptions.Tools = [.. requestChatOptions.Tools, .. this._agentOptions.ChatOptions.Tools];
|
||||
}
|
||||
}
|
||||
|
||||
return requestChatOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the thread, chat options, and messages for agent execution.
|
||||
/// </summary>
|
||||
/// <param name="thread">The conversation thread to use or create.</param>
|
||||
/// <param name="inputMessages">The input messages to use.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="runOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A tuple containing the thread, chat options, and thread messages.</returns>
|
||||
private async Task<(ChatClientAgentThread thread, ChatOptions? chatOptions, List<ChatMessage> threadMessages)> PrepareThreadAndMessagesAsync(
|
||||
private async Task<(ChatClientAgentThread, ChatOptions?, List<ChatMessage>)> PrepareThreadAndMessagesAsync(
|
||||
AgentThread? thread,
|
||||
IReadOnlyCollection<ChatMessage> inputMessages,
|
||||
AgentRunOptions? options,
|
||||
AgentRunOptions? runOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Retrieve chat options from the provided AgentRunOptions if available.
|
||||
ChatOptions? chatOptions = (options as ChatClientAgentRunOptions)?.ChatOptions;
|
||||
ChatOptions? chatOptions = this.CreateConfiguredChatOptions(runOptions);
|
||||
|
||||
var chatClientThread = this.ValidateOrCreateThreadType<ChatClientAgentThread>(thread, () => new());
|
||||
|
||||
@@ -192,7 +299,7 @@ public sealed class ChatClientAgent : Agent
|
||||
}
|
||||
|
||||
// Update the messages with agent instructions.
|
||||
this.UpdateThreadMessagesWithAgentInstructions(threadMessages, options);
|
||||
this.UpdateThreadMessagesWithAgentInstructions(threadMessages, runOptions);
|
||||
|
||||
// Add the input messages to the end of thread messages.
|
||||
threadMessages.AddRange(inputMessages);
|
||||
@@ -202,13 +309,13 @@ public sealed class ChatClientAgent : Agent
|
||||
if (!string.IsNullOrWhiteSpace(chatClientThread.Id) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && chatClientThread.Id != chatOptions.ConversationId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The {nameof(ChatOptions.ConversationId)} provided via {nameof(ChatOptions)} is different to the id of the provided {nameof(AgentThread)}. Only one thread id can be used for a run.");
|
||||
$"The {nameof(chatOptions.ConversationId)} provided via {nameof(Microsoft.Extensions.AI.ChatOptions)} is different to the id of the provided {nameof(AgentThread)}. Only one thread id can be used for a run.");
|
||||
}
|
||||
|
||||
// Only clone and update ChatOptions if we have an id on the thread and we don't have the same one already in ChatOptions.
|
||||
if (!string.IsNullOrWhiteSpace(chatClientThread.Id) && chatClientThread.Id != chatOptions?.ConversationId)
|
||||
{
|
||||
chatOptions = chatOptions is null ? new ChatOptions() : chatOptions.Clone();
|
||||
chatOptions ??= new();
|
||||
chatOptions.ConversationId = chatClientThread.Id;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,4 +32,22 @@ public class ChatClientAgentOptions
|
||||
/// Gets or sets the agent description.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default chatOptions to use.
|
||||
/// </summary>
|
||||
public ChatOptions? ChatOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
|
||||
/// </summary>
|
||||
internal ChatClientAgentOptions Clone()
|
||||
=> new()
|
||||
{
|
||||
Id = this.Id,
|
||||
Name = this.Name,
|
||||
Instructions = this.Instructions,
|
||||
Description = this.Description,
|
||||
ChatOptions = this.ChatOptions?.Clone()
|
||||
};
|
||||
}
|
||||
|
||||
+861
@@ -0,0 +1,861 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.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, 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, 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, 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 AgentRunOptions correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncWithMessagesPassesAgentRunOptionsCorrectlyAsync()
|
||||
{
|
||||
// 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, new() { Instructions = "base instructions" });
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
var runOptions = new AgentRunOptions { AdditionalInstructions = "additional instructions" };
|
||||
|
||||
// Act
|
||||
await ChatClientAgentExtensions.RunAsync(agent, messages, agentRunOptions: runOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(capturedMessages, m => m.Text == "base instructions" && m.Role == ChatRole.System);
|
||||
Assert.Contains(capturedMessages, m => m.Text == "additional instructions" && m.Role == ChatRole.System);
|
||||
Assert.Contains(capturedMessages, m => m.Text == "test" && m.Role == ChatRole.User);
|
||||
Assert.All(capturedChatOptions, Assert.Null);
|
||||
}
|
||||
|
||||
/// <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, 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, 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, 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, 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, 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, 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);
|
||||
Assert.Contains(capturedMessages, m => m.Text == "test instructions" && m.Role == ChatRole.System);
|
||||
}
|
||||
|
||||
/// <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, 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, new() { Instructions = "base instructions" });
|
||||
const string TestPrompt = "test prompt";
|
||||
var runOptions = new AgentRunOptions { AdditionalInstructions = "additional instructions" };
|
||||
|
||||
// Act
|
||||
await ChatClientAgentExtensions.RunAsync(agent, TestPrompt, agentRunOptions: runOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(capturedMessages, m => m.Text == "base instructions" && m.Role == ChatRole.System);
|
||||
Assert.Contains(capturedMessages, m => m.Text == "additional instructions" && m.Role == ChatRole.System);
|
||||
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, 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, 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, new() { Instructions = "test instructions" });
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test message") };
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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, 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, new() { Instructions = "test instructions" });
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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, new() { Instructions = "test instructions" });
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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, 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, new() { Instructions = "test instructions" });
|
||||
const string TestPrompt = "test prompt";
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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, 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, 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, new() { Instructions = "test instructions" });
|
||||
const string TestPrompt = "test prompt";
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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);
|
||||
Assert.Contains(capturedMessages, m => m.Text == "test instructions" && m.Role == ChatRole.System);
|
||||
}
|
||||
|
||||
/// <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, new() { Instructions = "test instructions" });
|
||||
const string TestPrompt = "test prompt";
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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, new() { Instructions = "test instructions" });
|
||||
const string TestPrompt = "test prompt";
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
// Act
|
||||
var updates = new List<ChatResponseUpdate>();
|
||||
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, 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
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.UnitTests.ChatCompletion;
|
||||
|
||||
public class ChatClientAgentRunOptionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify that ChatClientAgentRunOptions constructor works with null source and null chatOptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConstructorWorksWithNullSourceAndNullChatOptions()
|
||||
{
|
||||
// Act
|
||||
var runOptions = new ChatClientAgentRunOptions();
|
||||
|
||||
// Assert
|
||||
Assert.Null(runOptions.OnIntermediateMessages);
|
||||
Assert.Null(runOptions.AdditionalInstructions);
|
||||
Assert.Null(runOptions.ChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatClientAgentRunOptions constructor works with null source and provided chatOptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConstructorWorksWithNullSourceAndProvidedChatOptions()
|
||||
{
|
||||
// Arrange
|
||||
var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
|
||||
|
||||
// Act
|
||||
var runOptions = new ChatClientAgentRunOptions(null, chatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Null(runOptions.OnIntermediateMessages);
|
||||
Assert.Null(runOptions.AdditionalInstructions);
|
||||
Assert.Same(chatOptions, runOptions.ChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatClientAgentRunOptions constructor copies properties from source AgentRunOptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConstructorCopiesPropertiesFromSourceAgentRunOptions()
|
||||
{
|
||||
// Arrange
|
||||
var sourceRunOptions = new AgentRunOptions
|
||||
{
|
||||
AdditionalInstructions = "additional instructions",
|
||||
OnIntermediateMessages = messages => Task.CompletedTask
|
||||
};
|
||||
var chatOptions = new ChatOptions { MaxOutputTokens = 200 };
|
||||
|
||||
// Act
|
||||
var runOptions = new ChatClientAgentRunOptions(sourceRunOptions, chatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Same(sourceRunOptions.OnIntermediateMessages, runOptions.OnIntermediateMessages);
|
||||
Assert.Equal("additional instructions", runOptions.AdditionalInstructions);
|
||||
Assert.Same(chatOptions, runOptions.ChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatClientAgentRunOptions constructor works with source but null chatOptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConstructorWorksWithSourceButNullChatOptions()
|
||||
{
|
||||
// Arrange
|
||||
var sourceRunOptions = new AgentRunOptions
|
||||
{
|
||||
AdditionalInstructions = "test instructions"
|
||||
};
|
||||
|
||||
// Act
|
||||
var runOptions = new ChatClientAgentRunOptions(sourceRunOptions, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test instructions", runOptions.AdditionalInstructions);
|
||||
Assert.Null(runOptions.ChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatClientAgentRunOptions ChatOptions property is set and mutable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatOptionsPropertyIsReadOnly()
|
||||
{
|
||||
// Arrange
|
||||
var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
|
||||
var runOptions = new ChatClientAgentRunOptions(null, chatOptions);
|
||||
chatOptions.MaxOutputTokens = 200; // Change the property to verify mutability
|
||||
|
||||
// Act & Assert
|
||||
Assert.Same(chatOptions, runOptions.ChatOptions);
|
||||
|
||||
// Verify that the property doesn't have a setter by checking if it's the same instance
|
||||
var retrievedOptions = runOptions.ChatOptions!;
|
||||
Assert.Same(chatOptions, retrievedOptions);
|
||||
Assert.Equal(200, retrievedOptions.MaxOutputTokens); // Ensure the change is reflected
|
||||
}
|
||||
}
|
||||
@@ -623,6 +623,405 @@ public class ChatClientAgentTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region ChatOptions Property Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions property returns null when agent options are null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatOptionsReturnsNullWhenAgentOptionsAreNull()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
ChatClientAgent agent = new(chatClient, null);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Null(agent.ChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions property returns null when agent options ChatOptions is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatOptionsReturnsNullWhenAgentOptionsChatOptionsIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var agentOptions = new ChatClientAgentOptions { ChatOptions = null };
|
||||
ChatClientAgent agent = new(chatClient, agentOptions);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Null(agent.ChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions property returns a cloned copy when agent options have ChatOptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ChatOptionsReturnsClonedCopyWhenAgentOptionsHaveChatOptions()
|
||||
{
|
||||
// Arrange
|
||||
var chatClient = new Mock<IChatClient>().Object;
|
||||
var originalChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.5f };
|
||||
var agentOptions = new ChatClientAgentOptions { ChatOptions = originalChatOptions };
|
||||
ChatClientAgent agent = new(chatClient, agentOptions);
|
||||
|
||||
// Act
|
||||
var returnedChatOptions = agent.ChatOptions;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(returnedChatOptions);
|
||||
Assert.NotSame(originalChatOptions, returnedChatOptions); // Should be a different instance (cloned)
|
||||
Assert.Equal(originalChatOptions.MaxOutputTokens, returnedChatOptions.MaxOutputTokens);
|
||||
Assert.Equal(originalChatOptions.Temperature, returnedChatOptions.Temperature);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ChatOptions Merging Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging works when agent has ChatOptions but request doesn't.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f };
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new()
|
||||
{
|
||||
Instructions = "test instructions",
|
||||
ChatOptions = agentChatOptions
|
||||
});
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.Equal(100, capturedChatOptions.MaxOutputTokens);
|
||||
Assert.Equal(0.7f, capturedChatOptions.Temperature);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging works when request has ChatOptions but agent doesn't.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingUsesRequestOptionsWhenAgentHasNoneAsync()
|
||||
{
|
||||
// Arrange
|
||||
var requestChatOptions = new ChatOptions { MaxOutputTokens = 200, Temperature = 0.3f };
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages, chatOptions: requestChatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.Equivalent(requestChatOptions, capturedChatOptions); // Should be the same instance since no merging needed
|
||||
Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
|
||||
Assert.Equal(0.3f, capturedChatOptions.Temperature);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging prioritizes request options over agent options.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agentChatOptions = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 100,
|
||||
Temperature = 0.7f,
|
||||
TopP = 0.9f,
|
||||
ModelId = "agent-model"
|
||||
};
|
||||
var requestChatOptions = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 200,
|
||||
Temperature = 0.3f
|
||||
// TopP and ModelId not set, should use agent values
|
||||
};
|
||||
var expectedChatOptionsMerge = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 200, // Request value takes priority
|
||||
Temperature = 0.3f, // Request value takes priority
|
||||
TopP = 0.9f, // Agent value used when request doesn't specify
|
||||
ModelId = "agent-model" // Agent value used when request doesn't specify
|
||||
};
|
||||
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new()
|
||||
{
|
||||
Instructions = "test instructions",
|
||||
ChatOptions = agentChatOptions
|
||||
});
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages, chatOptions: requestChatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the same instance (modified in place)
|
||||
Assert.Equal(200, capturedChatOptions.MaxOutputTokens); // Request value takes priority
|
||||
Assert.Equal(0.3f, capturedChatOptions.Temperature); // Request value takes priority
|
||||
Assert.Equal(0.9f, capturedChatOptions.TopP); // Agent value used when request doesn't specify
|
||||
Assert.Equal("agent-model", capturedChatOptions.ModelId); // Agent value used when request doesn't specify
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging returns null when both agent and request have no ChatOptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingReturnsNullWhenBothAgentAndRequestHaveNoneAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Null(capturedChatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging concatenates Tools from agent and request.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingConcatenatesToolsFromAgentAndRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agentTool = AIFunctionFactory.Create(() => "agent tool");
|
||||
var requestTool = AIFunctionFactory.Create(() => "request tool");
|
||||
|
||||
var agentChatOptions = new ChatOptions
|
||||
{
|
||||
Tools = [agentTool]
|
||||
};
|
||||
var requestChatOptions = new ChatOptions
|
||||
{
|
||||
Tools = [requestTool]
|
||||
};
|
||||
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new()
|
||||
{
|
||||
Instructions = "test instructions",
|
||||
ChatOptions = agentChatOptions
|
||||
});
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages, chatOptions: requestChatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.NotNull(capturedChatOptions.Tools);
|
||||
Assert.Equal(2, capturedChatOptions.Tools.Count);
|
||||
// Request tools should come first, then agent tools
|
||||
Assert.Contains(requestTool, capturedChatOptions.Tools);
|
||||
Assert.Contains(agentTool, capturedChatOptions.Tools);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging uses agent Tools when request has no Tools.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingUsesAgentToolsWhenRequestHasNoToolsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agentTool = AIFunctionFactory.Create(() => "agent tool");
|
||||
|
||||
var agentChatOptions = new ChatOptions
|
||||
{
|
||||
Tools = [agentTool]
|
||||
};
|
||||
var requestChatOptions = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 100
|
||||
// No Tools specified
|
||||
};
|
||||
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new()
|
||||
{
|
||||
Instructions = "test instructions",
|
||||
ChatOptions = agentChatOptions
|
||||
});
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages, chatOptions: requestChatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.NotNull(capturedChatOptions.Tools);
|
||||
Assert.Single(capturedChatOptions.Tools);
|
||||
Assert.Contains(agentTool, capturedChatOptions.Tools); // Should contain the agent's tool
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that ChatOptions merging handles all scalar properties correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ChatOptionsMergingHandlesAllScalarPropertiesCorrectlyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agentChatOptions = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 100,
|
||||
Temperature = 0.7f,
|
||||
TopP = 0.9f,
|
||||
TopK = 50,
|
||||
PresencePenalty = 0.1f,
|
||||
FrequencyPenalty = 0.2f,
|
||||
ModelId = "agent-model",
|
||||
Seed = 12345,
|
||||
ConversationId = "agent-conversation",
|
||||
AllowMultipleToolCalls = true,
|
||||
StopSequences = ["agent-stop"]
|
||||
};
|
||||
var requestChatOptions = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 200,
|
||||
Temperature = 0.3f,
|
||||
// Other properties not set, should use agent values
|
||||
StopSequences = ["request-stop"]
|
||||
};
|
||||
|
||||
var expectedChatOptionsMerge = new ChatOptions
|
||||
{
|
||||
MaxOutputTokens = 200,
|
||||
Temperature = 0.3f,
|
||||
|
||||
// Agent value used when request doesn't specify
|
||||
TopP = 0.9f,
|
||||
TopK = 50,
|
||||
PresencePenalty = 0.1f,
|
||||
FrequencyPenalty = 0.2f,
|
||||
ModelId = "agent-model",
|
||||
Seed = 12345,
|
||||
ConversationId = "agent-conversation",
|
||||
AllowMultipleToolCalls = true,
|
||||
|
||||
// Merged StopSequences
|
||||
StopSequences = ["request-stop", "agent-stop"]
|
||||
};
|
||||
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, new()
|
||||
{
|
||||
Instructions = "test instructions",
|
||||
ChatOptions = agentChatOptions
|
||||
});
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(messages, chatOptions: requestChatOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the equivalent instance (modified in place)
|
||||
|
||||
// Request values should take priority
|
||||
Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
|
||||
Assert.Equal(0.3f, capturedChatOptions.Temperature);
|
||||
|
||||
// Merge StopSequences
|
||||
Assert.Equal(["request-stop", "agent-stop"], capturedChatOptions.StopSequences);
|
||||
|
||||
// Agent values should be used when request doesn't specify
|
||||
Assert.Equal(0.9f, capturedChatOptions.TopP);
|
||||
Assert.Equal(50, capturedChatOptions.TopK);
|
||||
Assert.Equal(0.1f, capturedChatOptions.PresencePenalty);
|
||||
Assert.Equal(0.2f, capturedChatOptions.FrequencyPenalty);
|
||||
Assert.Equal("agent-model", capturedChatOptions.ModelId);
|
||||
Assert.Equal(12345, capturedChatOptions.Seed);
|
||||
Assert.Equal("agent-conversation", capturedChatOptions.ConversationId);
|
||||
Assert.Equal(true, capturedChatOptions.AllowMultipleToolCalls);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RunStreamingAsync Tests
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user