From d1d69af48299a051786a6d05add4d17a8cbd1f43 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:12:24 +0100 Subject: [PATCH] Add tests for ChatClientAgent with OpenAI responses (#82) * Add tests for ChatClientAgent with OpenAI responses * Address PR comments * Fix typo --- dotnet/agent-framework-dotnet.slnx | 3 + .../OpenAIResponse.IntegrationTests.csproj | 17 +++ .../OpenAIResponseFixture.cs | 107 ++++++++++++++++++ .../OpenAIResponseInvokeStreamingTests.cs | 13 +++ .../OpenAIResponseInvokeTests.cs | 13 +++ 5 files changed, 153 insertions(+) create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index f0bfc8efe7..9dc111f7d5 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -14,6 +14,9 @@ + + + diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj new file mode 100644 index 0000000000..0aabad91c8 --- /dev/null +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj @@ -0,0 +1,17 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + True + + + + + + + + + + + diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs new file mode 100644 index 0000000000..c1a7b9edbf --- /dev/null +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AgentConformance.IntegrationTests.Support; +using AgentConformanceTests; +using Microsoft.Agents; +using Microsoft.Extensions.AI; +using OpenAI; +using OpenAI.Responses; +using Shared.IntegrationTests; + +namespace OpenAIResponse.IntegrationTests; + +public class OpenAIResponseFixture(bool store) : AgentFixture +{ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + private OpenAIResponseClient _openAIResponseClient; + private IChatClient _chatClient; + private Agent _agent; +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + + public override Agent Agent => this._agent; + + public override async Task> GetChatHistoryAsync(AgentThread thread) + { + if (thread is not ChatClientAgentThread chatClientThread) + { + throw new InvalidOperationException("The thread must be of type ChatClientAgentThread to retrieve chat history."); + } + + if (store) + { + var inputItems = await this._openAIResponseClient.GetResponseInputItemsAsync(chatClientThread.Id).ToListAsync(); + var response = await this._openAIResponseClient.GetResponseAsync(chatClientThread.Id); + var responseItem = response.Value.OutputItems.FirstOrDefault()!; + + // Take the messages that were the chat history leading up to the current response + // remove the instruction messages, and reverse the order so that the most recent message is last. + var previousMessages = inputItems + .Select(ConvertToChatMessage) + .Where(x => x.Text != "You are a helpful assistant.") + .Reverse(); + + // Convert the response item to a chat message. + var responseMessage = ConvertToChatMessage(responseItem); + + // Concatenate the previous messages with the response message to get a full chat history + // that includes the current response. + return previousMessages + .Concat([responseMessage]) + .ToList(); + } + + return await chatClientThread.GetMessagesAsync().ToListAsync(); + } + + private static ChatMessage ConvertToChatMessage(ResponseItem item) + { + if (item is MessageResponseItem messageResponseItem) + { + var role = messageResponseItem.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant; + return new ChatMessage(role, messageResponseItem.Content.FirstOrDefault()?.Text); + } + + throw new NotSupportedException("This test currently only supports text messages"); + } + + public override Task DeleteThreadAsync(AgentThread thread) + { + // Chat Completion does not require/support deleting threads, so this is a no-op. + return Task.CompletedTask; + } + + public override Task InitializeAsync() + { + var config = TestConfiguration.LoadSection(); + + this._openAIResponseClient = new OpenAIClient(config.ApiKey) + .GetOpenAIResponseClient(config.ChatModelId); + this._chatClient = this._openAIResponseClient + .AsIChatClient(); + + var options = new ChatClientAgentOptions + { + Name = "HelpfulAssistant", + Instructions = "You are a helpful assistant.", + ChatOptions = new ChatOptions + { + RawRepresentationFactory = new Func((_) => new ResponseCreationOptions() { StoredOutputEnabled = store }) + }, + }; + + this._agent = + new ChatClientAgent(this._chatClient, options); + + return Task.CompletedTask; + } + + public override Task DisposeAsync() + { + this._chatClient.Dispose(); + return Task.CompletedTask; + } +} diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs new file mode 100644 index 0000000000..49d13af615 --- /dev/null +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace OpenAIResponse.IntegrationTests; + +public class OpenAIResponseStoreTrueInvokeStreamingTests() : RunStreamingAsyncTests(() => new(store: true)) +{ +} + +public class OpenAIResponseStoreFalseInvokeStreamingTests() : RunStreamingAsyncTests(() => new(store: false)) +{ +} diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs new file mode 100644 index 0000000000..a0135b3551 --- /dev/null +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace OpenAIResponse.IntegrationTests; + +public class OpenAIResponseStoreTrueInvokeTests() : RunAsyncTests(() => new(store: true)) +{ +} + +public class OpenAIResponseStoreFalseInvokeTests() : RunAsyncTests(() => new(store: false)) +{ +}