mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
88ea9d08c7
* Initial plan * Update code for Microsoft.Extensions.AI.Abstractions 10.4.0 breaking changes - Rename FunctionApprovalRequestContent → ToolApprovalRequestContent - Rename FunctionApprovalResponseContent → ToolApprovalResponseContent - Rename UserInputRequestContent → ToolApprovalRequestContent - Rename UserInputResponseContent → ToolApprovalResponseContent - Update .FunctionCall property → .ToolCall with FunctionCallContent casts where needed - Update .Id property → .RequestId on the renamed types - Rename FunctionApprovalRequestEventGenerator → ToolApprovalRequestEventGenerator - Rename FunctionApprovalResponseEventGenerator → ToolApprovalResponseEventGenerator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update OpenAI 2.9.1, ME.AI 10.4.0, fix breaking API changes Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix remaining ME.AI 10.4.0 breaking changes: MCP approval types, .Output→.Outputs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Use pattern matching with `when` for ToolApprovalRequestContent/FunctionCallContent Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Update Azure.AI.OpenAI to 2.9.0-beta.1 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix remaining GetResponsesClient(model) build failures for Azure.AI.OpenAI 2.9.0-beta.1 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Address review feedback: remove redundant type checks in TestRequestAgent.cs and fix error message in AIAgentHostExecutor.cs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Update Azure.AI.Projects to 2.0.0-beta.2 with namespace migration - Azure.AI.Projects 2.0.0-beta.1 → 2.0.0-beta.2 - Azure.AI.Projects.OpenAI → Azure.AI.Extensions.OpenAI (transitive) - Agent types moved to Azure.AI.Projects.Agents namespace - AgentRecord.Versions.Latest → AgentRecord.GetLatestVersion() - OpenAPIFunctionDefinition → OpenApiFunctionDefinition - BingCustomSearchToolParameters → BingCustomSearchToolOptions - MemorySearchPreviewTool.UpdateDelay → UpdateDelayInSecs - Azure.Identity 1.17.1 → 1.19.0 - Microsoft.Identity.Client.Extensions.Msal 4.78.0 → 4.83.1 Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix remaining type renames for Azure.AI.Projects 2.0.0-beta.2 - BrowserAutomationToolParameters → BrowserAutomationToolOptions - MemoryUpdateOptions.UpdateDelay stays as UpdateDelay (not renamed) - WaitForMemoriesUpdateAsync parameter order: pollingInterval before options - AIProjectAgentsOperations → AgentsClient Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> * Fix format errors and OpenTelemetry test for ME.AI 10.4.0 - Remove unused 'using Azure.AI.Extensions.OpenAI' and fix import ordering in Agent_With_AzureAIProject/Program.cs - Update OpenTelemetryAgentTests: gen_ai.tool.definitions is now always emitted regardless of EnableSensitiveData per ME.AI 10.4.0 change (dotnet/extensions#7346). Tool definitions are not considered sensitive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix GetRepoFolder() to work in git worktrees Use 'workflow-samples' directory as repo root marker instead of '.git', which fails in worktrees (.git is a file) and also matches too early when a '.github' folder exists in subdirectories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix formatting: remove unused usings and fix import ordering dotnet format applied across 59 impacted projects. Primarily removes unnecessary 'using Azure.AI.Projects' where Azure.AI.Projects.Agents provides all needed types, and fixes import ordering per editorconfig. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Disable AzureAIAgentsPersistent integration tests (#4769) Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent which was removed in ME.AI 10.4.0 (renamed to ToolApprovalResponseContent), causing TypeLoadException at runtime. Mark all 6 test classes with IntegrationDisabled trait until Persistent ships a version targeting ME.AI 10.4.0+. Upstream fix: https://github.com/Azure/azure-sdk-for-net/pull/56929 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add README with compatibility note for AzureAI.Persistent (#4769) Documents that Azure.AI.Agents.Persistent 1.2.0-beta.9 is only compatible with ME.AI ≤10.3.0 and OpenAI ≤2.8.0 due to type renames in ME.AI 10.4.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix file encoding: restore UTF-8 BOM on Persistent test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Mark AzureAI.Persistent as IsPackable=false (#4769) Prevent shipping until Azure.AI.Agents.Persistent targets ME.AI 10.4.0+. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Moving IsPackable after import --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
1418 lines
57 KiB
C#
1418 lines
57 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.ClientModel;
|
|
using System.ClientModel.Primitives;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting.Server;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using OpenAI;
|
|
using OpenAI.Responses;
|
|
|
|
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Integration tests that start a web server and use the OpenAI Responses SDK client to verify protocol compatibility.
|
|
/// These tests validate both streaming and non-streaming request scenarios.
|
|
/// </summary>
|
|
public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable
|
|
{
|
|
private WebApplication? _app;
|
|
private HttpClient? _httpClient;
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
this._httpClient?.Dispose();
|
|
if (this._app != null)
|
|
{
|
|
await this._app.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses work correctly with the OpenAI SDK client.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_WithSimpleMessage_ReturnsStreamingUpdatesAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "One Two Three";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Count to 3");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
Assert.NotEmpty(updates);
|
|
|
|
// Verify we got various streaming update types
|
|
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
|
|
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
|
|
Assert.Contains(updates, u => u is StreamingResponseOutputTextDeltaUpdate);
|
|
|
|
// Verify content was received
|
|
string content = contentBuilder.ToString();
|
|
Assert.Equal(ExpectedResponse, content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that non-streaming responses work correctly with the OpenAI SDK client.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_WithSimpleMessage_ReturnsCompleteResponseAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "non-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Hello! How can I help you today?";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Hello");
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.NotNull(response.Id);
|
|
|
|
// Verify content
|
|
string content = response.GetOutputText();
|
|
Assert.Equal(ExpectedResponse, content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses can handle multiple content chunks.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_WithMultipleChunks_StreamsAllContentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "multi-chunk-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "This is a test response with multiple words";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
// Verify all content was received
|
|
string receivedContent = contentBuilder.ToString();
|
|
Assert.Equal(ExpectedResponse, receivedContent);
|
|
|
|
// Verify multiple content chunks were received
|
|
List<StreamingResponseOutputTextDeltaUpdate> contentUpdates = updates.OfType<StreamingResponseOutputTextDeltaUpdate>().ToList();
|
|
Assert.True(contentUpdates.Count > 1, "Expected multiple content chunks in streaming response");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that multiple agents can be accessed via the same server.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_WithMultipleAgents_EachAgentRespondsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string Agent1Name = "agent-one";
|
|
const string Agent1Instructions = "You are agent one.";
|
|
const string Agent1Response = "Response from agent one";
|
|
|
|
const string Agent2Name = "agent-two";
|
|
const string Agent2Instructions = "You are agent two.";
|
|
const string Agent2Response = "Response from agent two";
|
|
|
|
this._httpClient = await this.CreateTestServerWithMultipleAgentsAsync(
|
|
(Agent1Name, Agent1Instructions, Agent1Response),
|
|
(Agent2Name, Agent2Instructions, Agent2Response));
|
|
|
|
ResponsesClient responseClient1 = this.CreateResponseClient(Agent1Name);
|
|
ResponsesClient responseClient2 = this.CreateResponseClient(Agent2Name);
|
|
|
|
// Act
|
|
ResponseResult response1 = await responseClient1.CreateResponseAsync("test-model", "Hello");
|
|
ResponseResult response2 = await responseClient2.CreateResponseAsync("test-model", "Hello");
|
|
|
|
// Assert
|
|
string content1 = response1.GetOutputText();
|
|
string content2 = response2.GetOutputText();
|
|
|
|
Assert.Equal(Agent1Response, content1);
|
|
Assert.Equal(Agent2Response, content2);
|
|
Assert.NotEqual(content1, content2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming and non-streaming work correctly for the same agent.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_SameAgentStreamingAndNonStreaming_BothWorkCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "dual-mode-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "This is the response";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act - Non-streaming
|
|
ResponseResult nonStreamingResponse = await responseClient.CreateResponseAsync("test-model", "Test");
|
|
|
|
// Act - Streaming
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
StringBuilder streamingContent = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
streamingContent.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
// Assert
|
|
string nonStreamingContent = nonStreamingResponse.GetOutputText();
|
|
Assert.Equal(ExpectedResponse, nonStreamingContent);
|
|
Assert.Equal(ExpectedResponse, streamingContent.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the response status is correctly set for completed responses.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_CompletedResponse_HasCorrectStatusAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "status-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Complete";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.NotNull(response.Id);
|
|
Assert.Equal(ExpectedResponse, response.GetOutputText());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses contain the expected event sequence.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_VerifyEventSequence_ContainsExpectedEventsAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "event-sequence-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Test response with multiple words";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
// Verify event sequence
|
|
Assert.NotEmpty(updates);
|
|
|
|
// First event should be created
|
|
Assert.IsType<StreamingResponseCreatedUpdate>(updates[0]);
|
|
|
|
// Last event should be completed
|
|
StreamingResponseUpdate lastUpdate = updates[^1];
|
|
Assert.IsType<StreamingResponseCompletedUpdate>(lastUpdate);
|
|
|
|
// Should contain text delta events in between
|
|
List<StreamingResponseUpdate> textDeltas = updates.Where(u => u is StreamingResponseOutputTextDeltaUpdate).ToList();
|
|
Assert.NotEmpty(textDeltas);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses properly handle empty responses.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_EmptyResponse_HandlesGracefullyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "empty-response-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
// Should still receive created and completed events
|
|
Assert.NotEmpty(updates);
|
|
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
|
|
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that non-streaming responses include proper metadata.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_IncludesMetadata_HasRequiredFieldsAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "metadata-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Response with metadata";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
Assert.NotNull(response.Id);
|
|
Assert.NotNull(response.Model);
|
|
Assert.NotEqual(default, response.CreatedAt);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses handle very long text correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_LongText_StreamsAllContentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "long-text-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
string expectedResponse = string.Join(" ", Enumerable.Range(1, 100).Select(i => $"Word{i}"));
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, expectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Generate long text");
|
|
|
|
// Assert
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
string receivedContent = contentBuilder.ToString();
|
|
Assert.Equal(expectedResponse, receivedContent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses properly track output indices.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_OutputIndices_AreConsistentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "output-index-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Test output index";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<int> outputIndices = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputItemAddedUpdate itemAdded)
|
|
{
|
|
outputIndices.Add(itemAdded.OutputIndex);
|
|
}
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
outputIndices.Add(textDelta.OutputIndex);
|
|
}
|
|
}
|
|
|
|
// All output indices should be the same (first output)
|
|
Assert.NotEmpty(outputIndices);
|
|
Assert.All(outputIndices, index => Assert.Equal(0, index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses handle single-word responses correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_SingleWord_StreamsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "single-word-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Hello";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses preserve special characters and formatting.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_SpecialCharacters_PreservesFormattingAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "special-chars-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Hello! How are you? I'm fine. 100% great!";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that non-streaming responses handle special characters correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_SpecialCharacters_PreservesContentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "special-chars-nonstreaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Symbols: @#$%^&*() Quotes: \"Hello\" 'World' Unicode: 你好 🌍";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
string content = response.GetOutputText();
|
|
Assert.Equal(ExpectedResponse, content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses include item IDs consistently.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_ItemIds_AreConsistentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "item-id-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Testing item IDs";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<string> itemIds = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputItemAddedUpdate itemAdded)
|
|
{
|
|
itemIds.Add(itemAdded.Item.Id);
|
|
}
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta && !string.IsNullOrEmpty(textDelta.ItemId))
|
|
{
|
|
itemIds.Add(textDelta.ItemId);
|
|
}
|
|
}
|
|
|
|
// All item IDs should be the same within a single response
|
|
Assert.NotEmpty(itemIds);
|
|
Assert.All(itemIds, id => Assert.Equal(itemIds[0], id));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that multiple sequential non-streaming requests work correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_MultipleSequentialRequests_AllSucceedAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "sequential-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Response";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act & Assert - Make 5 sequential requests
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", $"Request {i}");
|
|
Assert.NotNull(response);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.Equal(ExpectedResponse, response.GetOutputText());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that multiple sequential streaming requests work correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_MultipleSequentialRequests_AllStreamCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "sequential-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Streaming response";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act & Assert - Make 3 sequential streaming requests
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", $"Request {i}");
|
|
StringBuilder contentBuilder = new();
|
|
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that response IDs are unique across multiple requests.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_MultipleRequests_GenerateUniqueIdsAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "unique-id-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Response";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
List<string> responseIds = [];
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", $"Request {i}");
|
|
responseIds.Add(response.Id);
|
|
}
|
|
|
|
// Assert
|
|
Assert.Equal(10, responseIds.Count);
|
|
Assert.Equal(responseIds.Count, responseIds.Distinct().Count()); // All IDs should be unique
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses track sequence numbers correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_SequenceNumbers_AreMonotonicallyIncreasingAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "sequence-number-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Test sequence numbers with multiple words";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<int> sequenceNumbers = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
sequenceNumbers.Add(update.SequenceNumber);
|
|
}
|
|
|
|
// Verify sequence numbers are monotonically increasing starting from 0
|
|
Assert.NotEmpty(sequenceNumbers);
|
|
Assert.Equal(0, sequenceNumbers[0]);
|
|
for (int i = 1; i < sequenceNumbers.Count; i++)
|
|
{
|
|
Assert.True(sequenceNumbers[i] > sequenceNumbers[i - 1], $"Sequence number {sequenceNumbers[i]} should be greater than {sequenceNumbers[i - 1]}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that non-streaming responses have correct model information.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_ModelInformation_IsCorrectAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "model-info-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Test model info";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
Assert.NotNull(response.Model);
|
|
Assert.NotEmpty(response.Model);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses properly handle responses with punctuation.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_Punctuation_PreservesContentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "punctuation-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Hello, world! How are you today? I'm doing well.";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that non-streaming responses work with very short input.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_ShortInput_ReturnsValidResponseAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "short-input-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "OK";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Hi");
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.Equal(ExpectedResponse, response.GetOutputText());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses contain content index information.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_ContentIndices_AreConsistentAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "content-index-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Test content indices";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<int> contentIndices = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentIndices.Add(textDelta.ContentIndex);
|
|
}
|
|
}
|
|
|
|
// All content indices should be the same for a single text response
|
|
Assert.NotEmpty(contentIndices);
|
|
Assert.All(contentIndices, index => Assert.Equal(0, index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that non-streaming responses handle newlines correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_Newlines_PreservesFormattingAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "newline-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Line 1\nLine 2\nLine 3";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
string content = response.GetOutputText();
|
|
Assert.Equal(ExpectedResponse, content);
|
|
Assert.Contains("\n", content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming responses handle newlines correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_Newlines_PreservesFormattingAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "newline-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "First line\nSecond line\nThird line";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
StringBuilder contentBuilder = new();
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
|
|
{
|
|
contentBuilder.Append(textDelta.Delta);
|
|
}
|
|
}
|
|
|
|
string content = contentBuilder.ToString();
|
|
Assert.Equal(ExpectedResponse, content);
|
|
Assert.Contains("\n", content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with image content are properly handled in non-streaming mode.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_ImageContent_ReturnsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "image-content-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ImageUrl = "https://example.com/test-image.png";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.ImageContentMockChatClient(ImageUrl));
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Show me an image");
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.NotNull(response.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with image content stream correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_ImageContent_StreamsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "image-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ImageUrl = "https://example.com/test-image.png";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.ImageContentMockChatClient(ImageUrl));
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Show me an image");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
Assert.NotEmpty(updates);
|
|
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
|
|
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with audio content are properly handled.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_AudioContent_ReturnsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "audio-content-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string AudioData = "base64_audio_data_here";
|
|
const string Transcript = "This is the audio transcript";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.AudioContentMockChatClient(AudioData, Transcript));
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Generate audio");
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.NotNull(response.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with audio content stream correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_AudioContent_StreamsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "audio-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string AudioData = "base64_audio_data";
|
|
const string Transcript = "Audio transcript";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.AudioContentMockChatClient(AudioData, Transcript));
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Generate audio");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
Assert.NotEmpty(updates);
|
|
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
|
|
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with function calls are properly handled.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_FunctionCall_ReturnsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "function-call-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string FunctionName = "get_weather";
|
|
const string Arguments = "{\"location\":\"Seattle\"}";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments));
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "What's the weather?");
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.NotNull(response.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with function calls stream correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_FunctionCall_StreamsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "function-call-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string FunctionName = "calculate";
|
|
const string Arguments = "{\"expression\":\"2+2\"}";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments));
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Calculate 2+2");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
Assert.NotEmpty(updates);
|
|
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with mixed content types are properly handled.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_MixedContent_ReturnsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "mixed-content-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.MixedContentMockChatClient());
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Show me various content");
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.Equal(ResponseStatus.Completed, response.Status);
|
|
Assert.NotNull(response.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that responses with mixed content types stream correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_MixedContent_StreamsCorrectlyAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "mixed-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
|
|
agentName: AgentName,
|
|
instructions: Instructions,
|
|
chatClient: new TestHelpers.MixedContentMockChatClient());
|
|
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Show me various content");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
Assert.NotEmpty(updates);
|
|
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
|
|
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
|
|
|
|
// Should have multiple output item added events due to different content types
|
|
List<StreamingResponseUpdate> itemAddedUpdates = updates.Where(u => u is StreamingResponseOutputItemAddedUpdate).ToList();
|
|
Assert.NotEmpty(itemAddedUpdates);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that streaming text content includes proper done events.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_TextDone_IncludesDoneEventAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "text-done-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Complete text response";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
// Should contain completed event (text done is represented by completed status)
|
|
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that content part added events are included in streaming responses.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_ContentPartAdded_IncludesEventAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "content-part-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Response with content parts";
|
|
|
|
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
|
|
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
|
|
|
|
// Act
|
|
AsyncCollectionResult<StreamingResponseUpdate> streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
|
|
|
|
// Assert
|
|
List<StreamingResponseUpdate> updates = [];
|
|
await foreach (StreamingResponseUpdate update in streamingResult)
|
|
{
|
|
updates.Add(update);
|
|
}
|
|
|
|
// Should contain content part added event
|
|
Assert.Contains(updates, u => u is StreamingResponseContentPartAddedUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that when a client provides a conversation ID, the underlying IChatClient
|
|
/// does NOT receive that conversation ID via ChatOptions.ConversationId.
|
|
/// This ensures that the host's conversation management is separate from the IChatClient's
|
|
/// conversation handling (if any).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_WithConversationId_DoesNotForwardConversationIdToIChatClientAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "conversation-id-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Response";
|
|
|
|
this._httpClient = await this.CreateTestServerWithConversationsAsync(AgentName, Instructions, ExpectedResponse);
|
|
var mockChatClient = this.ResolveMockChatClient();
|
|
|
|
// First, create a conversation
|
|
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
|
|
string createConvJson = System.Text.Json.JsonSerializer.Serialize(createConversationRequest);
|
|
using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage createConvResponse = await this._httpClient.PostAsync(
|
|
new Uri("/v1/conversations", UriKind.Relative),
|
|
createConvContent);
|
|
Assert.True(createConvResponse.IsSuccessStatusCode, $"Create conversation failed: {createConvResponse.StatusCode}");
|
|
|
|
string convResponseJson = await createConvResponse.Content.ReadAsStringAsync();
|
|
using var convDoc = System.Text.Json.JsonDocument.Parse(convResponseJson);
|
|
string conversationId = convDoc.RootElement.GetProperty("id").GetString()!;
|
|
|
|
// Act - Send request with conversation ID using raw HTTP
|
|
// (OpenAI SDK doesn't expose ConversationId directly on CreateResponseOptions)
|
|
var requestBody = new
|
|
{
|
|
input = "Test",
|
|
agent = new { name = AgentName },
|
|
conversation = conversationId,
|
|
stream = false
|
|
};
|
|
string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody);
|
|
using StringContent content = new(requestJson, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage httpResponse = await this._httpClient.PostAsync(
|
|
new Uri($"/{AgentName}/v1/responses", UriKind.Relative),
|
|
content);
|
|
|
|
// Assert - Response is successful
|
|
Assert.True(httpResponse.IsSuccessStatusCode, $"Response status: {httpResponse.StatusCode}");
|
|
|
|
// Assert - The IChatClient should have received ChatOptions, but without the ConversationId set
|
|
Assert.NotNull(mockChatClient.LastChatOptions);
|
|
Assert.Null(mockChatClient.LastChatOptions.ConversationId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that when a client provides a conversation ID in streaming mode, the underlying
|
|
/// IChatClient does NOT receive that conversation ID via ChatOptions.ConversationId.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponseStreaming_WithConversationId_DoesNotForwardConversationIdToIChatClientAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "conversation-streaming-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string ExpectedResponse = "Streaming response";
|
|
|
|
this._httpClient = await this.CreateTestServerWithConversationsAsync(AgentName, Instructions, ExpectedResponse);
|
|
var mockChatClient = this.ResolveMockChatClient();
|
|
|
|
// First, create a conversation
|
|
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
|
|
string createConvJson = System.Text.Json.JsonSerializer.Serialize(createConversationRequest);
|
|
using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage createConvResponse = await this._httpClient.PostAsync(
|
|
new Uri("/v1/conversations", UriKind.Relative),
|
|
createConvContent);
|
|
Assert.True(createConvResponse.IsSuccessStatusCode, $"Create conversation failed: {createConvResponse.StatusCode}");
|
|
|
|
string convResponseJson = await createConvResponse.Content.ReadAsStringAsync();
|
|
using var convDoc = System.Text.Json.JsonDocument.Parse(convResponseJson);
|
|
string conversationId = convDoc.RootElement.GetProperty("id").GetString()!;
|
|
|
|
// Act - Send streaming request with conversation ID using raw HTTP
|
|
var requestBody = new
|
|
{
|
|
input = "Test",
|
|
agent = new { name = AgentName },
|
|
conversation = conversationId,
|
|
stream = true
|
|
};
|
|
string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody);
|
|
using StringContent content = new(requestJson, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage httpResponse = await this._httpClient.PostAsync(
|
|
new Uri($"/{AgentName}/v1/responses", UriKind.Relative),
|
|
content);
|
|
|
|
// Assert - Response is successful and is SSE
|
|
Assert.True(httpResponse.IsSuccessStatusCode, $"Response status: {httpResponse.StatusCode}");
|
|
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
|
|
|
|
// Consume the SSE stream to complete the request
|
|
string sseContent = await httpResponse.Content.ReadAsStringAsync();
|
|
|
|
// Verify streaming completed successfully by checking for response.completed event
|
|
Assert.Contains("response.completed", sseContent);
|
|
|
|
// Assert - The IChatClient should have received ChatOptions, but without the ConversationId set
|
|
Assert.NotNull(mockChatClient.LastChatOptions);
|
|
Assert.Null(mockChatClient.LastChatOptions.ConversationId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that conversation history is passed to the agent on subsequent requests.
|
|
/// This test reproduces the bug described in GitHub issue #3484.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CreateResponse_WithConversation_SecondRequestIncludesPriorMessagesAsync()
|
|
{
|
|
// Arrange
|
|
const string AgentName = "memory-agent";
|
|
const string Instructions = "You are a helpful assistant.";
|
|
const string AgentResponse = "Nice to meet you Alice";
|
|
|
|
var mockChatClient = new TestHelpers.ConversationMemoryMockChatClient(AgentResponse);
|
|
this._httpClient = await this.CreateTestServerWithCustomClientAndConversationsAsync(
|
|
AgentName, Instructions, mockChatClient);
|
|
|
|
// Create a conversation
|
|
string createConvJson = System.Text.Json.JsonSerializer.Serialize(
|
|
new { metadata = new { agent_id = AgentName } });
|
|
using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage createConvResponse = await this._httpClient.PostAsync(
|
|
new Uri("/v1/conversations", UriKind.Relative), createConvContent);
|
|
Assert.True(createConvResponse.IsSuccessStatusCode);
|
|
|
|
string convJson = await createConvResponse.Content.ReadAsStringAsync();
|
|
using var convDoc = System.Text.Json.JsonDocument.Parse(convJson);
|
|
string conversationId = convDoc.RootElement.GetProperty("id").GetString()!;
|
|
|
|
// Act - First message
|
|
await this.SendRawResponseAsync(AgentName, "My name is Alice", conversationId, stream: false);
|
|
|
|
// Act - Second message in same conversation
|
|
await this.SendRawResponseAsync(AgentName, "What is my name?", conversationId, stream: false);
|
|
|
|
// Assert
|
|
Assert.Equal(2, mockChatClient.CallHistory.Count);
|
|
|
|
// First call: should have 1 message (just the user input)
|
|
Assert.Single(mockChatClient.CallHistory[0]);
|
|
Assert.Equal(ChatRole.User, mockChatClient.CallHistory[0][0].Role);
|
|
|
|
// Second call: should have 3 messages (prior user + prior assistant + new user)
|
|
Assert.Equal(3, mockChatClient.CallHistory[1].Count);
|
|
Assert.Equal(ChatRole.User, mockChatClient.CallHistory[1][0].Role);
|
|
Assert.Equal(ChatRole.Assistant, mockChatClient.CallHistory[1][1].Role);
|
|
Assert.Equal(ChatRole.User, mockChatClient.CallHistory[1][2].Role);
|
|
}
|
|
|
|
private async Task<HttpResponseMessage> SendRawResponseAsync(
|
|
string agentName, string input, string conversationId, bool stream)
|
|
{
|
|
var requestBody = new
|
|
{
|
|
input,
|
|
agent = new { name = agentName },
|
|
conversation = conversationId,
|
|
stream
|
|
};
|
|
string json = System.Text.Json.JsonSerializer.Serialize(requestBody);
|
|
using StringContent content = new(json, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response = await this._httpClient!.PostAsync(
|
|
new Uri($"/{agentName}/v1/responses", UriKind.Relative), content);
|
|
Assert.True(response.IsSuccessStatusCode, $"Response failed: {response.StatusCode}");
|
|
|
|
// Consume the full response body to ensure execution completes
|
|
await response.Content.ReadAsStringAsync();
|
|
return response;
|
|
}
|
|
|
|
private ResponsesClient CreateResponseClient(string agentName)
|
|
{
|
|
return new ResponsesClient(
|
|
credential: new ApiKeyCredential("test-api-key"),
|
|
options: new OpenAIClientOptions
|
|
{
|
|
Endpoint = new Uri(this._httpClient!.BaseAddress!, $"/{agentName}/v1/"),
|
|
Transport = new HttpClientPipelineTransport(this._httpClient)
|
|
});
|
|
}
|
|
|
|
private TestHelpers.SimpleMockChatClient ResolveMockChatClient()
|
|
{
|
|
ArgumentNullException.ThrowIfNull(this._app, nameof(this._app));
|
|
|
|
var chatClient = this._app.Services.GetRequiredKeyedService<IChatClient>("chat-client");
|
|
if (chatClient is not TestHelpers.SimpleMockChatClient mockChatClient)
|
|
{
|
|
throw new InvalidOperationException("Mock chat client not found or of incorrect type.");
|
|
}
|
|
|
|
return mockChatClient;
|
|
}
|
|
|
|
private async Task<HttpClient> CreateTestServerAsync(string agentName, string instructions, string responseText = "Test response")
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
|
|
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
|
builder.AddOpenAIResponses();
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
|
|
|
|
this._app = builder.Build();
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIResponses(agent);
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
return testServer.CreateClient();
|
|
}
|
|
|
|
private async Task<HttpClient> CreateTestServerWithConversationsAsync(string agentName, string instructions, string responseText = "Test response")
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
|
|
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
|
builder.AddOpenAIResponses();
|
|
builder.AddOpenAIConversations();
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
|
|
|
|
this._app = builder.Build();
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIResponses(agent);
|
|
this._app.MapOpenAIConversations();
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
return testServer.CreateClient();
|
|
}
|
|
|
|
private async Task<HttpClient> CreateTestServerWithCustomClientAndConversationsAsync(string agentName, string instructions, IChatClient chatClient)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient);
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}");
|
|
builder.AddOpenAIResponses();
|
|
builder.AddOpenAIConversations();
|
|
|
|
this._app = builder.Build();
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIResponses(agent);
|
|
this._app.MapOpenAIConversations();
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
return testServer.CreateClient();
|
|
}
|
|
|
|
private async Task<HttpClient> CreateTestServerWithCustomClientAsync(string agentName, string instructions, IChatClient chatClient)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient);
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}");
|
|
builder.AddOpenAIResponses();
|
|
|
|
this._app = builder.Build();
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIResponses(agent);
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
return testServer.CreateClient();
|
|
}
|
|
|
|
private async Task<HttpClient> CreateTestServerWithMultipleAgentsAsync(
|
|
params (string Name, string Instructions, string ResponseText)[] agents)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
foreach ((string name, string instructions, string responseText) in agents)
|
|
{
|
|
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
|
|
builder.Services.AddKeyedSingleton($"chat-client-{name}", mockChatClient);
|
|
builder.AddAIAgent(name, instructions, chatClientServiceKey: $"chat-client-{name}");
|
|
}
|
|
|
|
builder.AddOpenAIResponses();
|
|
|
|
this._app = builder.Build();
|
|
|
|
foreach ((string name, string _, string _) in agents)
|
|
{
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(name);
|
|
this._app.MapOpenAIResponses(agent);
|
|
}
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
return testServer.CreateClient();
|
|
}
|
|
}
|