// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; internal static class TestHelpers { /// /// Simple mock implementation of IChatClient for basic testing purposes. /// internal sealed class SimpleMockChatClient : IChatClient { private readonly string _responseText; public ChatOptions? LastChatOptions { get; private set; } public SimpleMockChatClient(string responseText = "Test response") { this._responseText = responseText; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { if (options is not null) { this.LastChatOptions = options; } // Count input messages to simulate context size int messageCount = messages.Count(); ChatMessage message = new(ChatRole.Assistant, this._responseText); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10 + (messageCount * 5), // More messages = more tokens OutputTokenCount = 5, TotalTokenCount = 15 + (messageCount * 5) } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (options is not null) { this.LastChatOptions = options; } await Task.Delay(1, cancellationToken); // Count input messages to simulate context size int messageCount = messages.Count(); // Split response into words to simulate streaming string[] words = this._responseText.Split(' '); for (int i = 0; i < words.Length; i++) { string content = i < words.Length - 1 ? words[i] + " " : words[i]; ChatResponseUpdate update = new() { Contents = [new TextContent(content)], Role = ChatRole.Assistant }; // Add usage to the last update if (i == words.Length - 1) { update.Contents.Add(new UsageContent(new UsageDetails { InputTokenCount = 10 + (messageCount * 5), OutputTokenCount = 5, TotalTokenCount = 15 + (messageCount * 5) })); } yield return update; } } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Stateful mock implementation of IChatClient that returns different responses for each call. /// internal sealed class StatefulMockChatClient : IChatClient { private readonly string[] _responseTexts; private int _callIndex; public StatefulMockChatClient(string[] responseTexts) { this._responseTexts = responseTexts; this._callIndex = 0; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { // Get the response text for this call string responseText = this._callIndex < this._responseTexts.Length ? this._responseTexts[this._callIndex] : this._responseTexts[this._responseTexts.Length - 1]; this._callIndex++; // Count input messages to simulate context size int messageCount = messages.Count(); ChatMessage message = new(ChatRole.Assistant, responseText); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10 + (messageCount * 5), // More messages = more tokens OutputTokenCount = 5, TotalTokenCount = 15 + (messageCount * 5) } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); // Get the response text for this call string responseText = this._callIndex < this._responseTexts.Length ? this._responseTexts[this._callIndex] : this._responseTexts[this._responseTexts.Length - 1]; this._callIndex++; // Count input messages to simulate context size int messageCount = messages.Count(); // Split response into words to simulate streaming string[] words = responseText.Split(' '); for (int i = 0; i < words.Length; i++) { string content = i < words.Length - 1 ? words[i] + " " : words[i]; ChatResponseUpdate update = new() { Contents = [new TextContent(content)], Role = ChatRole.Assistant }; // Add usage to the last update if (i == words.Length - 1) { update.Contents.Add(new UsageContent(new UsageDetails { InputTokenCount = 10 + (messageCount * 5), OutputTokenCount = 5, TotalTokenCount = 15 + (messageCount * 5) })); } yield return update; } } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Mock implementation of IChatClient that returns responses with image content. /// internal sealed class ImageContentMockChatClient : IChatClient { private readonly string _imageUrl; public ImageContentMockChatClient(string imageUrl = "https://example.com/image.png") { this._imageUrl = imageUrl; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { ChatMessage message = new(ChatRole.Assistant, [ new TextContent("Here is an image:"), new UriContent(this._imageUrl, "image/png") ]); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); yield return new ChatResponseUpdate { Contents = [new TextContent("Here is an image:")], Role = ChatRole.Assistant }; yield return new ChatResponseUpdate { Contents = [ new UriContent(this._imageUrl, "image/png"), new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 }) ], Role = ChatRole.Assistant }; } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Mock implementation of IChatClient that returns responses with audio content. /// internal sealed class AudioContentMockChatClient : IChatClient { private readonly byte[] _audioData; private readonly string _transcript; public AudioContentMockChatClient(string audioData = "base64audiodata", string transcript = "This is a transcript") { this._audioData = System.Text.Encoding.UTF8.GetBytes(audioData); this._transcript = transcript; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { ChatMessage message = new(ChatRole.Assistant, [ new DataContent(this._audioData, "audio/wav") { AdditionalProperties = new AdditionalPropertiesDictionary { ["transcript"] = this._transcript } } ]); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); yield return new ChatResponseUpdate { Contents = [ new DataContent(this._audioData, "audio/wav") { AdditionalProperties = new AdditionalPropertiesDictionary { ["transcript"] = this._transcript } }, new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 }) ], Role = ChatRole.Assistant }; } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Mock implementation of IChatClient that returns responses with function calls. /// internal sealed class FunctionCallMockChatClient : IChatClient { private readonly string _functionName; private readonly Dictionary _arguments; public FunctionCallMockChatClient(string functionName = "test_function", string arguments = "{\"param\":\"value\"}") { this._functionName = functionName; this._arguments = System.Text.Json.JsonSerializer.Deserialize>(arguments) ?? []; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { ChatMessage message = new(ChatRole.Assistant, [ new FunctionCallContent("call_123", this._functionName) { Arguments = this._arguments } ]); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.ToolCalls, Usage = new UsageDetails { InputTokenCount = 80, OutputTokenCount = 25, TotalTokenCount = 105 } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); yield return new ChatResponseUpdate { Contents = [ new FunctionCallContent("call_123", this._functionName) { Arguments = this._arguments }, new UsageContent(new UsageDetails { InputTokenCount = 80, OutputTokenCount = 25, TotalTokenCount = 105 }) ], Role = ChatRole.Assistant }; } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Mock implementation of IChatClient that returns mixed content types. /// internal sealed class MixedContentMockChatClient : IChatClient { public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { ChatMessage message = new(ChatRole.Assistant, [ new TextContent("Here are multiple content types:"), new UriContent("https://example.com/image.png", "image/png"), new TextContent("And some more text after the image.") ]); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); yield return new ChatResponseUpdate { Contents = [new TextContent("Here"), new TextContent(" are"), new TextContent(" multiple")], Role = ChatRole.Assistant }; yield return new ChatResponseUpdate { Contents = [new TextContent(" content"), new TextContent(" types:")], Role = ChatRole.Assistant }; yield return new ChatResponseUpdate { Contents = [new UriContent("https://example.com/image.png", "image/png")], Role = ChatRole.Assistant }; yield return new ChatResponseUpdate { Contents = [new TextContent("And"), new TextContent(" some"), new TextContent(" more")], Role = ChatRole.Assistant }; yield return new ChatResponseUpdate { Contents = [ new TextContent(" text"), new TextContent(" after"), new TextContent(" the"), new TextContent(" image."), new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 }) ], Role = ChatRole.Assistant }; } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Mock implementation of IChatClient that returns function call content for tool testing. /// internal sealed class ToolCallMockChatClient : IChatClient { private readonly string _functionName; private readonly Dictionary _arguments; public ToolCallMockChatClient(string functionName, string argumentsJson) { this._functionName = functionName; // Parse JSON arguments into dictionary using var doc = System.Text.Json.JsonDocument.Parse(argumentsJson); this._arguments = []; foreach (var prop in doc.RootElement.EnumerateObject()) { this._arguments[prop.Name] = prop.Value.ValueKind switch { System.Text.Json.JsonValueKind.String => prop.Value.GetString(), System.Text.Json.JsonValueKind.Number => prop.Value.GetDouble(), System.Text.Json.JsonValueKind.True => true, System.Text.Json.JsonValueKind.False => false, System.Text.Json.JsonValueKind.Null => null, _ => prop.Value.ToString() }; } } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { int messageCount = messages.Count(); FunctionCallContent functionCall = new("call_test123", this._functionName, this._arguments); ChatMessage message = new(ChatRole.Assistant, [functionCall]); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.ToolCalls, Usage = new UsageDetails { InputTokenCount = 10 + (messageCount * 5), OutputTokenCount = 5, TotalTokenCount = 15 + (messageCount * 5) } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); int messageCount = messages.Count(); FunctionCallContent functionCall = new("call_test123", this._functionName, this._arguments); yield return new ChatResponseUpdate { Contents = [functionCall, new UsageContent(new UsageDetails { InputTokenCount = 10 + (messageCount * 5), OutputTokenCount = 5, TotalTokenCount = 15 + (messageCount * 5) })], Role = ChatRole.Assistant }; } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Mock IChatClient that captures the full message list on each call. /// Used to verify conversation history is passed correctly. /// internal sealed class ConversationMemoryMockChatClient : IChatClient { private readonly string _responseText; /// Each entry is the messages list received for that call. public List> CallHistory { get; } = []; public ConversationMemoryMockChatClient(string responseText = "Test response") { this._responseText = responseText; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { this.CallHistory.Add(messages.ToList()); ChatMessage message = new(ChatRole.Assistant, this._responseText); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { this.CallHistory.Add(messages.ToList()); await Task.Delay(1, cancellationToken); string[] words = this._responseText.Split(' '); for (int i = 0; i < words.Length; i++) { string content = i < words.Length - 1 ? words[i] + " " : words[i]; ChatResponseUpdate update = new() { Contents = [new TextContent(content)], Role = ChatRole.Assistant }; if (i == words.Length - 1) { update.Contents.Add(new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 })); } yield return update; } } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } /// /// Custom content mock implementation of IChatClient that returns custom content based on a provider function. /// internal sealed class CustomContentMockChatClient : IChatClient { private readonly Func> _contentProvider; public CustomContentMockChatClient(Func> contentProvider) { this._contentProvider = contentProvider; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); public Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { ChatMessage lastMessage = messages.Last(); IEnumerable contents = this._contentProvider(lastMessage); ChatMessage message = new(ChatRole.Assistant, contents.ToList()); ChatResponse response = new([message]) { ModelId = "test-model", FinishReason = ChatFinishReason.Stop, Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 } }; return Task.FromResult(response); } public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await Task.Delay(1, cancellationToken); ChatMessage lastMessage = messages.Last(); IEnumerable contents = this._contentProvider(lastMessage); List contentList = contents.ToList(); // Stream each content item separately for (int i = 0; i < contentList.Count; i++) { List updateContents = [contentList[i]]; // Add usage to the last update if (i == contentList.Count - 1) { updateContents.Add(new UsageContent(new UsageDetails { InputTokenCount = 10, OutputTokenCount = 5, TotalTokenCount = 15 })); } yield return new ChatResponseUpdate { Contents = updateContents, Role = ChatRole.Assistant }; } } public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null; public void Dispose() { } } }