Add ChatClientAgent constructor overload (#188)

* Add ChatClientAgent constructor overload

* Add unit tests for new constructor

* Up code coverage with extra tests

* Address PR comments.

* Address PR comment

* Add additional test to increase code coverage.
This commit is contained in:
westey
2025-07-16 14:36:17 +01:00
committed by GitHub
Unverified
parent e6d611abe4
commit 95a3264c8b
17 changed files with 276 additions and 107 deletions
@@ -9,7 +9,7 @@
<RootNamespace>GettingStarted</RootNamespace>
<OutputType>Library</OutputType>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CA1707;CA1716;IDE0009;IDE1006;</NoWarn>
<NoWarn>$(NoWarn);CA1707;CA1716;IDE0009;IDE1006;OPENAI001;</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedSamples>true</InjectSharedSamples>
</PropertyGroup>
@@ -29,12 +29,7 @@ public sealed class ChatClientAgent_With_AzureOpenAIChatCompletion(ITestOutputHe
.AsIChatClient();
// Define the agent
ChatClientAgent agent =
new(chatClient, new()
{
Name = JokerName,
Instructions = JokerInstructions,
});
ChatClientAgent agent = new(chatClient, JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
@@ -24,12 +24,7 @@ public sealed class ChatClientAgent_With_OpenAIChatCompletion(ITestOutputHelper
.AsIChatClient();
// Define the agent
ChatClientAgent agent =
new(chatClient, new()
{
Name = JokerName,
Instructions = JokerInstructions,
});
ChatClientAgent agent = new(chatClient, JokerInstructions, JokerName);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
@@ -6,8 +6,6 @@ using Microsoft.Shared.Samples;
using OpenAI;
using OpenAI.Responses;
#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
namespace Providers;
/// <summary>
@@ -18,29 +16,63 @@ public sealed class ChatClientAgent_With_OpenAIResponsesChatCompletion(ITestOutp
private const string JokerName = "Joker";
private const string JokerInstructions = "You are good at telling jokes.";
[Theory]
[InlineData(false)] // This will use in-memory messages to store the thread state.
[InlineData(true)] // This will use the conversation id to reference the thread state on the server side.
public async Task RunWithChatCompletion(bool useConversationIdThread)
/// <summary>
/// This will use the conversation id to reference the thread state on the server side.
/// </summary>
[Fact]
public async Task RunWithChatCompletionServiceManagedThread()
{
// Get the chat client to use for the agent.
using var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient();
// Define the agent
ChatClientAgent agent = new(chatClient, JokerInstructions, JokerName);
// Start a new thread for the agent conversation based on the type.
AgentThread thread = agent.GetNewThread();
// Respond to user input.
await RunAgentAsync("Tell me a joke about a pirate.");
await RunAgentAsync("Now add some emojis to the joke.");
// Local function to invoke agent and display the conversation messages for the thread.
async Task RunAgentAsync(string input)
{
this.WriteUserMessage(input);
var response = await agent.RunAsync(input, thread);
this.WriteResponseOutput(response);
}
}
/// <summary>
/// This will use in-memory messages to store the thread state.
/// </summary>
[Fact]
public async Task RunWithChatCompletionInMemoryThread()
{
// Get the chat client to use for the agent.
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient();
// Define the agent
ChatClientAgent agent =
new(chatClient, new()
new(chatClient, options: new()
{
Name = JokerName,
Instructions = JokerInstructions,
ChatOptions = new ChatOptions
{
RawRepresentationFactory = (_) => new ResponseCreationOptions() { StoredOutputEnabled = useConversationIdThread }
// We can use the RawRepresentationFactory to provide Response service specific
// options. Here we can indicate that we do not want the service to store the
// conversation in a service managed thread.
RawRepresentationFactory = (_) => new ResponseCreationOptions() { StoredOutputEnabled = false }
}
});
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
// Start a new thread for the agent conversation based on the type.
AgentThread thread = agent.GetNewThread();
@@ -33,11 +33,7 @@ public sealed class Step01_ChatClientAgent_Running(ITestOutputHelper output) : A
IChatClient chatClient = base.GetChatClient(provider);
// Define the agent
Agent agent = new ChatClientAgent(chatClient, options: new()
{
Name = ParrotName,
Instructions = ParrotInstructions,
});
Agent agent = new ChatClientAgent(chatClient, ParrotInstructions, ParrotName);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Fortune favors the bold."));
@@ -12,12 +12,33 @@ namespace Steps;
/// </summary>
public sealed class Step02_ChatClientAgent_UsingFunctionTools(ITestOutputHelper output) : AgentSample(output)
{
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
public async Task RunningWithToolsBasic(ChatClientProviders provider)
{
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Get the chat client to use for the agent.
var chatClient = base.GetChatClient(provider);
// Define the agent and add the GetSpecials tool.
var agent = new ChatClientAgent(
chatClient,
instructions: "Answer questions about the menu.",
tools: [AIFunctionFactory.Create(menuTools.GetSpecials)]);
// Respond to user input, invoking functions where appropriate.
Console.WriteLine(await agent.RunAsync("What is the special soup and its price?"));
}
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
public async Task RunningWithTools(ChatClientProviders provider)
{
// Creating a Menu Tools to be used by the agent.
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Define the options for the chat client agent.
@@ -65,7 +86,7 @@ public sealed class Step02_ChatClientAgent_UsingFunctionTools(ITestOutputHelper
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
public async Task StreamingRunWithTools(ChatClientProviders provider)
{
// Creating a Menu Tools to be used by the agent.
// Creating a MenuTools instance to be used by the agent.
var menuTools = new MenuTools();
// Define the options for the chat client agent.
@@ -105,7 +105,6 @@ public sealed class Step03_ChatClientAgent_UsingCodeInterpreterTools(ITestOutput
/// <returns>The code interpreter output as a string.</returns>
private static string? GetCodeInterpreterOutput(object rawRepresentation, ChatClientProviders provider)
{
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
switch (provider)
{
case ChatClientProviders.OpenAIAssistant
@@ -114,7 +113,6 @@ public sealed class Step03_ChatClientAgent_UsingCodeInterpreterTools(ITestOutput
string.Empty,
stepDetails.CodeInterpreterOutputs.SelectMany(l => l.Logs)
)}";
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
case ChatClientProviders.AzureAIAgentsPersistent
when rawRepresentation is Azure.AI.Agents.Persistent.RunStepDetailsUpdate stepDetails:
@@ -25,9 +25,35 @@ public sealed class ChatClientAgent : Agent
/// Initializes a new instance of the <see cref="ChatClientAgent"/> class.
/// </summary>
/// <param name="chatClient">The chat client to use for invoking the agent.</param>
/// <param name="options">Optional agent options to configure the agent.</param>
/// <param name="instructions">Optional instructions for the agent.</param>
/// <param name="name">Optional name for the agent.</param>
/// <param name="description">Optional description for the agent.</param>
/// <param name="tools">Optional list of tools that the agent can use during invocation.</param>
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options = null, ILoggerFactory? loggerFactory = null)
public ChatClientAgent(IChatClient chatClient, string? instructions = null, string? name = null, string? description = null, IList<AITool>? tools = null, ILoggerFactory? loggerFactory = null)
: this(
chatClient,
new ChatClientAgentOptions()
{
Name = name,
Description = description,
Instructions = instructions,
ChatOptions = tools is null ? null : new ChatOptions()
{
Tools = tools,
}
},
loggerFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgent"/> class.
/// </summary>
/// <param name="chatClient">The chat client to use for invoking the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null)
{
Throw.IfNull(chatClient);
@@ -73,7 +73,7 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture
return new ChatClientAgent(
this._persistentAgentsClient.AsIChatClient(persistentAgent.Id),
new()
options: new()
{
Id = persistentAgent.Id,
ChatOptions = new() { Tools = aiTools }
@@ -27,7 +27,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test message") };
// Act & Assert - Should not throw
@@ -59,7 +59,7 @@ public class ChatClientAgentExtensionsTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
@@ -82,7 +82,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act - Call extension method (should not throw)
@@ -115,7 +115,7 @@ public class ChatClientAgentExtensionsTests
})
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "base instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
var runOptions = new AgentRunOptions();
@@ -142,7 +142,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
var thread = agent.GetNewThread();
@@ -177,7 +177,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ThrowsAsync(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act & Assert
@@ -202,7 +202,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
@@ -244,7 +244,7 @@ public class ChatClientAgentExtensionsTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
@@ -260,7 +260,7 @@ public class ChatClientAgentExtensionsTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
@@ -286,7 +286,7 @@ public class ChatClientAgentExtensionsTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
@@ -312,7 +312,7 @@ public class ChatClientAgentExtensionsTests
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" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
@@ -345,7 +345,7 @@ public class ChatClientAgentExtensionsTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "base instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" });
const string TestPrompt = "test prompt";
var runOptions = new AgentRunOptions();
@@ -371,7 +371,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
var thread = agent.GetNewThread();
@@ -406,7 +406,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ThrowsAsync(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act & Assert
@@ -437,7 +437,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test message") };
// Act
@@ -488,7 +488,7 @@ public class ChatClientAgentExtensionsTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
@@ -518,7 +518,7 @@ public class ChatClientAgentExtensionsTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
@@ -554,7 +554,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
var thread = agent.GetNewThread();
@@ -592,7 +592,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Throws(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act & Assert
@@ -629,7 +629,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
@@ -680,7 +680,7 @@ public class ChatClientAgentExtensionsTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
@@ -701,7 +701,7 @@ public class ChatClientAgentExtensionsTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(async () =>
@@ -734,7 +734,7 @@ public class ChatClientAgentExtensionsTests
capturedMessages.AddRange(msgs))
.Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
@@ -767,7 +767,7 @@ public class ChatClientAgentExtensionsTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 200),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act
@@ -803,7 +803,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
var thread = agent.GetNewThread();
@@ -841,7 +841,7 @@ public class ChatClientAgentExtensionsTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Throws(new OperationCanceledException());
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
const string TestPrompt = "test prompt";
// Act & Assert
@@ -21,7 +21,7 @@ public class ChatClientAgentTests
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent =
new(chatClient,
new()
options: new()
{
Id = "test-agent-id",
Name = "test name",
@@ -54,7 +54,7 @@ public class ChatClientAgentTests
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "I'm here!")]));
ChatClientAgent agent =
new(mockService.Object, new()
new(mockService.Object, options: new()
{
Instructions = "test instructions"
});
@@ -90,7 +90,7 @@ public class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(chatClient, options: new() { Instructions = "test instructions" });
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => agent.RunAsync((IReadOnlyCollection<ChatMessage>)null!));
@@ -111,7 +111,7 @@ public class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
// Act
await agent.RunAsync([new(ChatRole.User, "test")], chatOptions: chatOptions);
@@ -139,7 +139,7 @@ public class ChatClientAgentTests
null,
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var runOptions = new AgentRunOptions();
// Act
@@ -172,7 +172,7 @@ public class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "base instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions" });
var runOptions = new AgentRunOptions();
// Act
@@ -202,7 +202,7 @@ public class ChatClientAgentTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse(responseMessages));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions", Name = "TestAgent" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions", Name = "TestAgent" });
// Act
var result = await agent.RunAsync([new(ChatRole.User, "test")]);
@@ -229,7 +229,7 @@ public class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
// Create a thread using the agent's GetNewThread method
var thread = agent.GetNewThread();
@@ -261,7 +261,7 @@ public class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = null });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = null });
// Act
await agent.RunAsync([new(ChatRole.User, "test message")]);
@@ -291,7 +291,7 @@ public class ChatClientAgentTests
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
// Act
await agent.RunAsync([]);
@@ -319,7 +319,7 @@ public class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new("ConvId");
@@ -338,7 +338,7 @@ public class ChatClientAgentTests
var chatOptions = new ChatOptions { ConversationId = "ConvId" };
Mock<IChatClient> mockService = new();
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new("ThreadId");
@@ -361,7 +361,7 @@ public class ChatClientAgentTests
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100 && opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new("ConvId");
@@ -386,7 +386,7 @@ public class ChatClientAgentTests
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
ChatClientAgentThread thread = new("ConvId");
@@ -419,7 +419,7 @@ public class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, null);
ChatClientAgent agent = new(chatClient);
// Act & Assert
Assert.NotNull(agent.Id);
@@ -469,7 +469,7 @@ public class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, null);
ChatClientAgent agent = new(chatClient);
// Act & Assert
Assert.Null(agent.Name);
@@ -513,7 +513,7 @@ public class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, null);
ChatClientAgent agent = new(chatClient);
// Act & Assert
Assert.Null(agent.Description);
@@ -557,7 +557,7 @@ public class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, null);
ChatClientAgent agent = new(chatClient);
// Act & Assert
Assert.Null(agent.Instructions);
@@ -580,6 +580,68 @@ public class ChatClientAgentTests
#endregion
#region Options params Constructor Tests
/// <summary>
/// Checks that all params are set correctly when using the constructor with optional parameters.
/// </summary>
[Fact]
public void ConstructorUsesOptionalParams()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, instructions: "TestInstructions", name: "TestName", description: "TestDescription", tools: [AIFunctionFactory.Create(() => { })]);
// Act & Assert
Assert.Equal("TestInstructions", agent.Instructions);
Assert.Equal("TestName", agent.Name);
Assert.Equal("TestDescription", agent.Description);
Assert.NotNull(agent.ChatOptions);
Assert.NotNull(agent.ChatOptions.Tools);
Assert.Single(agent.ChatOptions.Tools!);
}
/// <summary>
/// Verify that ChatOptions property returns null when no params are provided that require a ChatOptions instance.
/// </summary>
[Fact]
public void ChatOptionsReturnsNullWhenConstructorToolsNotProvided()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, instructions: "TestInstructions", name: "TestName", description: "TestDescription");
// Act & Assert
Assert.Equal("TestInstructions", agent.Instructions);
Assert.Equal("TestName", agent.Name);
Assert.Equal("TestDescription", agent.Description);
Assert.Null(agent.ChatOptions);
}
#endregion
#region Options Constructor Tests
/// <summary>
/// Checks that the various properties on <see cref="ChatClientAgent"/> are null or defaulted when not provided to the constructor.
/// </summary>
[Fact]
public void OptionsPropertiesNullOrDefaultWhenNotProvidedToConstructor()
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, options: null!);
// Act & Assert
Assert.NotNull(agent.Id);
Assert.Null(agent.Instructions);
Assert.Null(agent.Name);
Assert.Null(agent.Description);
Assert.Null(agent.ChatOptions);
}
#endregion
#region ChatOptions Property Tests
/// <summary>
@@ -590,7 +652,7 @@ public class ChatClientAgentTests
{
// Arrange
var chatClient = new Mock<IChatClient>().Object;
ChatClientAgent agent = new(chatClient, null);
ChatClientAgent agent = new(chatClient);
// Act & Assert
Assert.Null(agent.ChatOptions);
@@ -656,7 +718,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new()
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
@@ -691,7 +753,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
@@ -746,7 +808,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new()
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
@@ -785,7 +847,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new() { Instructions = "test instructions" });
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
@@ -825,7 +887,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new()
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
@@ -874,7 +936,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new()
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
@@ -891,6 +953,52 @@ public class ChatClientAgentTests
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
{
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()
{
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.RawRepresentationFactory);
Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
}
/// <summary>
/// Verify that ChatOptions merging handles all scalar properties correctly.
/// </summary>
@@ -950,7 +1058,7 @@ public class ChatClientAgentTests
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, new()
ChatClientAgent agent = new(mockService.Object, options: new()
{
Instructions = "test instructions",
ChatOptions = agentChatOptions
@@ -1007,7 +1115,7 @@ public class ChatClientAgentTests
It.IsAny<CancellationToken>())).Returns(returnUpdates.ToAsyncEnumerable());
ChatClientAgent agent =
new(mockService.Object, new()
new(mockService.Object, options: new()
{
Instructions = "test instructions"
});
@@ -69,7 +69,7 @@ public class ChatClientAgentThreadTests
.ReturnsAsync(new ChatResponse([assistantMessage]));
// Create ChatClientAgent with the mocked client
var agent = new ChatClientAgent(mockChatClient.Object, new()
var agent = new ChatClientAgent(mockChatClient.Object, options: new()
{
Instructions = "You are a helpful assistant"
});
@@ -190,7 +190,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "response")]));
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
// Act
var thread = agent.GetNewThread();
@@ -210,7 +210,7 @@ public class ChatClientAgentThreadTests
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
// Act
var thread1 = agent.GetNewThread();
@@ -242,7 +242,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([assistantMessage]) { ConversationId = responseConversationId });
var agent = new ChatClientAgent(mockChatClient.Object, new() { Instructions = "Test instructions" });
var agent = new ChatClientAgent(mockChatClient.Object, options: new() { Instructions = "Test instructions" });
// Act
var thread = agent.GetNewThread();
@@ -298,7 +298,7 @@ public class ChatClientAgentThreadTests
.ReturnsAsync(new ChatResponse([messages[1]]))
.ReturnsAsync(new ChatResponse([messages[3]]));
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act - Add messages through multiple agent runs
@@ -349,7 +349,7 @@ public class ChatClientAgentThreadTests
.Returns(returnUpdates.ToAsyncEnumerable());
// Create ChatClientAgent with the mocked client
var agent = new ChatClientAgent(mockChatClient.Object, new()
var agent = new ChatClientAgent(mockChatClient.Object, options: new()
{
Instructions = "You are a helpful assistant"
});
@@ -415,7 +415,7 @@ public class ChatClientAgentThreadTests
.Returns(firstReturnUpdates.ToAsyncEnumerable())
.Returns(secondReturnUpdates.ToAsyncEnumerable());
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act - Make two streaming calls
@@ -486,7 +486,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.Returns(streamingUpdates.ToAsyncEnumerable());
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act - First, make a regular call to populate the thread
@@ -538,7 +538,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.Returns(returnUpdates.ToAsyncEnumerable());
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act - Run the agent with streaming that returns no updates
@@ -593,7 +593,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.Returns(returnUpdates.ToAsyncEnumerable());
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act - Run the agent with streaming that returns multiple updates
@@ -638,7 +638,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Streaming failed"));
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act & Assert - Verify that streaming throws an exception
@@ -688,7 +688,7 @@ public class ChatClientAgentThreadTests
It.IsAny<CancellationToken>()))
.Returns(GetUpdatesWithExceptionAsync());
var agent = new ChatClientAgent(mockChatClient.Object, new());
var agent = new ChatClientAgent(mockChatClient.Object, options: new());
var thread = agent.GetNewThread();
// Act & Assert - Verify that streaming throws an exception after some updates
@@ -4,6 +4,7 @@
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
<NoWarn>$(NoWarn);OPENAI001;</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -13,8 +13,6 @@ using Shared.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public class OpenAIAssistantFixture : IChatClientAgentFixture
{
private static readonly OpenAIConfiguration s_config = TestConfiguration.LoadSection<OpenAIConfiguration>();
@@ -67,7 +65,7 @@ public class OpenAIAssistantFixture : IChatClientAgentFixture
return new ChatClientAgent(
this._assistantClient.AsIChatClient(assistant.Value.Id),
new()
options: new()
{
Id = assistant.Value.Id,
ChatOptions = new() { Tools = aiTools }
@@ -50,7 +50,7 @@ public class OpenAIChatCompletionFixture : IChatClientAgentFixture
.GetChatClient(this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId)
.AsIChatClient();
return Task.FromResult(new ChatClientAgent(chatClient, new()
return Task.FromResult(new ChatClientAgent(chatClient, options: new()
{
Name = name,
Instructions = instructions,
@@ -4,6 +4,7 @@
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
<NoWarn>$(NoWarn);OPENAI001;</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -12,8 +12,6 @@ using OpenAI;
using OpenAI.Responses;
using Shared.IntegrationTests;
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
@@ -80,7 +78,7 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
{
return Task.FromResult(new ChatClientAgent(
this._openAIResponseClient.AsIChatClient(),
new()
options: new()
{
Name = name,
Instructions = instructions,