.NET: Merge AgentRunOptions.AdditionalProperties into ChatOptions.AdditionalProperties (#3184)

* Merge AgentRunOptions.AdditionalProperties into ChatOptions.AdditionalProperties

* Fix namespace and typo.
This commit is contained in:
westey
2026-01-15 12:15:24 +00:00
committed by GitHub
Unverified
parent 15d0c34d9f
commit 3dbdecedda
3 changed files with 461 additions and 435 deletions
@@ -519,16 +519,16 @@ public sealed partial class ChatClientAgent : AIAgent
{
ChatOptions? requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone();
// If no agent chat options were provided, return the request chat options as is.
// If no agent chat options were provided, return the request chat options with just agent run options overrides.
if (this._agentOptions?.ChatOptions is null)
{
return GetContinuationTokenAndApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
return ApplyAgentRunOptionsOverrides(requestChatOptions, runOptions);
}
// If no request chat options were provided, use the agent's chat options clone.
// If no request chat options were provided, use the agent's chat options clone with agent run options overrides.
if (requestChatOptions is null)
{
return GetContinuationTokenAndApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions);
return ApplyAgentRunOptionsOverrides(this._agentOptions?.ChatOptions.Clone(), runOptions);
}
// If both are present, we need to merge them.
@@ -557,9 +557,9 @@ public sealed partial class ChatClientAgent : AIAgent
// 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 propertyKey in this._agentOptions.ChatOptions.AdditionalProperties.Keys)
foreach (var kvp in this._agentOptions.ChatOptions.AdditionalProperties)
{
_ = requestChatOptions.AdditionalProperties.TryAdd(propertyKey, this._agentOptions.ChatOptions.AdditionalProperties[propertyKey]);
_ = requestChatOptions.AdditionalProperties.TryAdd(kvp.Key, kvp.Value);
}
}
else
@@ -624,9 +624,9 @@ public sealed partial class ChatClientAgent : AIAgent
}
}
return GetContinuationTokenAndApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
return ApplyAgentRunOptionsOverrides(requestChatOptions, runOptions);
static (ChatOptions?, ChatClientAgentContinuationToken?) GetContinuationTokenAndApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
static (ChatOptions?, ChatClientAgentContinuationToken?) ApplyAgentRunOptionsOverrides(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
{
if (agentRunOptions?.AllowBackgroundResponses is not null)
{
@@ -643,6 +643,17 @@ public sealed partial class ChatClientAgent : AIAgent
chatOptions.ContinuationToken = agentContinuationToken!.InnerToken;
}
// Add/Replace any additional properties from the AgentRunOptions, since they should always take precedence.
if (agentRunOptions?.AdditionalProperties is { Count: > 0 })
{
chatOptions ??= new ChatOptions();
chatOptions.AdditionalProperties ??= new();
foreach (var kvp in agentRunOptions.AdditionalProperties)
{
chatOptions.AdditionalProperties[kvp.Key] = kvp.Value;
}
}
return (chatOptions, agentContinuationToken);
}
}
@@ -1120,433 +1120,6 @@ public partial class ChatClientAgentTests
#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, Instructions = "test instructions" };
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, options: new()
{
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);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
[Fact]
public async Task ChatOptionsMergingUsesAgentOptionsConstructorWhenRequestHasNoneAsync()
{
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, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
/// <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, Instructions = "test instructions" };
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);
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
/// <summary>
/// Verify that ChatOptions merging prioritizes request options over agent options.
/// </summary>
[Fact]
public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsync()
{
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
ModelId = "agent-model",
AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "agent-value" }
};
var requestChatOptions = new ChatOptions
{
// TopP and ModelId not set, should use agent values
MaxOutputTokens = 200,
Temperature = 0.3f,
AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "request-value" },
Instructions = "request instructions"
};
var expectedChatOptionsMerge = new ChatOptions
{
MaxOutputTokens = 200, // Request value takes priority
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
Instructions = "test instructions\nrequest instructions" // Request is in addition to agent instructions
};
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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.NotNull(capturedChatOptions.AdditionalProperties);
Assert.Equal("request-value", capturedChatOptions.AdditionalProperties["key"]); // 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);
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
{
Instructions = "test instructions",
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
{
// No Tools specified
MaxOutputTokens = 100
};
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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 uses RawRepresentationFactory from request first, with fallback to agent.
/// </summary>
[Theory]
[InlineData("MockAgentSetting", "MockRequestSetting", "MockRequestSetting")]
[InlineData("MockAgentSetting", null, "MockAgentSetting")]
[InlineData(null, "MockRequestSetting", "MockRequestSetting")]
public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsync(string? agentSetting, string? requestSetting, string expectedSetting)
{
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
RawRepresentationFactory = _ => agentSetting
};
var requestChatOptions = new ChatOptions
{
RawRepresentationFactory = _ => requestSetting
};
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.RawRepresentationFactory);
Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
}
/// <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,
Instructions = "agent instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
AllowMultipleToolCalls = true,
StopSequences = ["agent-stop"]
};
var requestChatOptions = new ChatOptions
{
MaxOutputTokens = 200,
Temperature = 0.3f,
Instructions = "request instructions",
// 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,
Instructions = "agent instructions\nrequest instructions",
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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 GetService Method Tests
/// <summary>
@@ -0,0 +1,442 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains tests for <see cref="ChatOptions"/> merging in <see cref="ChatClientAgent"/>.
/// </summary>
public class ChatClientAgent_ChatOptionsMergingTests
{
/// <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, Instructions = "test instructions" };
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, options: new()
{
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);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
[Fact]
public async Task ChatOptionsMergingUsesAgentOptionsConstructorWhenRequestHasNoneAsync()
{
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, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
/// <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, Instructions = "test instructions" };
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);
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
/// <summary>
/// Verify that <see cref="ChatOptions"/> merging prioritizes <see cref="AgentRunOptions"/> over request <see cref="ChatOptions"/> and that in turn over agent level <see cref="ChatOptions"/>.
/// </summary>
[Fact]
public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsync()
{
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
ModelId = "agent-model",
AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "agent-value", ["key3"] = "agent-value" }
};
var requestChatOptions = new ChatOptions
{
// TopP and ModelId not set, should use agent values
MaxOutputTokens = 200,
Temperature = 0.3f,
AdditionalProperties = new AdditionalPropertiesDictionary { ["key2"] = "request-value", ["key3"] = "request-value" },
Instructions = "request instructions"
};
var agentRunOptionsAdditionalProperties = new AdditionalPropertiesDictionary { ["key3"] = "runoptions-value" };
var expectedChatOptionsMerge = new ChatOptions
{
MaxOutputTokens = 200, // Request value takes priority
Temperature = 0.3f, // Request value takes priority
// Check that each level of precedence is respected in AdditionalProperties
AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "request-value", ["key3"] = "runoptions-value" },
TopP = 0.9f, // 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();
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions) { AdditionalProperties = agentRunOptionsAdditionalProperties });
// 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.NotNull(capturedChatOptions.AdditionalProperties);
Assert.Equal("agent-value", capturedChatOptions.AdditionalProperties["key1"]); // Agent value used when request doesn't specify
Assert.Equal("request-value", capturedChatOptions.AdditionalProperties["key2"]); // Request ChatOptions value takes priority over agent ChatOptions value
Assert.Equal("runoptions-value", capturedChatOptions.AdditionalProperties["key3"]); // Run options value takes priority over request and agent ChatOptions values
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);
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
{
Instructions = "test instructions",
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
{
// No Tools specified
MaxOutputTokens = 100
};
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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 uses RawRepresentationFactory from request first, with fallback to agent.
/// </summary>
[Theory]
[InlineData("MockAgentSetting", "MockRequestSetting", "MockRequestSetting")]
[InlineData("MockAgentSetting", null, "MockAgentSetting")]
[InlineData(null, "MockRequestSetting", "MockRequestSetting")]
public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsync(string? agentSetting, string? requestSetting, string expectedSetting)
{
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
RawRepresentationFactory = _ => agentSetting
};
var requestChatOptions = new ChatOptions
{
RawRepresentationFactory = _ => requestSetting
};
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.RawRepresentationFactory);
Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
}
/// <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,
Instructions = "agent instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
AllowMultipleToolCalls = true,
StopSequences = ["agent-stop"]
};
var requestChatOptions = new ChatOptions
{
MaxOutputTokens = 200,
Temperature = 0.3f,
Instructions = "request instructions",
// 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,
Instructions = "agent instructions\nrequest instructions",
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, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(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);
}
}