Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
// Create options with properties from the request
var chatOptions = new ChatOptions
{
ConversationId = request.Conversation?.Id,
// Note: We intentionally do NOT set ConversationId on ChatOptions here.
// The conversation ID from the client request is used by the hosting layer
// to manage conversation storage, but should not be forwarded to the underlying
// IChatClient as it has its own concept of conversations (or none at all).
// ---
// ConversationId = request.Conversation?.Id,

Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = request.MaxOutputTokens,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,16 @@ public async IAsyncEnumerable<StreamingResponseEvent> ExecuteAsync(
{
string agentName = GetAgentName(request)!;
AIAgent agent = this._serviceProvider.GetRequiredKeyedService<AIAgent>(agentName);

var chatOptions = new ChatOptions
{
ConversationId = request.Conversation?.Id,
// Note: We intentionally do NOT set ConversationId on ChatOptions here.
// The conversation ID from the client request is used by the hosting layer
// to manage conversation storage, but should not be forwarded to the underlying
// IChatClient as it has its own concept of conversations (or none at all).
// ---
// ConversationId = request.Conversation?.Id,

Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = request.MaxOutputTokens,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,116 @@ public async Task CreateResponseStreaming_ContentPartAdded_IncludesEventAsync()
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 ResponseCreationOptions)
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);
}

private OpenAIResponseClient CreateResponseClient(string agentName)
{
return new OpenAIResponseClient(
Expand All @@ -1103,6 +1213,19 @@ private OpenAIResponseClient CreateResponseClient(string agentName)
});
}

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();
Expand All @@ -1125,6 +1248,30 @@ private async Task<HttpClient> CreateTestServerAsync(string agentName, string in
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> CreateTestServerWithCustomClientAsync(string agentName, string instructions, IChatClient chatClient)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ internal sealed class SimpleMockChatClient : IChatClient
{
private readonly string _responseText;

public ChatOptions? LastChatOptions { get; private set; }

public SimpleMockChatClient(string responseText = "Test response")
{
this._responseText = responseText;
Expand All @@ -31,6 +33,11 @@ public Task<ChatResponse> GetResponseAsync(
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);
Expand All @@ -53,6 +60,11 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
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
Expand Down
Loading