mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
dont forward conversationId to ichatclient (#2632)
This commit is contained in:
committed by
GitHub
Unverified
parent
9c1b2f9a3f
commit
417a529e0e
@@ -36,7 +36,13 @@ internal sealed class AIAgentResponseExecutor : IResponseExecutor
|
||||
// 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,
|
||||
|
||||
+8
-1
@@ -82,9 +82,16 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
|
||||
{
|
||||
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,
|
||||
|
||||
+147
@@ -1091,6 +1091,116 @@ public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable
|
||||
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(
|
||||
@@ -1103,6 +1213,19 @@ public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -1125,6 +1248,30 @@ public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable
|
||||
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();
|
||||
|
||||
@@ -19,6 +19,8 @@ internal static class TestHelpers
|
||||
{
|
||||
private readonly string _responseText;
|
||||
|
||||
public ChatOptions? LastChatOptions { get; private set; }
|
||||
|
||||
public SimpleMockChatClient(string responseText = "Test response")
|
||||
{
|
||||
this._responseText = responseText;
|
||||
@@ -31,6 +33,11 @@ internal static class TestHelpers
|
||||
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);
|
||||
@@ -53,6 +60,11 @@ internal static class TestHelpers
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user