mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Fixes for ChatClientAgent to improve MEAI static analysis compliance (#502)
* Fixes for ChatClientAgent to improve MEAI static analysis compliance * Improve instruction handling and fix unit tests. * Address PR comments.
This commit is contained in:
committed by
GitHub
Unverified
parent
379e3b9a00
commit
938d91d1db
@@ -256,6 +256,7 @@ namespace Azure.AI.Agents.Persistent
|
||||
// Populate the run options from the ChatOptions, if provided.
|
||||
if (options is not null)
|
||||
{
|
||||
runOptions.OverrideInstructions ??= options.Instructions ?? _agent.Instructions;
|
||||
runOptions.MaxCompletionTokens ??= options.MaxOutputTokens;
|
||||
runOptions.OverrideModelName ??= options.ModelId;
|
||||
runOptions.TopP ??= options.TopP;
|
||||
|
||||
+3
-3
@@ -5,13 +5,13 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>Provides extensions for configuring <see cref="AgentInvokingChatClient"/> instances.</summary>
|
||||
/// <summary>Provides extensions for configuring <see cref="AgentInvokedChatClient"/> instances.</summary>
|
||||
public static class AgentChatClientBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables automatic function call invocation on the chat pipeline.
|
||||
/// </summary>
|
||||
/// <remarks>This works by adding an instance of <see cref="AgentInvokingChatClient"/> with default options.</remarks>
|
||||
/// <remarks>This works by adding an instance of <see cref="AgentInvokedChatClient"/> with default options.</remarks>
|
||||
/// <param name="builder">The <see cref="ChatClientBuilder"/> being used to build the chat pipeline.</param>
|
||||
/// <returns>The supplied <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
@@ -22,7 +22,7 @@ public static class AgentChatClientBuilderExtensions
|
||||
|
||||
return builder.Use((innerClient, services) =>
|
||||
{
|
||||
return new AgentInvokingChatClient(innerClient);
|
||||
return new AgentInvokedChatClient(innerClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -5,13 +5,13 @@ namespace Microsoft.Extensions.AI.Agents;
|
||||
/// <summary>
|
||||
/// Internal chat client that handle agent invocation details for the chat client pipeline.
|
||||
/// </summary>
|
||||
internal sealed class AgentInvokingChatClient : DelegatingChatClient
|
||||
internal sealed class AgentInvokedChatClient : DelegatingChatClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AgentInvokingChatClient"/> class.
|
||||
/// Initializes a new instance of the <see cref="AgentInvokedChatClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatClient">The chat client to invoke agents.</param>
|
||||
internal AgentInvokingChatClient(IChatClient chatClient)
|
||||
internal AgentInvokedChatClient(IChatClient chatClient)
|
||||
: base(chatClient)
|
||||
{
|
||||
}
|
||||
@@ -9,6 +9,8 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
#pragma warning disable S3358 // Ternary operators should not be nested
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
@@ -33,12 +35,12 @@ public sealed class ChatClientAgent : AIAgent
|
||||
public ChatClientAgent(IChatClient chatClient, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
|
||||
: this(
|
||||
chatClient,
|
||||
new ChatClientAgentOptions()
|
||||
new ChatClientAgentOptions
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Instructions = instructions,
|
||||
ChatOptions = tools is null ? null : new ChatOptions()
|
||||
ChatOptions = tools is null ? null : new ChatOptions
|
||||
{
|
||||
Tools = tools,
|
||||
}
|
||||
@@ -55,7 +57,7 @@ public sealed class ChatClientAgent : AIAgent
|
||||
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
|
||||
public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
_ = Throw.IfNull(chatClient);
|
||||
|
||||
// Options must be cloned since ChatClientAgentOptions is mutable.
|
||||
this._agentOptions = options?.Clone();
|
||||
@@ -65,13 +67,14 @@ public sealed class ChatClientAgent : AIAgent
|
||||
// Get the type of the chat client before wrapping it as an agent invoking chat client.
|
||||
this._chatClientType = chatClient.GetType();
|
||||
|
||||
this.ChatClient = chatClient.AsAgentInvokingChatClient();
|
||||
// If the user has not opted out of using our default decorators, we wrap the chat client.
|
||||
this.ChatClient = options?.UseProvidedChatClientAsIs is true ? chatClient : chatClient.AsAgentInvokedChatClient();
|
||||
|
||||
this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The underlying chat client used by the agent to invoke chat completions.
|
||||
/// Gets the underlying chat client used by the agent to invoke chat completions.
|
||||
/// </summary>
|
||||
public IChatClient ChatClient { get; }
|
||||
|
||||
@@ -101,7 +104,7 @@ public sealed class ChatClientAgent : AIAgent
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
(AgentThread safeThread, ChatOptions? chatOptions, List<ChatMessage> threadMessages) =
|
||||
await this.PrepareThreadAndMessagesAsync(thread, messages, options, cancellationToken).ConfigureAwait(false);
|
||||
@@ -190,15 +193,17 @@ public sealed class ChatClientAgent : AIAgent
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null)
|
||||
=> base.GetService(serviceType, serviceKey)
|
||||
?? (serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
|
||||
: serviceType == typeof(IChatClient) ? this.ChatClient
|
||||
: this.ChatClient.GetService(serviceType, serviceKey));
|
||||
{
|
||||
return base.GetService(serviceType, serviceKey)
|
||||
?? (serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
|
||||
: serviceType == typeof(IChatClient) ? this.ChatClient
|
||||
: this.ChatClient.GetService(serviceType, serviceKey));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread GetNewThread()
|
||||
{
|
||||
var thread = new AgentThread() { MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke() };
|
||||
var thread = new AgentThread { MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke() };
|
||||
return thread;
|
||||
}
|
||||
|
||||
@@ -235,6 +240,7 @@ public sealed class ChatClientAgent : AIAgent
|
||||
requestChatOptions.AllowMultipleToolCalls ??= this._agentOptions.ChatOptions.AllowMultipleToolCalls;
|
||||
requestChatOptions.ConversationId ??= this._agentOptions.ChatOptions.ConversationId;
|
||||
requestChatOptions.FrequencyPenalty ??= this._agentOptions.ChatOptions.FrequencyPenalty;
|
||||
requestChatOptions.Instructions ??= this._agentOptions.ChatOptions.Instructions;
|
||||
requestChatOptions.MaxOutputTokens ??= this._agentOptions.ChatOptions.MaxOutputTokens;
|
||||
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
|
||||
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
|
||||
@@ -250,7 +256,7 @@ public sealed class ChatClientAgent : AIAgent
|
||||
{
|
||||
foreach (var propertyKey in this._agentOptions.ChatOptions.AdditionalProperties.Keys)
|
||||
{
|
||||
requestChatOptions.AdditionalProperties.TryAdd(propertyKey, this._agentOptions.ChatOptions.AdditionalProperties[propertyKey]);
|
||||
_ = requestChatOptions.AdditionalProperties.TryAdd(propertyKey, this._agentOptions.ChatOptions.AdditionalProperties[propertyKey]);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -326,7 +332,7 @@ public sealed class ChatClientAgent : AIAgent
|
||||
/// <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<(AgentThread, ChatOptions?, List<ChatMessage>)> PrepareThreadAndMessagesAsync(
|
||||
private async Task<(AgentThread AgentThread, ChatOptions? ChatOptions, List<ChatMessage> ThreadMessages)> PrepareThreadAndMessagesAsync(
|
||||
AgentThread? thread,
|
||||
IReadOnlyCollection<ChatMessage> inputMessages,
|
||||
AgentRunOptions? runOptions,
|
||||
@@ -343,21 +349,27 @@ public sealed class ChatClientAgent : AIAgent
|
||||
threadMessages.Add(message);
|
||||
}
|
||||
|
||||
// Update the messages with agent instructions.
|
||||
this.UpdateThreadMessagesWithAgentInstructions(threadMessages, runOptions);
|
||||
|
||||
// Add the input messages to the end of thread messages.
|
||||
threadMessages.AddRange(inputMessages);
|
||||
|
||||
// If a user provided two different thread ids, via the thread object and options, we should throw
|
||||
// since we don't know which one to use.
|
||||
if (!string.IsNullOrWhiteSpace(thread.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && thread.ConversationId != chatOptions.ConversationId)
|
||||
if (!string.IsNullOrWhiteSpace(thread.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && thread.ConversationId != chatOptions!.ConversationId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"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.");
|
||||
$"""
|
||||
The {nameof(chatOptions.ConversationId)} provided via {nameof(Microsoft.Extensions.AI.ChatOptions)} is different to the id of the provided {nameof(AgentThread)}.
|
||||
Only one 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(this.Instructions))
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? this.Instructions : $"{this.Instructions}\n{chatOptions.Instructions}";
|
||||
}
|
||||
|
||||
// Only create or update ChatOptions if we have an id on the thread and we don't have the same one already in ChatOptions.
|
||||
if (!string.IsNullOrWhiteSpace(thread.ConversationId) && thread.ConversationId != chatOptions?.ConversationId)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
@@ -373,7 +385,9 @@ public sealed class ChatClientAgent : AIAgent
|
||||
{
|
||||
// We were passed a thread that is service managed, but we got no conversation id back from the chat client,
|
||||
// meaning the service doesn't support service managed threads, so the thread cannot be used with this service.
|
||||
#pragma warning disable S2302 // "nameof" should be used - False positive.
|
||||
throw new InvalidOperationException("Service did not return a valid conversation id when using a service managed thread.");
|
||||
#pragma warning restore S2302 // "nameof" should be used
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(responseConversationId))
|
||||
@@ -391,14 +405,6 @@ public sealed class ChatClientAgent : AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateThreadMessagesWithAgentInstructions(List<ChatMessage> threadMessages, AgentRunOptions? options)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this.Instructions))
|
||||
{
|
||||
threadMessages.Insert(0, new(ChatRole.System, this.Instructions) { AuthorName = this.Name });
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLoggingAgentName() => this.Name ?? "UnnamedAgent";
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -79,6 +79,20 @@ public class ChatClientAgentOptions
|
||||
/// </summary>
|
||||
public Func<IChatMessageStore>? ChatMessageStoreFactory { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use the provided <see cref="IChatClient"/> instance as is,
|
||||
/// without applying any default decorators.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default the <see cref="ChatClientAgent"/> applies decorators to the provided <see cref="IChatClient"/>
|
||||
/// for doing for example automatic function invocation. Setting this property to <see langword="true"/>
|
||||
/// disables adding these default decorators.
|
||||
/// Disabling is recommended if you want to decorate the <see cref="IChatClient"/> with different decorators
|
||||
/// than the default ones. The provided <see cref="IChatClient"/> instance should then already be decorated
|
||||
/// with the desired decorators.
|
||||
/// </remarks>
|
||||
public bool UseProvidedChatClientAsIs { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
|
||||
/// </summary>
|
||||
|
||||
+3
-5
@@ -11,8 +11,8 @@ public sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
/// Initializes a new instance of the <see cref="ChatClientAgentRunOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatOptions">Optional chat options to pass to the agent's invocation.</param>
|
||||
public ChatClientAgentRunOptions(ChatOptions? chatOptions = null) :
|
||||
this(null, chatOptions)
|
||||
public ChatClientAgentRunOptions(ChatOptions? chatOptions = null)
|
||||
: this(null, chatOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ public sealed class ChatClientAgentRunOptions : AgentRunOptions
|
||||
this.ChatOptions = chatOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional chat options to pass to the agent's invocation
|
||||
/// </summary>
|
||||
/// <summary>Gets or sets optional chat options to pass to the agent's invocation.</summary>
|
||||
public ChatOptions? ChatOptions { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@ namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
internal static class ChatClientExtensions
|
||||
{
|
||||
internal static IChatClient AsAgentInvokingChatClient(this IChatClient chatClient)
|
||||
internal static IChatClient AsAgentInvokedChatClient(this IChatClient chatClient)
|
||||
{
|
||||
var chatBuilder = chatClient.AsBuilder();
|
||||
|
||||
if (chatClient is not AgentInvokingChatClient agentInvokingChatClient)
|
||||
if (chatClient is not AgentInvokedChatClient agentInvokedChatClient)
|
||||
{
|
||||
chatBuilder.UseAgentInvocation();
|
||||
}
|
||||
|
||||
if (chatClient.GetService<NewFunctionInvokingChatClient>() is null)
|
||||
{
|
||||
chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
|
||||
_ = chatBuilder.Use((IChatClient innerClient, IServiceProvider services) =>
|
||||
{
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
|
||||
|
||||
+2
-5
@@ -125,9 +125,9 @@ public class ChatClientAgentExtensionsTests
|
||||
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 == "test" && m.Role == ChatRole.User);
|
||||
Assert.All(capturedChatOptions, Assert.Null);
|
||||
Assert.Single(capturedChatOptions);
|
||||
Assert.Equal("base instructions", capturedChatOptions[0].Instructions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -296,7 +296,6 @@ public class ChatClientAgentExtensionsTests
|
||||
|
||||
// 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>
|
||||
@@ -355,7 +354,6 @@ public class ChatClientAgentExtensionsTests
|
||||
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 == "test prompt" && m.Role == ChatRole.User);
|
||||
}
|
||||
|
||||
@@ -749,7 +747,6 @@ public class ChatClientAgentExtensionsTests
|
||||
// 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>
|
||||
|
||||
+28
-18
@@ -36,7 +36,7 @@ public class ChatClientAgentTests
|
||||
Assert.Equal("test description", agent.Description);
|
||||
Assert.Equal("test instructions", agent.Instructions);
|
||||
Assert.NotNull(agent.ChatClient);
|
||||
Assert.Equal("AgentInvokingChatClient", agent.ChatClient.GetType().Name);
|
||||
Assert.Equal("AgentInvokedChatClient", agent.ChatClient.GetType().Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +139,7 @@ public class ChatClientAgentTests
|
||||
null,
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
|
||||
ChatClientAgent agent = new(mockService.Object);
|
||||
var runOptions = new AgentRunOptions();
|
||||
|
||||
// Act
|
||||
@@ -158,7 +158,7 @@ public class ChatClientAgentTests
|
||||
/// Verify that RunAsync includes base instructions in messages.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncIncludesBaseInstructionsAsync()
|
||||
public async Task RunAsyncIncludesBaseInstructionsInOptionsAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
@@ -166,7 +166,7 @@ public class ChatClientAgentTests
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.Is<ChatOptions>(x => x.Instructions == "base instructions"),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
capturedMessages.AddRange(msgs))
|
||||
@@ -179,7 +179,6 @@ public class ChatClientAgentTests
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], options: runOptions);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(capturedMessages, m => m.Text == "base instructions" && m.Role == ChatRole.System);
|
||||
Assert.Contains(capturedMessages, m => m.Text == "test" && m.Role == ChatRole.User);
|
||||
}
|
||||
|
||||
@@ -238,8 +237,7 @@ public class ChatClientAgentTests
|
||||
await agent.RunAsync([new(ChatRole.User, "new message")], thread: thread);
|
||||
|
||||
// Assert
|
||||
// Should contain: instructions + new message
|
||||
Assert.Contains(capturedMessages, m => m.Text == "test instructions");
|
||||
// Should contain: new message
|
||||
Assert.Contains(capturedMessages, m => m.Text == "new message");
|
||||
}
|
||||
|
||||
@@ -298,9 +296,7 @@ public class ChatClientAgentTests
|
||||
|
||||
// Assert
|
||||
// Should only contain the instructions
|
||||
Assert.Single(capturedMessages);
|
||||
Assert.Equal("test instructions", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.System, capturedMessages[0].Role);
|
||||
Assert.Empty(capturedMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -324,7 +320,8 @@ public class ChatClientAgentTests
|
||||
AgentThread thread = new() { ConversationId = "ConvId" };
|
||||
|
||||
// Act & Assert
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], thread, chatOptions: chatOptions);
|
||||
var response = await agent.RunAsync([new(ChatRole.User, "test")], thread, chatOptions: chatOptions);
|
||||
Assert.NotNull(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -424,6 +421,7 @@ public class ChatClientAgentTests
|
||||
// Act & Assert
|
||||
Assert.NotNull(agent.Id);
|
||||
Assert.NotEmpty(agent.Id);
|
||||
|
||||
// Base implementation returns a GUID, so it should be parseable as a GUID
|
||||
Assert.True(Guid.TryParse(agent.Id, out _));
|
||||
}
|
||||
@@ -442,6 +440,7 @@ public class ChatClientAgentTests
|
||||
// Act & Assert
|
||||
Assert.NotNull(agent.Id);
|
||||
Assert.NotEmpty(agent.Id);
|
||||
|
||||
// Base implementation returns a GUID, so it should be parseable as a GUID
|
||||
Assert.True(Guid.TryParse(agent.Id, out _));
|
||||
}
|
||||
@@ -732,6 +731,7 @@ public class ChatClientAgentTests
|
||||
Assert.NotNull(capturedChatOptions);
|
||||
Assert.Equal(100, capturedChatOptions.MaxOutputTokens);
|
||||
Assert.Equal(0.7f, capturedChatOptions.Temperature);
|
||||
Assert.Equal("test instructions", capturedChatOptions.Instructions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -741,7 +741,7 @@ public class ChatClientAgentTests
|
||||
public async Task ChatOptionsMergingUsesRequestOptionsWhenAgentHasNoneAsync()
|
||||
{
|
||||
// Arrange
|
||||
var requestChatOptions = new ChatOptions { MaxOutputTokens = 200, Temperature = 0.3f };
|
||||
var requestChatOptions = new ChatOptions { MaxOutputTokens = 200, Temperature = 0.3f, Instructions = "test instructions" };
|
||||
Mock<IChatClient> mockService = new();
|
||||
ChatOptions? capturedChatOptions = null;
|
||||
mockService.Setup(
|
||||
@@ -753,7 +753,7 @@ public class ChatClientAgentTests
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
|
||||
ChatClientAgent agent = new(mockService.Object);
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
@@ -764,6 +764,7 @@ public class ChatClientAgentTests
|
||||
Assert.Equivalent(requestChatOptions, capturedChatOptions); // Should be the same instance since no merging needed
|
||||
Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
|
||||
Assert.Equal(0.3f, capturedChatOptions.Temperature);
|
||||
Assert.Equal("test instructions", capturedChatOptions.Instructions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -785,7 +786,8 @@ public class ChatClientAgentTests
|
||||
{
|
||||
MaxOutputTokens = 200,
|
||||
Temperature = 0.3f,
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" }
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" },
|
||||
Instructions = "request instructions"
|
||||
// TopP and ModelId not set, should use agent values
|
||||
};
|
||||
var expectedChatOptionsMerge = new ChatOptions
|
||||
@@ -794,7 +796,8 @@ public class ChatClientAgentTests
|
||||
Temperature = 0.3f, // Request value takes priority
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary() { ["key"] = "request-value" }, // Request value takes priority
|
||||
TopP = 0.9f, // Agent value used when request doesn't specify
|
||||
ModelId = "agent-model" // Agent value used when request doesn't specify
|
||||
ModelId = "agent-model", // Agent value used when request doesn't specify
|
||||
Instructions = "test instructions\nrequest instructions" // Request is in addition to agent instructions
|
||||
};
|
||||
|
||||
Mock<IChatClient> mockService = new();
|
||||
@@ -847,7 +850,7 @@ public class ChatClientAgentTests
|
||||
capturedChatOptions = opts)
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
|
||||
ChatClientAgent agent = new(mockService.Object);
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
|
||||
|
||||
// Act
|
||||
@@ -1014,6 +1017,7 @@ public class ChatClientAgentTests
|
||||
TopK = 50,
|
||||
PresencePenalty = 0.1f,
|
||||
FrequencyPenalty = 0.2f,
|
||||
Instructions = "agent instructions",
|
||||
ModelId = "agent-model",
|
||||
Seed = 12345,
|
||||
ConversationId = "agent-conversation",
|
||||
@@ -1024,6 +1028,7 @@ public class ChatClientAgentTests
|
||||
{
|
||||
MaxOutputTokens = 200,
|
||||
Temperature = 0.3f,
|
||||
Instructions = "request instructions",
|
||||
// Other properties not set, should use agent values
|
||||
StopSequences = ["request-stop"]
|
||||
};
|
||||
@@ -1038,6 +1043,7 @@ public class ChatClientAgentTests
|
||||
TopK = 50,
|
||||
PresencePenalty = 0.1f,
|
||||
FrequencyPenalty = 0.2f,
|
||||
Instructions = "test instructions\nrequest instructions",
|
||||
ModelId = "agent-model",
|
||||
Seed = 12345,
|
||||
ConversationId = "agent-conversation",
|
||||
@@ -1142,8 +1148,8 @@ public class ChatClientAgentTests
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsAssignableFrom<IChatClient>(result);
|
||||
// Note: The result will be the AgentInvokingChatClient wrapper, not the original mock
|
||||
Assert.Equal("AgentInvokingChatClient", result.GetType().Name);
|
||||
// Note: The result will be the AgentInvokedChatClient wrapper, not the original mock
|
||||
Assert.Equal("AgentInvokedChatClient", result.GetType().Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1383,6 +1389,7 @@ public class ChatClientAgentTests
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(agent, result);
|
||||
|
||||
// Verify that the ChatClient's GetService was not called for this type since base.GetService() handled it
|
||||
mockChatClient.Verify(c => c.GetService(typeof(ChatClientAgent), null), Times.Never);
|
||||
}
|
||||
@@ -1406,6 +1413,7 @@ public class ChatClientAgentTests
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Same(agent, result);
|
||||
|
||||
// Verify that the ChatClient's GetService was not called for this type since base.GetService() handled it
|
||||
mockChatClient.Verify(c => c.GetService(typeof(AIAgent), null), Times.Never);
|
||||
}
|
||||
@@ -1430,6 +1438,7 @@ public class ChatClientAgentTests
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsAssignableFrom<IChatClient>(result);
|
||||
|
||||
// Verify that the ChatClient's GetService was NOT called because IChatClient is handled by the agent itself
|
||||
mockChatClient.Verify(c => c.GetService(typeof(IChatClient), "some-key"), Times.Never);
|
||||
}
|
||||
@@ -1454,6 +1463,7 @@ public class ChatClientAgentTests
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("test-result", result);
|
||||
|
||||
// Verify that the ChatClient's GetService was called after base.GetService() returned null
|
||||
mockChatClient.Verify(c => c.GetService(typeof(string), "some-key"), Times.Once);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user