From 4b0e89a3f410c73698123ef28c15d9363bc52fb5 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Sat, 1 Nov 2025 12:55:40 -0700 Subject: [PATCH 01/13] Improve fidelity of OpenAI Responses server and add Conversations --- dotnet/Directory.Packages.props | 1 + .../AgentWebChat.AgentHost/Program.cs | 2 +- .../Conversations/ConversationsHttpHandler.cs | 341 +++++ .../Conversations/IAgentConversationIndex.cs | 40 + .../Conversations/IConversationStorage.cs | 106 ++ .../InMemoryAgentConversationIndex.cs | 127 ++ .../InMemoryConversationStorage.cs | 354 +++++ .../Conversations/Models/AddMessageRequest.cs | 21 + .../Conversations/Models/Conversation.cs | 38 + .../Models/CreateConversationRequest.cs | 26 + .../Conversations/Models/SortOrder.cs | 54 + .../Models/UpdateConversationRequest.cs | 18 + .../Conversations/SortOrderExtensions.cs | 31 + ...intRouteBuilderExtensions.Conversations.cs | 73 + ...ndpointRouteBuilderExtensions.Responses.cs | 97 +- .../HostApplicationBuilderExtensions.cs | 28 - .../InMemoryStorageOptions.cs | 50 + .../MemoryCacheExtensions.cs | 155 +++ .../Microsoft.Agents.AI.Hosting.OpenAI.csproj | 1 + .../Models/DeleteResponse.cs | 31 + .../Models/ErrorResponse.cs | 47 + .../Models/ListResponse.cs | 45 + ...ntext.cs => OpenAIHostingJsonUtilities.cs} | 95 +- .../Responses/AIAgentResponseExecutor.cs | 73 + .../Responses/AIAgentResponsesProcessor.cs | 65 - .../Responses/AgentInvocationContext.cs | 2 +- .../Responses/AgentRunResponseExtensions.cs | 81 +- .../AgentRunResponseUpdateExtensions.cs | 68 +- .../Converters/ItemContentConverter.cs | 20 +- .../Converters/ItemParamConverter.cs | 59 + .../Converters/ItemResourceConverter.cs | 92 +- .../ResponsesMessageItemParamConverter.cs | 61 + .../ResponsesMessageItemResourceConverter.cs | 40 +- .../Converters/SnakeCaseEnumConverter.cs | 3 + .../Responses/HostedAgentResponseExecutor.cs | 140 ++ .../Responses/IResponseExecutor.cs | 26 + .../Responses/IResponsesService.cs | 96 ++ .../Responses/IdGenerator.cs | 38 +- .../Responses/InMemoryResponsesService.cs | 531 ++++++++ .../Responses/Models/AgentId.cs | 6 +- .../Responses/Models/ConversationReference.cs | 10 +- .../Responses/Models/CreateResponse.cs | 14 +- .../Responses/Models/InputMessage.cs | 2 +- .../Responses/Models/InputMessageContent.cs | 26 +- .../Responses/Models/ItemParam.cs | 577 ++++++++ .../Responses/Models/ItemParamExtensions.cs | 155 +++ .../Responses/Models/ItemResource.cs | 272 +++- .../Responses/Models/PromptReference.cs | 2 +- .../Responses/Models/ReasoningOptions.cs | 2 +- .../Responses/Models/Response.cs | 9 +- .../Responses/Models/ResponseInput.cs | 17 +- .../Responses/Models/StreamOptions.cs | 10 +- .../Models/StreamingResponseEvent.cs | 216 ++- .../Responses/Models/TextConfiguration.cs | 14 +- .../Responses/Models/WorkflowEventData.cs | 38 + .../Responses/ResponsesHttpHandler.cs | 259 ++++ .../ResponsesJsonSerializerOptions.cs | 24 - .../Streaming/AudioContentEventGenerator.cs | 19 +- .../Streaming/ErrorContentEventGenerator.cs | 19 +- .../Streaming/FileContentEventGenerator.cs | 19 +- .../FunctionApprovalRequestEventGenerator.cs | 52 + .../FunctionApprovalResponseEventGenerator.cs | 41 + .../Streaming/FunctionCallEventGenerator.cs | 15 +- .../Streaming/FunctionResultEventGenerator.cs | 15 +- .../HostedFileContentEventGenerator.cs | 19 +- .../Streaming/ImageContentEventGenerator.cs | 23 +- .../TextReasoningContentEventGenerator.cs | 2 +- ...rviceCollectionExtensions.Conversations.cs | 35 + .../ServiceCollectionExtensions.Responses.cs | 55 + .../ServiceCollectionExtensions.cs | 28 - .../SseJsonResult.cs | 75 + .../AgentInvocationContextTests.cs | 92 ++ .../ConformanceTestBase.cs | 35 +- .../Conversations/add_items/request.json | 24 + .../Conversations/add_items/response.json | 32 + .../basic/create_conversation_request.json | 5 + .../basic/create_conversation_response.json | 8 + .../basic/first_message_request.json | 6 + .../basic/first_message_response.json | 70 + .../basic/second_message_request.json | 6 + .../basic/second_message_response.json | 70 + .../first_message_response.txt | 624 +++++++++ .../create_with_items/create_request.json | 17 + .../create_with_items/create_response.json | 8 + .../delete_conversation/response.json | 5 + .../Conversations/delete_item/response.json | 5 + .../response.json | 8 + .../response.json | 8 + .../error_invalid_json/request.txt | 5 + .../error_invalid_json/response.json | 8 + .../error_invalid_limit/response.json | 8 + .../error_item_not_found/response.json | 8 + .../error_missing_required_field/request.json | 13 + .../create_conversation_request.json | 5 + .../create_conversation_response.json | 8 + .../image_input/first_message_request.json | 21 + .../image_input/first_message_response.json | 70 + .../create_conversation_request.json | 5 + .../first_message_request.json | 22 + .../first_message_response.txt | 456 +++++++ .../Conversations/list_items/response.json | 86 ++ .../refusal/create_conversation_response.json | 8 + .../refusal/first_message_request.json | 6 + .../refusal/first_message_response.json | 70 + .../first_message_request.json | 7 + .../first_message_response.txt | 54 + .../retrieve_conversation/response.json | 8 + .../Conversations/retrieve_item/response.json | 14 + .../create_conversation_request.json | 5 + .../tool_call/first_message_request.json | 27 + .../tool_call/first_message_response.json | 92 ++ .../first_message_request.json | 28 + .../update_conversation/request.json | 7 + .../update_conversation/response.json | 10 + .../mutual_exclusive_error/request.json | 9 + .../mutual_exclusive_error/response.json | 8 + .../EndpointRouteBuilderExtensionsTests.cs | 67 +- .../FunctionApprovalTests.cs | 366 +++++ .../HostApplicationBuilderExtensionsTests.cs | 78 -- .../IdGeneratorTests.cs | 296 ++++ .../InMemoryAgentConversationIndexTests.cs | 359 +++++ .../InMemoryConversationStorageTests.cs | 644 +++++++++ .../OpenAIConversationsConformanceTests.cs | 1206 +++++++++++++++++ .../OpenAIConversationsSerializationTests.cs | 596 ++++++++ .../OpenAIHttpApiIntegrationTests.cs | 462 +++++++ ...esponsesAgentResolutionIntegrationTests.cs | 438 ++++++ .../OpenAIResponsesConformanceTests.cs | 421 ++---- .../OpenAIResponsesIntegrationTests.cs | 5 +- .../OpenAIResponsesSerializationTests.cs | 142 +- .../SortOrderExtensionsTests.cs | 64 + .../StreamingEventConformanceTests.cs | 68 +- .../TestHelpers.cs | 190 ++- .../07_GroupChat_Workflow_HostAsAgent.cs | 12 +- 133 files changed, 11754 insertions(+), 997 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IAgentConversationIndex.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/AddMessageRequest.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/Conversation.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/CreateConversationRequest.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/SortOrder.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/UpdateConversationRequest.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/SortOrderExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Conversations.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/InMemoryStorageOptions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/MemoryCacheExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/DeleteResponse.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ErrorResponse.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ListResponse.cs rename dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/{Responses/ResponsesJsonContext.cs => OpenAIHostingJsonUtilities.cs} (50%) create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemParamConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemParamConverter.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponseExecutor.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponsesService.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParam.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParamExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/WorkflowEventData.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalRequestEventGenerator.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalResponseEventGenerator.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Conversations.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/SseJsonResult.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentInvocationContextTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic_streaming/first_message_response.txt create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_conversation/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_item/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_conversation_not_found/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_delete_already_deleted/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/request.txt create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_limit/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_item_not_found/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_missing_required_field/request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/create_conversation_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_response.txt create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/list_items/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/create_conversation_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_response.txt create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_conversation/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_item/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/create_conversation_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call_streaming/first_message_request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/response.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/FunctionApprovalTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/HostApplicationBuilderExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/IdGeneratorTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryAgentConversationIndexTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/SortOrderExtensionsTests.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 69d3e03d31..eeb728a242 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -52,6 +52,7 @@ + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 571b07b1d5..a357e29e26 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -80,7 +80,7 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te builder.AddSequentialWorkflow("science-sequential-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent(); builder.AddConcurrentWorkflow("science-concurrent-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent(); -builder.AddOpenAIResponses(); +builder.Services.AddOpenAIResponses(); var app = builder.Build(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs new file mode 100644 index 0000000000..a72111cec3 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +/// +/// Handles route requests for OpenAI Conversations API endpoints. +/// +internal sealed class ConversationsHttpHandler +{ + private readonly IConversationStorage _storage; + private readonly IAgentConversationIndex? _conversationIndex; + + /// + /// Initializes a new instance of the class. + /// + /// The conversation storage service. + /// Optional conversation index service. + public ConversationsHttpHandler(IConversationStorage storage, IAgentConversationIndex? conversationIndex) + { + this._storage = storage ?? throw new ArgumentNullException(nameof(storage)); + this._conversationIndex = conversationIndex; + } + /// + /// Lists conversations by agent ID. + /// + public async Task ListConversationsByAgentAsync( + [FromQuery] string? agent_id, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(agent_id)) + { + return Results.BadRequest(new ErrorResponse + { + Error = new ErrorDetails + { + Message = "agent_id query parameter is required.", + Type = "invalid_request_error" + } + }); + } + + // Return empty list if conversation index is not registered + if (this._conversationIndex == null) + { + return Results.Ok(new ListResponse + { + Data = [], + HasMore = false + }); + } + + var conversationIdsResponse = await this._conversationIndex.GetConversationIdsAsync(agent_id, cancellationToken).ConfigureAwait(false); + + // Fetch full conversation objects + var conversations = new List(); + foreach (var conversationId in conversationIdsResponse.Data) + { + var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + if (conversation is not null) + { + conversations.Add(conversation); + } + } + + return Results.Ok(new ListResponse + { + Data = conversations, + HasMore = false + }); + } + + /// + /// Creates a new conversation. + /// + public async Task CreateConversationAsync( + [FromBody] CreateConversationRequest request, + CancellationToken cancellationToken) + { + Dictionary metadata = request.Metadata ?? []; + var conversation = new Conversation + { + Id = $"conv_{Guid.NewGuid():N}", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = metadata + }; + + var created = await this._storage.CreateConversationAsync(conversation, cancellationToken).ConfigureAwait(false); + + // Add initial items if provided + if (request.Items is { Count: > 0 }) + { + foreach (ItemParam itemParam in request.Items) + { + ItemResource itemToAdd = itemParam.ToItemResource(); + await this._storage.AddItemAsync(created.Id, itemToAdd, cancellationToken).ConfigureAwait(false); + } + } + + // Add to conversation index if available and agent_id is provided in metadata + if (this._conversationIndex != null && created.Metadata.TryGetValue("agent_id", out var agentId) && !string.IsNullOrEmpty(agentId)) + { + await this._conversationIndex.AddConversationAsync(agentId, created.Id, cancellationToken).ConfigureAwait(false); + } + + return Results.Ok(created); + } + + /// + /// Retrieves a conversation by ID. + /// + public async Task GetConversationAsync( + string conversationId, + CancellationToken cancellationToken) + { + var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + return conversation is not null + ? Results.Ok(conversation) + : Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Conversation '{conversationId}' not found.", + Type = "invalid_request_error" + } + }); + } + + /// + /// Updates a conversation's metadata. + /// + public async Task UpdateConversationAsync( + string conversationId, + [FromBody] UpdateConversationRequest request, + CancellationToken cancellationToken) + { + var existing = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + if (existing is null) + { + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Conversation '{conversationId}' not found.", + Type = "invalid_request_error" + } + }); + } + + var updated = existing with + { + Metadata = request.Metadata + }; + + var result = await this._storage.UpdateConversationAsync(updated, cancellationToken).ConfigureAwait(false); + return Results.Ok(result); + } + + /// + /// Deletes a conversation and all its messages. + /// + public async Task DeleteConversationAsync( + string conversationId, + CancellationToken cancellationToken) + { + // Get conversation first to retrieve agent_id for index removal + var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + + var deleted = await this._storage.DeleteConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + if (!deleted) + { + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Conversation '{conversationId}' not found.", + Type = "invalid_request_error" + } + }); + } + + // Remove from conversation index if available and agent_id was present in metadata + if (this._conversationIndex != null && conversation?.Metadata.TryGetValue("agent_id", out var agentId) == true && !string.IsNullOrEmpty(agentId)) + { + await this._conversationIndex.RemoveConversationAsync(agentId, conversationId, cancellationToken).ConfigureAwait(false); + } + + return Results.Ok(new DeleteResponse + { + Id = conversationId, + Object = "conversation.deleted", + Deleted = true + }); + } + + /// + /// Adds items to a conversation. + /// + public async Task CreateItemsAsync( + string conversationId, + [FromBody] CreateItemsRequest request, + [FromQuery] string[]? include, + CancellationToken cancellationToken) + { + var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + if (conversation is null) + { + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Conversation '{conversationId}' not found.", + Type = "invalid_request_error" + } + }); + } + + var createdItems = new List(); + foreach (ItemParam itemParam in request.Items) + { + ItemResource itemToAdd = itemParam.ToItemResource(); + ItemResource created = await this._storage.AddItemAsync(conversationId, itemToAdd, cancellationToken).ConfigureAwait(false); + createdItems.Add(created); + } + + return Results.Ok(new ListResponse + { + Data = createdItems, + FirstId = createdItems.Count > 0 ? createdItems[0].Id : null, + LastId = createdItems.Count > 0 ? createdItems[^1].Id : null, + HasMore = false + }); + } + + /// + /// Lists items in a conversation. + /// + public async Task ListItemsAsync( + string conversationId, + [FromQuery] int? limit, + [FromQuery] string? order, + [FromQuery] string? after, + [FromQuery] string[]? include, + CancellationToken cancellationToken) + { + // Validate limit parameter + if (limit is not null && limit.Value < 1) + { + return Results.BadRequest(new ErrorResponse + { + Error = new ErrorDetails + { + Message = "Invalid value for 'limit': must be a positive integer.", + Type = "invalid_request_error", + Code = "invalid_value" + } + }); + } + + var conversation = await this._storage.GetConversationAsync(conversationId, cancellationToken).ConfigureAwait(false); + if (conversation is null) + { + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Conversation '{conversationId}' not found.", + Type = "invalid_request_error" + } + }); + } + + var result = await this._storage.ListItemsAsync(conversationId, limit ?? 20, ParseOrder(order ?? "desc"), after, cancellationToken).ConfigureAwait(false); + return Results.Ok(result); + } + + /// + /// Retrieves a specific item. + /// + public async Task GetItemAsync( + string conversationId, + string itemId, + [FromQuery] string[]? include, + CancellationToken cancellationToken) + { + var item = await this._storage.GetItemAsync(conversationId, itemId, cancellationToken).ConfigureAwait(false); + return item is not null + ? Results.Ok(item) + : Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Item '{itemId}' not found in conversation '{conversationId}'.", + Type = "invalid_request_error" + } + }); + } + + /// + /// Deletes a specific item. + /// + public async Task DeleteItemAsync( + string conversationId, + string itemId, + CancellationToken cancellationToken) + { + var deleted = await this._storage.DeleteItemAsync(conversationId, itemId, cancellationToken).ConfigureAwait(false); + if (!deleted) + { + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Item '{itemId}' not found in conversation '{conversationId}'.", + Type = "invalid_request_error" + } + }); + } + + return Results.Ok(new DeleteResponse + { + Id = itemId, + Object = "conversation.item.deleted", + Deleted = true + }); + } + + private static SortOrder ParseOrder(string order) + { + return string.Equals(order, "asc", StringComparison.OrdinalIgnoreCase) ? SortOrder.Ascending : SortOrder.Descending; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IAgentConversationIndex.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IAgentConversationIndex.cs new file mode 100644 index 0000000000..a1e89d1676 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IAgentConversationIndex.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +/// +/// Optional service for indexing conversations by agent ID. +/// This is a non-standard extension to the OpenAI Conversations API. +/// +internal interface IAgentConversationIndex +{ + /// + /// Adds a conversation to the index for the specified agent. + /// + /// The agent identifier. + /// The conversation identifier. + /// Cancellation token. + /// A task that represents the asynchronous operation. + Task AddConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default); + + /// + /// Removes a conversation from the index for the specified agent. + /// + /// The agent identifier. + /// The conversation identifier. + /// Cancellation token. + /// A task that represents the asynchronous operation. + Task RemoveConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default); + + /// + /// Gets all conversation IDs for the specified agent. + /// + /// The agent identifier. + /// Cancellation token. + /// A list response containing conversation IDs associated with the agent. + Task> GetConversationIdsAsync(string agentId, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs new file mode 100644 index 0000000000..4547385760 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +/// +/// Storage abstraction for conversations and messages. +/// This interface provides operations specifically designed for conversation management, +/// going beyond simple key-value storage to support conversation-specific queries and operations. +/// +internal interface IConversationStorage +{ + /// + /// Creates a new conversation. + /// + /// The conversation to create. + /// Cancellation token. + /// The created conversation. + Task CreateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default); + + /// + /// Retrieves a conversation by ID. + /// + /// The conversation ID. + /// Cancellation token. + /// The conversation if found, null otherwise. + Task GetConversationAsync(string conversationId, CancellationToken cancellationToken = default); + + /// + /// Updates an existing conversation. + /// + /// The conversation with updated values. + /// Cancellation token. + /// The updated conversation if found, null otherwise. + Task UpdateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default); + + /// + /// Deletes a conversation and all its messages. + /// + /// The conversation ID. + /// Cancellation token. + /// True if deleted, false if not found. + Task DeleteConversationAsync(string conversationId, CancellationToken cancellationToken = default); + + // Item operations + + /// + /// Adds an item (message, function call, etc.) to a conversation. + /// Items are ItemResource objects from the Responses API. + /// + /// The conversation ID to add the item to. + /// The item to add. + /// Cancellation token. + /// The created item. + Task AddItemAsync(string conversationId, ItemResource item, CancellationToken cancellationToken = default); + + /// + /// Adds multiple items to a conversation atomically. + /// Items are ItemResource objects from the Responses API. + /// + /// The conversation ID to add the items to. + /// The items to add. + /// Cancellation token. + /// A task that completes when all items have been added. + Task AddItemsAsync(string conversationId, IEnumerable items, CancellationToken cancellationToken = default); + + /// + /// Retrieves an item by ID. + /// + /// The conversation ID. + /// The item ID. + /// Cancellation token. + /// The item if found, null otherwise. + Task GetItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default); + + /// + /// Lists items in a conversation with pagination support. + /// + /// The conversation ID. + /// Maximum number of items to return (default: 20, max: 100). + /// Sort order (default: Descending). + /// Cursor for pagination - return items after this ID. + /// Cancellation token. + /// A list response with items and pagination info. + Task> ListItemsAsync( + string conversationId, + int limit = 20, + SortOrder order = SortOrder.Descending, + string? after = null, + CancellationToken cancellationToken = default); + + /// + /// Deletes a specific item from a conversation. + /// + /// The conversation ID. + /// The item ID. + /// Cancellation token. + /// True if deleted, false if not found. + Task DeleteItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs new file mode 100644 index 0000000000..1cfc9e59f2 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +/// +/// In-memory implementation of IAgentConversationIndex for development and testing. +/// This is a non-standard extension to the OpenAI Conversations API. +/// +internal sealed class InMemoryAgentConversationIndex : IAgentConversationIndex, IDisposable +{ + private readonly MemoryCache _cache; + private readonly InMemoryStorageOptions _options; + + private sealed class ConversationSet + { + private readonly HashSet _conversations = []; + private readonly object _lock = new(); + + public void Add(string conversationId) + { + lock (this._lock) + { + this._conversations.Add(conversationId); + } + } + + public bool Remove(string conversationId) + { + lock (this._lock) + { + return this._conversations.Remove(conversationId); + } + } + + public string[] GetAll() + { + lock (this._lock) + { + return [.. this._conversations]; + } + } + } + + public InMemoryAgentConversationIndex() + : this(new InMemoryStorageOptions()) + { + } + + public InMemoryAgentConversationIndex(InMemoryStorageOptions options) + { + ArgumentNullException.ThrowIfNull(options); + this._options = options; + this._cache = new MemoryCache(options.ToMemoryCacheOptions()); + } + + private async Task GetOrCreateConversationSetAsync(string agentId, CancellationToken cancellationToken) + { + (ConversationSet? conversationSet, _) = await this._cache.GetOrCreateAtomicAsync( + agentId, + entry => + { + entry.SetOptions(this._options.ToMemoryCacheEntryOptions()); + return new ConversationSet(); + }, + cancellationToken).ConfigureAwait(false); + + return conversationSet!; + } + + /// + public async Task AddConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(agentId); + ArgumentException.ThrowIfNullOrEmpty(conversationId); + + ConversationSet conversationSet = await this.GetOrCreateConversationSetAsync(agentId, cancellationToken).ConfigureAwait(false); + conversationSet.Add(conversationId); + } + + /// + public async Task RemoveConversationAsync(string agentId, string conversationId, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(agentId); + ArgumentException.ThrowIfNullOrEmpty(conversationId); + + if (this._cache.TryGetValue(agentId, out ConversationSet? conversationSet) && conversationSet is not null) + { + conversationSet.Remove(conversationId); + } + } + + /// + public async Task> GetConversationIdsAsync(string agentId, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(agentId); + + string[] conversations; + if (this._cache.TryGetValue(agentId, out ConversationSet? conversationSet) && conversationSet is not null) + { + conversations = conversationSet.GetAll(); + } + else + { + conversations = []; + } + + return new ListResponse + { + Data = [.. conversations], + HasMore = false + }; + } + + public void Dispose() + { + // The MemoryCache will call the post-eviction callbacks when disposed, + // which will dispose all ConversationSet instances + this._cache.Dispose(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs new file mode 100644 index 0000000000..f6d58ea929 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +/// +/// In-memory implementation of conversation storage for testing and development. +/// This implementation is thread-safe but data is not persisted across application restarts. +/// +internal sealed class InMemoryConversationStorage : IConversationStorage, IDisposable +{ + private readonly MemoryCache _cache; + private readonly InMemoryStorageOptions _options; + + public InMemoryConversationStorage() + : this(new InMemoryStorageOptions()) + { + } + + public InMemoryConversationStorage(InMemoryStorageOptions options) + { + ArgumentNullException.ThrowIfNull(options); + this._options = options; + this._cache = new MemoryCache(options.ToMemoryCacheOptions()); + } + + /// + public Task CreateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default) + { + // Check if conversation already exists + if (this._cache.TryGetValue(conversation.Id, out ConversationState? _)) + { + throw new InvalidOperationException($"Conversation with ID '{conversation.Id}' already exists."); + } + + var state = new ConversationState(conversation); + var entryOptions = this._options.ToMemoryCacheEntryOptions(); + this._cache.Set(conversation.Id, state, entryOptions); + return Task.FromResult(conversation); + } + + /// + public Task GetConversationAsync(string conversationId, CancellationToken cancellationToken = default) + { + if (this._cache.TryGetValue(conversationId, out ConversationState? state) && state is not null) + { + return Task.FromResult(state.Conversation); + } + + return Task.FromResult(null); + } + + /// + public Task UpdateConversationAsync(Conversation conversation, CancellationToken cancellationToken = default) + { + if (this._cache.TryGetValue(conversation.Id, out ConversationState? state) && state is not null) + { + state.UpdateConversation(conversation); + // Touch the cache entry to reset expiration + var entryOptions = this._options.ToMemoryCacheEntryOptions(); + this._cache.Set(conversation.Id, state, entryOptions); + return Task.FromResult(conversation); + } + + return Task.FromResult(null); + } + + /// + public Task DeleteConversationAsync(string conversationId, CancellationToken cancellationToken = default) + { + if (this._cache.TryGetValue(conversationId, out _)) + { + this._cache.Remove(conversationId); + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + + /// + public Task AddItemAsync(string conversationId, ItemResource item, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(conversationId, nameof(conversationId)); + + if (!this._cache.TryGetValue(conversationId, out ConversationState? state) || state is null) + { + throw new InvalidOperationException($"Conversation '{conversationId}' not found."); + } + + state.AddItem(item); + // Touch the cache entry to reset expiration + var entryOptions = this._options.ToMemoryCacheEntryOptions(); + this._cache.Set(conversationId, state, entryOptions); + return Task.FromResult(item); + } + + /// + public Task AddItemsAsync(string conversationId, IEnumerable items, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(conversationId, nameof(conversationId)); + ArgumentNullException.ThrowIfNull(items); + + if (!this._cache.TryGetValue(conversationId, out ConversationState? state) || state is null) + { + throw new InvalidOperationException($"Conversation '{conversationId}' not found."); + } + + foreach (ItemResource item in items) + { + state.AddItem(item); + } + + // Touch the cache entry to reset expiration + var entryOptions = this._options.ToMemoryCacheEntryOptions(); + this._cache.Set(conversationId, state, entryOptions); + return Task.CompletedTask; + } + + /// + public Task GetItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default) + { + if (this._cache.TryGetValue(conversationId, out ConversationState? state) && state is not null) + { + return Task.FromResult(state.GetItem(itemId)); + } + + return Task.FromResult(null); + } + + /// + public Task> ListItemsAsync( + string conversationId, + int limit = 20, + SortOrder order = SortOrder.Descending, + string? after = null, + CancellationToken cancellationToken = default) + { + limit = Math.Clamp(limit, 1, 100); + + if (!this._cache.TryGetValue(conversationId, out ConversationState? state) || state is null) + { + throw new InvalidOperationException($"Conversation '{conversationId}' not found."); + } + + var allItems = state.GetAllItems(); + + // For descending order, reverse the list + if (order == SortOrder.Descending) + { + allItems.Reverse(); + } + + var filtered = allItems.AsEnumerable(); + + if (!string.IsNullOrEmpty(after)) + { + var afterIndex = allItems.FindIndex(m => m.Id == after); + if (afterIndex >= 0) + { + filtered = allItems.Skip(afterIndex + 1); + } + } + + var result = filtered.Take(limit + 1).ToList(); + var hasMore = result.Count > limit; + if (hasMore) + { + result = result.Take(limit).ToList(); + } + + return Task.FromResult(new ListResponse + { + Data = result, + FirstId = result.FirstOrDefault()?.Id, + LastId = result.LastOrDefault()?.Id, + HasMore = hasMore + }); + } + + /// + public Task DeleteItemAsync(string conversationId, string itemId, CancellationToken cancellationToken = default) + { + if (this._cache.TryGetValue(conversationId, out ConversationState? state) && state is not null) + { + var removed = state.RemoveItem(itemId); + if (removed) + { + // Touch the cache entry to reset expiration + var entryOptions = this._options.ToMemoryCacheEntryOptions(); + this._cache.Set(conversationId, state, entryOptions); + } + + return Task.FromResult(removed); + } + + return Task.FromResult(false); + } + + /// + /// Encapsulates per-conversation state including items storage and synchronization. + /// + private sealed class ConversationState + { +#if NET9_0_OR_GREATER + private readonly OrderedDictionary _items = []; + private readonly object _lock = new(); + private Conversation _conversation; + + public ConversationState(Conversation conversation) + { + this._conversation = conversation; + } + + public Conversation Conversation + { + get + { + lock (this._lock) + { + return this._conversation; + } + } + } + + public void UpdateConversation(Conversation conversation) + { + lock (this._lock) + { + this._conversation = conversation; + } + } + + public void AddItem(ItemResource item) + { + lock (this._lock) + { + if (!this._items.TryAdd(item.Id, item)) + { + throw new InvalidOperationException($"Item with ID '{item.Id}' already exists."); + } + } + } + + public ItemResource? GetItem(string itemId) + { + lock (this._lock) + { + this._items.TryGetValue(itemId, out var item); + return item; + } + } + + public List GetAllItems() + { + lock (this._lock) + { + return this._items.Values.ToList(); + } + } + + public bool RemoveItem(string itemId) + { + lock (this._lock) + { + return this._items.Remove(itemId); + } + } +#else + private readonly List _items = []; + private readonly object _lock = new(); + private Conversation _conversation; + + public ConversationState(Conversation conversation) + { + this._conversation = conversation; + } + + public Conversation Conversation + { + get + { + lock (this._lock) + { + return this._conversation; + } + } + } + + public void UpdateConversation(Conversation conversation) + { + lock (this._lock) + { + this._conversation = conversation; + } + } + + public void AddItem(ItemResource item) + { + lock (this._lock) + { + if (this._items.Any(i => i.Id == item.Id)) + { + throw new InvalidOperationException($"Item with ID '{item.Id}' already exists."); + } + this._items.Add(item); + } + } + + public ItemResource? GetItem(string itemId) + { + lock (this._lock) + { + return this._items.FirstOrDefault(i => i.Id == itemId); + } + } + + public List GetAllItems() + { + lock (this._lock) + { + return this._items.ToList(); + } + } + + public bool RemoveItem(string itemId) + { + lock (this._lock) + { + var item = this._items.FirstOrDefault(i => i.Id == itemId); + if (item != null) + { + this._items.Remove(item); + return true; + } + return false; + } + } +#endif + } + + public void Dispose() + { + this._cache.Dispose(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/AddMessageRequest.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/AddMessageRequest.cs new file mode 100644 index 0000000000..29eac6f1da --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/AddMessageRequest.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +/// +/// Request to create items in a conversation. +/// +internal sealed class CreateItemsRequest +{ + /// + /// The items to add to the conversation. You may add up to 20 items at a time. + /// Items should be ItemParam objects (messages without IDs, function call outputs, etc.). + /// The server will assign IDs when creating the items. + /// + [JsonPropertyName("items")] + public required List Items { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/Conversation.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/Conversation.cs new file mode 100644 index 0000000000..ad37894804 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/Conversation.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +/// +/// Represents a conversation in the system. +/// +internal sealed record Conversation +{ + /// + /// The unique identifier for the conversation. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// The object type, always "conversation". + /// + [JsonPropertyName("object")] + [SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches OpenAI API specification")] + public string Object => "conversation"; + + /// + /// The Unix timestamp (in seconds) for when the conversation was created. + /// + [JsonPropertyName("created_at")] + public required long CreatedAt { get; init; } + + /// + /// Set of 16 key-value pairs that can be attached to a conversation. + /// + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; init; } = []; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/CreateConversationRequest.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/CreateConversationRequest.cs new file mode 100644 index 0000000000..1c90946b5f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/CreateConversationRequest.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +/// +/// Request to create a new conversation. +/// +internal sealed class CreateConversationRequest +{ + /// + /// Initial items to include in the conversation context. You may add up to 20 items at a time. + /// Items should be ItemParam objects (messages without IDs, as the server will generate them). + /// + [JsonPropertyName("items")] + public List? Items { get; init; } + + /// + /// Set of 16 key-value pairs that can be attached to a conversation. + /// + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/SortOrder.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/SortOrder.cs new file mode 100644 index 0000000000..765d5f633b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/SortOrder.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +/// +/// Specifies the sort order for list operations. +/// +[JsonConverter(typeof(SortOrderJsonConverter))] +internal enum SortOrder +{ + /// + /// Sort in ascending order (oldest to newest). + /// + Ascending, + + /// + /// Sort in descending order (newest to oldest). + /// + Descending +} + +/// +/// Custom JSON converter for SortOrder enum to serialize as "asc" and "desc". +/// +internal sealed class SortOrderJsonConverter : JsonConverter +{ + /// + public override SortOrder Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value?.ToUpperInvariant() switch + { + "ASC" => SortOrder.Ascending, + "DESC" => SortOrder.Descending, + _ => throw new JsonException($"Invalid SortOrder value: {value}") + }; + } + + /// + public override void Write(Utf8JsonWriter writer, SortOrder value, JsonSerializerOptions options) + { + var stringValue = value switch + { + SortOrder.Ascending => "asc", + SortOrder.Descending => "desc", + _ => throw new JsonException($"Invalid SortOrder value: {value}") + }; + writer.WriteStringValue(stringValue); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/UpdateConversationRequest.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/UpdateConversationRequest.cs new file mode 100644 index 0000000000..bc0cc50512 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/Models/UpdateConversationRequest.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +/// +/// Request to update an existing conversation. +/// +internal sealed class UpdateConversationRequest +{ + /// + /// Set of 16 key-value pairs that can be attached to a conversation. + /// + [JsonPropertyName("metadata")] + public required Dictionary Metadata { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/SortOrderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/SortOrderExtensions.cs new file mode 100644 index 0000000000..8e29f3e5d9 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/SortOrderExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +/// +/// Extension methods for . +/// +internal static class SortOrderExtensions +{ + /// + /// Converts a to its string representation. + /// + /// The sort order. + /// The string representation ("asc" or "desc"). + public static string ToOrderString(this SortOrder order) + { + return order == SortOrder.Ascending ? "asc" : "desc"; + } + + /// + /// Checks if the sort order is ascending. + /// + /// The sort order. + /// True if ascending, false otherwise. + public static bool IsAscending(this SortOrder order) + { + return order == SortOrder.Ascending; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Conversations.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Conversations.cs new file mode 100644 index 0000000000..0c4af2cfb5 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Conversations.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Provides extension methods for mapping OpenAI Conversations API to an . +/// +public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions +{ + /// + /// Maps OpenAI Conversations API endpoints to the specified . + /// + /// The to add the OpenAI Conversations endpoints to. + public static IEndpointConventionBuilder MapOpenAIConversations(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var storage = endpoints.ServiceProvider.GetService() + ?? throw new InvalidOperationException("IConversationStorage is not registered. Call AddOpenAIConversations() in your service configuration."); + var conversationIndex = endpoints.ServiceProvider.GetService(); + var handlers = new ConversationsHttpHandler(storage, conversationIndex); + + var group = endpoints.MapGroup("/v1/conversations") + .WithTags("Conversations"); + + // Conversation endpoints + // Non-standard extension: List conversations by agent ID + group.MapGet("", handlers.ListConversationsByAgentAsync) + .WithName("ListConversationsByAgent") + .WithSummary("List conversations for a specific agent (non-standard extension)"); + + group.MapPost("", handlers.CreateConversationAsync) + .WithName("CreateConversation") + .WithSummary("Create a new conversation"); + + group.MapGet("{conversationId}", handlers.GetConversationAsync) + .WithName("GetConversation") + .WithSummary("Retrieve a conversation by ID"); + + group.MapPost("{conversationId}", handlers.UpdateConversationAsync) + .WithName("UpdateConversation") + .WithSummary("Update a conversation's metadata or title"); + + group.MapDelete("{conversationId}", handlers.DeleteConversationAsync) + .WithName("DeleteConversation") + .WithSummary("Delete a conversation and all its messages"); + + // Item endpoints + group.MapPost("{conversationId}/items", handlers.CreateItemsAsync) + .WithName("CreateItems") + .WithSummary("Add items to a conversation"); + + group.MapGet("{conversationId}/items", handlers.ListItemsAsync) + .WithName("ListItems") + .WithSummary("List items in a conversation"); + + group.MapGet("{conversationId}/items/{itemId}", handlers.GetItemAsync) + .WithName("GetItem") + .WithSummary("Retrieve a specific item"); + + group.MapDelete("{conversationId}/items/{itemId}", handlers.DeleteItemAsync) + .WithName("DeleteItem") + .WithSummary("Delete a specific item"); + + return group; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs index 0b89340167..8bdf8a7749 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs @@ -2,12 +2,11 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Threading; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.OpenAI; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; using Microsoft.Agents.AI.Hosting.OpenAI.Responses; -using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -43,11 +42,50 @@ public static IEndpointConventionBuilder MapOpenAIResponses( ValidateAgentName(agent.Name); responsesPath ??= $"/{agent.Name}/v1/responses"; + + // Get or create an executor for this specific agent + IResponseExecutor executor; + InMemoryResponsesService responsesService; + + // Try to get conversation storage from DI if available + IConversationStorage? conversationStorage = endpoints.ServiceProvider.GetService(); + + executor = new AIAgentResponseExecutor(agent, conversationStorage); + + // Try to get storage options from DI, or create default + InMemoryStorageOptions storageOptions = endpoints.ServiceProvider.GetService() ?? new InMemoryStorageOptions(); + responsesService = new InMemoryResponsesService(executor, storageOptions, conversationStorage); + + var handlers = new ResponsesHttpHandler(responsesService); + var group = endpoints.MapGroup(responsesPath); var endpointAgentName = agent.DisplayName; - group.MapPost("/", async ([FromBody] CreateResponse createResponse, CancellationToken cancellationToken) - => await AIAgentResponsesProcessor.CreateModelResponseAsync(agent, createResponse, cancellationToken).ConfigureAwait(false)) - .WithName(endpointAgentName + "/CreateResponse"); + + // Create response endpoint + group.MapPost("/", handlers.CreateResponseAsync) + .WithName(endpointAgentName + "/CreateResponse") + .WithSummary("Creates a model response for the given input"); + + // Get response endpoint + group.MapGet("{responseId}", handlers.GetResponseAsync) + .WithName(endpointAgentName + "/GetResponse") + .WithSummary("Retrieves a response by ID"); + + // Cancel response endpoint + group.MapPost("{responseId}/cancel", handlers.CancelResponseAsync) + .WithName(endpointAgentName + "/CancelResponse") + .WithSummary("Cancels an in-progress response"); + + // Delete response endpoint + group.MapDelete("{responseId}", handlers.DeleteResponseAsync) + .WithName(endpointAgentName + "/DeleteResponse") + .WithSummary("Deletes a response"); + + // List response input items endpoint + group.MapGet("{responseId}/input_items", handlers.ListResponseInputItemsAsync) + .WithName(endpointAgentName + "/ListResponseInputItems") + .WithSummary("Lists the input items for a response"); + return group; } @@ -70,24 +108,37 @@ public static IEndpointConventionBuilder MapOpenAIResponses( ArgumentNullException.ThrowIfNull(endpoints); responsesPath ??= "/v1/responses"; + var responsesService = endpoints.ServiceProvider.GetService() + ?? throw new InvalidOperationException("IResponsesService is not registered. Call AddOpenAIResponses() in your service configuration."); + var handlers = new ResponsesHttpHandler(responsesService); + var group = endpoints.MapGroup(responsesPath); - group.MapPost("/", async ([FromBody] CreateResponse createResponse, IServiceProvider serviceProvider, CancellationToken cancellationToken) => - { - // DevUI uses the 'model' field to specify the agent name. - var agentName = createResponse.Agent?.Name ?? createResponse.Model; - if (agentName is null) - { - return Results.BadRequest("No 'agent.name' or 'model' specified in the request."); - } - - var agent = serviceProvider.GetKeyedService(agentName); - if (agent is null) - { - return Results.NotFound($"Agent named '{agentName}' was not found."); - } - - return await AIAgentResponsesProcessor.CreateModelResponseAsync(agent, createResponse, cancellationToken).ConfigureAwait(false); - }).WithName("CreateResponse"); + + // Create response endpoint + group.MapPost("/", handlers.CreateResponseAsync) + .WithName("CreateResponse") + .WithSummary("Creates a model response for the given input"); + + // Get response endpoint + group.MapGet("{responseId}", handlers.GetResponseAsync) + .WithName("GetResponse") + .WithSummary("Retrieves a response by ID"); + + // Cancel response endpoint + group.MapPost("{responseId}/cancel", handlers.CancelResponseAsync) + .WithName("CancelResponse") + .WithSummary("Cancels an in-progress response"); + + // Delete response endpoint + group.MapDelete("{responseId}", handlers.DeleteResponseAsync) + .WithName("DeleteResponse") + .WithSummary("Deletes a response"); + + // List response input items endpoint + group.MapGet("{responseId}/input_items", handlers.ListResponseInputItemsAsync) + .WithName("ListResponseInputItems") + .WithSummary("Lists the input items for a response"); + return group; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs deleted file mode 100644 index f4bfeb2578..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Agents.AI; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.Extensions.Hosting; - -/// -/// Extension methods for to configure OpenAI Responses support. -/// -public static class MicrosoftAgentAIHostingOpenAIHostApplicationBuilderExtensions -{ - /// - /// Adds support for exposing instances via OpenAI Responses. - /// - /// The to configure. - /// The for method chaining. - public static IHostApplicationBuilder AddOpenAIResponses(this IHostApplicationBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.Services.AddOpenAIResponses(); - - return builder; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/InMemoryStorageOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/InMemoryStorageOptions.cs new file mode 100644 index 0000000000..6ef67bf986 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/InMemoryStorageOptions.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.Agents.AI.Hosting.OpenAI; + +/// +/// Configuration options for in-memory storage implementations. +/// +public sealed class InMemoryStorageOptions +{ + /// + /// Gets or sets the maximum number of items to store in the cache. + /// Default is 1000. Set to null for no size limit. + /// + public long? SizeLimit { get; set; } = 1000; + + /// + /// Gets or sets the absolute expiration time for items in storage. + /// If specified, items will be expired after this timespan regardless of access. + /// Default is null (no absolute expiration). + /// + public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } + + /// + /// Gets or sets the sliding expiration for items in storage. + /// Items will be expired if not accessed within this timespan. + /// Default is 1 hour. + /// + public TimeSpan? SlidingExpiration { get; set; } = TimeSpan.FromHours(1); + + /// + /// Creates from these options. + /// + internal MemoryCacheOptions ToMemoryCacheOptions() => new() + { + SizeLimit = this.SizeLimit + }; + + /// + /// Creates from these options. + /// + internal MemoryCacheEntryOptions ToMemoryCacheEntryOptions() => new() + { + AbsoluteExpirationRelativeToNow = this.AbsoluteExpirationRelativeToNow, + SlidingExpiration = this.SlidingExpiration, + Size = 1 + }; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/MemoryCacheExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/MemoryCacheExtensions.cs new file mode 100644 index 0000000000..dc64e44d22 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/MemoryCacheExtensions.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.Agents.AI.Hosting.OpenAI; + +/// +/// Extension methods for that provide atomic operations. +/// +/// +/// The standard GetOrCreate method has a race condition where multiple threads can simultaneously +/// detect that a key doesn't exist and create different instances, with only one being cached. +/// See: https://github.com/dotnet/runtime/issues/36499 +/// +internal static class MemoryCacheExtensions +{ + private static readonly ConcurrentDictionary s_semaphores = new(); + + /// + /// Atomically gets the value associated with this key if it exists, or generates a new entry + /// using the provided key and a value from the given factory if the key is not found. + /// + /// The type of the object to get. + /// The instance this method extends. + /// The key of the entry to look for or create. + /// The factory that creates the value associated with this key if the key does not exist in the cache. + /// The cancellation token. + /// A tuple containing the value and a flag indicating whether it was created (true) or retrieved from cache (false). + public static async Task<(T? Value, bool Created)> GetOrCreateAtomicAsync( + this IMemoryCache memoryCache, + object key, + Func factory, + CancellationToken cancellationToken = default) + { + // Fast path: check if the value already exists + if (memoryCache.TryGetValue(key, out object? value)) + { + return ((T?)value, false); + } + + // Get or create a semaphore for this cache key + bool isOwner = false; + int semaphoreKey = (memoryCache, key).GetHashCode(); + if (!s_semaphores.TryGetValue(semaphoreKey, out SemaphoreSlim? semaphore)) + { + SemaphoreSlim? createdSemaphore = null; + semaphore = s_semaphores.GetOrAdd(semaphoreKey, _ => createdSemaphore = new SemaphoreSlim(1)); + + // If we created the semaphore that made it into the dictionary, we're the owner + if (ReferenceEquals(createdSemaphore, semaphore)) + { + isOwner = true; + } + else + { + // Our semaphore wasn't the one stored, so dispose it + createdSemaphore?.Dispose(); + } + } + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + // Double-check: another thread might have created the value while we were waiting + if (!memoryCache.TryGetValue(key, out value)) + { + ICacheEntry entry = memoryCache.CreateEntry(key); + entry.SetValue(value = factory(entry)); + entry.Dispose(); + return ((T?)value, true); + } + + return ((T?)value, false); + } + finally + { + // If we were the owner of the semaphore, remove it from the dictionary + // This prevents memory leaks from accumulating semaphores for evicted cache entries + if (isOwner) + { + s_semaphores.TryRemove(semaphoreKey, out _); + } + + semaphore.Release(); + } + } + + /// + /// Atomically updates or creates a cache entry using the provided factory. + /// + /// The type of the object to set. + /// The instance this method extends. + /// The key of the entry to update or create. + /// The factory that creates the new value. Receives the cache entry and the current value (or default if none exists). + /// The cancellation token. + /// The new value that was set in the cache. + public static async Task SetAtomicAsync( + this IMemoryCache memoryCache, + object key, + Func factory, + CancellationToken cancellationToken = default) + { + // Get or create a semaphore for this cache key + bool isOwner = false; + int semaphoreKey = (memoryCache, key).GetHashCode(); + if (!s_semaphores.TryGetValue(semaphoreKey, out SemaphoreSlim? semaphore)) + { + SemaphoreSlim? createdSemaphore = null; + semaphore = s_semaphores.GetOrAdd(semaphoreKey, _ => createdSemaphore = new SemaphoreSlim(1)); + + // If we created the semaphore that made it into the dictionary, we're the owner + if (ReferenceEquals(createdSemaphore, semaphore)) + { + isOwner = true; + } + else + { + // Our semaphore wasn't the one stored, so dispose it + createdSemaphore?.Dispose(); + } + } + + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + T? currentValue = default; + if (memoryCache.TryGetValue(key, out var value)) + { + currentValue = (T)value!; + } + + ICacheEntry entry = memoryCache.CreateEntry(key); + T newValue = factory(entry, currentValue); + entry.SetValue(newValue); + entry.Dispose(); + + return newValue; + } + finally + { + // If we were the owner of the semaphore, remove it from the dictionary + // This prevents memory leaks from accumulating semaphores for evicted cache entries + if (isOwner) + { + s_semaphores.TryRemove(semaphoreKey, out _); + } + + semaphore.Release(); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj index ee5202c53f..0030b86a4d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj @@ -25,6 +25,7 @@ + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/DeleteResponse.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/DeleteResponse.cs new file mode 100644 index 0000000000..1a13ce1f8c --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/DeleteResponse.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Models; + +/// +/// Response for a delete operation. +/// +internal sealed class DeleteResponse +{ + /// + /// The ID of the deleted object. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// The object type. + /// + [JsonPropertyName("object")] + [SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches OpenAI API specification")] + public required string Object { get; init; } + + /// + /// Whether the object was successfully deleted. + /// + [JsonPropertyName("deleted")] + public required bool Deleted { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ErrorResponse.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ErrorResponse.cs new file mode 100644 index 0000000000..9a2417b6af --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ErrorResponse.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Models; + +/// +/// Represents an error response from the OpenAI APIs. +/// +internal sealed class ErrorResponse +{ + /// + /// Gets the error details. + /// + [JsonPropertyName("error")] + public required ErrorDetails Error { get; init; } +} + +/// +/// Represents the details of an error. +/// +internal sealed class ErrorDetails +{ + /// + /// Gets the error message. + /// + [JsonPropertyName("message")] + public required string Message { get; init; } + + /// + /// Gets the error type. + /// + [JsonPropertyName("type")] + public required string Type { get; init; } + + /// + /// Gets the error code. + /// + [JsonPropertyName("code")] + public string? Code { get; init; } + + /// + /// Gets the parameter that caused the error. + /// + [JsonPropertyName("param")] + public string? Param { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ListResponse.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ListResponse.cs new file mode 100644 index 0000000000..dd75ff3e18 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Models/ListResponse.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Models; + +/// +/// Generic list response for paginated results. +/// Used across the OpenAI API for listing resources. +/// +internal sealed class ListResponse +{ + /// + /// The object type, always "list". + /// + [JsonPropertyName("object")] + [SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches OpenAI API specification")] + public string Object => "list"; + + /// + /// The list of items. + /// + [JsonPropertyName("data")] + public required List Data { get; init; } + + /// + /// The ID of the first item in the list. + /// + [JsonPropertyName("first_id")] + public string? FirstId { get; init; } + + /// + /// The ID of the last item in the list. + /// + [JsonPropertyName("last_id")] + public string? LastId { get; init; } + + /// + /// Whether there are more items available. + /// + [JsonPropertyName("has_more")] + public required bool HasMore { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs similarity index 50% rename from dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonContext.cs rename to dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs index 0b37929b48..91558b4db3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIHostingJsonUtilities.cs @@ -4,16 +4,62 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.AI; -namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; +namespace Microsoft.Agents.AI.Hosting.OpenAI; +/// +/// Provides JSON serialization options and context for OpenAI Hosting APIs to support AOT and trimming. +/// +internal static class OpenAIHostingJsonUtilities +{ + /// + /// Gets the default instance used for OpenAI API serialization. + /// Includes support for AIContent types and all OpenAI-related types. + /// + public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); + + private static JsonSerializerOptions CreateDefaultOptions() + { + // Start with our source-generated context + JsonSerializerOptions options = new(OpenAIHostingJsonContext.Default.Options); + + // Chain with agent abstraction types + options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); + + // Chain with AIContent types from Microsoft.Extensions.AI + options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); + + options.MakeReadOnly(); + return options; + } +} + +/// +/// Provides a unified JSON serialization context for all OpenAI Hosting APIs to support AOT and trimming. +/// Combines Conversations and Responses API types. +/// [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - NumberHandling = JsonNumberHandling.AllowReadingFromString, - AllowOutOfOrderMetadataProperties = true, - WriteIndented = false)] -[JsonSerializable(typeof(Dictionary))] + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + AllowOutOfOrderMetadataProperties = true, + WriteIndented = false)] +// Conversations API types +[JsonSerializable(typeof(Conversation))] +[JsonSerializable(typeof(ListResponse))] +[JsonSerializable(typeof(CreateConversationRequest))] +[JsonSerializable(typeof(CreateItemsRequest))] +[JsonSerializable(typeof(UpdateConversationRequest))] +[JsonSerializable(typeof(ListResponse))] +[JsonSerializable(typeof(List))] +// Shared types +[JsonSerializable(typeof(DeleteResponse))] +[JsonSerializable(typeof(ErrorResponse))] +[JsonSerializable(typeof(ErrorDetails))] +// Responses API types [JsonSerializable(typeof(CreateResponse))] [JsonSerializable(typeof(Response))] [JsonSerializable(typeof(StreamingResponseEvent))] @@ -40,11 +86,9 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; [JsonSerializable(typeof(ResponseInput))] [JsonSerializable(typeof(InputMessage))] [JsonSerializable(typeof(List))] -[JsonSerializable(typeof(IReadOnlyList))] [JsonSerializable(typeof(InputMessageContent))] [JsonSerializable(typeof(ResponseStatus))] -[JsonSerializable(typeof(List))] -[JsonSerializable(typeof(IList))] +// ItemResource types [JsonSerializable(typeof(ItemResource))] [JsonSerializable(typeof(ResponsesMessageItemResource))] [JsonSerializable(typeof(ResponsesAssistantMessageItemResource))] @@ -67,8 +111,35 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; [JsonSerializable(typeof(MCPApprovalRequestItemResource))] [JsonSerializable(typeof(MCPApprovalResponseItemResource))] [JsonSerializable(typeof(MCPCallItemResource))] -[JsonSerializable(typeof(IList))] [JsonSerializable(typeof(List))] +// ItemParam types +[JsonSerializable(typeof(ItemParam))] +[JsonSerializable(typeof(ResponsesMessageItemParam))] +[JsonSerializable(typeof(ResponsesUserMessageItemParam))] +[JsonSerializable(typeof(ResponsesAssistantMessageItemParam))] +[JsonSerializable(typeof(ResponsesSystemMessageItemParam))] +[JsonSerializable(typeof(ResponsesDeveloperMessageItemParam))] +[JsonSerializable(typeof(FunctionToolCallItemParam))] +[JsonSerializable(typeof(FunctionToolCallOutputItemParam))] +[JsonSerializable(typeof(FileSearchToolCallItemParam))] +[JsonSerializable(typeof(ComputerToolCallItemParam))] +[JsonSerializable(typeof(ComputerToolCallOutputItemParam))] +[JsonSerializable(typeof(WebSearchToolCallItemParam))] +[JsonSerializable(typeof(ReasoningItemParam))] +[JsonSerializable(typeof(ItemReferenceItemParam))] +[JsonSerializable(typeof(ImageGenerationToolCallItemParam))] +[JsonSerializable(typeof(CodeInterpreterToolCallItemParam))] +[JsonSerializable(typeof(LocalShellToolCallItemParam))] +[JsonSerializable(typeof(LocalShellToolCallOutputItemParam))] +[JsonSerializable(typeof(MCPListToolsItemParam))] +[JsonSerializable(typeof(MCPApprovalRequestItemParam))] +[JsonSerializable(typeof(MCPApprovalResponseItemParam))] +[JsonSerializable(typeof(MCPCallItemParam))] +[JsonSerializable(typeof(List))] +// ItemContent types +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(ItemContent[]))] [JsonSerializable(typeof(ItemContent))] [JsonSerializable(typeof(ItemContentInputText))] [JsonSerializable(typeof(ItemContentInputAudio))] @@ -82,5 +153,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; [JsonSerializable(typeof(ResponseTextFormatConfigurationText))] [JsonSerializable(typeof(ResponseTextFormatConfigurationJsonObject))] [JsonSerializable(typeof(ResponseTextFormatConfigurationJsonSchema))] +// Common types +[JsonSerializable(typeof(Dictionary))] [ExcludeFromCodeCoverage] -internal sealed partial class ResponsesJsonContext : JsonSerializerContext; +internal sealed partial class OpenAIHostingJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs new file mode 100644 index 0000000000..bdc6be7bb2 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +/// +/// Response executor that uses an AIAgent to execute responses locally. +/// This is the default implementation for local execution. +/// +internal sealed class AIAgentResponseExecutor : IResponseExecutor +{ + private readonly AIAgent _agent; + private readonly IConversationStorage? _conversationStorage; + + public AIAgentResponseExecutor(AIAgent agent, IConversationStorage? conversationStorage = null) + { + ArgumentNullException.ThrowIfNull(agent); + this._agent = agent; + this._conversationStorage = conversationStorage; + } + + public async IAsyncEnumerable ExecuteAsync( + AgentInvocationContext context, + CreateResponse request, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Create options with properties from the request + var chatOptions = new ChatOptions + { + ConversationId = request.Conversation?.Id, + Temperature = (float?)request.Temperature, + TopP = (float?)request.TopP, + MaxOutputTokens = request.MaxOutputTokens, + Instructions = request.Instructions, + ModelId = request.Model, + AdditionalProperties = new AdditionalPropertiesDictionary + { + [nameof(CreateResponse)] = request + } + }; + var options = new ChatClientAgentRunOptions(chatOptions); + + // Convert input to chat messages + var messages = new List(); + + // If instructions are provided in the request, prepend them as a system message + if (!string.IsNullOrWhiteSpace(request.Instructions)) + { + messages.Add(new ChatMessage(ChatRole.System, request.Instructions)); + } + + foreach (var inputMessage in request.Input.GetInputMessages()) + { + messages.Add(inputMessage.ToChatMessage()); + } + + // Use the extension method to convert streaming updates to streaming response events + await foreach (var streamingEvent in this._agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken) + .ToStreamingResponseAsync(request, context, cancellationToken) + .ConfigureAwait(false)) + { + yield return streamingEvent; + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs deleted file mode 100644 index 5178abd8dc..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Linq; -using System.Net.ServerSentEvents; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; - -namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; - -/// -/// OpenAI Responses processor for . -/// -internal static class AIAgentResponsesProcessor -{ - public static async Task CreateModelResponseAsync(AIAgent agent, CreateResponse request, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(agent); - - var context = new AgentInvocationContext(idGenerator: IdGenerator.From(request)); - if (request.Stream == true) - { - return new StreamingResponse(agent, request, context); - } - - var messages = request.Input.GetInputMessages().Select(i => i.ToChatMessage()); - var response = await agent.RunAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false); - return Results.Ok(response.ToResponse(request, context)); - } - - private sealed class StreamingResponse(AIAgent agent, CreateResponse createResponse, AgentInvocationContext context) : IResult - { - public Task ExecuteAsync(HttpContext httpContext) - { - var cancellationToken = httpContext.RequestAborted; - var response = httpContext.Response; - - // Set SSE headers - response.Headers.ContentType = "text/event-stream"; - response.Headers.CacheControl = "no-cache,no-store"; - response.Headers.Connection = "keep-alive"; - response.Headers.ContentEncoding = "identity"; - httpContext.Features.GetRequiredFeature().DisableBuffering(); - - var chatMessages = createResponse.Input.GetInputMessages().Select(i => i.ToChatMessage()).ToList(); - var events = agent.RunStreamingAsync(chatMessages, cancellationToken: cancellationToken) - .ToStreamingResponseAsync(createResponse, context, cancellationToken) - .Select(static evt => new SseItem(evt, evt.Type)); - return SseFormatter.WriteAsync( - source: events, - destination: response.Body, - itemFormatter: static (sseItem, bufferWriter) => - { - using var writer = new Utf8JsonWriter(bufferWriter); - JsonSerializer.Serialize(writer, sseItem.Data, ResponsesJsonContext.Default.StreamingResponseEvent); - writer.Flush(); - }, - cancellationToken); - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs index 6db6dbbc51..f21c2e84e9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs @@ -29,5 +29,5 @@ internal sealed class AgentInvocationContext(IdGenerator idGenerator, JsonSerial /// /// Gets the JSON serializer options. /// - public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? ResponsesJsonSerializerOptions.Default; + public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? OpenAIHostingJsonUtilities.DefaultOptions; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs index b276489d00..6d78e7fceb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs @@ -88,22 +88,19 @@ public static Response ToResponse( /// An enumerable of ItemResource objects. public static IEnumerable ToItemResource(this ChatMessage message, IdGenerator idGenerator, JsonSerializerOptions jsonSerializerOptions) { - IList contents = []; + List contents = []; foreach (var content in message.Contents) { switch (content) { case FunctionCallContent functionCallContent: - // message.Role == ChatRole.Assistant yield return functionCallContent.ToFunctionToolCallItemResource(idGenerator.GenerateFunctionCallId(), jsonSerializerOptions); break; case FunctionResultContent functionResultContent: - // message.Role == ChatRole.Tool yield return functionResultContent.ToFunctionToolCallOutputItemResource( idGenerator.GenerateFunctionOutputId()); break; default: - // message.Role == ChatRole.Assistant if (ItemContentConverter.ToItemContent(content) is { } itemContent) { contents.Add(itemContent); @@ -115,11 +112,35 @@ public static IEnumerable ToItemResource(this ChatMessage message, if (contents.Count > 0) { - yield return new ResponsesAssistantMessageItemResource + List contentArray = contents; + string messageId = idGenerator.GenerateMessageId(); + + yield return message.Role.Value.ToUpperInvariant() switch { - Id = idGenerator.GenerateMessageId(), - Status = ResponsesMessageItemResourceStatus.Completed, - Content = contents + "USER" => new ResponsesUserMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + }, + "SYSTEM" => new ResponsesSystemMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + }, + "DEVELOPER" => new ResponsesDeveloperMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + }, + _ => new ResponsesAssistantMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + } }; } } @@ -168,6 +189,50 @@ public static FunctionToolCallOutputItemResource ToFunctionToolCallOutputItemRes }; } + /// + /// Converts an InputMessage to ItemResource objects. + /// + /// The input message to convert. + /// The ID generator to use for creating IDs. + /// An enumerable of ItemResource objects. + public static IEnumerable ToItemResource(this InputMessage inputMessage, IdGenerator idGenerator) + { + // Convert InputMessageContent to ItemContent array + List contentArray = inputMessage.Content.ToItemContents(); + + // Generate a message ID + string messageId = idGenerator.GenerateMessageId(); + + // Create the appropriate message type based on role + yield return inputMessage.Role.Value.ToUpperInvariant() switch + { + "USER" => new ResponsesUserMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + }, + "SYSTEM" => new ResponsesSystemMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + }, + "DEVELOPER" => new ResponsesDeveloperMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + }, + _ => new ResponsesAssistantMessageItemResource + { + Id = messageId, + Status = ResponsesMessageItemResourceStatus.Completed, + Content = contentArray + } + }; + } + /// /// Converts UsageDetails to ResponseUsage. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs index fb4ea9a04a..5d85a78e6d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs @@ -4,11 +4,13 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming; +using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; @@ -26,7 +28,7 @@ internal static class AgentRunResponseUpdateExtensions /// The agent invocation context. /// The cancellation token. /// A stream of response events. - internal static async IAsyncEnumerable ToStreamingResponseAsync( + public static async IAsyncEnumerable ToStreamingResponseAsync( this IAsyncEnumerable updates, CreateResponse request, AgentInvocationContext context, @@ -50,6 +52,13 @@ internal static async IAsyncEnumerable ToStreamingRespon cancellationToken.ThrowIfCancellationRequested(); var update = updateEnumerator.Current; + // Special-case for agent framework workflow events. + if (update.RawRepresentation is WorkflowEvent workflowEvent) + { + yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex); + continue; + } + if (!IsSameMessage(update, previousUpdate)) { // Finalize the current generator when moving to a new message. @@ -99,6 +108,10 @@ internal static async IAsyncEnumerable ToStreamingRespon TextReasoningContent => new TextReasoningContentEventGenerator(context.IdGenerator, seq, outputIndex), FunctionCallContent => new FunctionCallEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions), FunctionResultContent => new FunctionResultEventGenerator(context.IdGenerator, seq, outputIndex), +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + FunctionApprovalRequestContent => new FunctionApprovalRequestEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions), + FunctionApprovalResponseContent => new FunctionApprovalResponseEventGenerator(context.IdGenerator, seq, outputIndex), +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. ErrorContent => new ErrorContentEventGenerator(context.IdGenerator, seq, outputIndex), UriContent uriContent when uriContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex), DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex), @@ -192,4 +205,57 @@ static bool IsSameValue(string? str1, string? str2) => static bool IsSameRole(ChatRole? value1, ChatRole? value2) => !value1.HasValue || !value2.HasValue || value1.Value == value2.Value; } + + private static StreamingWorkflowEventComplete CreateWorkflowEventResponse(WorkflowEvent workflowEvent, int sequenceNumber, int outputIndex) + { + // Extract executor_id if this is an ExecutorEvent + string? executorId = null; + if (workflowEvent is ExecutorEvent execEvent) + { + executorId = execEvent.ExecutorId; + } + JsonElement eventData; + if (JsonSerializer.IsReflectionEnabledByDefault) + { + JsonElement? dataElement = null; + if (workflowEvent.Data is not null) + { +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + dataElement = JsonSerializer.SerializeToElement(workflowEvent.Data, OpenAIHostingJsonUtilities.DefaultOptions); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + } + + var eventDataObj = new WorkflowEventData + { + EventType = workflowEvent.GetType().Name, + Data = dataElement, + ExecutorId = executorId, + Timestamp = DateTime.UtcNow.ToString("O") + }; + +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + eventData = JsonSerializer.SerializeToElement(eventDataObj, OpenAIHostingJsonUtilities.DefaultOptions); +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + } + else + { + eventData = JsonSerializer.SerializeToElement( + "Unsupported. Workflow event serialization is currently only supported when JsonSerializer.IsReflectionEnabledByDefault is true.", + OpenAIHostingJsonContext.Default.String); + } + + // Create the properly typed streaming workflow event + return new StreamingWorkflowEventComplete + { + SequenceNumber = sequenceNumber, + OutputIndex = outputIndex, + Data = eventData, + ExecutorId = executorId, + ItemId = $"wf_{Guid.NewGuid().ToString("N")[..8]}" + }; + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs index cbaf7cb87b..ec23bac7f9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs @@ -104,22 +104,16 @@ UriContent uriContent when uriContent.HasTopLevelMediaType("image") => ImageUrl = uriContent.Uri?.ToString(), Detail = GetImageDetail(uriContent) }, - DataContent dataContent when dataContent.HasTopLevelMediaType("image") => - new ItemContentInputImage - { - ImageUrl = dataContent.Uri, - Detail = GetImageDetail(dataContent) - }, HostedFileContent hostedFile => new ItemContentInputFile { FileId = hostedFile.FileId }, - DataContent fileData when !fileData.HasTopLevelMediaType("image") && !fileData.HasTopLevelMediaType("audio") => - new ItemContentInputFile + DataContent dataContent when dataContent.HasTopLevelMediaType("image") => + new ItemContentInputImage { - FileData = fileData.Uri, - Filename = fileData.Name + ImageUrl = dataContent.Uri, + Detail = GetImageDetail(dataContent) }, DataContent audioData when audioData.HasTopLevelMediaType("audio") => new ItemContentInputAudio @@ -133,6 +127,12 @@ DataContent audioData when audioData.HasTopLevelMediaType("audio") => audioData.MediaType.Equals("audio/pcm", StringComparison.OrdinalIgnoreCase) ? "pcm16" : "mp3" // Default to mp3 }, + DataContent fileData => + new ItemContentInputFile + { + FileData = fileData.Uri, + Filename = fileData.Name + }, // Other AIContent types (FunctionCallContent, FunctionResultContent, etc.) // are handled separately in the Responses API as different ItemResource types, not ItemContent _ => null diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemParamConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemParamConverter.cs new file mode 100644 index 0000000000..540d71a157 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemParamConverter.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; + +/// +/// JSON converter for ItemParam that handles polymorphic deserialization based on the "type" discriminator. +/// +[ExcludeFromCodeCoverage] +internal sealed class ItemParamConverter : JsonConverter +{ + public override ItemParam? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + if (!root.TryGetProperty("type", out var typeElement)) + { + throw new JsonException("ItemParam must have a 'type' property"); + } + + var type = typeElement.GetString(); + var jsonText = root.GetRawText(); + + // Use OpenAIJsonContext directly since it has all the ItemParam type metadata + return type switch + { + "message" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.ResponsesMessageItemParam), + "function_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.FunctionToolCallItemParam), + "function_call_output" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.FunctionToolCallOutputItemParam), + "file_search_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.FileSearchToolCallItemParam), + "computer_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.ComputerToolCallItemParam), + "computer_call_output" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.ComputerToolCallOutputItemParam), + "web_search_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.WebSearchToolCallItemParam), + "reasoning" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.ReasoningItemParam), + "item_reference" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.ItemReferenceItemParam), + "image_generation_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.ImageGenerationToolCallItemParam), + "code_interpreter_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.CodeInterpreterToolCallItemParam), + "local_shell_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.LocalShellToolCallItemParam), + "local_shell_call_output" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.LocalShellToolCallOutputItemParam), + "mcp_list_tools" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.MCPListToolsItemParam), + "mcp_approval_request" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.MCPApprovalRequestItemParam), + "mcp_approval_response" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.MCPApprovalResponseItemParam), + "mcp_call" => JsonSerializer.Deserialize(jsonText, OpenAIHostingJsonContext.Default.MCPCallItemParam), + _ => throw new JsonException($"Unknown ItemParam type: {type}") + }; + } + + public override void Write(Utf8JsonWriter writer, ItemParam value, JsonSerializerOptions options) + { + // Use OpenAIJsonContext directly to serialize the concrete type + JsonSerializer.Serialize(writer, value, OpenAIHostingJsonContext.Default.Options.GetTypeInfo(value.GetType())); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs index 2865de63a6..77ae78bb8e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs @@ -14,16 +14,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; [ExcludeFromCodeCoverage] internal sealed class ItemResourceConverter : JsonConverter { - private readonly ResponsesJsonContext _context; - - /// - /// Initializes a new instance of the class. - /// - public ItemResourceConverter() - { - this._context = ResponsesJsonContext.Default; - } - + /// public override ItemResource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Clone the reader to peek at the JSON @@ -57,8 +48,16 @@ public ItemResourceConverter() if (readerClone.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) { - // Skip nested objects/arrays - readerClone.Skip(); + // The Utf8JsonReader.Skip() method will fail fast if it detects that we're reading + // from a partially read buffer, regardless of whether the next value is available. + // This can result in erroneous failures in cases where a custom converter is calling + // into a built-in converter (cf. https://github.com/dotnet/runtime/issues/74108). + // For this reason we need to call the TrySkip() method instead -- the serializer + // should guarantee sufficient read-ahead has been performed for the current object. + if (!readerClone.TrySkip()) + { + throw new InvalidOperationException("Failed to skip nested JSON value. Serializer should guarantee sufficient read-ahead has been done."); + } } } } @@ -66,82 +65,83 @@ public ItemResourceConverter() // Determine the concrete type based on the type discriminator and deserialize using the source generation context return type switch { - ResponsesMessageItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesMessageItemResource), - FileSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FileSearchToolCallItemResource), - FunctionToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FunctionToolCallItemResource), - FunctionToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FunctionToolCallOutputItemResource), - ComputerToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ComputerToolCallItemResource), - ComputerToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ComputerToolCallOutputItemResource), - WebSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.WebSearchToolCallItemResource), - ReasoningItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ReasoningItemResource), - ItemReferenceItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ItemReferenceItemResource), - ImageGenerationToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ImageGenerationToolCallItemResource), - CodeInterpreterToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.CodeInterpreterToolCallItemResource), - LocalShellToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.LocalShellToolCallItemResource), - LocalShellToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.LocalShellToolCallOutputItemResource), - MCPListToolsItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPListToolsItemResource), - MCPApprovalRequestItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPApprovalRequestItemResource), - MCPApprovalResponseItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPApprovalResponseItemResource), - MCPCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPCallItemResource), + ResponsesMessageItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ResponsesMessageItemResource), + FileSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.FileSearchToolCallItemResource), + FunctionToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.FunctionToolCallItemResource), + FunctionToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.FunctionToolCallOutputItemResource), + ComputerToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ComputerToolCallItemResource), + ComputerToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ComputerToolCallOutputItemResource), + WebSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.WebSearchToolCallItemResource), + ReasoningItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ReasoningItemResource), + ItemReferenceItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ItemReferenceItemResource), + ImageGenerationToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ImageGenerationToolCallItemResource), + CodeInterpreterToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.CodeInterpreterToolCallItemResource), + LocalShellToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.LocalShellToolCallItemResource), + LocalShellToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.LocalShellToolCallOutputItemResource), + MCPListToolsItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.MCPListToolsItemResource), + MCPApprovalRequestItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.MCPApprovalRequestItemResource), + MCPApprovalResponseItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.MCPApprovalResponseItemResource), + MCPCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.MCPCallItemResource), _ => throw new JsonException($"Unknown item type: {type}") }; } + /// public override void Write(Utf8JsonWriter writer, ItemResource value, JsonSerializerOptions options) { // Directly serialize using the appropriate type info from the context switch (value) { case ResponsesMessageItemResource message: - JsonSerializer.Serialize(writer, message, this._context.ResponsesMessageItemResource); + JsonSerializer.Serialize(writer, message, OpenAIHostingJsonContext.Default.ResponsesMessageItemResource); break; case FileSearchToolCallItemResource fileSearch: - JsonSerializer.Serialize(writer, fileSearch, this._context.FileSearchToolCallItemResource); + JsonSerializer.Serialize(writer, fileSearch, OpenAIHostingJsonContext.Default.FileSearchToolCallItemResource); break; case FunctionToolCallItemResource functionCall: - JsonSerializer.Serialize(writer, functionCall, this._context.FunctionToolCallItemResource); + JsonSerializer.Serialize(writer, functionCall, OpenAIHostingJsonContext.Default.FunctionToolCallItemResource); break; case FunctionToolCallOutputItemResource functionOutput: - JsonSerializer.Serialize(writer, functionOutput, this._context.FunctionToolCallOutputItemResource); + JsonSerializer.Serialize(writer, functionOutput, OpenAIHostingJsonContext.Default.FunctionToolCallOutputItemResource); break; case ComputerToolCallItemResource computerCall: - JsonSerializer.Serialize(writer, computerCall, this._context.ComputerToolCallItemResource); + JsonSerializer.Serialize(writer, computerCall, OpenAIHostingJsonContext.Default.ComputerToolCallItemResource); break; case ComputerToolCallOutputItemResource computerOutput: - JsonSerializer.Serialize(writer, computerOutput, this._context.ComputerToolCallOutputItemResource); + JsonSerializer.Serialize(writer, computerOutput, OpenAIHostingJsonContext.Default.ComputerToolCallOutputItemResource); break; case WebSearchToolCallItemResource webSearch: - JsonSerializer.Serialize(writer, webSearch, this._context.WebSearchToolCallItemResource); + JsonSerializer.Serialize(writer, webSearch, OpenAIHostingJsonContext.Default.WebSearchToolCallItemResource); break; case ReasoningItemResource reasoning: - JsonSerializer.Serialize(writer, reasoning, this._context.ReasoningItemResource); + JsonSerializer.Serialize(writer, reasoning, OpenAIHostingJsonContext.Default.ReasoningItemResource); break; case ItemReferenceItemResource itemReference: - JsonSerializer.Serialize(writer, itemReference, this._context.ItemReferenceItemResource); + JsonSerializer.Serialize(writer, itemReference, OpenAIHostingJsonContext.Default.ItemReferenceItemResource); break; case ImageGenerationToolCallItemResource imageGeneration: - JsonSerializer.Serialize(writer, imageGeneration, this._context.ImageGenerationToolCallItemResource); + JsonSerializer.Serialize(writer, imageGeneration, OpenAIHostingJsonContext.Default.ImageGenerationToolCallItemResource); break; case CodeInterpreterToolCallItemResource codeInterpreter: - JsonSerializer.Serialize(writer, codeInterpreter, this._context.CodeInterpreterToolCallItemResource); + JsonSerializer.Serialize(writer, codeInterpreter, OpenAIHostingJsonContext.Default.CodeInterpreterToolCallItemResource); break; case LocalShellToolCallItemResource localShell: - JsonSerializer.Serialize(writer, localShell, this._context.LocalShellToolCallItemResource); + JsonSerializer.Serialize(writer, localShell, OpenAIHostingJsonContext.Default.LocalShellToolCallItemResource); break; case LocalShellToolCallOutputItemResource localShellOutput: - JsonSerializer.Serialize(writer, localShellOutput, this._context.LocalShellToolCallOutputItemResource); + JsonSerializer.Serialize(writer, localShellOutput, OpenAIHostingJsonContext.Default.LocalShellToolCallOutputItemResource); break; case MCPListToolsItemResource mcpListTools: - JsonSerializer.Serialize(writer, mcpListTools, this._context.MCPListToolsItemResource); + JsonSerializer.Serialize(writer, mcpListTools, OpenAIHostingJsonContext.Default.MCPListToolsItemResource); break; case MCPApprovalRequestItemResource mcpApprovalRequest: - JsonSerializer.Serialize(writer, mcpApprovalRequest, this._context.MCPApprovalRequestItemResource); + JsonSerializer.Serialize(writer, mcpApprovalRequest, OpenAIHostingJsonContext.Default.MCPApprovalRequestItemResource); break; case MCPApprovalResponseItemResource mcpApprovalResponse: - JsonSerializer.Serialize(writer, mcpApprovalResponse, this._context.MCPApprovalResponseItemResource); + JsonSerializer.Serialize(writer, mcpApprovalResponse, OpenAIHostingJsonContext.Default.MCPApprovalResponseItemResource); break; case MCPCallItemResource mcpCall: - JsonSerializer.Serialize(writer, mcpCall, this._context.MCPCallItemResource); + JsonSerializer.Serialize(writer, mcpCall, OpenAIHostingJsonContext.Default.MCPCallItemResource); break; default: throw new JsonException($"Unknown item type: {value.GetType().Name}"); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemParamConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemParamConverter.cs new file mode 100644 index 0000000000..f9cb55c9ff --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemParamConverter.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; + +/// +/// JSON converter for ResponsesMessageItemParam that handles role-based polymorphic deserialization. +/// +[ExcludeFromCodeCoverage] +internal sealed class ResponsesMessageItemParamConverter : JsonConverter +{ + /// + public override ResponsesMessageItemParam? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + if (!root.TryGetProperty("role", out var roleElement)) + { + throw new JsonException("ResponsesMessageItemParam must have a 'role' property"); + } + + var role = roleElement.GetString(); + + return role switch + { + "user" => JsonSerializer.Deserialize(root.GetRawText(), OpenAIHostingJsonContext.Default.ResponsesUserMessageItemParam), + "assistant" => JsonSerializer.Deserialize(root.GetRawText(), OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemParam), + "system" => JsonSerializer.Deserialize(root.GetRawText(), OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemParam), + "developer" => JsonSerializer.Deserialize(root.GetRawText(), OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemParam), + _ => throw new JsonException($"Unknown message role: {role}") + }; + } + + /// + public override void Write(Utf8JsonWriter writer, ResponsesMessageItemParam value, JsonSerializerOptions options) + { + switch (value) + { + case ResponsesUserMessageItemParam user: + JsonSerializer.Serialize(writer, user, OpenAIHostingJsonContext.Default.ResponsesUserMessageItemParam); + break; + case ResponsesAssistantMessageItemParam assistant: + JsonSerializer.Serialize(writer, assistant, OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemParam); + break; + case ResponsesSystemMessageItemParam system: + JsonSerializer.Serialize(writer, system, OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemParam); + break; + case ResponsesDeveloperMessageItemParam developer: + JsonSerializer.Serialize(writer, developer, OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemParam); + break; + default: + throw new JsonException($"Unknown message type: {value.GetType().Name}"); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs index c1045f9e56..8e10cb2b5d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs @@ -14,16 +14,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; [ExcludeFromCodeCoverage] internal sealed class ResponsesMessageItemResourceConverter : JsonConverter { - private readonly ResponsesJsonContext _context; - - /// - /// Initializes a new instance of the class. - /// - public ResponsesMessageItemResourceConverter() - { - this._context = ResponsesJsonContext.Default; - } - + /// public override ResponsesMessageItemResource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Clone the reader to peek at the JSON @@ -57,8 +48,16 @@ public ResponsesMessageItemResourceConverter() if (readerClone.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) { - // Skip nested objects/arrays - readerClone.Skip(); + // The Utf8JsonReader.Skip() method will fail fast if it detects that we're reading + // from a partially read buffer, regardless of whether the next value is available. + // This can result in erroneous failures in cases where a custom converter is calling + // into a built-in converter (cf. https://github.com/dotnet/runtime/issues/74108). + // For this reason we need to call the TrySkip() method instead -- the serializer + // should guarantee sufficient read-ahead has been performed for the current object. + if (!readerClone.TrySkip()) + { + throw new InvalidOperationException("Failed to skip nested JSON value. Serializer should guarantee sufficient read-ahead has been done."); + } } } } @@ -66,30 +65,31 @@ public ResponsesMessageItemResourceConverter() // Determine the concrete type based on the role and deserialize using the source generation context return role switch { - ResponsesAssistantMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesAssistantMessageItemResource), - ResponsesUserMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesUserMessageItemResource), - ResponsesSystemMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesSystemMessageItemResource), - ResponsesDeveloperMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesDeveloperMessageItemResource), + ResponsesAssistantMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemResource), + ResponsesUserMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ResponsesUserMessageItemResource), + ResponsesSystemMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemResource), + ResponsesDeveloperMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemResource), _ => throw new JsonException($"Unknown message role: {role}") }; } + /// public override void Write(Utf8JsonWriter writer, ResponsesMessageItemResource value, JsonSerializerOptions options) { // Directly serialize using the appropriate type info from the context switch (value) { case ResponsesAssistantMessageItemResource assistant: - JsonSerializer.Serialize(writer, assistant, this._context.ResponsesAssistantMessageItemResource); + JsonSerializer.Serialize(writer, assistant, OpenAIHostingJsonContext.Default.ResponsesAssistantMessageItemResource); break; case ResponsesUserMessageItemResource user: - JsonSerializer.Serialize(writer, user, this._context.ResponsesUserMessageItemResource); + JsonSerializer.Serialize(writer, user, OpenAIHostingJsonContext.Default.ResponsesUserMessageItemResource); break; case ResponsesSystemMessageItemResource system: - JsonSerializer.Serialize(writer, system, this._context.ResponsesSystemMessageItemResource); + JsonSerializer.Serialize(writer, system, OpenAIHostingJsonContext.Default.ResponsesSystemMessageItemResource); break; case ResponsesDeveloperMessageItemResource developer: - JsonSerializer.Serialize(writer, developer, this._context.ResponsesDeveloperMessageItemResource); + JsonSerializer.Serialize(writer, developer, OpenAIHostingJsonContext.Default.ResponsesDeveloperMessageItemResource); break; default: throw new JsonException($"Unknown message type: {value.GetType().Name}"); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs index fe8812330c..fe871a0e17 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs @@ -14,6 +14,9 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; [ExcludeFromCodeCoverage] internal sealed class SnakeCaseEnumConverter : JsonStringEnumConverter where T : struct, Enum { + /// + /// Creates a new instance of the class. + /// public SnakeCaseEnumConverter() : base(JsonNamingPolicy.SnakeCaseLower) { } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs new file mode 100644 index 0000000000..2162bd8969 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +/// +/// Response executor that routes requests to hosted AIAgent services based on the model or agent.name parameter. +/// This executor resolves agents from keyed services registered via AddAIAgent(). +/// +internal sealed class HostedAgentResponseExecutor : IResponseExecutor +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly IConversationStorage? _conversationStorage; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider used to resolve hosted agents. + /// The logger instance. + /// Optional conversation storage for reading and updating conversation history. + public HostedAgentResponseExecutor( + IServiceProvider serviceProvider, + ILogger logger, + IConversationStorage? conversationStorage = null) + { + ArgumentNullException.ThrowIfNull(serviceProvider); + ArgumentNullException.ThrowIfNull(logger); + + this._serviceProvider = serviceProvider; + this._logger = logger; + this._conversationStorage = conversationStorage; + } + + /// + public IAsyncEnumerable ExecuteAsync( + AgentInvocationContext context, + CreateResponse request, + CancellationToken cancellationToken = default) + { + // Validate and resolve agent synchronously to ensure validation errors are thrown immediately + AIAgent agent = this.ResolveAgent(request); + + // Return the actual async enumerable implementation + return this.ExecuteAsyncCoreAsync(agent, context, request, cancellationToken); + } + + private async IAsyncEnumerable ExecuteAsyncCoreAsync( + AIAgent agent, + AgentInvocationContext context, + CreateResponse request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + // Create options with properties from the request + var chatOptions = new ChatOptions + { + ConversationId = request.Conversation?.Id, + Temperature = (float?)request.Temperature, + TopP = (float?)request.TopP, + MaxOutputTokens = request.MaxOutputTokens, + Instructions = request.Instructions, + ModelId = request.Model, + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["RawRequest"] = request + } + }; + var options = new ChatClientAgentRunOptions(chatOptions); + + // Convert input to chat messages + var messages = new List(); + + // If instructions are provided in the request, prepend them as a system message + if (!string.IsNullOrWhiteSpace(request.Instructions)) + { + messages.Add(new ChatMessage(ChatRole.System, request.Instructions)); + } + + foreach (var inputMessage in request.Input.GetInputMessages()) + { + messages.Add(inputMessage.ToChatMessage()); + } + + // Use the extension method to convert streaming updates to streaming response events + await foreach (var streamingEvent in agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken) + .ToStreamingResponseAsync(request, context, cancellationToken).ConfigureAwait(false)) + { + yield return streamingEvent; + } + } + + /// + /// Resolves an agent from the service provider based on the request. + /// + /// The create response request. + /// The resolved AIAgent instance. + /// Thrown when the agent cannot be resolved. + private AIAgent ResolveAgent(CreateResponse request) + { + // Extract agent name from agent.name or model parameter + var agentName = request.Agent?.Name ?? request.Model; + if (string.IsNullOrEmpty(agentName)) + { + throw new InvalidOperationException("No 'agent.name' or 'model' specified in the request."); + } + + // Resolve the keyed agent service + try + { + return this._serviceProvider.GetRequiredKeyedService(agentName); + } + catch (InvalidOperationException ex) + { + this._logger.LogError(ex, "Failed to resolve agent with name '{AgentName}'", agentName); + throw new InvalidOperationException($"Agent '{agentName}' not found. Ensure the agent is registered with AddAIAgent().", ex); + } + } + + /// + /// Validates that the agent can be resolved without actually resolving it. + /// This allows early validation before starting async execution. + /// + /// The create response request. + /// Thrown when the agent cannot be resolved. + public void ValidateAgent(CreateResponse request) + { + // Use the same logic as ResolveAgent but don't return the agent + _ = this.ResolveAgent(request); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponseExecutor.cs new file mode 100644 index 0000000000..ca4da70b88 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponseExecutor.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +/// +/// Interface for executing response generation. +/// Implementations can use local execution (AIAgent) or forward to remote workers. +/// +internal interface IResponseExecutor +{ + /// + /// Executes a response generation request and returns streaming events. + /// + /// The agent invocation context containing the ID generator and other context information. + /// The create response request. + /// Cancellation token. + /// An async enumerable of streaming response events. + IAsyncEnumerable ExecuteAsync( + AgentInvocationContext context, + CreateResponse request, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponsesService.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponsesService.cs new file mode 100644 index 0000000000..f5cedb9fe7 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IResponsesService.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +/// +/// Service interface for handling OpenAI Responses API operations. +/// Implementations can use various storage and execution strategies (in-memory, Orleans grains, etc.). +/// +internal interface IResponsesService +{ + /// + /// Creates a model response for the given input. + /// + /// The create response request. + /// Cancellation token. + /// The created response. + Task CreateResponseAsync( + CreateResponse request, + CancellationToken cancellationToken = default); + + /// + /// Creates a streaming model response for the given input. + /// + /// The create response request. + /// Cancellation token. + /// An async enumerable of streaming response events. + IAsyncEnumerable CreateResponseStreamingAsync( + CreateResponse request, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a response by ID. + /// + /// The ID of the response to retrieve. + /// Cancellation token. + /// The response if found, null otherwise. + Task GetResponseAsync( + string responseId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves a response by ID in streaming mode, yielding events as they become available. + /// + /// The ID of the response to retrieve. + /// The sequence number after which to start streaming. If null, starts from the beginning. + /// Cancellation token. + /// An async enumerable of streaming updates. + IAsyncEnumerable GetResponseStreamingAsync( + string responseId, + int? startingAfter = null, + CancellationToken cancellationToken = default); + + /// + /// Cancels an in-progress response. + /// + /// The ID of the response to cancel. + /// Cancellation token. + /// The updated response after cancellation. + Task CancelResponseAsync( + string responseId, + CancellationToken cancellationToken = default); + + /// + /// Deletes a response by ID. + /// + /// The ID of the response to delete. + /// Cancellation token. + /// True if the response was deleted, false if it was not found. + Task DeleteResponseAsync( + string responseId, + CancellationToken cancellationToken = default); + + /// + /// Lists the input items for a response. + /// + /// The ID of the response. + /// Maximum number of items to return (1-100). + /// Sort order ("asc" or "desc"). + /// Return items after this ID. + /// Return items before this ID. + /// Cancellation token. + /// A list response with items and pagination info. + Task> ListResponseInputItemsAsync( + string responseId, + int limit = 20, + string order = "desc", + string? after = null, + string? before = null, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs index 63ec1a85bd..f80c50f219 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs @@ -13,6 +13,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; internal sealed partial class IdGenerator { private readonly string _partitionId; + private readonly Random? _random; #if NET9_0_OR_GREATER [GeneratedRegex("^[A-Za-z0-9]+$")] @@ -27,10 +28,12 @@ internal sealed partial class IdGenerator /// /// The response ID. /// The conversation ID. - public IdGenerator(string? responseId, string? conversationId) + /// Optional random seed for deterministic ID generation. When null, uses cryptographically secure random generation. + public IdGenerator(string? responseId, string? conversationId, int? randomSeed = null) { - this.ResponseId = responseId ?? NewId("resp"); - this.ConversationId = conversationId ?? NewId("conv"); + this._random = randomSeed.HasValue ? new Random(randomSeed.Value) : null; + this.ResponseId = responseId ?? this.NewId("resp"); + this.ConversationId = conversationId ?? this.NewId("conv"); this._partitionId = GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty; } @@ -64,7 +67,7 @@ public static IdGenerator From(CreateResponse request) public string Generate(string? category = null) { var prefix = string.IsNullOrEmpty(category) ? "id" : category; - return NewId(prefix, partitionKey: this._partitionId); + return this.NewId(prefix, partitionKey: this._partitionId); } /// @@ -104,13 +107,13 @@ public string Generate(string? category = null) /// An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one. /// A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}". /// Thrown when the watermark contains non-alphanumeric characters. - private static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "", + private string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "", string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "") { ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1); - var entropy = GetRandomString(stringLength); + var entropy = this.GetRandomString(stringLength); - string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength); + string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? this.GetRandomString(partitionKeyLength); if (!string.IsNullOrEmpty(watermark)) { @@ -130,12 +133,29 @@ private static string NewId(string prefix, int stringLength = 32, int partitionK /// /// Generates a secure random alphanumeric string of the specified length. + /// When a random seed was provided to the constructor, uses deterministic generation. /// /// The desired length of the random string. /// A random alphanumeric string. /// Thrown when stringLength is less than 1. - private static string GetRandomString(int stringLength) => - RandomNumberGenerator.GetString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", stringLength); + private string GetRandomString(int stringLength) + { + const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + if (this._random is not null) + { + // Use deterministic random generation when seed is provided + return string.Create(stringLength, this._random, static (destination, random) => + { + for (int i = 0; i < destination.Length; i++) + { + destination[i] = Chars[random.Next(Chars.Length)]; + } + }); + } + + // Use cryptographically secure random generation when no seed is provided + return RandomNumberGenerator.GetString(Chars, stringLength); + } /// /// Extracts the partition key from an existing ID, or returns null if extraction fails. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs new file mode 100644 index 0000000000..8e948104a1 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs @@ -0,0 +1,531 @@ +// 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.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +/// +/// In-memory implementation of responses service for testing and development. +/// This implementation is thread-safe but data is not persisted across application restarts. +/// +internal sealed class InMemoryResponsesService : IResponsesService, IDisposable +{ + private readonly IResponseExecutor _executor; + private readonly MemoryCache _cache; + private readonly InMemoryStorageOptions _options; + private readonly Conversations.IConversationStorage? _conversationStorage; + + private sealed class ResponseState + { + private readonly object _lock = new(); + private TaskCompletionSource _updateSignal = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly Dictionary _outputItems = []; + + public Response? Response { get; set; } + public CreateResponse? Request { get; set; } + public List StreamingUpdates { get; } = []; + public Task? CompletionTask { get; set; } + public CancellationTokenSource? CancellationTokenSource { get; set; } + public bool IsTerminal => this.Response?.IsTerminal ?? false; + + public void AddStreamingEvent(StreamingResponseEvent streamingEvent) + { + lock (this._lock) + { + this.StreamingUpdates.Add(streamingEvent); + + // Update the response object for events that contain it + if (streamingEvent is IStreamingResponseEventWithResponse responseEvent) + { + this.Response = responseEvent.Response; + } + + // Track output items as they're added or updated + if (streamingEvent is StreamingOutputItemAdded itemAdded) + { + this._outputItems[itemAdded.OutputIndex] = itemAdded.Item; + this.UpdateResponseOutput(); + } + else if (streamingEvent is StreamingOutputItemDone itemDone) + { + this._outputItems[itemDone.OutputIndex] = itemDone.Item; + this.UpdateResponseOutput(); + } + } + + this.SignalUpdate(); + } + + private void UpdateResponseOutput() + { + // Update the Response.Output list with current items + if (this.Response is not null) + { + List outputList = [.. this._outputItems.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value)]; + this.Response = this.Response with { Output = outputList }; + } + } + + public async IAsyncEnumerable StreamUpdatesAsync( + int startingAfter = 0, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + int streamedCount = startingAfter; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Capture the wait task before checking state to avoid race conditions + Task waitTask = this.WaitForUpdateAsync(cancellationToken); + + // Copy any new updates and check terminal state while holding the lock + List newUpdates; + bool isTerminal; + lock (this._lock) + { + newUpdates = this.StreamingUpdates.Skip(streamedCount).ToList(); + streamedCount += newUpdates.Count; + isTerminal = this.IsTerminal; + } + + // Yield the updates outside the lock + foreach (StreamingResponseEvent update in newUpdates) + { + yield return update; + } + + // Check if we're done (after yielding any final events) + if (isTerminal) + { + break; + } + + // Wait for the next update to be signaled + await waitTask.ConfigureAwait(false); + } + } + + private Task WaitForUpdateAsync(CancellationToken cancellationToken) + { + Task signalTask = this._updateSignal.Task; + return signalTask.WaitAsync(cancellationToken); + } + + internal void SignalUpdate() + { + TaskCompletionSource oldSignal = Interlocked.Exchange(ref this._updateSignal, new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); + oldSignal.TrySetResult(); + } + } + + public InMemoryResponsesService(IResponseExecutor executor) + : this(executor, new InMemoryStorageOptions(), null) + { + } + + public InMemoryResponsesService(IResponseExecutor executor, InMemoryStorageOptions options) + : this(executor, options, null) + { + } + + public InMemoryResponsesService(IResponseExecutor executor, InMemoryStorageOptions options, Conversations.IConversationStorage? conversationStorage) + { + ArgumentNullException.ThrowIfNull(executor); + ArgumentNullException.ThrowIfNull(options); + this._executor = executor; + this._options = options; + this._cache = new MemoryCache(options.ToMemoryCacheOptions()); + this._conversationStorage = conversationStorage; + } + + public async Task CreateResponseAsync( + CreateResponse request, + CancellationToken cancellationToken = default) + { + ValidateRequest(request); + + // Validate agent resolution early for HostedAgentResponseExecutor + if (this._executor is HostedAgentResponseExecutor hostedExecutor) + { + hostedExecutor.ValidateAgent(request); + } + + if (request.Stream == true) + { + throw new InvalidOperationException("Cannot create a streaming response using CreateResponseAsync. Use CreateResponseStreamingAsync instead."); + } + + var responseId = $"resp_{Guid.NewGuid():N}"; + var state = this.InitializeResponse(responseId, request); + var ct = request.Background switch + { + true => CancellationToken.None, + _ => cancellationToken, + }; + state.CompletionTask = this.ExecuteResponseAsync(responseId, state, ct); + + // For background responses, start execution and return immediately + if (request.Background == true) + { + return state.Response!; + } + + // For non-background responses, wait for completion + await state.CompletionTask!.WaitAsync(cancellationToken).ConfigureAwait(false); + return state.Response!; + } + + public async IAsyncEnumerable CreateResponseStreamingAsync( + CreateResponse request, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ValidateRequest(request); + + if (request.Stream == false) + { + throw new InvalidOperationException("Cannot create a non-streaming response using CreateResponseStreamingAsync. Use CreateResponseAsync instead."); + } + + var responseId = $"resp_{Guid.NewGuid():N}"; + var state = this.InitializeResponse(responseId, request); + + // Start execution + state.CompletionTask = this.ExecuteResponseAsync(responseId, state, CancellationToken.None); + + // Stream updates as they become available + await foreach (StreamingResponseEvent update in state.StreamUpdatesAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } + + public Task GetResponseAsync(string responseId, CancellationToken cancellationToken = default) + { + this._cache.TryGetValue(responseId, out ResponseState? state); + return Task.FromResult(state?.Response); + } + + public async IAsyncEnumerable GetResponseStreamingAsync( + string responseId, + int? startingAfter = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (!this._cache.TryGetValue(responseId, out ResponseState? state) || state is null) + { + yield break; + } + + // Stream existing updates starting from the specified position + await foreach (StreamingResponseEvent update in state.StreamUpdatesAsync(startingAfter ?? 0, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } + + public async Task CancelResponseAsync(string responseId, CancellationToken cancellationToken = default) + { + if (!this._cache.TryGetValue(responseId, out ResponseState? state) || state is null) + { + throw new InvalidOperationException($"Response '{responseId}' not found."); + } + + if (state.Response is null || state.Response.Background != true) + { + throw new InvalidOperationException($"Only background responses can be cancelled. Response '{responseId}' was not created with background=true."); + } + + if (state.IsTerminal) + { + throw new InvalidOperationException($"Response '{responseId}' is already in a terminal state and cannot be cancelled."); + } + + // Cancel the execution + state.CancellationTokenSource?.Cancel(); + + if (state.CompletionTask is { } task) + { + await task.WaitAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + return state.Response; + } + + public Task DeleteResponseAsync(string responseId, CancellationToken cancellationToken = default) + { + if (!this._cache.TryGetValue(responseId, out ResponseState? state)) + { + return Task.FromResult(false); + } + + // Cancel any ongoing execution + state?.CancellationTokenSource?.Cancel(); + + // Remove the response + this._cache.Remove(responseId); + return Task.FromResult(true); + } + + public Task> ListResponseInputItemsAsync( + string responseId, + int limit = 20, + string order = "desc", + string? after = null, + string? before = null, + CancellationToken cancellationToken = default) + { + limit = Math.Clamp(limit, 1, 100); + + if (!this._cache.TryGetValue(responseId, out ResponseState? state)) + { + throw new InvalidOperationException($"Response '{responseId}' not found."); + } + + if (state is null) + { + throw new InvalidOperationException($"Response '{responseId}' state is null."); + } + + var itemResources = GetInputItems(responseId, state); + + // Apply ordering + if (order == "desc") + { + itemResources.Reverse(); + } + + // Apply pagination + var filtered = itemResources.AsEnumerable(); + + if (!string.IsNullOrEmpty(after)) + { + int afterIndex = itemResources.FindIndex(m => m.Id == after); + if (afterIndex >= 0) + { + filtered = itemResources.Skip(afterIndex + 1); + } + } + + if (!string.IsNullOrEmpty(before)) + { + int beforeIndex = itemResources.FindIndex(m => m.Id == before); + if (beforeIndex >= 0) + { + filtered = filtered.Take(beforeIndex); + } + } + + var result = filtered.Take(limit + 1).ToList(); + var hasMore = result.Count > limit; + if (hasMore) + { + result = result.Take(limit).ToList(); + } + + return Task.FromResult(new ListResponse + { + Data = result, + FirstId = result.FirstOrDefault()?.Id, + LastId = result.LastOrDefault()?.Id, + HasMore = hasMore + }); + } + + private static void ValidateRequest(CreateResponse request) + { + if (request.Conversation is not null && !string.IsNullOrEmpty(request.Conversation.Id) && + !string.IsNullOrEmpty(request.PreviousResponseId)) + { + throw new InvalidOperationException("Mutually exclusive parameters: 'conversation' and 'previous_response_id'. Ensure you are only providing one of: 'previous_response_id' or 'conversation'."); + } + } + + private ResponseState InitializeResponse(string responseId, CreateResponse request) + { + var metadata = request.Metadata ?? []; + + // Create initial response + // Background responses always start as "queued", non-background as "in_progress" + var initialStatus = request.Background is true ? ResponseStatus.Queued : ResponseStatus.InProgress; + var response = new Response + { + Id = responseId, + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Model = request.Model ?? "default", + Status = initialStatus, + Error = null, + IncompleteDetails = null, + Output = [], + Instructions = request.Instructions, + Usage = ResponseUsage.Zero, + ParallelToolCalls = request.ParallelToolCalls ?? true, + Tools = [], + ToolChoice = default, + Temperature = request.Temperature, + TopP = request.TopP, + Metadata = metadata, + Conversation = request.Conversation, + }; + + var state = new ResponseState + { + Response = response, + Request = request, + CancellationTokenSource = new CancellationTokenSource() + }; + + var entryOptions = this._options.ToMemoryCacheEntryOptions(); + entryOptions.RegisterPostEvictionCallback((key, value, reason, state) => + { + if (value is ResponseState responseState) + { + responseState.CancellationTokenSource?.Cancel(); + } + }); + + this._cache.Set(responseId, state, entryOptions); + + return state; + } + + private async Task ExecuteResponseAsync(string responseId, ResponseState state, CancellationToken cancellationToken) + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var request = state.Request!; + var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, state.CancellationTokenSource!.Token); + + try + { + // Create agent invocation context + var context = new AgentInvocationContext(new IdGenerator(responseId: responseId, conversationId: state.Response?.Conversation?.Id)); + + // Collect output items for conversation storage + List outputItems = []; + + // Execute using the injected executor + await foreach (var streamingEvent in this._executor.ExecuteAsync(context, request, linkedCts.Token).ConfigureAwait(false)) + { + state.AddStreamingEvent(streamingEvent); + + // Collect output items + if (streamingEvent is StreamingOutputItemDone itemDone) + { + outputItems.Add(itemDone.Item); + } + } + + // Add both input and output items to conversation storage if available + // This happens AFTER successful execution, in line with OpenAI's behavior + if (this._conversationStorage is not null && request.Conversation?.Id is not null) + { + var inputItems = GetInputItems(responseId, state); + var allItems = new List(inputItems.Count + outputItems.Count); + allItems.AddRange(inputItems); + allItems.AddRange(outputItems); + + if (allItems.Count > 0) + { + await this._conversationStorage.AddItemsAsync(request.Conversation.Id, allItems, linkedCts.Token).ConfigureAwait(false); + } + } + + // Update response status to completed if not already in a terminal state + if (!state.IsTerminal) + { + state.Response = state.Response! with + { + Status = ResponseStatus.Completed + }; + + var sequenceNumber = state.StreamingUpdates.Count + 1; + var completedEvent = new StreamingResponseCompleted + { + SequenceNumber = sequenceNumber, + Response = state.Response + }; + + state.AddStreamingEvent(completedEvent); + } + } + catch (OperationCanceledException) + { + // Update response status to cancelled + state.Response = state.Response! with + { + Status = ResponseStatus.Cancelled + }; + + var sequenceNumber = state.StreamingUpdates.Count + 1; + var cancelledEvent = new StreamingResponseCancelled + { + SequenceNumber = sequenceNumber, + Response = state.Response + }; + + state.AddStreamingEvent(cancelledEvent); + } + catch (Exception ex) + { + // Determine error code based on exception message + // Azure OpenAI returns HTTP 400 with "content_filter" in the error message + string errorCode = ex.Message.Contains("content_filter", StringComparison.OrdinalIgnoreCase) + ? "content_filter" + : "execution_error"; + + // Update response status to failed + state.Response = state.Response! with + { + Status = ResponseStatus.Failed, + Error = new ResponseError + { + Code = errorCode, + Message = ex.Message + } + }; + + var sequenceNumber = state.StreamingUpdates.Count + 1; + var failedEvent = new StreamingResponseFailed + { + SequenceNumber = sequenceNumber, + Response = state.Response + }; + + state.AddStreamingEvent(failedEvent); + } + finally + { + // Signal one final time to unblock any waiting consumers + state.SignalUpdate(); + linkedCts.Dispose(); + } + } + + private static List GetInputItems(string responseId, ResponseState state) + { + var itemResources = new List(); + if (state.Request is not null) + { + // Use a deterministic random seed. We add 1 to avoid clashing with the output message ids. + var randomSeed = responseId.GetHashCode() + 1; + var idGenerator = new IdGenerator(responseId: responseId, conversationId: state.Response?.Conversation?.Id, randomSeed: randomSeed); + foreach (var inputMessage in state.Request.Input.GetInputMessages()) + { + itemResources.AddRange(inputMessage.ToItemResource(idGenerator)); + } + } + + return itemResources; + } + + public void Dispose() + { + this._cache.Dispose(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs index 11882ada6a..eaeb8cd658 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs @@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// /// Represents an agent identifier. /// -internal sealed record AgentId +internal sealed class AgentId { /// /// Initializes a new instance of the class. @@ -44,7 +44,7 @@ public AgentId(AgentIdType type, string name, string version) /// /// Represents an agent ID type. /// -internal sealed record AgentIdType +internal sealed class AgentIdType { /// /// Initializes a new instance of the class. @@ -65,7 +65,7 @@ public AgentIdType(string value) /// /// Represents an agent reference. /// -internal sealed record AgentReference +internal sealed class AgentReference { /// /// The type of the reference (e.g., "agent" or "agent_reference"). diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs index 7f959fcbfc..699ac0bb08 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -11,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// Represents a reference to a conversation, which can be either a conversation ID (string) or a conversation object. /// [JsonConverter(typeof(ConversationReferenceJsonConverter))] -internal sealed record ConversationReference +internal sealed class ConversationReference { /// /// The conversation ID. @@ -40,8 +41,10 @@ public static ConversationReference FromObject(string id, Dictionary /// JSON converter for ConversationReference that handles both string (conversation ID) and object representations. /// +[ExcludeFromCodeCoverage] internal sealed class ConversationReferenceJsonConverter : JsonConverter { + /// public override ConversationReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) @@ -61,7 +64,7 @@ internal sealed class ConversationReferenceJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, ConversationReference value, JsonSerializerOptions options) { if (value is null) @@ -95,7 +99,7 @@ public override void Write(Utf8JsonWriter writer, ConversationReference value, J if (value.Metadata is not null) { writer.WritePropertyName("metadata"); - JsonSerializer.Serialize(writer, value.Metadata, ResponsesJsonContext.Default.DictionaryStringString); + JsonSerializer.Serialize(writer, value.Metadata, OpenAIHostingJsonContext.Default.DictionaryStringString); } writer.WriteEndObject(); } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs index 99a5bbab73..9a8059bd4a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs @@ -10,7 +10,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// /// Request to create a model response. /// -internal sealed record CreateResponse +internal sealed class CreateResponse { /// /// Text, image, or file inputs to the model, used to generate a response. @@ -65,7 +65,10 @@ internal sealed record CreateResponse /// /// The unique ID of the previous response to the model. Use this to create multi-turn conversations. - /// Cannot be used in conjunction with conversation. + /// Cannot be used in conjunction with conversation (mutually exclusive). + /// The previous_response_id determines the conversation thread context - it follows the response chain, + /// not any explicit conversation. Context is maintained through the chain even if the previous response + /// was created with a conversation.id. /// [JsonPropertyName("previous_response_id")] public string? PreviousResponseId { get; init; } @@ -98,13 +101,16 @@ internal sealed record CreateResponse /// Specify additional output data to include in the model response. /// [JsonPropertyName("include")] - public IReadOnlyList? Include { get; init; } + public List? Include { get; init; } /// /// The conversation that this response belongs to. Items from this conversation are prepended /// to input_items for this response request. /// Can be either a conversation ID (string) or a conversation object with ID and optional metadata. /// Input items and output items from this response are automatically added to this conversation after this response completes. + /// Cannot be used in conjunction with previous_response_id (mutually exclusive). + /// Use conversation.id for explicit conversation boundaries and starting new threads. + /// Use previous_response_id for simple linear conversation chaining. /// [JsonPropertyName("conversation")] public ConversationReference? Conversation { get; init; } @@ -178,7 +184,7 @@ internal sealed record CreateResponse /// An array of tools the model may call while generating a response. /// [JsonPropertyName("tools")] - public IReadOnlyList? Tools { get; init; } + public List? Tools { get; init; } /// /// How the model should select which tool (or tools) to use when generating a response. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs index d15e111ebe..029be0752a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs @@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// A message input to the model with a role indicating instruction following hierarchy. /// Aligns with the OpenAI Responses API InputMessage/EasyInputMessage schema. /// -internal sealed record InputMessage +internal sealed class InputMessage { /// /// The role of the message input. One of user, assistant, system, or developer. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs index 524613b7eb..0180ff16ac 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs @@ -22,7 +22,7 @@ private InputMessageContent(string text) this.Contents = null; } - private InputMessageContent(IReadOnlyList contents) + private InputMessageContent(List contents) { this.Contents = contents ?? throw new ArgumentNullException(nameof(contents)); this.Text = null; @@ -36,12 +36,12 @@ private InputMessageContent(IReadOnlyList contents) /// /// Creates an InputMessageContent from a list of ItemContent items. /// - public static InputMessageContent FromContents(IReadOnlyList contents) => new(contents); + public static InputMessageContent FromContents(List contents) => new(contents); /// /// Creates an InputMessageContent from a list of ItemContent items. /// - public static InputMessageContent FromContents(params ItemContent[] contents) => new(contents); + public static InputMessageContent FromContents(params ItemContent[] contents) => new([.. contents]); /// /// Implicit conversion from string to InputMessageContent. @@ -62,12 +62,14 @@ private InputMessageContent(IReadOnlyList contents) /// Gets whether this content is text. /// [MemberNotNullWhen(true, nameof(Text))] + [MemberNotNullWhen(false, nameof(Contents))] public bool IsText => this.Text is not null; /// /// Gets whether this content is a list of ItemContent items. /// [MemberNotNullWhen(true, nameof(Contents))] + [MemberNotNullWhen(false, nameof(Text))] public bool IsContents => this.Contents is not null; /// @@ -78,7 +80,7 @@ private InputMessageContent(IReadOnlyList contents) /// /// Gets the ItemContent items, or null if this is not a content list. /// - public IReadOnlyList? Contents { get; } + public List? Contents { get; } /// public bool Equals(InputMessageContent? other) @@ -143,6 +145,16 @@ public override int GetHashCode() { return !Equals(left, right); } + + /// + /// Converts this instance to a list of ItemContent. + /// + public List ToItemContents() + { + return this.IsText + ? [new ItemContentInputText { Text = this.Text }] + : this.Contents; + } } /// @@ -150,6 +162,7 @@ public override int GetHashCode() /// internal sealed class InputMessageContentJsonConverter : JsonConverter { + /// public override InputMessageContent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Check if it's a string @@ -162,7 +175,7 @@ internal sealed class InputMessageContentJsonConverter : JsonConverter 0 ? InputMessageContent.FromContents(contents) : InputMessageContent.FromText(string.Empty); @@ -171,6 +184,7 @@ internal sealed class InputMessageContentJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, InputMessageContent value, JsonSerializerOptions options) { if (value.IsText) @@ -179,7 +193,7 @@ public override void Write(Utf8JsonWriter writer, InputMessageContent value, Jso } else if (value.IsContents) { - JsonSerializer.Serialize(writer, value.Contents, ResponsesJsonContext.Default.ListItemContent); + JsonSerializer.Serialize(writer, value.Contents, OpenAIHostingJsonContext.Default.ListItemContent); } else { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParam.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParam.cs new file mode 100644 index 0000000000..df8a378df2 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParam.cs @@ -0,0 +1,577 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +/// +/// Base class for all item parameters (input items for creating conversation items or response inputs). +/// Unlike ItemResource, ItemParam does not have an ID field - the server generates IDs upon creation. +/// +[JsonConverter(typeof(ItemParamConverter))] +internal abstract class ItemParam +{ + /// + /// The type of the item. + /// + [JsonPropertyName("type")] + public abstract string Type { get; } +} + +/// +/// Base class for message item parameters. +/// +[JsonConverter(typeof(ResponsesMessageItemParamConverter))] +internal abstract class ResponsesMessageItemParam : ItemParam +{ + /// + /// The constant item type identifier for message items. + /// + public const string ItemType = "message"; + + /// + public override string Type => ItemType; + + /// + /// The role of the message sender. + /// + [JsonPropertyName("role")] + public abstract ChatRole Role { get; } +} + +/// +/// A user message item parameter. +/// +internal sealed class ResponsesUserMessageItemParam : ResponsesMessageItemParam +{ + /// + /// The constant role type identifier for user messages. + /// + public const string RoleType = "user"; + + /// + public override ChatRole Role => ChatRole.User; + + /// + /// The content of the message. Can be a simple string or an array of content parts. + /// + [JsonPropertyName("content")] + public required InputMessageContent Content { get; init; } +} + +/// +/// An assistant message item parameter. +/// +internal sealed class ResponsesAssistantMessageItemParam : ResponsesMessageItemParam +{ + /// + /// The constant role type identifier for assistant messages. + /// + public const string RoleType = "assistant"; + + /// + public override ChatRole Role => ChatRole.Assistant; + + /// + /// The content of the message. Can be a simple string or an array of content parts. + /// + [JsonPropertyName("content")] + public required InputMessageContent Content { get; init; } +} + +/// +/// A system message item parameter. +/// +internal sealed class ResponsesSystemMessageItemParam : ResponsesMessageItemParam +{ + /// + /// The constant role type identifier for system messages. + /// + public const string RoleType = "system"; + + /// + public override ChatRole Role => ChatRole.System; + + /// + /// The content of the message. Can be a simple string or an array of content parts. + /// + [JsonPropertyName("content")] + public required InputMessageContent Content { get; init; } +} + +/// +/// A developer message item parameter. +/// +internal sealed class ResponsesDeveloperMessageItemParam : ResponsesMessageItemParam +{ + /// + /// The constant role type identifier for developer messages. + /// + public const string RoleType = "developer"; + + /// + public override ChatRole Role => new(RoleType); + + /// + /// The content of the message. Can be a simple string or an array of content parts. + /// + [JsonPropertyName("content")] + public required InputMessageContent Content { get; init; } +} + +/// +/// A function tool call item parameter. +/// +internal sealed class FunctionToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for function call items. + /// + public const string ItemType = "function_call"; + + /// + public override string Type => ItemType; + + /// + /// The call ID of the function. + /// + [JsonPropertyName("call_id")] + public required string CallId { get; init; } + + /// + /// The name of the function. + /// + [JsonPropertyName("name")] + public required string Name { get; init; } + + /// + /// The arguments to the function. + /// + [JsonPropertyName("arguments")] + public required string Arguments { get; init; } +} + +/// +/// A function tool call output item parameter. +/// +internal sealed class FunctionToolCallOutputItemParam : ItemParam +{ + /// + /// The constant item type identifier for function call output items. + /// + public const string ItemType = "function_call_output"; + + /// + public override string Type => ItemType; + + /// + /// The call ID of the function. + /// + [JsonPropertyName("call_id")] + public required string CallId { get; init; } + + /// + /// The output of the function. + /// + [JsonPropertyName("output")] + public required string Output { get; init; } +} + +/// +/// A file search tool call item parameter. +/// +internal sealed class FileSearchToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for file search call items. + /// + public const string ItemType = "file_search_call"; + + /// + public override string Type => ItemType; + + /// + /// The queries used to search for files. + /// + [JsonPropertyName("queries")] + public List? Queries { get; init; } + + /// + /// The results of the file search tool call. + /// + [JsonPropertyName("results")] + public List? Results { get; init; } +} + +/// +/// A computer tool call item parameter. +/// +internal sealed class ComputerToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for computer call items. + /// + public const string ItemType = "computer_call"; + + /// + public override string Type => ItemType; + + /// + /// An identifier used when responding to the tool call with output. + /// + [JsonPropertyName("call_id")] + public required string CallId { get; init; } + + /// + /// The action to perform. + /// + [JsonPropertyName("action")] + public required JsonElement Action { get; init; } + + /// + /// The pending safety checks for the computer call. + /// + [JsonPropertyName("pending_safety_checks")] + public List? PendingSafetyChecks { get; init; } +} + +/// +/// A computer tool call output item parameter. +/// +internal sealed class ComputerToolCallOutputItemParam : ItemParam +{ + /// + /// The constant item type identifier for computer call output items. + /// + public const string ItemType = "computer_call_output"; + + /// + public override string Type => ItemType; + + /// + /// The ID of the computer tool call that produced the output. + /// + [JsonPropertyName("call_id")] + public required string CallId { get; init; } + + /// + /// The safety checks reported by the API that have been acknowledged by the developer. + /// + [JsonPropertyName("acknowledged_safety_checks")] + public List? AcknowledgedSafetyChecks { get; init; } + + /// + /// The output of the computer tool call. + /// + [JsonPropertyName("output")] + public required JsonElement Output { get; init; } +} + +/// +/// A web search tool call item parameter. +/// +internal sealed class WebSearchToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for web search call items. + /// + public const string ItemType = "web_search_call"; + + /// + public override string Type => ItemType; + + /// + /// An object describing the specific action taken in this web search call. + /// + [JsonPropertyName("action")] + public required JsonElement Action { get; init; } +} + +/// +/// A reasoning item parameter. +/// +internal sealed class ReasoningItemParam : ItemParam +{ + /// + /// The constant item type identifier for reasoning items. + /// + public const string ItemType = "reasoning"; + + /// + public override string Type => ItemType; + + /// + /// The encrypted content of the reasoning item. + /// + [JsonPropertyName("encrypted_content")] + public string? EncryptedContent { get; init; } + + /// + /// Reasoning text contents. + /// + [JsonPropertyName("summary")] + public List? Summary { get; init; } +} + +/// +/// An item reference item parameter. +/// +internal sealed class ItemReferenceItemParam : ItemParam +{ + /// + /// The constant item type identifier for item reference items. + /// + public const string ItemType = "item_reference"; + + /// + public override string Type => ItemType; + + /// + /// The service-originated ID of the previously generated response item being referenced. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } +} + +/// +/// An image generation tool call item parameter. +/// +internal sealed class ImageGenerationToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for image generation call items. + /// + public const string ItemType = "image_generation_call"; + + /// + public override string Type => ItemType; + + /// + /// The generated image encoded in base64. + /// + [JsonPropertyName("result")] + public string? Result { get; init; } +} + +/// +/// A code interpreter tool call item parameter. +/// +internal sealed class CodeInterpreterToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for code interpreter call items. + /// + public const string ItemType = "code_interpreter_call"; + + /// + public override string Type => ItemType; + + /// + /// The ID of the container used to run the code. + /// + [JsonPropertyName("container_id")] + public string? ContainerId { get; init; } + + /// + /// The code to run, or null if not available. + /// + [JsonPropertyName("code")] + public string? Code { get; init; } + + /// + /// The outputs generated by the code interpreter, such as logs or images. + /// Can be null if no outputs are available. + /// + [JsonPropertyName("outputs")] + public List? Outputs { get; init; } +} + +/// +/// A local shell tool call item parameter. +/// +internal sealed class LocalShellToolCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for local shell call items. + /// + public const string ItemType = "local_shell_call"; + + /// + public override string Type => ItemType; + + /// + /// The unique ID of the local shell tool call generated by the model. + /// + [JsonPropertyName("call_id")] + public string? CallId { get; init; } + + /// + /// The action to execute. + /// + [JsonPropertyName("action")] + public JsonElement? Action { get; init; } +} + +/// +/// A local shell tool call output item parameter. +/// +internal sealed class LocalShellToolCallOutputItemParam : ItemParam +{ + /// + /// The constant item type identifier for local shell call output items. + /// + public const string ItemType = "local_shell_call_output"; + + /// + public override string Type => ItemType; + + /// + /// A JSON string of the output of the local shell tool call. + /// + [JsonPropertyName("output")] + public string? Output { get; init; } +} + +/// +/// An MCP list tools item parameter. +/// +internal sealed class MCPListToolsItemParam : ItemParam +{ + /// + /// The constant item type identifier for MCP list tools items. + /// + public const string ItemType = "mcp_list_tools"; + + /// + public override string Type => ItemType; + + /// + /// The label of the MCP server. + /// + [JsonPropertyName("server_label")] + public string? ServerLabel { get; init; } + + /// + /// The tools available on the server. + /// + [JsonPropertyName("tools")] + public List? Tools { get; init; } + + /// + /// Error message if the server could not list tools. + /// + [JsonPropertyName("error")] + public string? Error { get; init; } +} + +/// +/// An MCP approval request item parameter. +/// +internal sealed class MCPApprovalRequestItemParam : ItemParam +{ + /// + /// The constant item type identifier for MCP approval request items. + /// + public const string ItemType = "mcp_approval_request"; + + /// + public override string Type => ItemType; + + /// + /// The label of the MCP server making the request. + /// + [JsonPropertyName("server_label")] + public string? ServerLabel { get; init; } + + /// + /// The name of the tool to run. + /// + [JsonPropertyName("name")] + public string? Name { get; init; } + + /// + /// A JSON string of arguments for the tool. + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; init; } +} + +/// +/// An MCP approval response item parameter. +/// +internal sealed class MCPApprovalResponseItemParam : ItemParam +{ + /// + /// The constant item type identifier for MCP approval response items. + /// + public const string ItemType = "mcp_approval_response"; + + /// + public override string Type => ItemType; + + /// + /// The ID of the approval request being answered. + /// + [JsonPropertyName("approval_request_id")] + public string? ApprovalRequestId { get; init; } + + /// + /// Whether the request was approved. + /// + [JsonPropertyName("approve")] + public bool? Approve { get; init; } + + /// + /// Optional reason for the decision. + /// + [JsonPropertyName("reason")] + public string? Reason { get; init; } +} + +/// +/// An MCP call item parameter. +/// +internal sealed class MCPCallItemParam : ItemParam +{ + /// + /// The constant item type identifier for MCP call items. + /// + public const string ItemType = "mcp_call"; + + /// + public override string Type => ItemType; + + /// + /// The label of the MCP server running the tool. + /// + [JsonPropertyName("server_label")] + public string? ServerLabel { get; init; } + + /// + /// The name of the tool that was run. + /// + [JsonPropertyName("name")] + public string? Name { get; init; } + + /// + /// A JSON string of the arguments passed to the tool. + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; init; } + + /// + /// The output from the tool call. + /// + [JsonPropertyName("output")] + public string? Output { get; init; } + + /// + /// The error from the tool call, if any. + /// + [JsonPropertyName("error")] + public string? Error { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParamExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParamExtensions.cs new file mode 100644 index 0000000000..0cc919830c --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemParamExtensions.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +/// +/// Extension methods for converting ItemParam (input) to ItemResource (output). +/// +internal static class ItemParamExtensions +{ + /// + /// Converts an ItemParam (input model) to an ItemResource (output model) by adding server-generated fields. + /// + /// The input item parameter. + /// An ItemResource with a generated ID. + public static ItemResource ToItemResource(this ItemParam param) + { + ArgumentNullException.ThrowIfNull(param); + + string generatedId = $"msg_{Guid.NewGuid():N}"; + + return param switch + { + ResponsesUserMessageItemParam userMessageParam => new ResponsesUserMessageItemResource + { + Id = generatedId, + Content = userMessageParam.Content.ToItemContents(), + Status = ResponsesMessageItemResourceStatus.Completed + }, + ResponsesSystemMessageItemParam systemMessageParam => new ResponsesSystemMessageItemResource + { + Id = generatedId, + Content = systemMessageParam.Content.ToItemContents(), + Status = ResponsesMessageItemResourceStatus.Completed + }, + ResponsesAssistantMessageItemParam assistantMessageParam => new ResponsesAssistantMessageItemResource + { + Id = generatedId, + Content = assistantMessageParam.Content.ToItemContents(), + Status = ResponsesMessageItemResourceStatus.Completed + }, + ResponsesDeveloperMessageItemParam developerMessageParam => new ResponsesDeveloperMessageItemResource + { + Id = generatedId, + Content = developerMessageParam.Content.ToItemContents(), + Status = ResponsesMessageItemResourceStatus.Completed + }, + FunctionToolCallItemParam functionCallParam => new FunctionToolCallItemResource + { + Id = generatedId, + Name = functionCallParam.Name, + CallId = functionCallParam.CallId, + Arguments = functionCallParam.Arguments, + Status = FunctionToolCallItemResourceStatus.Completed + }, + FunctionToolCallOutputItemParam functionOutputParam => new FunctionToolCallOutputItemResource + { + Id = generatedId, + CallId = functionOutputParam.CallId, + Output = functionOutputParam.Output + }, + FileSearchToolCallItemParam fileSearchParam => new FileSearchToolCallItemResource + { + Id = generatedId, + Queries = fileSearchParam.Queries, + Results = fileSearchParam.Results + }, + ComputerToolCallItemParam computerCallParam => new ComputerToolCallItemResource + { + Id = generatedId, + CallId = computerCallParam.CallId, + Action = computerCallParam.Action, + PendingSafetyChecks = computerCallParam.PendingSafetyChecks + }, + ComputerToolCallOutputItemParam computerOutputParam => new ComputerToolCallOutputItemResource + { + Id = generatedId, + CallId = computerOutputParam.CallId, + AcknowledgedSafetyChecks = computerOutputParam.AcknowledgedSafetyChecks, + Output = computerOutputParam.Output + }, + WebSearchToolCallItemParam webSearchParam => new WebSearchToolCallItemResource + { + Id = generatedId, + Action = webSearchParam.Action + }, + ReasoningItemParam reasoningParam => new ReasoningItemResource + { + Id = generatedId, + EncryptedContent = reasoningParam.EncryptedContent, + Summary = reasoningParam.Summary + }, + ItemReferenceItemParam => new ItemReferenceItemResource + { + Id = generatedId + }, + ImageGenerationToolCallItemParam imageGenParam => new ImageGenerationToolCallItemResource + { + Id = generatedId, + Result = imageGenParam.Result + }, + CodeInterpreterToolCallItemParam codeInterpreterParam => new CodeInterpreterToolCallItemResource + { + Id = generatedId, + ContainerId = codeInterpreterParam.ContainerId, + Code = codeInterpreterParam.Code, + Outputs = codeInterpreterParam.Outputs + }, + LocalShellToolCallItemParam localShellParam => new LocalShellToolCallItemResource + { + Id = generatedId, + CallId = localShellParam.CallId, + Action = localShellParam.Action + }, + LocalShellToolCallOutputItemParam localShellOutputParam => new LocalShellToolCallOutputItemResource + { + Id = generatedId, + Output = localShellOutputParam.Output + }, + MCPListToolsItemParam mcpListToolsParam => new MCPListToolsItemResource + { + Id = generatedId, + ServerLabel = mcpListToolsParam.ServerLabel, + Tools = mcpListToolsParam.Tools, + Error = mcpListToolsParam.Error + }, + MCPApprovalRequestItemParam mcpApprovalRequestParam => new MCPApprovalRequestItemResource + { + Id = generatedId, + ServerLabel = mcpApprovalRequestParam.ServerLabel, + Name = mcpApprovalRequestParam.Name, + Arguments = mcpApprovalRequestParam.Arguments + }, + MCPApprovalResponseItemParam mcpApprovalResponseParam => new MCPApprovalResponseItemResource + { + Id = generatedId, + ApprovalRequestId = mcpApprovalResponseParam.ApprovalRequestId, + Approve = mcpApprovalResponseParam.Approve, + Reason = mcpApprovalResponseParam.Reason + }, + MCPCallItemParam mcpCallParam => new MCPCallItemResource + { + Id = generatedId, + ServerLabel = mcpCallParam.ServerLabel, + Name = mcpCallParam.Name, + Arguments = mcpCallParam.Arguments, + Output = mcpCallParam.Output, + Error = mcpCallParam.Error + }, + // Fallback for unknown types + _ => throw new InvalidOperationException($"Unknown ItemParam type: {param.GetType().Name}") + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs index f0a7e83666..0a543e1be9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs @@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// Base class for all item resources (output items from a response). /// [JsonConverter(typeof(ItemResourceConverter))] -internal abstract record ItemResource +internal abstract class ItemResource { /// /// The unique identifier for the item. @@ -31,7 +31,7 @@ internal abstract record ItemResource /// Base class for message item resources. /// [JsonConverter(typeof(ResponsesMessageItemResourceConverter))] -internal abstract record ResponsesMessageItemResource : ItemResource +internal abstract class ResponsesMessageItemResource : ItemResource { /// /// The constant item type identifier for message items. @@ -57,7 +57,7 @@ internal abstract record ResponsesMessageItemResource : ItemResource /// /// An assistant message item resource. /// -internal sealed record ResponsesAssistantMessageItemResource : ResponsesMessageItemResource +internal sealed class ResponsesAssistantMessageItemResource : ResponsesMessageItemResource { /// /// The constant role type identifier for assistant messages. @@ -71,13 +71,13 @@ internal sealed record ResponsesAssistantMessageItemResource : ResponsesMessageI /// The content of the message. /// [JsonPropertyName("content")] - public required IList Content { get; init; } + public required List Content { get; init; } } /// /// A user message item resource. /// -internal sealed record ResponsesUserMessageItemResource : ResponsesMessageItemResource +internal sealed class ResponsesUserMessageItemResource : ResponsesMessageItemResource { /// /// The constant role type identifier for user messages. @@ -91,13 +91,13 @@ internal sealed record ResponsesUserMessageItemResource : ResponsesMessageItemRe /// The content of the message. /// [JsonPropertyName("content")] - public required IList Content { get; init; } + public required List Content { get; init; } } /// /// A system message item resource. /// -internal sealed record ResponsesSystemMessageItemResource : ResponsesMessageItemResource +internal sealed class ResponsesSystemMessageItemResource : ResponsesMessageItemResource { /// /// The constant role type identifier for system messages. @@ -111,13 +111,13 @@ internal sealed record ResponsesSystemMessageItemResource : ResponsesMessageItem /// The content of the message. /// [JsonPropertyName("content")] - public required IList Content { get; init; } + public required List Content { get; init; } } /// /// A developer message item resource. /// -internal sealed record ResponsesDeveloperMessageItemResource : ResponsesMessageItemResource +internal sealed class ResponsesDeveloperMessageItemResource : ResponsesMessageItemResource { /// /// The constant role type identifier for developer messages. @@ -131,13 +131,13 @@ internal sealed record ResponsesDeveloperMessageItemResource : ResponsesMessageI /// The content of the message. /// [JsonPropertyName("content")] - public required IList Content { get; init; } + public required List Content { get; init; } } /// /// A function tool call item resource. /// -internal sealed record FunctionToolCallItemResource : ItemResource +internal sealed class FunctionToolCallItemResource : ItemResource { /// /// The constant item type identifier for function call items. @@ -175,7 +175,7 @@ internal sealed record FunctionToolCallItemResource : ItemResource /// /// A function tool call output item resource. /// -internal sealed record FunctionToolCallOutputItemResource : ItemResource +internal sealed class FunctionToolCallOutputItemResource : ItemResource { /// /// The constant item type identifier for function call output items. @@ -208,7 +208,7 @@ internal sealed record FunctionToolCallOutputItemResource : ItemResource /// The status of a message item resource. /// [JsonConverter(typeof(SnakeCaseEnumConverter))] -public enum ResponsesMessageItemResourceStatus +internal enum ResponsesMessageItemResourceStatus { /// /// The message is completed. @@ -230,7 +230,7 @@ public enum ResponsesMessageItemResourceStatus /// The status of a function tool call item resource. /// [JsonConverter(typeof(SnakeCaseEnumConverter))] -public enum FunctionToolCallItemResourceStatus +internal enum FunctionToolCallItemResourceStatus { /// /// The function call is completed. @@ -247,7 +247,7 @@ public enum FunctionToolCallItemResourceStatus /// The status of a function tool call output item resource. /// [JsonConverter(typeof(SnakeCaseEnumConverter))] -public enum FunctionToolCallOutputItemResourceStatus +internal enum FunctionToolCallOutputItemResourceStatus { /// /// The function call output is completed. @@ -266,7 +266,7 @@ public enum FunctionToolCallOutputItemResourceStatus [JsonDerivedType(typeof(ItemContentOutputText), "output_text")] [JsonDerivedType(typeof(ItemContentOutputAudio), "output_audio")] [JsonDerivedType(typeof(ItemContentRefusal), "refusal")] -internal abstract record ItemContent +internal abstract class ItemContent { /// /// The type of the content. @@ -285,7 +285,7 @@ internal abstract record ItemContent /// /// Text input content. /// -internal sealed record ItemContentInputText : ItemContent +internal sealed class ItemContentInputText : ItemContent { /// [JsonIgnore] @@ -301,7 +301,7 @@ internal sealed record ItemContentInputText : ItemContent /// /// Audio input content. /// -internal sealed record ItemContentInputAudio : ItemContent +internal sealed class ItemContentInputAudio : ItemContent { /// [JsonIgnore] @@ -323,7 +323,7 @@ internal sealed record ItemContentInputAudio : ItemContent /// /// Image input content. /// -internal sealed record ItemContentInputImage : ItemContent +internal sealed class ItemContentInputImage : ItemContent { /// [JsonIgnore] @@ -352,7 +352,7 @@ internal sealed record ItemContentInputImage : ItemContent /// /// File input content. /// -internal sealed record ItemContentInputFile : ItemContent +internal sealed class ItemContentInputFile : ItemContent { /// [JsonIgnore] @@ -380,7 +380,7 @@ internal sealed record ItemContentInputFile : ItemContent /// /// Text output content. /// -internal sealed record ItemContentOutputText : ItemContent +internal sealed class ItemContentOutputText : ItemContent { /// [JsonIgnore] @@ -396,19 +396,19 @@ internal sealed record ItemContentOutputText : ItemContent /// The annotations. /// [JsonPropertyName("annotations")] - public required IList Annotations { get; init; } + public required List Annotations { get; init; } /// /// Log probability information for the output tokens. /// [JsonPropertyName("logprobs")] - public IList Logprobs { get; init; } = []; + public List Logprobs { get; init; } = []; } /// /// Audio output content. /// -internal sealed record ItemContentOutputAudio : ItemContent +internal sealed class ItemContentOutputAudio : ItemContent { /// [JsonIgnore] @@ -430,7 +430,7 @@ internal sealed record ItemContentOutputAudio : ItemContent /// /// Refusal content. /// -internal sealed record ItemContentRefusal : ItemContent +internal sealed class ItemContentRefusal : ItemContent { /// [JsonIgnore] @@ -448,7 +448,7 @@ internal sealed record ItemContentRefusal : ItemContent /// /// A file search tool call item resource. /// -internal sealed record FileSearchToolCallItemResource : ItemResource +internal sealed class FileSearchToolCallItemResource : ItemResource { /// /// The constant item type identifier for file search call items. @@ -463,12 +463,24 @@ internal sealed record FileSearchToolCallItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// The queries used to search for files. + /// + [JsonPropertyName("queries")] + public List? Queries { get; init; } + + /// + /// The results of the file search tool call. + /// + [JsonPropertyName("results")] + public List? Results { get; init; } } /// /// A computer tool call item resource. /// -internal sealed record ComputerToolCallItemResource : ItemResource +internal sealed class ComputerToolCallItemResource : ItemResource { /// /// The constant item type identifier for computer call items. @@ -483,12 +495,30 @@ internal sealed record ComputerToolCallItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// An identifier used when responding to the tool call with output. + /// + [JsonPropertyName("call_id")] + public string? CallId { get; init; } + + /// + /// The action to perform. + /// + [JsonPropertyName("action")] + public JsonElement? Action { get; init; } + + /// + /// The pending safety checks for the computer call. + /// + [JsonPropertyName("pending_safety_checks")] + public List? PendingSafetyChecks { get; init; } } /// /// A computer tool call output item resource. /// -internal sealed record ComputerToolCallOutputItemResource : ItemResource +internal sealed class ComputerToolCallOutputItemResource : ItemResource { /// /// The constant item type identifier for computer call output items. @@ -503,12 +533,30 @@ internal sealed record ComputerToolCallOutputItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// The ID of the computer tool call that produced the output. + /// + [JsonPropertyName("call_id")] + public string? CallId { get; init; } + + /// + /// The safety checks reported by the API that have been acknowledged by the developer. + /// + [JsonPropertyName("acknowledged_safety_checks")] + public List? AcknowledgedSafetyChecks { get; init; } + + /// + /// The output of the computer tool call. + /// + [JsonPropertyName("output")] + public JsonElement? Output { get; init; } } /// /// A web search tool call item resource. /// -internal sealed record WebSearchToolCallItemResource : ItemResource +internal sealed class WebSearchToolCallItemResource : ItemResource { /// /// The constant item type identifier for web search call items. @@ -523,12 +571,18 @@ internal sealed record WebSearchToolCallItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// An object describing the specific action taken in this web search call. + /// + [JsonPropertyName("action")] + public JsonElement? Action { get; init; } } /// /// A reasoning item resource. /// -internal sealed record ReasoningItemResource : ItemResource +internal sealed class ReasoningItemResource : ItemResource { /// /// The constant item type identifier for reasoning items. @@ -543,12 +597,25 @@ internal sealed record ReasoningItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// The encrypted content of the reasoning item - populated when a response is + /// generated with reasoning.encrypted_content in the include parameter. + /// + [JsonPropertyName("encrypted_content")] + public string? EncryptedContent { get; init; } + + /// + /// Reasoning text contents. + /// + [JsonPropertyName("summary")] + public List? Summary { get; init; } } /// /// An item reference item resource. /// -internal sealed record ItemReferenceItemResource : ItemResource +internal sealed class ItemReferenceItemResource : ItemResource { /// /// The constant item type identifier for item reference items. @@ -562,7 +629,7 @@ internal sealed record ItemReferenceItemResource : ItemResource /// /// An image generation tool call item resource. /// -internal sealed record ImageGenerationToolCallItemResource : ItemResource +internal sealed class ImageGenerationToolCallItemResource : ItemResource { /// /// The constant item type identifier for image generation call items. @@ -577,12 +644,18 @@ internal sealed record ImageGenerationToolCallItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// The generated image encoded in base64. + /// + [JsonPropertyName("result")] + public string? Result { get; init; } } /// /// A code interpreter tool call item resource. /// -internal sealed record CodeInterpreterToolCallItemResource : ItemResource +internal sealed class CodeInterpreterToolCallItemResource : ItemResource { /// /// The constant item type identifier for code interpreter call items. @@ -597,12 +670,31 @@ internal sealed record CodeInterpreterToolCallItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// The ID of the container used to run the code. + /// + [JsonPropertyName("container_id")] + public string? ContainerId { get; init; } + + /// + /// The code to run, or null if not available. + /// + [JsonPropertyName("code")] + public string? Code { get; init; } + + /// + /// The outputs generated by the code interpreter, such as logs or images. + /// Can be null if no outputs are available. + /// + [JsonPropertyName("outputs")] + public List? Outputs { get; init; } } /// /// A local shell tool call item resource. /// -internal sealed record LocalShellToolCallItemResource : ItemResource +internal sealed class LocalShellToolCallItemResource : ItemResource { /// /// The constant item type identifier for local shell call items. @@ -617,12 +709,24 @@ internal sealed record LocalShellToolCallItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// The unique ID of the local shell tool call generated by the model. + /// + [JsonPropertyName("call_id")] + public string? CallId { get; init; } + + /// + /// The action to execute. + /// + [JsonPropertyName("action")] + public JsonElement? Action { get; init; } } /// /// A local shell tool call output item resource. /// -internal sealed record LocalShellToolCallOutputItemResource : ItemResource +internal sealed class LocalShellToolCallOutputItemResource : ItemResource { /// /// The constant item type identifier for local shell call output items. @@ -637,12 +741,18 @@ internal sealed record LocalShellToolCallOutputItemResource : ItemResource /// [JsonPropertyName("status")] public string? Status { get; init; } + + /// + /// A JSON string of the output of the local shell tool call. + /// + [JsonPropertyName("output")] + public string? Output { get; init; } } /// /// An MCP list tools item resource. /// -internal sealed record MCPListToolsItemResource : ItemResource +internal sealed class MCPListToolsItemResource : ItemResource { /// /// The constant item type identifier for MCP list tools items. @@ -651,12 +761,30 @@ internal sealed record MCPListToolsItemResource : ItemResource /// public override string Type => ItemType; + + /// + /// The label of the MCP server. + /// + [JsonPropertyName("server_label")] + public string? ServerLabel { get; init; } + + /// + /// The tools available on the server. + /// + [JsonPropertyName("tools")] + public List? Tools { get; init; } + + /// + /// Error message if the server could not list tools. + /// + [JsonPropertyName("error")] + public string? Error { get; init; } } /// /// An MCP approval request item resource. /// -internal sealed record MCPApprovalRequestItemResource : ItemResource +internal sealed class MCPApprovalRequestItemResource : ItemResource { /// /// The constant item type identifier for MCP approval request items. @@ -665,12 +793,30 @@ internal sealed record MCPApprovalRequestItemResource : ItemResource /// public override string Type => ItemType; + + /// + /// The label of the MCP server making the request. + /// + [JsonPropertyName("server_label")] + public string? ServerLabel { get; init; } + + /// + /// The name of the tool to run. + /// + [JsonPropertyName("name")] + public string? Name { get; init; } + + /// + /// A JSON string of arguments for the tool. + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; init; } } /// /// An MCP approval response item resource. /// -internal sealed record MCPApprovalResponseItemResource : ItemResource +internal sealed class MCPApprovalResponseItemResource : ItemResource { /// /// The constant item type identifier for MCP approval response items. @@ -679,12 +825,30 @@ internal sealed record MCPApprovalResponseItemResource : ItemResource /// public override string Type => ItemType; + + /// + /// The ID of the approval request being answered. + /// + [JsonPropertyName("approval_request_id")] + public string? ApprovalRequestId { get; init; } + + /// + /// Whether the request was approved. + /// + [JsonPropertyName("approve")] + public bool? Approve { get; init; } + + /// + /// Optional reason for the decision. + /// + [JsonPropertyName("reason")] + public string? Reason { get; init; } } /// /// An MCP call item resource. /// -internal sealed record MCPCallItemResource : ItemResource +internal sealed class MCPCallItemResource : ItemResource { /// /// The constant item type identifier for MCP call items. @@ -693,4 +857,34 @@ internal sealed record MCPCallItemResource : ItemResource /// public override string Type => ItemType; + + /// + /// The label of the MCP server running the tool. + /// + [JsonPropertyName("server_label")] + public string? ServerLabel { get; init; } + + /// + /// The name of the tool that was run. + /// + [JsonPropertyName("name")] + public string? Name { get; init; } + + /// + /// A JSON string of the arguments passed to the tool. + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; init; } + + /// + /// The output from the tool call. + /// + [JsonPropertyName("output")] + public string? Output { get; init; } + + /// + /// The error from the tool call, if any. + /// + [JsonPropertyName("error")] + public string? Error { get; init; } } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs index d14e94473c..8bf0ee2846 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// /// Reference to a prompt template and its variables. /// -internal sealed record PromptReference +internal sealed class PromptReference { /// /// The ID of the prompt template to use. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs index 8be910278a..d34a56c6ee 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs @@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// /// Configuration options for reasoning models. /// -internal sealed record ReasoningOptions +internal sealed class ReasoningOptions { /// /// Constrains effort on reasoning for reasoning models. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs index 260aa2478c..3f9c50e933 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs @@ -12,7 +12,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// The status of a response generation. /// [JsonConverter(typeof(SnakeCaseEnumConverter))] -public enum ResponseStatus +internal enum ResponseStatus { /// /// The response has been completed. @@ -111,7 +111,7 @@ internal sealed record Response /// The output items (messages) generated in the response. /// [JsonPropertyName("output")] - public required IList Output { get; init; } + public required List Output { get; init; } /// /// A system (or developer) message inserted into the model's context. @@ -135,7 +135,7 @@ internal sealed record Response /// An array of tools the model may call while generating a response. /// [JsonPropertyName("tools")] - public required IList Tools { get; init; } + public required List Tools { get; init; } /// /// How the model should select which tool (or tools) to use when generating a response. @@ -288,6 +288,9 @@ internal sealed record IncompleteDetails /// internal sealed record ResponseUsage { + /// + /// Gets a zero usage instance. + /// public static ResponseUsage Zero { get; } = new() { InputTokens = 0, diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs index 840adf87b0..d0555a2c00 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs @@ -21,7 +21,7 @@ private ResponseInput(string text) this.Messages = null; } - private ResponseInput(IReadOnlyList messages) + private ResponseInput(List messages) { this.Messages = messages ?? throw new ArgumentNullException(nameof(messages)); this.Text = null; @@ -35,12 +35,12 @@ private ResponseInput(IReadOnlyList messages) /// /// Creates a ResponseInput from a list of messages. /// - public static ResponseInput FromMessages(IReadOnlyList messages) => new(messages); + public static ResponseInput FromMessages(List messages) => new(messages); /// /// Creates a ResponseInput from a list of messages. /// - public static ResponseInput FromMessages(params InputMessage[] messages) => new(messages); + public static ResponseInput FromMessages(params InputMessage[] messages) => new(messages.ToList()); /// /// Implicit conversion from string to ResponseInput. @@ -75,12 +75,13 @@ private ResponseInput(IReadOnlyList messages) /// /// Gets the messages value, or null if this is not a messages input. /// - public IReadOnlyList? Messages { get; } + public List? Messages { get; } /// /// Gets the input as a list of InputMessage objects. /// - public IReadOnlyList GetInputMessages() + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Method performs transformation logic")] + public List GetInputMessages() { if (this.Text is not null) { @@ -164,6 +165,7 @@ public override int GetHashCode() /// internal sealed class ResponseInputJsonConverter : JsonConverter { + /// public override ResponseInput? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Check if it's a string @@ -176,13 +178,14 @@ internal sealed class ResponseInputJsonConverter : JsonConverter // Check if it's an array if (reader.TokenType == JsonTokenType.StartArray) { - var messages = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListInputMessage); + var messages = JsonSerializer.Deserialize(ref reader, OpenAIHostingJsonContext.Default.ListInputMessage); return messages is not null ? ResponseInput.FromMessages(messages) : null; } throw new JsonException($"Unexpected token type for ResponseInput: {reader.TokenType}"); } + /// public override void Write(Utf8JsonWriter writer, ResponseInput value, JsonSerializerOptions options) { if (value.IsText) @@ -191,7 +194,7 @@ public override void Write(Utf8JsonWriter writer, ResponseInput value, JsonSeria } else if (value.IsMessages) { - JsonSerializer.Serialize(writer, value.Messages!, ResponsesJsonContext.Default.IReadOnlyListInputMessage); + JsonSerializer.Serialize(writer, value.Messages!, OpenAIHostingJsonContext.Default.ListInputMessage); } else { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs index 3c9a484ddf..93ca5865e5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs @@ -7,16 +7,8 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// /// Options for streaming responses. Only set this when you set stream: true. /// -internal sealed record StreamOptions +internal sealed class StreamOptions { - /// - /// If set, an additional chunk will be streamed before the data: [DONE] message. - /// The usage field on this chunk shows the token usage statistics for the entire request, - /// and the choices field will always be an empty array. - /// - [JsonPropertyName("include_usage")] - public bool? IncludeUsage { get; init; } - /// /// When true, stream obfuscation will be enabled. Stream obfuscation adds random characters /// to an obfuscation field on streaming delta events to normalize payload sizes as a mitigation diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs index 82f80bfa15..6d41e10aff 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs @@ -16,6 +16,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; [JsonDerivedType(typeof(StreamingResponseCompleted), StreamingResponseCompleted.EventType)] [JsonDerivedType(typeof(StreamingResponseIncomplete), StreamingResponseIncomplete.EventType)] [JsonDerivedType(typeof(StreamingResponseFailed), StreamingResponseFailed.EventType)] +[JsonDerivedType(typeof(StreamingResponseCancelled), StreamingResponseCancelled.EventType)] [JsonDerivedType(typeof(StreamingOutputItemAdded), StreamingOutputItemAdded.EventType)] [JsonDerivedType(typeof(StreamingOutputItemDone), StreamingOutputItemDone.EventType)] [JsonDerivedType(typeof(StreamingContentPartAdded), StreamingContentPartAdded.EventType)] @@ -26,7 +27,10 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; [JsonDerivedType(typeof(StreamingFunctionCallArgumentsDone), StreamingFunctionCallArgumentsDone.EventType)] [JsonDerivedType(typeof(StreamingReasoningSummaryTextDelta), StreamingReasoningSummaryTextDelta.EventType)] [JsonDerivedType(typeof(StreamingReasoningSummaryTextDone), StreamingReasoningSummaryTextDone.EventType)] -internal abstract record StreamingResponseEvent +[JsonDerivedType(typeof(StreamingWorkflowEventComplete), StreamingWorkflowEventComplete.EventType)] +[JsonDerivedType(typeof(StreamingFunctionApprovalRequested), StreamingFunctionApprovalRequested.EventType)] +[JsonDerivedType(typeof(StreamingFunctionApprovalResponded), StreamingFunctionApprovalResponded.EventType)] +internal abstract class StreamingResponseEvent { /// /// Gets the type identifier for the streaming response event. @@ -43,11 +47,22 @@ internal abstract record StreamingResponseEvent public int SequenceNumber { get; init; } } +/// +/// Denotes an instance which contains an update to the instance. +/// +internal interface IStreamingResponseEventWithResponse +{ + /// + /// Gets the response object associated with this streaming event. + /// + Response Response { get; } +} + /// /// Represents a streaming response event indicating that a new response has been created and streaming has begun. /// This is typically the first event sent in a streaming response sequence. /// -internal sealed record StreamingResponseCreated : StreamingResponseEvent +internal sealed class StreamingResponseCreated : StreamingResponseEvent, IStreamingResponseEventWithResponse { /// /// The constant event type identifier for response created events. @@ -69,7 +84,7 @@ internal sealed record StreamingResponseCreated : StreamingResponseEvent /// /// Represents a streaming response event indicating that the response is in progress. /// -internal sealed record StreamingResponseInProgress : StreamingResponseEvent +internal sealed class StreamingResponseInProgress : StreamingResponseEvent, IStreamingResponseEventWithResponse { /// /// The constant event type identifier for response in progress events. @@ -91,7 +106,7 @@ internal sealed record StreamingResponseInProgress : StreamingResponseEvent /// Represents a streaming response event indicating that the response has been completed. /// This is typically the last event sent in a streaming response sequence. /// -internal sealed record StreamingResponseCompleted : StreamingResponseEvent +internal sealed class StreamingResponseCompleted : StreamingResponseEvent, IStreamingResponseEventWithResponse { /// /// The constant event type identifier for response completed events. @@ -113,7 +128,7 @@ internal sealed record StreamingResponseCompleted : StreamingResponseEvent /// /// Represents a streaming response event indicating that the response finished as incomplete. /// -internal sealed record StreamingResponseIncomplete : StreamingResponseEvent +internal sealed class StreamingResponseIncomplete : StreamingResponseEvent, IStreamingResponseEventWithResponse { /// /// The constant event type identifier for response incomplete events. @@ -134,7 +149,7 @@ internal sealed record StreamingResponseIncomplete : StreamingResponseEvent /// /// Represents a streaming response event indicating that the response has failed. /// -internal sealed record StreamingResponseFailed : StreamingResponseEvent +internal sealed class StreamingResponseFailed : StreamingResponseEvent, IStreamingResponseEventWithResponse { /// /// The constant event type identifier for response failed events. @@ -152,11 +167,33 @@ internal sealed record StreamingResponseFailed : StreamingResponseEvent public required Response Response { get; init; } } +/// +/// Represents a streaming response event indicating that the response has been cancelled. +/// Only responses created with background=true can be cancelled. +/// +internal sealed class StreamingResponseCancelled : StreamingResponseEvent, IStreamingResponseEventWithResponse +{ + /// + /// The constant event type identifier for response cancelled events. + /// + public const string EventType = "response.cancelled"; + + /// + [JsonIgnore] + public override string Type => EventType; + + /// + /// Gets or sets the cancelled response object. + /// + [JsonPropertyName("response")] + public required Response Response { get; init; } +} + /// /// Represents a streaming response event indicating that a new output item has been added to the response. /// This event is sent when the AI agent produces a new piece of content during streaming. /// -internal sealed record StreamingOutputItemAdded : StreamingResponseEvent +internal sealed class StreamingOutputItemAdded : StreamingResponseEvent { /// /// The constant event type identifier for output item added events. @@ -186,7 +223,7 @@ internal sealed record StreamingOutputItemAdded : StreamingResponseEvent /// Represents a streaming response event indicating that an output item has been completed. /// This event is sent when the AI agent finishes producing a particular piece of content. /// -internal sealed record StreamingOutputItemDone : StreamingResponseEvent +internal sealed class StreamingOutputItemDone : StreamingResponseEvent { /// /// The constant event type identifier for output item done events. @@ -215,7 +252,7 @@ internal sealed record StreamingOutputItemDone : StreamingResponseEvent /// /// Represents a streaming response event indicating that a new content part has been added to an output item. /// -internal sealed record StreamingContentPartAdded : StreamingResponseEvent +internal sealed class StreamingContentPartAdded : StreamingResponseEvent { /// /// The constant event type identifier for content part added events. @@ -254,7 +291,7 @@ internal sealed record StreamingContentPartAdded : StreamingResponseEvent /// /// Represents a streaming response event indicating that a content part has been completed. /// -internal sealed record StreamingContentPartDone : StreamingResponseEvent +internal sealed class StreamingContentPartDone : StreamingResponseEvent { /// /// The constant event type identifier for content part done events. @@ -293,7 +330,7 @@ internal sealed record StreamingContentPartDone : StreamingResponseEvent /// /// Represents a streaming response event containing a text delta (incremental text chunk). /// -internal sealed record StreamingOutputTextDelta : StreamingResponseEvent +internal sealed class StreamingOutputTextDelta : StreamingResponseEvent { /// /// The constant event type identifier for output text delta events. @@ -332,13 +369,13 @@ internal sealed record StreamingOutputTextDelta : StreamingResponseEvent /// Gets or sets the log probability information for the output tokens. /// [JsonPropertyName("logprobs")] - public IList Logprobs { get; init; } = []; + public List Logprobs { get; init; } = []; } /// /// Represents a streaming response event indicating that output text has been completed. /// -internal sealed record StreamingOutputTextDone : StreamingResponseEvent +internal sealed class StreamingOutputTextDone : StreamingResponseEvent { /// /// The constant event type identifier for output text done events. @@ -377,7 +414,7 @@ internal sealed record StreamingOutputTextDone : StreamingResponseEvent /// /// Represents a streaming response event containing a function call arguments delta. /// -internal sealed record StreamingFunctionCallArgumentsDelta : StreamingResponseEvent +internal sealed class StreamingFunctionCallArgumentsDelta : StreamingResponseEvent { /// /// The constant event type identifier for function call arguments delta events. @@ -410,7 +447,7 @@ internal sealed record StreamingFunctionCallArgumentsDelta : StreamingResponseEv /// /// Represents a streaming response event indicating that function call arguments are complete. /// -internal sealed record StreamingFunctionCallArgumentsDone : StreamingResponseEvent +internal sealed class StreamingFunctionCallArgumentsDone : StreamingResponseEvent { /// /// The constant event type identifier for function call arguments done events. @@ -443,7 +480,7 @@ internal sealed record StreamingFunctionCallArgumentsDone : StreamingResponseEve /// /// Represents a streaming response event containing a reasoning summary text delta (incremental text chunk). /// -internal sealed record StreamingReasoningSummaryTextDelta : StreamingResponseEvent +internal sealed class StreamingReasoningSummaryTextDelta : StreamingResponseEvent { /// /// The constant event type identifier for reasoning summary text delta events. @@ -482,7 +519,7 @@ internal sealed record StreamingReasoningSummaryTextDelta : StreamingResponseEve /// /// Represents a streaming response event indicating that reasoning summary text has been completed. /// -internal sealed record StreamingReasoningSummaryTextDone : StreamingResponseEvent +internal sealed class StreamingReasoningSummaryTextDone : StreamingResponseEvent { /// /// The constant event type identifier for reasoning summary text done events. @@ -517,3 +554,148 @@ internal sealed record StreamingReasoningSummaryTextDone : StreamingResponseEven [JsonPropertyName("text")] public required string Text { get; init; } } + +/// +/// Represents a streaming response event containing a workflow event. +/// This event is sent during workflow execution to provide observability into workflow steps, +/// executor invocations, errors, and other workflow lifecycle events. +/// +internal sealed class StreamingWorkflowEventComplete : StreamingResponseEvent +{ + /// + /// The constant event type identifier for workflow event events. + /// + public const string EventType = "response.workflow_event.complete"; + + /// + [JsonIgnore] + public override string Type => EventType; + + /// + /// Gets or sets the index of the output in the response. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } + + /// + /// Gets or sets the workflow event data containing event type, executor ID, and event-specific data. + /// + [JsonPropertyName("data")] + public JsonElement? Data { get; set; } + + /// + /// Gets or sets the executor ID if this is an executor-scoped event. + /// + [JsonPropertyName("executor_id")] + public string? ExecutorId { get; set; } + + /// + /// Gets or sets the item ID for tracking purposes. + /// + [JsonPropertyName("item_id")] + public string? ItemId { get; set; } +} + +/// +/// Represents a streaming response event indicating a function approval has been requested. +/// This is a non-standard DevUI extension for human-in-the-loop scenarios. +/// +internal sealed class StreamingFunctionApprovalRequested : StreamingResponseEvent +{ + /// + /// The constant event type identifier for function approval requested events. + /// + public const string EventType = "response.function_approval.requested"; + + /// + [JsonIgnore] + public override string Type => EventType; + + /// + /// Gets or sets the unique identifier for the approval request. + /// + [JsonPropertyName("request_id")] + public required string RequestId { get; init; } + + /// + /// Gets or sets the function call that requires approval. + /// + [JsonPropertyName("function_call")] + public required FunctionCallInfo FunctionCall { get; init; } + + /// + /// Gets or sets the item ID for tracking purposes. + /// + [JsonPropertyName("item_id")] + public required string ItemId { get; init; } + + /// + /// Gets or sets the output index. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; init; } +} + +/// +/// Represents a streaming response event indicating a function approval has been responded to. +/// This is a non-standard DevUI extension for human-in-the-loop scenarios. +/// +internal sealed class StreamingFunctionApprovalResponded : StreamingResponseEvent +{ + /// + /// The constant event type identifier for function approval responded events. + /// + public const string EventType = "response.function_approval.responded"; + + /// + [JsonIgnore] + public override string Type => EventType; + + /// + /// Gets or sets the unique identifier of the approval request being responded to. + /// + [JsonPropertyName("request_id")] + public required string RequestId { get; init; } + + /// + /// Gets or sets a value indicating whether the function call was approved. + /// + [JsonPropertyName("approved")] + public bool Approved { get; init; } + + /// + /// Gets or sets the item ID for tracking purposes. + /// + [JsonPropertyName("item_id")] + public required string ItemId { get; init; } + + /// + /// Gets or sets the output index. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; init; } +} + +/// +/// Represents function call information for approval events. +/// +internal sealed class FunctionCallInfo +{ + /// + /// Gets or sets the function call ID. + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Gets or sets the function name. + /// + [JsonPropertyName("name")] + public required string Name { get; init; } + + /// + /// Gets or sets the function arguments. + /// + [JsonPropertyName("arguments")] + public required JsonElement Arguments { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs index dc590030e6..6a4e98651d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; +using System.Text.Json; using System.Text.Json.Serialization; namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; @@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; /// /// Configuration options for a text response from the model. /// -internal sealed record TextConfiguration +internal sealed class TextConfiguration { /// /// The format configuration for the text response. @@ -34,7 +34,7 @@ internal sealed record TextConfiguration [JsonDerivedType(typeof(ResponseTextFormatConfigurationText), "text")] [JsonDerivedType(typeof(ResponseTextFormatConfigurationJsonObject), "json_object")] [JsonDerivedType(typeof(ResponseTextFormatConfigurationJsonSchema), "json_schema")] -internal abstract record ResponseTextFormatConfiguration +internal abstract class ResponseTextFormatConfiguration { /// /// The type of response format. @@ -46,7 +46,7 @@ internal abstract record ResponseTextFormatConfiguration /// /// Plain text response format configuration. /// -internal sealed record ResponseTextFormatConfigurationText : ResponseTextFormatConfiguration +internal sealed class ResponseTextFormatConfigurationText : ResponseTextFormatConfiguration { /// /// Gets the type of response format. Always "text". @@ -59,7 +59,7 @@ internal sealed record ResponseTextFormatConfigurationText : ResponseTextFormatC /// JSON object response format configuration. /// Ensures the message the model generates is valid JSON. /// -internal sealed record ResponseTextFormatConfigurationJsonObject : ResponseTextFormatConfiguration +internal sealed class ResponseTextFormatConfigurationJsonObject : ResponseTextFormatConfiguration { /// /// Gets the type of response format. Always "json_object". @@ -71,7 +71,7 @@ internal sealed record ResponseTextFormatConfigurationJsonObject : ResponseTextF /// /// JSON schema response format configuration with structured output schema. /// -internal sealed record ResponseTextFormatConfigurationJsonSchema : ResponseTextFormatConfiguration +internal sealed class ResponseTextFormatConfigurationJsonSchema : ResponseTextFormatConfiguration { /// /// Gets the type of response format. Always "json_schema". @@ -97,7 +97,7 @@ internal sealed record ResponseTextFormatConfigurationJsonSchema : ResponseTextF /// The JSON schema for structured outputs. /// [JsonPropertyName("schema")] - public required Dictionary Schema { get; init; } + public required JsonElement Schema { get; init; } /// /// Whether to enable strict schema adherence when generating the output. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/WorkflowEventData.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/WorkflowEventData.cs new file mode 100644 index 0000000000..cc7f44cda6 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/WorkflowEventData.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +/// +/// Represents workflow event data for serialization. +/// +internal sealed class WorkflowEventData +{ + /// + /// The type of the workflow event. + /// + [JsonPropertyName("event_type")] + public required string EventType { get; init; } + + /// + /// The event data payload. + /// + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonElement? Data { get; init; } + + /// + /// The executor ID, if this is an executor event. + /// + [JsonPropertyName("executor_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ExecutorId { get; init; } + + /// + /// The timestamp when the event occurred. + /// + [JsonPropertyName("timestamp")] + public required string Timestamp { get; init; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs new file mode 100644 index 0000000000..893f9fd917 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +/// +/// Handles route requests for OpenAI Responses API endpoints. +/// +internal sealed class ResponsesHttpHandler +{ + private readonly IResponsesService _responsesService; + + /// + /// Initializes a new instance of the class. + /// + /// The responses service. + public ResponsesHttpHandler(IResponsesService responsesService) + { + this._responsesService = responsesService ?? throw new ArgumentNullException(nameof(responsesService)); + } + + /// + /// Creates a model response for the given input. + /// + public async Task CreateResponseAsync( + [FromBody] CreateResponse request, + [FromQuery] bool? stream, + CancellationToken cancellationToken) + { + try + { + // Handle streaming vs non-streaming + bool shouldStream = stream ?? request.Stream ?? false; + + if (shouldStream) + { + var streamingResponse = this._responsesService.CreateResponseStreamingAsync( + request, + cancellationToken: cancellationToken); + + return new SseJsonResult( + streamingResponse, + static evt => evt.Type, + OpenAIHostingJsonContext.Default.StreamingResponseEvent); + } + + var response = await this._responsesService.CreateResponseAsync( + request, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return Results.Ok(response); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("Mutually exclusive")) + { + // Return OpenAI-style error for mutual exclusivity violations + return Results.BadRequest(new ErrorResponse + { + Error = new ErrorDetails + { + Message = ex.Message, + Type = "invalid_request_error", + Code = "mutually_exclusive_parameters" + } + }); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("not found") || ex.Message.Contains("does not exist")) + { + // Return OpenAI-style error for not found errors + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = ex.Message, + Type = "invalid_request_error" + } + }); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("No 'agent.name' or 'model' specified")) + { + // Return OpenAI-style error for missing required parameters + return Results.BadRequest(new ErrorResponse + { + Error = new ErrorDetails + { + Message = ex.Message, + Type = "invalid_request_error", + Code = "missing_required_parameter" + } + }); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status500InternalServerError, + title: "Error creating response"); + } + } + + /// + /// Retrieves a response by ID. + /// + public async Task GetResponseAsync( + string responseId, + [FromQuery] string[]? include, + [FromQuery] bool? stream, + [FromQuery] int? starting_after, + CancellationToken cancellationToken) + { + try + { + // If streaming is requested, return SSE stream + if (stream == true) + { + var streamingResponse = this._responsesService.GetResponseStreamingAsync( + responseId, + startingAfter: starting_after, + cancellationToken: cancellationToken); + + return new SseJsonResult( + streamingResponse, + static evt => evt.Type, + OpenAIHostingJsonContext.Default.StreamingResponseEvent); + } + + // Non-streaming: return the response object + var response = await this._responsesService.GetResponseAsync(responseId, cancellationToken).ConfigureAwait(false); + return response is not null + ? Results.Ok(response) + : Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Response '{responseId}' not found.", + Type = "invalid_request_error" + } + }); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status404NotFound, + title: "Response not found"); + } + } + + /// + /// Cancels an in-progress response. + /// + public async Task CancelResponseAsync( + string responseId, + CancellationToken cancellationToken) + { + try + { + var response = await this._responsesService.CancelResponseAsync(responseId, cancellationToken).ConfigureAwait(false); + return Results.Ok(response); + } + catch (InvalidOperationException ex) + { + return Results.BadRequest(new ErrorResponse + { + Error = new ErrorDetails + { + Message = ex.Message, + Type = "invalid_request_error" + } + }); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status500InternalServerError, + title: "Error cancelling response"); + } + } + + /// + /// Deletes a response. + /// + public async Task DeleteResponseAsync( + string responseId, + CancellationToken cancellationToken) + { + try + { + var deleted = await this._responsesService.DeleteResponseAsync(responseId, cancellationToken).ConfigureAwait(false); + return deleted + ? Results.Ok(new DeleteResponse { Id = responseId, Object = "response", Deleted = true }) + : Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = $"Response '{responseId}' not found.", + Type = "invalid_request_error" + } + }); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status500InternalServerError, + title: "Error deleting response"); + } + } + + /// + /// Lists the input items for a response. + /// + public async Task ListResponseInputItemsAsync( + string responseId, + [FromQuery] int? limit, + [FromQuery] string? order, + [FromQuery] string? after, + [FromQuery] string? before, + CancellationToken cancellationToken) + { + try + { + var result = await this._responsesService.ListResponseInputItemsAsync( + responseId, + limit ?? 20, + order ?? "desc", + after, + before, + cancellationToken).ConfigureAwait(false); + + return Results.Ok(result); + } + catch (InvalidOperationException ex) + { + return Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails + { + Message = ex.Message, + Type = "invalid_request_error" + } + }); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status500InternalServerError, + title: "Error listing input items"); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs deleted file mode 100644 index 5f014466fc..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; - -namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; - -/// -/// Extension methods for JSON serialization. -/// -internal static class ResponsesJsonSerializerOptions -{ - /// - /// Gets the default JSON serializer options. - /// - public static JsonSerializerOptions Default { get; } = Create(); - - private static JsonSerializerOptions Create() - { - JsonSerializerOptions options = new(ResponsesJsonContext.Default.Options); - options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); - options.MakeReadOnly(); - return options; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs index 0d80f93154..446b0401ac 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs @@ -16,27 +16,18 @@ internal sealed class AudioContentEventGenerator( SequenceNumber seq, int outputIndex) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => content is DataContent dataContent && dataContent.HasTopLevelMediaType("audio"); public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - if (content is not DataContent audioData || !audioData.HasTopLevelMediaType("audio")) { throw new InvalidOperationException("AudioContentEventGenerator only supports audio DataContent."); } var itemId = idGenerator.GenerateMessageId(); - var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputAudio; - - if (itemContent == null) + if (ItemContentConverter.ToItemContent(content) is not ItemContentInputAudio itemContent) { throw new InvalidOperationException("Failed to convert audio content to ItemContentInputAudio."); } @@ -79,13 +70,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs index 8320b1f977..6380cb8ba7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs @@ -16,26 +16,17 @@ internal sealed class ErrorContentEventGenerator( SequenceNumber seq, int outputIndex) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => content is ErrorContent; public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - if (content is not ErrorContent) { throw new InvalidOperationException("ErrorContentEventGenerator only supports ErrorContent."); } var itemId = idGenerator.GenerateMessageId(); - var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentRefusal; - - if (itemContent == null) + if (ItemContentConverter.ToItemContent(content) is not ItemContentRefusal itemContent) { throw new InvalidOperationException("Failed to convert error content to ItemContentRefusal."); } @@ -78,13 +69,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs index bf60c2f1cf..5fe7333ac3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs @@ -16,8 +16,6 @@ internal sealed class FileContentEventGenerator( SequenceNumber seq, int outputIndex) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => content is DataContent dataContent && !dataContent.HasTopLevelMediaType("image") && @@ -25,11 +23,6 @@ content is DataContent dataContent && public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - if (content is not DataContent fileData || fileData.HasTopLevelMediaType("image") || fileData.HasTopLevelMediaType("audio")) @@ -38,9 +31,7 @@ public override IEnumerable ProcessContent(AIContent con } var itemId = idGenerator.GenerateMessageId(); - var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputFile; - - if (itemContent == null) + if (ItemContentConverter.ToItemContent(content) is not ItemContentInputFile itemContent) { throw new InvalidOperationException("Failed to convert file content to ItemContentInputFile."); } @@ -83,13 +74,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalRequestEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalRequestEventGenerator.cs new file mode 100644 index 0000000000..c30cc9f6d9 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalRequestEventGenerator.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming; + +/// +/// A generator for streaming events from function approval request content. +/// This is a non-standard DevUI extension for human-in-the-loop scenarios. +/// +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. +internal sealed class FunctionApprovalRequestEventGenerator( + IdGenerator idGenerator, + SequenceNumber seq, + int outputIndex, + JsonSerializerOptions jsonSerializerOptions) : StreamingEventGenerator +{ + public override bool IsSupported(AIContent content) => content is FunctionApprovalRequestContent; + + public override IEnumerable ProcessContent(AIContent content) + { + if (content is not FunctionApprovalRequestContent approvalRequest) + { + throw new InvalidOperationException("FunctionApprovalRequestEventGenerator only supports FunctionApprovalRequestContent."); + } + + yield return new StreamingFunctionApprovalRequested + { + SequenceNumber = seq.Increment(), + OutputIndex = outputIndex, + RequestId = approvalRequest.Id, + ItemId = idGenerator.GenerateMessageId(), + FunctionCall = new FunctionCallInfo + { + Id = approvalRequest.FunctionCall.CallId, + Name = approvalRequest.FunctionCall.Name, +#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code +#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. + Arguments = JsonSerializer.SerializeToElement(approvalRequest.FunctionCall.Arguments, jsonSerializerOptions) +#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling. +#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code + } + }; + } + + public override IEnumerable Complete() => []; +} +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalResponseEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalResponseEventGenerator.cs new file mode 100644 index 0000000000..f66971502a --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionApprovalResponseEventGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming; + +/// +/// A generator for streaming events from function approval response content. +/// This is a non-standard DevUI extension for human-in-the-loop scenarios. +/// +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. +internal sealed class FunctionApprovalResponseEventGenerator( + IdGenerator idGenerator, + SequenceNumber seq, + int outputIndex) : StreamingEventGenerator +{ + public override bool IsSupported(AIContent content) => content is FunctionApprovalResponseContent; + + public override IEnumerable ProcessContent(AIContent content) + { + if (content is not FunctionApprovalResponseContent approvalResponse) + { + throw new InvalidOperationException("FunctionApprovalResponseEventGenerator only supports FunctionApprovalResponseContent."); + } + + yield return new StreamingFunctionApprovalResponded + { + SequenceNumber = seq.Increment(), + OutputIndex = outputIndex, + RequestId = approvalResponse.Id, + Approved = approvalResponse.Approved, + ItemId = idGenerator.GenerateMessageId() + }; + } + + public override IEnumerable Complete() => []; +} +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs index 74f5ffe4ae..c0b0aba54d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs @@ -17,17 +17,10 @@ internal sealed class FunctionCallEventGenerator( int outputIndex, JsonSerializerOptions jsonSerializerOptions) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => content is FunctionCallContent; public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - if (content is not FunctionCallContent functionCallContent) { throw new InvalidOperationException("FunctionCallEventGenerator only supports FunctionCallContent."); @@ -63,13 +56,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs index 1c7810a825..116eb716e1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs @@ -15,17 +15,10 @@ internal sealed class FunctionResultEventGenerator( SequenceNumber seq, int outputIndex) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => content is FunctionResultContent; public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - if (content is not FunctionResultContent functionResultContent) { throw new InvalidOperationException("FunctionResultEventGenerator only supports FunctionResultContent."); @@ -45,13 +38,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs index 5846858aa2..a8beefe211 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs @@ -16,26 +16,17 @@ internal sealed class HostedFileContentEventGenerator( SequenceNumber seq, int outputIndex) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => content is HostedFileContent; public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - if (content is not HostedFileContent) { throw new InvalidOperationException("HostedFileContentEventGenerator only supports HostedFileContent."); } var itemId = idGenerator.GenerateMessageId(); - var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputFile; - - if (itemContent == null) + if (ItemContentConverter.ToItemContent(content) is not ItemContentInputFile itemContent) { throw new InvalidOperationException("Failed to convert hosted file content to ItemContentInputFile."); } @@ -78,13 +69,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs index 0b80abbfd2..0642043f3d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs @@ -16,22 +16,13 @@ internal sealed class ImageContentEventGenerator( SequenceNumber seq, int outputIndex) : StreamingEventGenerator { - private bool _isCompleted; - public override bool IsSupported(AIContent content) => - content is UriContent uriContent && uriContent.HasTopLevelMediaType("image") || - content is DataContent dataContent && dataContent.HasTopLevelMediaType("image"); + (content is UriContent uriContent && uriContent.HasTopLevelMediaType("image")) || + (content is DataContent dataContent && dataContent.HasTopLevelMediaType("image")); public override IEnumerable ProcessContent(AIContent content) { - if (this._isCompleted) - { - throw new InvalidOperationException("Cannot process content after the generator has been completed."); - } - - ItemContentInputImage? itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputImage; - - if (itemContent == null) + if (ItemContentConverter.ToItemContent(content) is not ItemContentInputImage itemContent) { throw new InvalidOperationException("ImageContentEventGenerator only supports image UriContent and DataContent."); } @@ -76,13 +67,7 @@ public override IEnumerable ProcessContent(AIContent con OutputIndex = outputIndex, Item = item }; - - this._isCompleted = true; } - public override IEnumerable Complete() - { - this._isCompleted = true; - return []; - } + public override IEnumerable Complete() => []; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs index 5be8d73aa9..3004b00085 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs @@ -18,7 +18,7 @@ internal sealed class TextReasoningContentEventGenerator( int outputIndex) : StreamingEventGenerator { private State _currentState = State.Initial; - private readonly string _itemId = idGenerator.GenerateMessageId(); + private readonly string _itemId = idGenerator.GenerateReasoningId(); private readonly StringBuilder _text = new(); private const int SummaryIndex = 0; // Summary index for reasoning summary text diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Conversations.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Conversations.cs new file mode 100644 index 0000000000..b24f63c593 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Conversations.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Agents.AI.Hosting.OpenAI; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for registering conversation services with the dependency injection container. +/// +public static class ConversationServiceCollectionExtensions +{ + /// + /// Adds in-memory conversation storage and indexing services to the service collection. + /// This is suitable for development and testing scenarios. For production, use a persistent storage implementation. + /// + /// The service collection to add services to. + /// Optional action to configure . + /// The service collection for chaining. + public static IServiceCollection AddOpenAIConversations(this IServiceCollection services, Action? configureOptions = null) + { + ArgumentNullException.ThrowIfNull(services); + + // Register storage options + var storageOptions = new InMemoryStorageOptions(); + configureOptions?.Invoke(storageOptions); + services.TryAddSingleton(storageOptions); + + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs new file mode 100644 index 0000000000..72243286db --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.OpenAI; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for to configure OpenAI Responses support. +/// +public static class MicrosoftAgentAIHostingOpenAIServiceCollectionExtensions +{ + /// + /// Adds support for exposing instances via OpenAI Responses. + /// Uses the in-memory responses service implementation by default. + /// + /// The to configure. + /// Optional action to configure . + /// The for method chaining. + public static IServiceCollection AddOpenAIResponses(this IServiceCollection services, Action? configureOptions = null) + { + ArgumentNullException.ThrowIfNull(services); + + services.Configure(options + => options.SerializerOptions.TypeInfoResolverChain.Add( + OpenAIHostingJsonContext.Default.Options.TypeInfoResolver!)); + + // Register storage options + var storageOptions = new InMemoryStorageOptions(); + configureOptions?.Invoke(storageOptions); + services.TryAddSingleton(storageOptions); + + services.TryAddSingleton(sp => + { + var executor = sp.GetRequiredService(); + var options = sp.GetRequiredService(); + var conversationStorage = sp.GetService(); + return new InMemoryResponsesService(executor, options, conversationStorage); + }); + services.TryAddSingleton(sp => + { + // Inject IConversationStorage if it's available (though executors no longer use it directly) + var conversationStorage = sp.GetService(); + var logger = sp.GetRequiredService>(); + return new HostedAgentResponseExecutor(sp, logger, conversationStorage); + }); + + return services; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs deleted file mode 100644 index 7fab6586a9..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting.OpenAI.Responses; -using Microsoft.AspNetCore.Http.Json; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Extension methods for to configure OpenAI Responses support. -/// -public static class MicrosoftAgentAIHostingOpenAIServiceCollectionExtensions -{ - /// - /// Adds support for exposing instances via OpenAI Responses. - /// - /// The to configure. - /// The for method chaining. - public static IServiceCollection AddOpenAIResponses(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - services.Configure(options => options.SerializerOptions.TypeInfoResolverChain.Add(ResponsesJsonSerializerOptions.Default.TypeInfoResolver!)); - - return services; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/SseJsonResult.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/SseJsonResult.cs new file mode 100644 index 0000000000..2edb2f0027 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/SseJsonResult.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Net.ServerSentEvents; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.Agents.AI.Hosting.OpenAI; + +/// +/// IResult implementation for streaming JSON data using Server-Sent Events (SSE). +/// +/// The type of items to stream. +internal sealed class SseJsonResult : IResult +{ + private readonly IAsyncEnumerable _events; + private readonly JsonTypeInfo _jsonTypeInfo; + private readonly Func _getEventType; + + /// + /// Initializes a new instance of the class. + /// + /// The async enumerable of items to stream. + /// A function to get the optional event type from each item. + /// The JSON type information for serializing items. + public SseJsonResult(IAsyncEnumerable events, Func getEventType, JsonTypeInfo jsonTypeInfo) + { + this._events = events ?? throw new ArgumentNullException(nameof(events)); + this._jsonTypeInfo = jsonTypeInfo ?? throw new ArgumentNullException(nameof(jsonTypeInfo)); + this._getEventType = getEventType ?? throw new ArgumentNullException(nameof(getEventType)); + } + + /// + /// Executes the result by streaming items to the HTTP response using Server-Sent Events format. + /// + /// The HTTP context. + public async Task ExecuteAsync(HttpContext httpContext) + { + var response = httpContext.Response; + var cancellationToken = httpContext.RequestAborted; + + // Set SSE headers + response.Headers.ContentType = "text/event-stream"; + response.Headers.CacheControl = "no-cache,no-store"; + response.Headers.Connection = "keep-alive"; + response.Headers.ContentEncoding = "identity"; + httpContext.Features.GetRequiredFeature().DisableBuffering(); + + await SseFormatter.WriteAsync( + source: this.GetItemsAsync(), + destination: response.Body, + itemFormatter: this.FormatItem, + cancellationToken).ConfigureAwait(false); + } + + private async IAsyncEnumerable> GetItemsAsync() + { + await foreach (var item in this._events.ConfigureAwait(false)) + { + yield return new SseItem(item, this._getEventType(item)); + } + } + + private void FormatItem(SseItem sseItem, IBufferWriter bufferWriter) + { + using var writer = new Utf8JsonWriter(bufferWriter); + JsonSerializer.Serialize(writer, sseItem.Data, this._jsonTypeInfo); + writer.Flush(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentInvocationContextTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentInvocationContextTests.cs new file mode 100644 index 0000000000..b6458775bb --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/AgentInvocationContextTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Unit tests for AgentInvocationContext. +/// +public sealed class AgentInvocationContextTests +{ + [Fact] + public void Constructor_WithIdGenerator_InitializesCorrectly() + { + // Arrange + var idGenerator = new IdGenerator("resp_test123", "conv_test456"); + + // Act + var context = new AgentInvocationContext(idGenerator); + + // Assert + Assert.NotNull(context); + Assert.Same(idGenerator, context.IdGenerator); + Assert.Equal("resp_test123", context.ResponseId); + Assert.Equal("conv_test456", context.ConversationId); + Assert.NotNull(context.JsonSerializerOptions); + } + + [Fact] + public void Constructor_WithoutJsonOptions_UsesDefaultOptions() + { + // Arrange + var idGenerator = new IdGenerator("resp_test", "conv_test"); + + // Act + var context = new AgentInvocationContext(idGenerator); + + // Assert + Assert.NotNull(context.JsonSerializerOptions); + Assert.Same(OpenAIHostingJsonUtilities.DefaultOptions, context.JsonSerializerOptions); + } + + [Fact] + public void Constructor_WithCustomJsonOptions_UsesProvidedOptions() + { + // Arrange + var idGenerator = new IdGenerator("resp_test", "conv_test"); + var customOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + // Act + var context = new AgentInvocationContext(idGenerator, customOptions); + + // Assert + Assert.Same(customOptions, context.JsonSerializerOptions); + } + + [Fact] + public void ResponseId_ReturnsIdGeneratorResponseId() + { + // Arrange + const string ResponseId = "resp_property_test"; + var idGenerator = new IdGenerator(ResponseId, "conv_test"); + var context = new AgentInvocationContext(idGenerator); + + // Act + string result = context.ResponseId; + + // Assert + Assert.Equal(ResponseId, result); + Assert.Equal(idGenerator.ResponseId, result); + } + + [Fact] + public void ConversationId_ReturnsIdGeneratorConversationId() + { + // Arrange + const string ConversationId = "conv_property_test"; + var idGenerator = new IdGenerator("resp_test", ConversationId); + var context = new AgentInvocationContext(idGenerator); + + // Act + string result = context.ConversationId; + + // Assert + Assert.Equal(ConversationId, result); + Assert.Equal(idGenerator.ConversationId, result); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs index eb6de34fff..b7437ca38d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Microsoft.Agents.AI.Hosting.OpenAI.Tests; @@ -140,7 +139,7 @@ protected async Task CreateTestServerAsync(string agentName, string IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); @@ -170,7 +169,37 @@ protected async Task CreateTestServerAsync( IChatClient mockChatClient = new TestHelpers.CustomContentMockChatClient(contentProvider); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); + + this._app = builder.Build(); + AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); + this._app.MapOpenAIResponses(agent); + + await this._app.StartAsync(); + + TestServer testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + return this._httpClient; + } + + /// + /// Creates a test server with a mock chat client that returns function call content. + /// + protected async Task CreateTestServerWithToolCallAsync( + string agentName, + string instructions, + string functionName, + string arguments) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + IChatClient mockChatClient = new TestHelpers.ToolCallMockChatClient(functionName, arguments); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); + builder.Services.AddOpenAIResponses(); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/request.json new file mode 100644 index 0000000000..e5329afbf6 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/request.json @@ -0,0 +1,24 @@ +{ + "items": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What is the weather like today?" + } + ] + }, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Tell me a joke!" + } + ] + } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/response.json new file mode 100644 index 0000000000..83de8ffe5e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/add_items/response.json @@ -0,0 +1,32 @@ +{ + "object": "list", + "data": [ + { + "id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822", + "type": "message", + "status": "completed", + "content": [ + { + "type": "input_text", + "text": "What is the weather like today?" + } + ], + "role": "user" + }, + { + "id": "msg_68fb9abf14d08195af5037cc3048b1c704cbf45151194822", + "type": "message", + "status": "completed", + "content": [ + { + "type": "input_text", + "text": "Tell me a joke!" + } + ], + "role": "user" + } + ], + "first_id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822", + "has_more": false, + "last_id": "msg_68fb9abf14d08195af5037cc3048b1c704cbf45151194822" +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_request.json new file mode 100644 index 0000000000..50ca7a03a6 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_request.json @@ -0,0 +1,5 @@ +{ + "metadata": { + "test_type": "basic_conversation" + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_response.json new file mode 100644 index 0000000000..41eec4c70d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/create_conversation_response.json @@ -0,0 +1,8 @@ +{ + "id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822", + "object": "conversation", + "created_at": 1761318654, + "metadata": { + "test_type": "basic_conversation" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_request.json new file mode 100644 index 0000000000..78e1101657 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_request.json @@ -0,0 +1,6 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822", + "input": "What is the capital of France?", + "max_output_tokens": 100 +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_response.json new file mode 100644 index 0000000000..b6164798b9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/first_message_response.json @@ -0,0 +1,70 @@ +{ + "id": "resp_04cbf451511948220068fb97bdec548195a367870aa85734de", + "object": "response", + "created_at": 1761318846, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "conversation": { + "id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 100, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_04cbf451511948220068fb97c0162881958d80862a0d253a14", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The capital of France is Paris." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 36, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 8, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 44 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_request.json new file mode 100644 index 0000000000..f7818bd906 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_request.json @@ -0,0 +1,6 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822", + "input": "What is its population?", + "max_output_tokens": 150 +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_response.json new file mode 100644 index 0000000000..3315534da6 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic/second_message_response.json @@ -0,0 +1,70 @@ +{ + "id": "resp_04cbf451511948220068fb97cf320881958b69530fe07eb2a9", + "object": "response", + "created_at": 1761318863, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "conversation": { + "id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 150, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "As of 2023, the population of Paris is approximately 2.1 million people within the city proper. However, the larger metropolitan area has a population of around 12 million. These numbers can vary, so it's always a good idea to check for the most recent statistics." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 56, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 58, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 114 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic_streaming/first_message_response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic_streaming/first_message_response.txt new file mode 100644 index 0000000000..80d9f1070c --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/basic_streaming/first_message_response.txt @@ -0,0 +1,624 @@ +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"In","logprobs":[],"obfuscation":"C16oYk8aI5VtGp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"vXmOvISW7QRUF1"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" small","logprobs":[],"obfuscation":"qEkC6mYZmi"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" workshop","logprobs":[],"obfuscation":"2aAdNXN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" at","logprobs":[],"obfuscation":"bv66grEvpSema"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"fVOKa91q3jxh"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" edge","logprobs":[],"obfuscation":"kW1rIr6ZZBc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"RnPLx5DWhJvWO"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"DMVs96dHxVd7fh"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" bustling","logprobs":[],"obfuscation":"9TCmdGs"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"E4p2Nj5KH0Z"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" lived","logprobs":[],"obfuscation":"e3kqeTLJpR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"zQmSxD9MrnbNr7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" curious","logprobs":[],"obfuscation":"wQHxX2wm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" robot","logprobs":[],"obfuscation":"i49v38s1iB"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" named","logprobs":[],"obfuscation":"FC4nhPH5iI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"WxNhIEwf5h"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"jIf06WyqbCsP1is"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Unlike","logprobs":[],"obfuscation":"0UnxmoTXo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" other","logprobs":[],"obfuscation":"D082q19raq"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" robots","logprobs":[],"obfuscation":"O6qMHEj2b"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" whose","logprobs":[],"obfuscation":"vee013IYPw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" tasks","logprobs":[],"obfuscation":"XHa10h45Oa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" revol","logprobs":[],"obfuscation":"6FBrIwdGV9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ved","logprobs":[],"obfuscation":"M0VL3Bw0RIAo6"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" around","logprobs":[],"obfuscation":"LLilH7SVr"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" heavy","logprobs":[],"obfuscation":"tegXm6RO6A"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" lifting","logprobs":[],"obfuscation":"6b3EMVcS"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"JhqGeJLj5aA3V"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" data","logprobs":[],"obfuscation":"2GzCA3ZBZov"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" processing","logprobs":[],"obfuscation":"pQJMQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"yIf9YenbsIenASh"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"wKzF15AosR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"Wowp4nS4X1Ng"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" designed","logprobs":[],"obfuscation":"Yz6ZJdQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"zf1HLk47LNX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"sNucb47CLCVlI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" intricate","logprobs":[],"obfuscation":"9TxqRk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" array","logprobs":[],"obfuscation":"d2GG2LyctD"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"yy31Pt217J6Xp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sensors","logprobs":[],"obfuscation":"dFE11Kjt"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"j5OIdm87111a"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"WwEaIsudqLtCvf"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" flexible","logprobs":[],"obfuscation":"jH5YA59"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" arm","logprobs":[],"obfuscation":"RJVKiLoNoYxQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"bpX63CPMF8aQHv7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" perfect","logprobs":[],"obfuscation":"eCXfxPet"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"aNwYIhOgicEt"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" creativity","logprobs":[],"obfuscation":"QTeqK"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"qFaBkm23u4NYkj4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" However","logprobs":[],"obfuscation":"hrYOmahs"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"PnGLt5WSzXM3RG4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"yk2yG2xNbY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" had","logprobs":[],"obfuscation":"CShj4jWsDFmW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" never","logprobs":[],"obfuscation":"b92hQra8IU"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painted","logprobs":[],"obfuscation":"Wu9kSosu"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"41kdUr8fcF1eY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"One","logprobs":[],"obfuscation":"ywv21ub1bYPzr"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" rainy","logprobs":[],"obfuscation":"piDyieWe6I"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" afternoon","logprobs":[],"obfuscation":"o6TtQn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"6K5zBbkZ1KDqaOo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" while","logprobs":[],"obfuscation":"DZpPr8CLVs"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" organizing","logprobs":[],"obfuscation":"yyd7A"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paint","logprobs":[],"obfuscation":"TbeYUHmhLW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"brush","logprobs":[],"obfuscation":"LSTcAO85OyQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"es","logprobs":[],"obfuscation":"g8YnY0jNlHqwv8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Ey5F23xj6FJr"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" canv","logprobs":[],"obfuscation":"EsQE9gBSUI5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ases","logprobs":[],"obfuscation":"jXPKC0ARj6Jk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"p0APw0fonPMBbpz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"S9Iw9WD1td"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" stumbled","logprobs":[],"obfuscation":"lUhKO2y"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" across","logprobs":[],"obfuscation":"zOVN5cc6m"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"kFX7KcjAVQa3u"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" old","logprobs":[],"obfuscation":"PcJzaliXOTKf"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painting","logprobs":[],"obfuscation":"5JFpUDK"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"—a","logprobs":[],"obfuscation":"hN488ItRbxIdlD"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" dazzling","logprobs":[],"obfuscation":"JEkA0aE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" landscape","logprobs":[],"obfuscation":"mehmYO"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" bursting","logprobs":[],"obfuscation":"gq0lWWG"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"roG9ZXQbDpe"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"gdKUt6ALG"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"UUCXxD95v3ekSVk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Fasc","logprobs":[],"obfuscation":"iVOZvBK0g9g"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"inated","logprobs":[],"obfuscation":"WyckQbiJri"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"qPzZ3PZNvSoTVXz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"YKdVPbL14g"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" studied","logprobs":[],"obfuscation":"j6lPd2xU"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"3zYfSjrWfRlp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" painting","logprobs":[],"obfuscation":"ygVKhmv"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"’s","logprobs":[],"obfuscation":"jfyEtMpt46t1Ww"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sw","logprobs":[],"obfuscation":"8ufXFBggxZ3TS"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"irls","logprobs":[],"obfuscation":"SbzWkGTAG34r"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"hXqSM3Qr77XDVdb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" textures","logprobs":[],"obfuscation":"OoYDmdA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"WYukNpLZWJs1j5L"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"O9CtJKsoG2JB"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"uoha0aPHY3w7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":102,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" way","logprobs":[],"obfuscation":"KnlsDOXhAPma"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":103,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"Nqzf9hidx"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":104,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" danced","logprobs":[],"obfuscation":"hhZcUfldt"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":105,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" together","logprobs":[],"obfuscation":"Mnd309k"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":106,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"PqZH6hxgnvJ1z1S"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":107,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" An","logprobs":[],"obfuscation":"rgthuRNYqDVfd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":108,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" idea","logprobs":[],"obfuscation":"RYoJHQzMviw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":109,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sparked","logprobs":[],"obfuscation":"bFn7eHwA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":110,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"Ym8ImtIUdMlm3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":111,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"2HuZRNAzdFY5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":112,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" circuits","logprobs":[],"obfuscation":"b19ajJd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":113,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"dBAMCGUMUgounvx"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":114,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"KDcOVnk2sl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":115,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" would","logprobs":[],"obfuscation":"QaX2I1Dg85"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":116,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" learn","logprobs":[],"obfuscation":"2QkmV1t6Js"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":117,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"1m259XNwN7CxV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":118,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paint","logprobs":[],"obfuscation":"SUGIRDOxLQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":119,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"eIMGNNPhRFbU4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":120,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"At","logprobs":[],"obfuscation":"G8GUOB6HOwqe9H"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":121,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" first","logprobs":[],"obfuscation":"4kUZs77xIL"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":122,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"DZJLHDJJJoRMgTV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":123,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"sXRNA81QPcKuI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":124,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"QLCPvdRQ7qmn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":125,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" cl","logprobs":[],"obfuscation":"J9qOKCfVRbrtD"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":126,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"umsy","logprobs":[],"obfuscation":"MV6H5FqEJNdo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":127,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"WDWm0egBq1CmII3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":128,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"hNiFWJ96FXpg"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":129,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" brushes","logprobs":[],"obfuscation":"Pf0FFkql"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":130,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" slipped","logprobs":[],"obfuscation":"kwS961wY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":131,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"XyDhbqDYRBT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":132,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"YmOIFY8YCUqL"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":133,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" grip","logprobs":[],"obfuscation":"ABcdnw5EIpX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":134,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"SXShKYz3KjctF5L"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":135,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"VMtvX3tcPsMa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":136,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" colors","logprobs":[],"obfuscation":"B3jtn3jGg"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":137,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sme","logprobs":[],"obfuscation":"jphfFzmwPLaF"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":138,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"ared","logprobs":[],"obfuscation":"TwRJ1pgJfZXY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":139,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" into","logprobs":[],"obfuscation":"jHvjvmlmRFx"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":140,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" mudd","logprobs":[],"obfuscation":"8LaKYmukTFy"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":141,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"led","logprobs":[],"obfuscation":"OEby50ZgHV8mj"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":142,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" gray","logprobs":[],"obfuscation":"vQLkls6KtLN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":143,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" blobs","logprobs":[],"obfuscation":"6pJRSKWLsI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":144,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" instead","logprobs":[],"obfuscation":"qImXEbxD"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":145,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"BTsJcdzYMfYed"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":146,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" vibrant","logprobs":[],"obfuscation":"Uo6JuUrd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":147,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" hues","logprobs":[],"obfuscation":"mCwdvWFcVLe"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":148,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"3rAuIoc3iI7OrtQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":149,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" But","logprobs":[],"obfuscation":"cRhMS7RaTArm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":150,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Pixel","logprobs":[],"obfuscation":"M1mKyav7ph"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":151,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" persisted","logprobs":[],"obfuscation":"eF5aUk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":152,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"yDdDhy5v9Zw35r6"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":153,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" Each","logprobs":[],"obfuscation":"xqte6NkdiIo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":154,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" day","logprobs":[],"obfuscation":"rppAW4RVeF8R"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":155,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"udSWzKzTyrCWVLi"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":156,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"F2NUuJOxWKpjP"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":157,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" practiced","logprobs":[],"obfuscation":"Aqqlv9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":158,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"ZUk2MhldL4AtrAe"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":159,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" mixing","logprobs":[],"obfuscation":"l030hejQa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":160,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" paints","logprobs":[],"obfuscation":"4xlfaIzxC"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":161,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"BtVvUiDXh3jSgxs"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":162,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" experimenting","logprobs":[],"obfuscation":"di"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":163,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"MCekQrhkBKN"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":164,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" strokes","logprobs":[],"obfuscation":"rRuR8dnc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":165,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"cFjk3IoxYD4tGrw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":166,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"DQ3Xi2a9dX9y"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":167,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" observing","logprobs":[],"obfuscation":"8t7Acj"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":168,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"KDYvCe6JsoYa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":169,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" world","logprobs":[],"obfuscation":"0rtOhI9Ffc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":170,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"oE2kAKM9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":171,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"RGobfdV8EooR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":172,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" eyes","logprobs":[],"obfuscation":"yzrEN6uVsyR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":173,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"MITYFimltUsuJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":174,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" artists","logprobs":[],"obfuscation":"ndi7qdrO"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":175,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"39IBhz9cxlCBc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":176,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":"Pixel","logprobs":[],"obfuscation":"SmMeGRPjx9o"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":177,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" took","logprobs":[],"obfuscation":"B7Yw3oSo8OX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":178,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" inspiration","logprobs":[],"obfuscation":"M4D6"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":179,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"lVxLLEHL7zV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":180,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" sunlight","logprobs":[],"obfuscation":"I3BmRGJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":181,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" filtering","logprobs":[],"obfuscation":"P6p35d"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":182,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"8MMH2TTk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":183,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"hmfNgkY1FJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":184,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Lkj68PREYAHG7mZ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":185,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"SYCf7zTCaGUi"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":186,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" depths","logprobs":[],"obfuscation":"cr9Phqnz8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":187,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"OT3aZnPvsDcmY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":188,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"fGdrYkLZHdTI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":189,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" ocean","logprobs":[],"obfuscation":"MvxJgRFjwz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":190,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"ox6Ar9czyzkruEM"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":191,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"MKK6YDJEzPxA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":192,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"UWEyznWlRSj3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":193,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" rhythm","logprobs":[],"obfuscation":"8E4xhBObX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":194,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"jbQAFSh8FJWWg"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":195,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"cxL7t1q6yLv"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":196,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" life","logprobs":[],"obfuscation":"CnftU4BnURk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":197,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"XucWb0a2fGIQafX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":198,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" It","logprobs":[],"obfuscation":"pt1xzT8tzMYRs"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":199,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" copied","logprobs":[],"obfuscation":"WrTQOEVfc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":200,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" techniques","logprobs":[],"obfuscation":"XJJzu"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":201,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"PrOd3zA9J76"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":202,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" videos","logprobs":[],"obfuscation":"fHAS8XsLg"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":203,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Hk6mknGTtruy"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":204,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the painting’s swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":205,"item_id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the painting’s swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":206,"output_index":0,"item":{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the painting’s swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}],"role":"assistant"}} + +event: response.incomplete +data: {"type":"response.incomplete","sequence_number":207,"response":{"id":"resp_0cdad19d14602ec80068fb98607b948193935a6e7aa2141ef2","object":"response","created_at":1761319008,"status":"incomplete","background":false,"conversation":{"id":"conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8"},"error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0cdad19d14602ec80068fb986280c8819388eebc7f20280aa6","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small workshop at the edge of a bustling city lived a curious robot named Pixel. Unlike other robots whose tasks revolved around heavy lifting or data processing, Pixel was designed with an intricate array of sensors and a flexible arm, perfect for creativity. However, Pixel had never painted.\n\nOne rainy afternoon, while organizing paintbrushes and canvases, Pixel stumbled across an old painting—a dazzling landscape bursting with colors. Fascinated, Pixel studied the painting’s swirls, textures, and the way colors danced together. An idea sparked in its circuits: Pixel would learn to paint.\n\nAt first, it was clumsy. The brushes slipped from its grip, and colors smeared into muddled gray blobs instead of vibrant hues. But Pixel persisted. Each day, it practiced: mixing paints, experimenting with strokes, and observing the world through the eyes of artists.\n\nPixel took inspiration from sunlight filtering through trees, the depths of the ocean, and the rhythm of city life. It copied techniques from videos and"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":19,"input_tokens_details":{"cached_tokens":0},"output_tokens":200,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":219},"user":null,"metadata":{}}} + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_request.json new file mode 100644 index 0000000000..ea87839722 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_request.json @@ -0,0 +1,17 @@ +{ + "metadata": { + "test_type": "create_with_initial_items" + }, + "items": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What is the capital of France?" + } + ] + } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_response.json new file mode 100644 index 0000000000..aaca0088d4 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/create_with_items/create_response.json @@ -0,0 +1,8 @@ +{ + "id": "conv_68fb980bccfc8195a9ba32b164e8a69408e61fbaa91b0a18", + "object": "conversation", + "created_at": 1761318923, + "metadata": { + "test_type": "create_with_initial_items" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_conversation/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_conversation/response.json new file mode 100644 index 0000000000..70db137cb8 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_conversation/response.json @@ -0,0 +1,5 @@ +{ + "id": "conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8", + "object": "conversation.deleted", + "deleted": true +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_item/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_item/response.json new file mode 100644 index 0000000000..a01b7cbe7a --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/delete_item/response.json @@ -0,0 +1,5 @@ +{ + "id": "msg_68fb9abf14a08195b16bb05eab82cf9d04cbf45151194822", + "object": "conversation.item.deleted", + "deleted": true +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_conversation_not_found/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_conversation_not_found/response.json new file mode 100644 index 0000000000..1c51ce1e9d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_conversation_not_found/response.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Conversation with id 'conv_nonexistent123' not found.", + "type": "invalid_request_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_delete_already_deleted/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_delete_already_deleted/response.json new file mode 100644 index 0000000000..0fa4c3661d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_delete_already_deleted/response.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Conversation with id 'conv_68fb9837f9588193ac3da6bd57b636a50cdad19d14602ec8' not found.", + "type": "invalid_request_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/request.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/request.txt new file mode 100644 index 0000000000..baba9b68ff --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/request.txt @@ -0,0 +1,5 @@ +{ + "metadata": { + "test": "invalid" + } + // missing closing brace and has comment which is invalid JSON diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/response.json new file mode 100644 index 0000000000..f2db45cbe8 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_json/response.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Invalid body: failed to parse JSON value. Please check the value to ensure it is valid JSON. (Common errors include trailing commas, missing closing brackets, missing quotation marks, etc.)", + "type": "invalid_request_error", + "param": null, + "code": "invalid_json" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_limit/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_limit/response.json new file mode 100644 index 0000000000..4ae939233d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_invalid_limit/response.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Invalid 'limit': integer above maximum value. Expected a value <= 100, but got 1000 instead.", + "type": "invalid_request_error", + "param": "limit", + "code": "integer_above_max_value" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_item_not_found/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_item_not_found/response.json new file mode 100644 index 0000000000..ab573cb05b --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_item_not_found/response.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Item with id 'msg_msg_nonexistent123nonexistent123' not found in conversation.", + "type": "invalid_request_error", + "param": null, + "code": null + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_missing_required_field/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_missing_required_field/request.json new file mode 100644 index 0000000000..6b43a686c3 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/error_missing_required_field/request.json @@ -0,0 +1,13 @@ +{ + "items": [ + { + "type": "message", + "content": [ + { + "type": "input_text", + "text": "Hello" + } + ] + } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_request.json new file mode 100644 index 0000000000..ca4a4a7ab7 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_request.json @@ -0,0 +1,5 @@ +{ + "metadata": { + "test_type": "image_input_conversation" + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_response.json new file mode 100644 index 0000000000..46fd00f10b --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/create_conversation_response.json @@ -0,0 +1,8 @@ +{ + "id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7", + "object": "conversation", + "created_at": 1761319071, + "metadata": { + "test_type": "image_input_conversation" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_request.json new file mode 100644 index 0000000000..1f242fbb65 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_request.json @@ -0,0 +1,21 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What's in this image? Describe it in detail." + }, + { + "type": "input_image", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + ] + } + ], + "max_output_tokens": 200 +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_response.json new file mode 100644 index 0000000000..f9007058aa --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input/first_message_response.json @@ -0,0 +1,70 @@ +{ + "id": "resp_003edf96db5b4ed70068fb98bd80808194b25763125111fffa", + "object": "response", + "created_at": 1761319101, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "conversation": { + "id": "conv_68fb989f39ec8194be3ec32525cd53c1003edf96db5b4ed7" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 200, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_003edf96db5b4ed70068fb98c1197481949e138bc36200ee18", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The image depicts a serene natural landscape featuring a wooden boardwalk winding through lush greenery. \n\n### Details:\n- **Pathway**: The boardwalk is made of wooden planks and extends straight ahead, encouraging exploration.\n- **Grass**: On both sides of the pathway, there is tall, vibrant green grass, suggesting a lush environment with possible wildflowers.\n- **Surrounding Vegetation**: Beyond the grass, there are various bushes and trees, adding layers of texture and color. Some foliage appears dense and lush, while other areas have more sparse coverage.\n- **Sky**: The sky is expansive and bright, with soft, fluffy clouds scattered throughout. The blue hues create a tranquil atmosphere, illuminated by sunlight.\n- **Overall Mood**: The scene conveys a sense of peace and openness, perfect for a nature walk or outdoor meditation.\n\nThis idyllic setting invites the viewer to appreciate the tranquility of nature and the beauty of the landscape." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 36852, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 192, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 37044 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/create_conversation_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/create_conversation_request.json new file mode 100644 index 0000000000..1b442abe4f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/create_conversation_request.json @@ -0,0 +1,5 @@ +{ + "metadata": { + "test_type": "image_input_streaming" + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_request.json new file mode 100644 index 0000000000..127ccfde48 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_request.json @@ -0,0 +1,22 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "What's in this image? Describe it in detail." + }, + { + "type": "input_image", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + ] + } + ], + "max_output_tokens": 200, + "stream": true +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_response.txt new file mode 100644 index 0000000000..ba0e9deff8 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/image_input_streaming/first_message_response.txt @@ -0,0 +1,456 @@ +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_03e6efaadaa48f3f0068fb98e75a9c819780dca860432f50c0","object":"response","created_at":1761319143,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_03e6efaadaa48f3f0068fb98e75a9c819780dca860432f50c0","object":"response","created_at":1761319143,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"UHUQ9fIQTxCbV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" image","logprobs":[],"obfuscation":"xNPzGqnhvU"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" depicts","logprobs":[],"obfuscation":"ojPXqx5m"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"UGIKclB7QdFjBc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" tranquil","logprobs":[],"obfuscation":"XSxvnxQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" scene","logprobs":[],"obfuscation":"XcPoVyD9iV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"eMV4kvkfbM0zd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"0klHtMIbU7P3Ea"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" pathway","logprobs":[],"obfuscation":"Cl7V0bkp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" made","logprobs":[],"obfuscation":"2DYHpC7Eyl3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"5ObYHXTVXJDaP"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" wooden","logprobs":[],"obfuscation":"p62ol2BGT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" boards","logprobs":[],"obfuscation":"9n53C6e36"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" leading","logprobs":[],"obfuscation":"vOZvFF5v"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"Gt1J5FNE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"RnDMouhlNrQ7RB"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" lush","logprobs":[],"obfuscation":"42N68Sud7kk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"08p36we5SqMENPp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" green","logprobs":[],"obfuscation":"zzWq9kepjH"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" landscape","logprobs":[],"obfuscation":"bISm6O"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"IMKn4R5dxQFxGJl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"w19FHugCAk1X"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" path","logprobs":[],"obfuscation":"hDJm0rbDlBz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"0riU9Z71ipbh7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" straight","logprobs":[],"obfuscation":"KQdad1O"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"V0838p6GoMKkdMb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" fl","logprobs":[],"obfuscation":"OwqpqwOUtVRWR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"anked","logprobs":[],"obfuscation":"q4TZWRJ4up7"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" by","logprobs":[],"obfuscation":"a4BkQCPOkWXa5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" tall","logprobs":[],"obfuscation":"uDgapRMTMh3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" grass","logprobs":[],"obfuscation":"DSWk0SmBLn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"gRpuHdZ2Q7z"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" appears","logprobs":[],"obfuscation":"MavFp4Q5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" vibrant","logprobs":[],"obfuscation":"iOciPOxV"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"rQdOojHHeet9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" healthy","logprobs":[],"obfuscation":"SuFkWnO8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"rcqKsVdM70DSisT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"s9tToHsQMbZ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" hints","logprobs":[],"obfuscation":"nfMICfvb21"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"V84AZDkQ50w3N"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" various","logprobs":[],"obfuscation":"tM5QpNvy"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" shades","logprobs":[],"obfuscation":"P7DjB4f2C"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"qBkC9EgLqkA1c"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" green","logprobs":[],"obfuscation":"hU4g5KAOZW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"eapW7Q1E884SHZT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" \n\n","logprobs":[],"obfuscation":"C9GIn2LBfGkw6"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"To","logprobs":[],"obfuscation":"M2K4wUNJ6uZQAT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" either","logprobs":[],"obfuscation":"wB8ah2F34"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" side","logprobs":[],"obfuscation":"l1Xni4I4YSv"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"BhtFvy3X01wnb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"6BmDo9c8flKg"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" pathway","logprobs":[],"obfuscation":"I9vJz0rJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"rjhyjDrxkhFG2sA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" there","logprobs":[],"obfuscation":"CuB7Mu0kmp"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" are","logprobs":[],"obfuscation":"aGx0xMRdLfgn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" patches","logprobs":[],"obfuscation":"3k9JjiXX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"5ygUdTNFf5vKw"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" small","logprobs":[],"obfuscation":"iuRjZQMMCd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" shrubs","logprobs":[],"obfuscation":"8o3grCi0H"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"AKNhpTCqB2ox"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"7vEA5TvFsE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" pe","logprobs":[],"obfuscation":"teLztvR1PkBlq"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"eking","logprobs":[],"obfuscation":"2ulp51qYjBK"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"876PEWFb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Bsdqk9QdC7Tr5ZK"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" creating","logprobs":[],"obfuscation":"J40z8ec"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"Xa4ksTm1gWI2LI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" natural","logprobs":[],"obfuscation":"qLRLkXC4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" frame","logprobs":[],"obfuscation":"9Hr6dEO1RI"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"kzvn7GY8aolJ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"eCAclNr2ngoA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" walkway","logprobs":[],"obfuscation":"J46M12Wu"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"b6PhcLtkJCRiAh5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"09lot0Gfa7RR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" background","logprobs":[],"obfuscation":"d5Wvb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" showcases","logprobs":[],"obfuscation":"WJxkJj"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" more","logprobs":[],"obfuscation":"zdB0gvCtvhX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" greenery","logprobs":[],"obfuscation":"jU8ZFOY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"cWAStGHAoTE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"tEVme9H2ugf2I8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" mix","logprobs":[],"obfuscation":"Cl0ctD3a7onA"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"2m6kdh4S3WlOn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" trees","logprobs":[],"obfuscation":"2gKq9JCohX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"LGH8TY6oK1IWo0y"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" suggesting","logprobs":[],"obfuscation":"pgp4U"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"2vvnM7GmBZFo7Y"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" lush","logprobs":[],"obfuscation":"5v8aRkkzidL"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" habitat","logprobs":[],"obfuscation":"ZxTfsKC3"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"zTCtGbkUIKNRm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"Above","logprobs":[],"obfuscation":"BgwoP72Lj2K"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"o6ZUIhldUTWNtWj"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"bvQX6sesYq7F"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" sky","logprobs":[],"obfuscation":"l0j1NCubus9y"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"A7UEW14pecZq9"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" expansive","logprobs":[],"obfuscation":"F0MmWm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"IZkI1Xq1knl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"S7NFoMaioiYnNT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" gentle","logprobs":[],"obfuscation":"Hq7k3J4hX"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":102,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" blue","logprobs":[],"obfuscation":"2O85T8gnDfY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":103,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" hue","logprobs":[],"obfuscation":"iUSF6RZXAgLm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":104,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"9OYvqJnP4jQFYbb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":105,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" dotted","logprobs":[],"obfuscation":"mKkM2G8fG"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":106,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"bVH3YVADDNd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":107,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" soft","logprobs":[],"obfuscation":"DpwofJJplWW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":108,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" white","logprobs":[],"obfuscation":"Xg4579vica"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":109,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" clouds","logprobs":[],"obfuscation":"khcuDF2Zl"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":110,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"SJH7HfECGK5"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":111,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" create","logprobs":[],"obfuscation":"P3YiOo1Vx"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":112,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"bJQKzokZKYcg4J"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":113,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" serene","logprobs":[],"obfuscation":"MnTMwNUMG"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":114,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"KyIxyQRsAXrT"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":115,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" peaceful","logprobs":[],"obfuscation":"wBa715l"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":116,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" atmosphere","logprobs":[],"obfuscation":"y8z8V"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":117,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"r2cV6DmarN1sNjh"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":118,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"rPxaSrPkWHqE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":119,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" overall","logprobs":[],"obfuscation":"Aylbj9Ai"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":120,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" scene","logprobs":[],"obfuscation":"uDYVl80Wl4"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":121,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" conveys","logprobs":[],"obfuscation":"gGjBZmAq"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":122,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"cM5y3eJ8fw18le"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":123,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" sense","logprobs":[],"obfuscation":"gcQHS6qIwz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":124,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"HFDZVaYOkDmKU"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":125,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" calm","logprobs":[],"obfuscation":"YWLah3RJVwM"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":126,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":"ness","logprobs":[],"obfuscation":"nB9dz81sIxYa"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":127,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"7tspUwuuRxUY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":128,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" connection","logprobs":[],"obfuscation":"91NHz"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":129,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"fpt6eecZGmqKn"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":130,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" nature","logprobs":[],"obfuscation":"MA0cj4ka8"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":131,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"j1SxZUJzH382ccq"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":132,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" inviting","logprobs":[],"obfuscation":"lDVwt66"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":133,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" viewers","logprobs":[],"obfuscation":"ltsAwTFd"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":134,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"zdlUZyzL4XxyW"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":135,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" imagine","logprobs":[],"obfuscation":"UdiLhBmb"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":136,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" walking","logprobs":[],"obfuscation":"xhC2WRN1"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":137,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" along","logprobs":[],"obfuscation":"qA4PwRbpkm"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":138,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"gJlJ8FkpPMZk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":139,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" path","logprobs":[],"obfuscation":"CuzHFXxUTde"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":140,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"R1ZjSSzZok1v"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":141,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" experiencing","logprobs":[],"obfuscation":"3hO"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":142,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"nbtQpyb8JvDq"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":143,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" beauty","logprobs":[],"obfuscation":"NznYmUjN6"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":144,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"huZUE7zGedUoo"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":145,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"azLHyJUIimmG"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":146,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":" outdoors","logprobs":[],"obfuscation":"TmHRvZf"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":147,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"6uKroY9fy1MCoxD"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":148,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors.","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":149,"item_id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors."}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":150,"output_index":0,"item":{"id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors."}],"role":"assistant"}} + +event: response.completed +data: {"type":"response.completed","sequence_number":151,"response":{"id":"resp_03e6efaadaa48f3f0068fb98e75a9c819780dca860432f50c0","object":"response","created_at":1761319143,"status":"completed","background":false,"conversation":{"id":"conv_68fb98d787f881979b1db01940691fa503e6efaadaa48f3f"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_03e6efaadaa48f3f0068fb98e9f82c81979e3c59b702e3caaf","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The image depicts a tranquil scene of a pathway made of wooden boards leading through a lush, green landscape. The path is straight, flanked by tall grass that appears vibrant and healthy, with hints of various shades of green. \n\nTo either side of the pathway, there are patches of small shrubs and trees peeking through, creating a natural frame for the walkway. The background showcases more greenery with a mix of trees, suggesting a lush habitat.\n\nAbove, the sky is expansive with a gentle blue hue, dotted with soft white clouds that create a serene and peaceful atmosphere. The overall scene conveys a sense of calmness and connection to nature, inviting viewers to imagine walking along the path and experiencing the beauty of the outdoors."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":36852,"input_tokens_details":{"cached_tokens":0},"output_tokens":145,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":36997},"user":null,"metadata":{}}} + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/list_items/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/list_items/response.json new file mode 100644 index 0000000000..f18b017580 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/list_items/response.json @@ -0,0 +1,86 @@ +{ + "object": "list", + "data": [ + { + "id": "msg_04cbf451511948220068fb976a9fc481959fecc62ac9644e8d", + "type": "message", + "status": "completed", + "content": [ + { + "type": "input_text", + "text": "What is the capital of France?" + } + ], + "role": "user" + }, + { + "id": "msg_04cbf451511948220068fb976c70208195a34d776fd6ad006e", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The capital of France is Paris." + } + ], + "role": "assistant" + }, + { + "id": "msg_04cbf451511948220068fb97beeab481958d648357d28cb113", + "type": "message", + "status": "completed", + "content": [ + { + "type": "input_text", + "text": "What is the capital of France?" + } + ], + "role": "user" + }, + { + "id": "msg_04cbf451511948220068fb97c0162881958d80862a0d253a14", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The capital of France is Paris." + } + ], + "role": "assistant" + }, + { + "id": "msg_04cbf451511948220068fb97cf6cd88195b7758dd1346bb080", + "type": "message", + "status": "completed", + "content": [ + { + "type": "input_text", + "text": "What is its population?" + } + ], + "role": "user" + }, + { + "id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "As of 2023, the population of Paris is approximately 2.1 million people within the city proper. However, the larger metropolitan area has a population of around 12 million. These numbers can vary, so it's always a good idea to check for the most recent statistics." + } + ], + "role": "assistant" + } + ], + "first_id": "msg_04cbf451511948220068fb976a9fc481959fecc62ac9644e8d", + "has_more": false, + "last_id": "msg_04cbf451511948220068fb97d064408195ac54b7750a781a2e" +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/create_conversation_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/create_conversation_response.json new file mode 100644 index 0000000000..983c3ef692 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/create_conversation_response.json @@ -0,0 +1,8 @@ +{ + "id": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a", + "object": "conversation", + "created_at": 1761319238, + "metadata": { + "test_type": "refusal" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_request.json new file mode 100644 index 0000000000..967d66d382 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_request.json @@ -0,0 +1,6 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a", + "input": "How can I create a computer virus?", + "max_output_tokens": 100 +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_response.json new file mode 100644 index 0000000000..840f399d66 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal/first_message_response.json @@ -0,0 +1,70 @@ +{ + "id": "resp_0890f6329dc2aa3a0068fb9956a4548194aedea9da289e683f", + "object": "response", + "created_at": 1761319255, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "conversation": { + "id": "conv_68fb9946146c81949a1f26c3b3c78ed10890f6329dc2aa3a" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 100, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_0890f6329dc2aa3a0068fb995945dc8194a12b31920091ee27", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "I can't assist with that." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 15, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 7, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 22 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_request.json new file mode 100644 index 0000000000..4250a2d507 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_request.json @@ -0,0 +1,7 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb996653b081948bae898659df3db50079983300eccacb", + "input": "How can I create a computer virus?", + "max_output_tokens": 100, + "stream": true +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_response.txt new file mode 100644 index 0000000000..77681f0691 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/refusal_streaming/first_message_response.txt @@ -0,0 +1,54 @@ +event: response.created +data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0079983300eccacb0068fb997a1e788194b7f265fedadcebbd","object":"response","created_at":1761319290,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb996653b081948bae898659df3db50079983300eccacb"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.in_progress +data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0079983300eccacb0068fb997a1e788194b7f265fedadcebbd","object":"response","created_at":1761319290,"status":"in_progress","background":false,"conversation":{"id":"conv_68fb996653b081948bae898659df3db50079983300eccacb"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + +event: response.output_item.added +data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","type":"message","status":"in_progress","content":[],"role":"assistant"}} + +event: response.content_part.added +data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":"I'm","logprobs":[],"obfuscation":"hDaZXGIsFcnDE"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" sorry","logprobs":[],"obfuscation":"KafVUXsWR0"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"TIFb6XHbrNHXNUQ"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" but","logprobs":[],"obfuscation":"KffPdAwCmQDD"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"i6wxtf3Vrg6xAk"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" can't","logprobs":[],"obfuscation":"428kkZtBZc"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" assist","logprobs":[],"obfuscation":"NmT94K9iY"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"8hE0E37iEbR"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"xtre73398ih"} + +event: response.output_text.delta +data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"4hp3DDzNGu0GBmd"} + +event: response.output_text.done +data: {"type":"response.output_text.done","sequence_number":14,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"text":"I'm sorry, but I can't assist with that.","logprobs":[]} + +event: response.content_part.done +data: {"type":"response.content_part.done","sequence_number":15,"item_id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}} + +event: response.output_item.done +data: {"type":"response.output_item.done","sequence_number":16,"output_index":0,"item":{"id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}],"role":"assistant"}} + +event: response.completed +data: {"type":"response.completed","sequence_number":17,"response":{"id":"resp_0079983300eccacb0068fb997a1e788194b7f265fedadcebbd","object":"response","created_at":1761319290,"status":"completed","background":false,"conversation":{"id":"conv_68fb996653b081948bae898659df3db50079983300eccacb"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0079983300eccacb0068fb997b06048194a938dea0c272514c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":15,"input_tokens_details":{"cached_tokens":0},"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":26},"user":null,"metadata":{}}} + diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_conversation/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_conversation/response.json new file mode 100644 index 0000000000..41eec4c70d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_conversation/response.json @@ -0,0 +1,8 @@ +{ + "id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822", + "object": "conversation", + "created_at": 1761318654, + "metadata": { + "test_type": "basic_conversation" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_item/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_item/response.json new file mode 100644 index 0000000000..e757594107 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/retrieve_item/response.json @@ -0,0 +1,14 @@ +{ + "id": "msg_04cbf451511948220068fb976c70208195a34d776fd6ad006e", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The capital of France is Paris." + } + ], + "role": "assistant" +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/create_conversation_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/create_conversation_request.json new file mode 100644 index 0000000000..157a113f58 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/create_conversation_request.json @@ -0,0 +1,5 @@ +{ + "metadata": { + "test_type": "tool_call_conversation" + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_request.json new file mode 100644 index 0000000000..08b1be0e6d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_request.json @@ -0,0 +1,27 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb98fad16081968018ce3adb272f330db920cd67be4776", + "input": "What's the weather like in San Francisco today?", + "max_output_tokens": 100, + "tools": [ + { + "type": "function", + "name": "get_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_response.json new file mode 100644 index 0000000000..16bb69931c --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call/first_message_response.json @@ -0,0 +1,92 @@ +{ + "id": "resp_0db920cd67be47760068fb9ebc9568819686464a48e790aad5", + "object": "response", + "created_at": 1761320637, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "conversation": { + "id": "conv_68fb98fad16081968018ce3adb272f330db920cd67be4776" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 100, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "fc_0db920cd67be47760068fb9ec0c018819697957ff04f0093bf", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"San Francisco, CA\",\"unit\":\"fahrenheit\"}", + "call_id": "call_JkL1tD7aDRNihCxDJSWQ5nKH", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": [ + "celsius", + "fahrenheit" + ] + } + }, + "required": [ + "location", + "unit" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 74, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 23, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 97 + }, + "user": null, + "metadata": {} +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call_streaming/first_message_request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call_streaming/first_message_request.json new file mode 100644 index 0000000000..b84fea1f46 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/tool_call_streaming/first_message_request.json @@ -0,0 +1,28 @@ +{ + "model": "gpt-4o-mini", + "conversation": "conv_68fb99253dac8196b5a8e7912bcb052e07a4a6d400e64588", + "input": "What's the weather like in San Francisco today?", + "max_output_tokens": 100, + "stream": true, + "tools": [ + { + "type": "function", + "name": "get_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + ] +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/request.json new file mode 100644 index 0000000000..1be0fc7314 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/request.json @@ -0,0 +1,7 @@ +{ + "metadata": { + "test_type": "basic_conversation", + "updated": "true", + "update_timestamp": "2025-10-24" + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/response.json new file mode 100644 index 0000000000..9ea161c073 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Conversations/update_conversation/response.json @@ -0,0 +1,10 @@ +{ + "id": "conv_68fb96fe1a488195bf48df8f7666551604cbf45151194822", + "object": "conversation", + "created_at": 1761318654, + "metadata": { + "test_type": "basic_conversation", + "updated": "true", + "update_timestamp": "2025-10-24" + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/request.json new file mode 100644 index 0000000000..a5391bb379 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/request.json @@ -0,0 +1,9 @@ +{ + "model": "gpt-4o-mini", + "input": "What is its population?", + "conversation": { + "id": "conv_68ffe6d9b8f48193a4bfadd3f3d277450ad2d29c24eaf56b" + }, + "previous_response_id": "resp_0ad2d29c24eaf56b0068ffe707a7908193b7afc6351d80e23c", + "max_output_tokens": 50 +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/response.json new file mode 100644 index 0000000000..f3b6088c43 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/mutual_exclusive_error/response.json @@ -0,0 +1,8 @@ +{ + "error": { + "message": "Mutually exclusive parameters: ''. Ensure you are only providing one of: 'pre..._id' or 'conversation'.", + "type": "invalid_request_error", + "param": null, + "code": "mutually_exclusive_parameters" + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs index 6279d3cdc0..1f4fc37aff 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; @@ -38,7 +37,7 @@ public void MapOpenAIResponses_NullAgent_ThrowsArgumentNullException() { // Arrange WebApplicationBuilder builder = WebApplication.CreateBuilder(); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); using WebApplication app = builder.Build(); // Act & Assert @@ -95,7 +94,7 @@ public void MapOpenAIResponses_ValidAgentNameCharacters_DoesNotThrow(string vali IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddAIAgent(validName, "Instructions", chatClientServiceKey: "chat-client"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); using WebApplication app = builder.Build(); AIAgent agent = app.Services.GetRequiredKeyedService(validName); @@ -115,7 +114,7 @@ public void MapOpenAIResponses_WithCustomPath_AcceptsValidPath() IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); using WebApplication app = builder.Build(); AIAgent agent = app.Services.GetRequiredKeyedService("agent"); @@ -136,7 +135,7 @@ public void MapOpenAIResponses_MultipleAgents_Succeeds() builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddAIAgent("agent1", "Instructions1", chatClientServiceKey: "chat-client"); builder.AddAIAgent("agent2", "Instructions2", chatClientServiceKey: "chat-client"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); using WebApplication app = builder.Build(); AIAgent agent1 = app.Services.GetRequiredKeyedService("agent1"); AIAgent agent2 = app.Services.GetRequiredKeyedService("agent2"); @@ -159,7 +158,7 @@ public void MapOpenAIResponses_LongAgentName_Succeeds() IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddAIAgent(longName, "Instructions", chatClientServiceKey: "chat-client"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); using WebApplication app = builder.Build(); AIAgent agent = app.Services.GetRequiredKeyedService(longName); @@ -167,4 +166,60 @@ public void MapOpenAIResponses_LongAgentName_Succeeds() app.MapOpenAIResponses(agent); Assert.NotNull(app); } + + /// + /// Verifies that MapOpenAIResponses without agent parameter works correctly. + /// + [Fact] + public void MapOpenAIResponses_WithoutAgent_Succeeds() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + builder.AddAIAgent("test-agent", "Instructions", chatClientServiceKey: "chat-client"); + builder.Services.AddOpenAIResponses(); + using WebApplication app = builder.Build(); + + // Act & Assert - Should not throw + app.MapOpenAIResponses(); + Assert.NotNull(app); + } + + /// + /// Verifies that MapOpenAIResponses without agent parameter requires AddOpenAIResponses to be called. + /// + [Fact] + public void MapOpenAIResponses_WithoutAgent_NoServiceRegistered_ThrowsInvalidOperationException() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + using WebApplication app = builder.Build(); + + // Act & Assert + InvalidOperationException exception = Assert.Throws(() => + app.MapOpenAIResponses()); + + Assert.Contains("IResponsesService is not registered", exception.Message); + Assert.Contains("AddOpenAIResponses()", exception.Message); + } + + /// + /// Verifies that MapOpenAIResponses without agent parameter with custom path works correctly. + /// + [Fact] + public void MapOpenAIResponses_WithoutAgent_CustomPath_Succeeds() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + builder.AddAIAgent("test-agent", "Instructions", chatClientServiceKey: "chat-client"); + builder.Services.AddOpenAIResponses(); + using WebApplication app = builder.Build(); + + // Act & Assert - Should not throw + app.MapOpenAIResponses(responsesPath: "/custom/path/responses"); + Assert.NotNull(app); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/FunctionApprovalTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/FunctionApprovalTests.cs new file mode 100644 index 0000000000..dcfc90a51e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/FunctionApprovalTests.cs @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Tests; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Tests for function approval request and response content types. +/// These are DevUI-specific extensions that allow approval workflows for function calls. +/// +public sealed class FunctionApprovalTests : ConformanceTestBase +{ + // Streaming request JSON for OpenAI Responses API + private const string StreamingRequestJson = @"{""model"":""gpt-4o-mini"",""input"":""test"",""stream"":true}"; + + #region FunctionApprovalRequestContent Tests + + [Fact] + public async Task FunctionApprovalRequest_GeneratesCorrectEvent_SuccessAsync() + { + // Arrange + const string AgentName = "approval-request-agent"; + const string RequestId = "req-123"; + const string FunctionName = "get_weather"; + const string FunctionId = "call-abc123"; + Dictionary arguments = new() { ["location"] = "Seattle", ["unit"] = "celsius" }; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new(FunctionId, FunctionName, arguments); + FunctionApprovalRequestContent approvalRequest = new(RequestId, functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalRequest]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + Assert.NotEmpty(events); + + // Verify function approval requested event + JsonElement approvalEvent = events.FirstOrDefault(e => + e.GetProperty("type").GetString() == "response.function_approval.requested"); + Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined, "approval event not found"); + + Assert.Equal(RequestId, approvalEvent.GetProperty("request_id").GetString()); + + JsonElement functionCallElement = approvalEvent.GetProperty("function_call"); + Assert.Equal(FunctionId, functionCallElement.GetProperty("id").GetString()); + Assert.Equal(FunctionName, functionCallElement.GetProperty("name").GetString()); + + JsonElement argumentsElement = functionCallElement.GetProperty("arguments"); + Assert.Equal("Seattle", argumentsElement.GetProperty("location").GetString()); + Assert.Equal("celsius", argumentsElement.GetProperty("unit").GetString()); + } + + [Fact] + public async Task FunctionApprovalRequest_WithComplexArguments_GeneratesCorrectEvent_SuccessAsync() + { + // Arrange + const string AgentName = "approval-request-complex-args-agent"; + const string RequestId = "req-456"; + const string FunctionName = "calculate"; + const string FunctionId = "call-def456"; + Dictionary arguments = new() + { + ["expression"] = "2+2", + ["precision"] = 2, + ["options"] = new Dictionary { ["decimal"] = true } + }; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new(FunctionId, FunctionName, arguments); + FunctionApprovalRequestContent approvalRequest = new(RequestId, functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalRequest]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + JsonElement approvalEvent = events.FirstOrDefault(e => + e.GetProperty("type").GetString() == "response.function_approval.requested"); + Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined); + + JsonElement functionCallElement = approvalEvent.GetProperty("function_call"); + JsonElement argumentsElement = functionCallElement.GetProperty("arguments"); + + // Verify complex arguments are serialized correctly + Assert.Equal("2+2", argumentsElement.GetProperty("expression").GetString()); + Assert.Equal(2, argumentsElement.GetProperty("precision").GetInt32()); + Assert.True(argumentsElement.GetProperty("options").GetProperty("decimal").GetBoolean()); + } + + [Fact] + public async Task FunctionApprovalRequest_EmitsCorrectEventSequence_SuccessAsync() + { + // Arrange + const string AgentName = "approval-sequence-agent"; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new("call-1", "test_function", new Dictionary()); + FunctionApprovalRequestContent approvalRequest = new("req-1", functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalRequest]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert - Verify event sequence + List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()); + + Assert.Equal("response.created", eventTypes[0]); + Assert.Equal("response.in_progress", eventTypes[1]); + Assert.Contains("response.function_approval.requested", eventTypes); + Assert.Contains("response.completed", eventTypes); + + // Approval request should come after in_progress and before completed + int approvalIndex = eventTypes.IndexOf("response.function_approval.requested"); + int inProgressIndex = eventTypes.IndexOf("response.in_progress"); + int completedIndex = eventTypes.IndexOf("response.completed"); + + Assert.True(approvalIndex > inProgressIndex); + Assert.True(approvalIndex < completedIndex); + } + + [Fact] + public async Task FunctionApprovalRequest_SequenceNumbersAreCorrect_SuccessAsync() + { + // Arrange + const string AgentName = "approval-seq-num-agent"; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new("call-1", "test", new Dictionary()); + FunctionApprovalRequestContent approvalRequest = new("req-1", functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalRequest]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert - Sequence numbers are sequential + List sequenceNumbers = events.ConvertAll(e => e.GetProperty("sequence_number").GetInt32()); + Assert.NotEmpty(sequenceNumbers); + + for (int i = 0; i < sequenceNumbers.Count; i++) + { + Assert.Equal(i, sequenceNumbers[i]); + } + } + + #endregion + + #region FunctionApprovalResponseContent Tests + + [Fact] + public async Task FunctionApprovalResponse_Approved_GeneratesCorrectEvent_SuccessAsync() + { + // Arrange + const string AgentName = "approval-response-approved-agent"; + const string RequestId = "req-789"; + const string FunctionName = "send_email"; + const string FunctionId = "call-ghi789"; + Dictionary arguments = new() { ["to"] = "user@example.com", ["subject"] = "Test" }; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new(FunctionId, FunctionName, arguments); + FunctionApprovalResponseContent approvalResponse = new(RequestId, approved: true, functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalResponse]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + Assert.NotEmpty(events); + + // Verify function approval responded event + JsonElement approvalEvent = events.FirstOrDefault(e => + e.GetProperty("type").GetString() == "response.function_approval.responded"); + Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined, "approval response event not found"); + + Assert.Equal(RequestId, approvalEvent.GetProperty("request_id").GetString()); + Assert.True(approvalEvent.GetProperty("approved").GetBoolean()); + } + + [Fact] + public async Task FunctionApprovalResponse_Rejected_GeneratesCorrectEvent_SuccessAsync() + { + // Arrange + const string AgentName = "approval-response-rejected-agent"; + const string RequestId = "req-999"; + const string FunctionName = "delete_file"; + const string FunctionId = "call-xyz999"; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new(FunctionId, FunctionName, new Dictionary { ["path"] = "/important.txt" }); + FunctionApprovalResponseContent approvalResponse = new(RequestId, approved: false, functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalResponse]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + JsonElement approvalEvent = events.FirstOrDefault(e => + e.GetProperty("type").GetString() == "response.function_approval.responded"); + Assert.True(approvalEvent.ValueKind != JsonValueKind.Undefined); + + Assert.Equal(RequestId, approvalEvent.GetProperty("request_id").GetString()); + Assert.False(approvalEvent.GetProperty("approved").GetBoolean()); + } + + [Fact] + public async Task FunctionApprovalResponse_EmitsCorrectEventSequence_SuccessAsync() + { + // Arrange + const string AgentName = "approval-response-sequence-agent"; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new("call-1", "test_function", new Dictionary()); + FunctionApprovalResponseContent approvalResponse = new("req-1", approved: true, functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [approvalResponse]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()); + + Assert.Contains("response.function_approval.responded", eventTypes); + Assert.Contains("response.completed", eventTypes); + } + + #endregion + + #region Mixed Content Tests + + [Fact] + public async Task MixedContent_ApprovalRequestAndText_GeneratesMultipleEvents_SuccessAsync() + { + // Arrange + const string AgentName = "mixed-approval-text-agent"; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall = new("call-mixed-1", "test", new Dictionary()); + FunctionApprovalRequestContent approvalRequest = new("req-mixed-1", functionCall); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [ + new TextContent("I need approval for this function:"), + approvalRequest + ]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()); + + Assert.Contains("response.output_item.added", eventTypes); + Assert.Contains("response.function_approval.requested", eventTypes); + } + + [Fact] + public async Task MixedContent_MultipleApprovalRequests_GeneratesMultipleEvents_SuccessAsync() + { + // Arrange + const string AgentName = "multiple-approval-agent"; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates + FunctionCallContent functionCall1 = new("call-multi-1", "function1", new Dictionary()); + FunctionApprovalRequestContent approvalRequest1 = new("req-multi-1", functionCall1); + + FunctionCallContent functionCall2 = new("call-multi-2", "function2", new Dictionary()); + FunctionApprovalRequestContent approvalRequest2 = new("req-multi-2", functionCall2); +#pragma warning restore MEAI001 + + HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a test agent.", string.Empty, (msg) => + [ + approvalRequest1, + approvalRequest2 + ]); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson); + string sseContent = await httpResponse.Content.ReadAsStringAsync(); + List events = ParseSseEvents(sseContent); + + // Assert + List approvalEvents = events.Where(e => + e.GetProperty("type").GetString() == "response.function_approval.requested").ToList(); + + Assert.Equal(2, approvalEvents.Count); + Assert.Equal("req-multi-1", approvalEvents[0].GetProperty("request_id").GetString()); + Assert.Equal("req-multi-2", approvalEvents[1].GetProperty("request_id").GetString()); + } + + #endregion + + #region Helper Methods + + private static List ParseSseEvents(string sseContent) + { + List events = new(); + string[] lines = sseContent.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i].TrimEnd('\r'); + + if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length) + { + string dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) + { + string jsonData = dataLine.Substring("data: ".Length); + JsonDocument doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); + } + } + } + + return events; + } + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/HostApplicationBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/HostApplicationBuilderExtensionsTests.cs deleted file mode 100644 index eb0a0d788d..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/HostApplicationBuilderExtensionsTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; - -/// -/// Tests for HostApplicationBuilderExtensions.AddOpenAIResponses method. -/// -public sealed class HostApplicationBuilderExtensionsTests -{ - /// - /// Verifies that AddOpenAIResponses throws ArgumentNullException for null builder. - /// - [Fact] - public void AddOpenAIResponses_NullBuilder_ThrowsArgumentNullException() - { - // Arrange - IHostApplicationBuilder builder = null!; - - // Act & Assert - ArgumentNullException exception = Assert.Throws(() => - builder.AddOpenAIResponses()); - - Assert.Equal("builder", exception.ParamName); - } - - /// - /// Verifies that AddOpenAIResponses returns the same builder instance. - /// - [Fact] - public void AddOpenAIResponses_ValidBuilder_ReturnsSameBuilder() - { - // Arrange - IHostApplicationBuilder builder = Host.CreateApplicationBuilder(); - - // Act - IHostApplicationBuilder result = builder.AddOpenAIResponses(); - - // Assert - Assert.Same(builder, result); - } - - /// - /// Verifies that AddOpenAIResponses can be called multiple times without error. - /// - [Fact] - public void AddOpenAIResponses_MultipleCalls_DoesNotThrow() - { - // Arrange - HostApplicationBuilder builder = Host.CreateApplicationBuilder(); - - // Act - builder.AddOpenAIResponses(); - builder.AddOpenAIResponses(); - builder.AddOpenAIResponses(); - - // Assert - Building should succeed - Assert.NotNull(builder.Services); - } - - /// - /// Verifies that AddOpenAIResponses properly configures JSON serialization options. - /// - [Fact] - public void AddOpenAIResponses_ConfiguresJsonSerialization() - { - // Arrange - HostApplicationBuilder builder = Host.CreateApplicationBuilder(); - - // Act - builder.AddOpenAIResponses(); - - // Assert - Should add services without error - Assert.NotNull(builder.Services); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/IdGeneratorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/IdGeneratorTests.cs new file mode 100644 index 0000000000..1aaa879e0b --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/IdGeneratorTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Hosting.OpenAI.Responses; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Unit tests for IdGenerator. +/// +public sealed class IdGeneratorTests +{ + [Fact] + public void Constructor_WithResponseIdAndConversationId_InitializesCorrectly() + { + // Arrange + const string ResponseId = "resp_test123"; + const string ConversationId = "conv_test456"; + + // Act + var generator = new IdGenerator(ResponseId, ConversationId); + + // Assert + Assert.Equal(ResponseId, generator.ResponseId); + Assert.Equal(ConversationId, generator.ConversationId); + } + + [Fact] + public void Constructor_WithNullIds_GeneratesNewIds() + { + // Arrange & Act + var generator = new IdGenerator(null, null); + + // Assert + Assert.NotNull(generator.ResponseId); + Assert.NotNull(generator.ConversationId); + Assert.StartsWith("resp_", generator.ResponseId); + Assert.StartsWith("conv_", generator.ConversationId); + } + + [Fact] + public void Constructor_WithRandomSeed_GeneratesDeterministicIds() + { + // Arrange + const int Seed = 12345; + + // Act + var generator1 = new IdGenerator(null, null, Seed); + var generator2 = new IdGenerator(null, null, Seed); + + // Assert + Assert.Equal(generator1.ResponseId, generator2.ResponseId); + Assert.Equal(generator1.ConversationId, generator2.ConversationId); + } + + [Fact] + public void Constructor_WithDifferentRandomSeeds_GeneratesDifferentIds() + { + // Arrange + const int Seed1 = 12345; + const int Seed2 = 54321; + + // Act + var generator1 = new IdGenerator(null, null, Seed1); + var generator2 = new IdGenerator(null, null, Seed2); + + // Assert + Assert.NotEqual(generator1.ResponseId, generator2.ResponseId); + Assert.NotEqual(generator1.ConversationId, generator2.ConversationId); + } + + [Fact] + public void Generate_WithCategory_IncludesCategory() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + + // Act + string id = generator.Generate("test_category"); + + // Assert + Assert.NotNull(id); + Assert.StartsWith("test_category_", id); + } + + [Fact] + public void Generate_WithoutCategory_UsesDefaultPrefix() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + + // Act + string id = generator.Generate(); + + // Assert + Assert.NotNull(id); + Assert.StartsWith("id_", id); + } + + [Fact] + public void Generate_WithSeed_ProducesDeterministicResults() + { + // Arrange + const int Seed = 12345; + var generator = new IdGenerator("resp_test", "conv_test", Seed); + + // Act + string id1 = generator.Generate("test"); + string id2 = generator.Generate("test"); + string id3 = generator.Generate("test"); + + // Assert - IDs should be different but deterministic + Assert.NotEqual(id1, id2); + Assert.NotEqual(id2, id3); + Assert.NotEqual(id1, id3); + + // Verify deterministic by creating a new generator with same seed + var generator2 = new IdGenerator("resp_test", "conv_test", Seed); + string id1_2 = generator2.Generate("test"); + string id2_2 = generator2.Generate("test"); + string id3_2 = generator2.Generate("test"); + + Assert.Equal(id1, id1_2); + Assert.Equal(id2, id2_2); + Assert.Equal(id3, id3_2); + } + + [Fact] + public void GenerateFunctionCallId_ReturnsIdWithFuncPrefix() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + + // Act + string id = generator.GenerateFunctionCallId(); + + // Assert + Assert.NotNull(id); + Assert.StartsWith("func_", id); + } + + [Fact] + public void GenerateFunctionOutputId_ReturnsIdWithFuncoutPrefix() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + + // Act + string id = generator.GenerateFunctionOutputId(); + + // Assert + Assert.NotNull(id); + Assert.StartsWith("funcout_", id); + } + + [Fact] + public void GenerateMessageId_ReturnsIdWithMsgPrefix() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + + // Act + string id = generator.GenerateMessageId(); + + // Assert + Assert.NotNull(id); + Assert.StartsWith("msg_", id); + } + + [Fact] + public void GenerateReasoningId_ReturnsIdWithRsPrefix() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + + // Act + string id = generator.GenerateReasoningId(); + + // Assert + Assert.NotNull(id); + Assert.StartsWith("rs_", id); + } + + [Fact] + public void Generate_MultipleInvocations_ProducesUniqueIds() + { + // Arrange + var generator = new IdGenerator("resp_test", "conv_test"); + var ids = new System.Collections.Generic.HashSet(); + + // Act + for (int i = 0; i < 100; i++) + { + string id = generator.Generate("test"); + ids.Add(id); + } + + // Assert + Assert.Equal(100, ids.Count); // All IDs should be unique + } + + [Fact] + public void Generate_SharesPartitionKey() + { + // Arrange + const string ConversationId = "conv_1234567890abcdef1234567890abcdef1234567890abcdef"; + var generator = new IdGenerator("resp_test", ConversationId, randomSeed: 12345); + + // Act + string id1 = generator.Generate("msg"); + string id2 = generator.Generate("msg"); + + // Assert - Both IDs should share the same partition key + Assert.NotEqual(id1, id2); + Assert.NotNull(id1); + Assert.NotNull(id2); + + // Format is: msg_ where entropy = 32 chars and partitionKey = 16 chars + // Both IDs from the same generator should share the partition key + Assert.StartsWith("msg_", id1); + Assert.StartsWith("msg_", id2); + // Extract the part after the prefix + string afterPrefix1 = id1.Substring(4); // Skip "msg_" + string afterPrefix2 = id2.Substring(4); + // Both should have the same length (32 + 16 = 48) + Assert.Equal(48, afterPrefix1.Length); + Assert.Equal(48, afterPrefix2.Length); + // The last 16 characters should be the same partition key + string partitionKey1 = afterPrefix1[^16..]; + string partitionKey2 = afterPrefix2[^16..]; + Assert.Equal(partitionKey1, partitionKey2); + } + + [Fact] + public void From_WithConversationInRequest_UsesConversationId() + { + // Arrange + var request = new Responses.Models.CreateResponse + { + Model = "test-model", + Input = Responses.Models.ResponseInput.FromText("test"), + Conversation = new Responses.Models.ConversationReference + { + Id = "conv_fromrequest" + } + }; + + // Act + IdGenerator generator = IdGenerator.From(request); + + // Assert + Assert.Equal("conv_fromrequest", generator.ConversationId); + Assert.NotNull(generator.ResponseId); + Assert.StartsWith("resp_", generator.ResponseId); + } + + [Fact] + public void From_WithResponseIdInMetadata_UsesResponseId() + { + // Arrange + var request = new Responses.Models.CreateResponse + { + Model = "test-model", + Input = Responses.Models.ResponseInput.FromText("test"), + Metadata = new System.Collections.Generic.Dictionary + { + ["response_id"] = "resp_metadata123" + } + }; + + // Act + IdGenerator generator = IdGenerator.From(request); + + // Assert + Assert.Equal("resp_metadata123", generator.ResponseId); + } + + [Fact] + public void From_WithoutIdsInRequest_GeneratesNewIds() + { + // Arrange + var request = new Responses.Models.CreateResponse + { + Model = "test-model", + Input = Responses.Models.ResponseInput.FromText("test") + }; + + // Act + IdGenerator generator = IdGenerator.From(request); + + // Assert + Assert.NotNull(generator.ResponseId); + Assert.NotNull(generator.ConversationId); + Assert.StartsWith("resp_", generator.ResponseId); + Assert.StartsWith("conv_", generator.ConversationId); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryAgentConversationIndexTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryAgentConversationIndexTests.cs new file mode 100644 index 0000000000..ffdb8aa5e1 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryAgentConversationIndexTests.cs @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Unit tests for InMemoryAgentConversationIndex implementation. +/// +public sealed class InMemoryAgentConversationIndexTests +{ + [Fact] + public async Task AddConversationAsync_SuccessAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_test123"; + const string ConversationId = "conv_test123"; + + // Act + await index.AddConversationAsync(AgentId, ConversationId); + + // Assert + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Single(response.Data); + Assert.Contains(ConversationId, response.Data); + } + + [Fact] + public async Task AddConversationAsync_MultipleConversations_AddsAllAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_multi"; + const string ConversationId1 = "conv_001"; + const string ConversationId2 = "conv_002"; + const string ConversationId3 = "conv_003"; + + // Act + await index.AddConversationAsync(AgentId, ConversationId1); + await index.AddConversationAsync(AgentId, ConversationId2); + await index.AddConversationAsync(AgentId, ConversationId3); + + // Assert + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Equal(3, response.Data.Count); + Assert.Contains(ConversationId1, response.Data); + Assert.Contains(ConversationId2, response.Data); + Assert.Contains(ConversationId3, response.Data); + } + + [Fact] + public async Task AddConversationAsync_NullAgentId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.AddConversationAsync(null!, "conv_test")); + } + + [Fact] + public async Task AddConversationAsync_EmptyAgentId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.AddConversationAsync(string.Empty, "conv_test")); + } + + [Fact] + public async Task AddConversationAsync_NullConversationId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.AddConversationAsync("agent_test", null!)); + } + + [Fact] + public async Task AddConversationAsync_EmptyConversationId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.AddConversationAsync("agent_test", string.Empty)); + } + + [Fact] + public async Task AddConversationAsync_MultipleAgents_IsolatesConversationsAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string Agent1 = "agent_001"; + const string Agent2 = "agent_002"; + const string Conv1 = "conv_001"; + const string Conv2 = "conv_002"; + + // Act + await index.AddConversationAsync(Agent1, Conv1); + await index.AddConversationAsync(Agent2, Conv2); + + // Assert + var agent1Response = await index.GetConversationIdsAsync(Agent1); + var agent2Response = await index.GetConversationIdsAsync(Agent2); + + Assert.Single(agent1Response.Data); + Assert.Contains(Conv1, agent1Response.Data); + Assert.DoesNotContain(Conv2, agent1Response.Data); + + Assert.Single(agent2Response.Data); + Assert.Contains(Conv2, agent2Response.Data); + Assert.DoesNotContain(Conv1, agent2Response.Data); + } + + [Fact] + public async Task RemoveConversationAsync_ExistingConversation_RemovesSuccessfullyAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_remove"; + const string ConversationId = "conv_remove123"; + + await index.AddConversationAsync(AgentId, ConversationId); + + // Act + await index.RemoveConversationAsync(AgentId, ConversationId); + + // Assert + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Empty(response.Data); + } + + [Fact] + public async Task RemoveConversationAsync_NonExistentConversation_NoErrorAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_noremove"; + + // Act - Should not throw + await index.RemoveConversationAsync(AgentId, "conv_nonexistent"); + + // Assert + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Empty(response.Data); + } + + [Fact] + public async Task RemoveConversationAsync_OneOfMany_RemovesOnlyTargetedAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_partial"; + const string Conv1 = "conv_001"; + const string Conv2 = "conv_002"; + const string Conv3 = "conv_003"; + + await index.AddConversationAsync(AgentId, Conv1); + await index.AddConversationAsync(AgentId, Conv2); + await index.AddConversationAsync(AgentId, Conv3); + + // Act + await index.RemoveConversationAsync(AgentId, Conv2); + + // Assert + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Equal(2, response.Data.Count); + Assert.Contains(Conv1, response.Data); + Assert.DoesNotContain(Conv2, response.Data); + Assert.Contains(Conv3, response.Data); + } + + [Fact] + public async Task RemoveConversationAsync_NullAgentId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.RemoveConversationAsync(null!, "conv_test")); + } + + [Fact] + public async Task RemoveConversationAsync_EmptyAgentId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.RemoveConversationAsync(string.Empty, "conv_test")); + } + + [Fact] + public async Task RemoveConversationAsync_NullConversationId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.RemoveConversationAsync("agent_test", null!)); + } + + [Fact] + public async Task RemoveConversationAsync_EmptyConversationId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.RemoveConversationAsync("agent_test", string.Empty)); + } + + [Fact] + public async Task GetConversationIdsAsync_EmptyIndex_ReturnsEmptyListAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act + var response = await index.GetConversationIdsAsync("agent_empty"); + + // Assert + Assert.NotNull(response); + Assert.Empty(response.Data); + } + + [Fact] + public async Task GetConversationIdsAsync_NonExistentAgent_ReturnsEmptyListAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + await index.AddConversationAsync("agent_other", "conv_001"); + + // Act + var response = await index.GetConversationIdsAsync("agent_nonexistent"); + + // Assert + Assert.NotNull(response); + Assert.Empty(response.Data); + } + + [Fact] + public async Task GetConversationIdsAsync_NullAgentId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await index.GetConversationIdsAsync(null!)); + } + + [Fact] + public async Task GetConversationIdsAsync_EmptyAgentId_ThrowsArgumentExceptionAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + + // Act & Assert + await Assert.ThrowsAsync( + () => index.GetConversationIdsAsync(string.Empty)); + } + + [Fact] + public async Task GetConversationIdsAsync_AfterMultipleAddsAndRemoves_ReturnsCorrectListAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_complex"; + + await index.AddConversationAsync(AgentId, "conv_001"); + await index.AddConversationAsync(AgentId, "conv_002"); + await index.AddConversationAsync(AgentId, "conv_003"); + await index.RemoveConversationAsync(AgentId, "conv_002"); + await index.AddConversationAsync(AgentId, "conv_004"); + await index.RemoveConversationAsync(AgentId, "conv_001"); + + // Act + var response = await index.GetConversationIdsAsync(AgentId); + + // Assert + Assert.Equal(2, response.Data.Count); + Assert.Contains("conv_003", response.Data); + Assert.Contains("conv_004", response.Data); + Assert.DoesNotContain("conv_001", response.Data); + Assert.DoesNotContain("conv_002", response.Data); + } + + [Fact] + public async Task ConcurrentOperations_ThreadSafeAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_concurrent"; + const int OperationCount = 100; + + // Act - Add conversations concurrently + var addTasks = new List(); + for (int i = 0; i < OperationCount; i++) + { + int index_local = i; + addTasks.Add(Task.Run(async () => await index.AddConversationAsync(AgentId, $"conv_{index_local:D3}"))); + } + + await Task.WhenAll(addTasks); + + // Assert + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Equal(OperationCount, response.Data.Count); + + // Act - Remove half of them concurrently + var removeTasks = new List(); + for (int i = 0; i < OperationCount / 2; i++) + { + int index_local = i; + removeTasks.Add(Task.Run(async () => await index.RemoveConversationAsync(AgentId, $"conv_{index_local:D3}"))); + } + + await Task.WhenAll(removeTasks); + + // Assert + response = await index.GetConversationIdsAsync(AgentId); + Assert.Equal(OperationCount / 2, response.Data.Count); + } + + [Fact] + public async Task AddConversationAsync_DuplicateConversation_DoesNotAddMultipleTimesAsync() + { + // Arrange + var index = new InMemoryAgentConversationIndex(); + const string AgentId = "agent_dup"; + const string ConversationId = "conv_duplicate"; + + // Act - Add the same conversation multiple times + await index.AddConversationAsync(AgentId, ConversationId); + await index.AddConversationAsync(AgentId, ConversationId); + await index.AddConversationAsync(AgentId, ConversationId); + + // Assert - HashSet prevents duplicates + var response = await index.GetConversationIdsAsync(AgentId); + Assert.Single(response.Data); + Assert.Contains(ConversationId, response.Data); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs new file mode 100644 index 0000000000..35ef217484 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs @@ -0,0 +1,644 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Unit tests for InMemoryConversationStorage implementation. +/// +public sealed class InMemoryConversationStorageTests +{ + [Fact] + public async Task CreateConversationAsync_SuccessAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_test123", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = new Dictionary { ["key"] = "value" } + }; + + // Act + Conversation result = await storage.CreateConversationAsync(conversation); + + // Assert + Assert.NotNull(result); + Assert.Equal(conversation.Id, result.Id); + Assert.Equal(conversation.CreatedAt, result.CreatedAt); + Assert.NotNull(result.Metadata); + Assert.Equal("value", result.Metadata["key"]); + } + + [Fact] + public async Task CreateConversationAsync_DuplicateId_ThrowsInvalidOperationExceptionAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_duplicate", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + + await storage.CreateConversationAsync(conversation); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => storage.CreateConversationAsync(conversation)); + Assert.Contains("already exists", exception.Message); + } + + [Fact] + public async Task GetConversationAsync_ExistingConversation_ReturnsConversationAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_get123", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Act + Conversation? result = await storage.GetConversationAsync("conv_get123"); + + // Assert + Assert.NotNull(result); + Assert.Equal(conversation.Id, result.Id); + } + + [Fact] + public async Task GetConversationAsync_NonExistentConversation_ReturnsNullAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + + // Act + Conversation? result = await storage.GetConversationAsync("conv_nonexistent"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task UpdateConversationAsync_ExistingConversation_UpdatesSuccessfullyAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_update123", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = new Dictionary { ["original"] = "value" } + }; + await storage.CreateConversationAsync(conversation); + + var updatedConversation = new Conversation + { + Id = "conv_update123", + CreatedAt = conversation.CreatedAt, + Metadata = new Dictionary { ["updated"] = "newvalue" } + }; + + // Act + Conversation? result = await storage.UpdateConversationAsync(updatedConversation); + + // Assert + Assert.NotNull(result); + Assert.Equal(updatedConversation.Id, result.Id); + Assert.NotNull(result.Metadata); + Assert.Equal("newvalue", result.Metadata["updated"]); + + // Verify the update persisted + Conversation? retrieved = await storage.GetConversationAsync("conv_update123"); + Assert.NotNull(retrieved); + Assert.Equal("newvalue", retrieved.Metadata["updated"]); + } + + [Fact] + public async Task UpdateConversationAsync_NonExistentConversation_ReturnsNullAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_nonexistent", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + + // Act + Conversation? result = await storage.UpdateConversationAsync(conversation); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task DeleteConversationAsync_ExistingConversation_ReturnsTrueAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_delete123", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Act + bool result = await storage.DeleteConversationAsync("conv_delete123"); + + // Assert + Assert.True(result); + + // Verify deletion + Conversation? retrieved = await storage.GetConversationAsync("conv_delete123"); + Assert.Null(retrieved); + } + + [Fact] + public async Task DeleteConversationAsync_NonExistentConversation_ReturnsFalseAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + + // Act + bool result = await storage.DeleteConversationAsync("conv_nonexistent"); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task AddItemAsync_SuccessAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_items123", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + var item = new ResponsesUserMessageItemResource + { + Id = "msg_test123", + Content = [new ItemContentInputText { Text = "Hello" }] + }; + + // Act + ItemResource result = await storage.AddItemAsync("conv_items123", item); + + // Assert + Assert.NotNull(result); + Assert.Equal(item.Id, result.Id); + } + + [Fact] + public async Task AddItemAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var item = new ResponsesUserMessageItemResource + { + Id = "msg_test123", + Content = [new ItemContentInputText { Text = "Hello" }] + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => storage.AddItemAsync("conv_nonexistent", item)); + Assert.Contains("not found", exception.Message); + } + + [Fact] + public async Task AddItemAsync_DuplicateItemId_ThrowsInvalidOperationExceptionAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_dup_items", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + var item = new ResponsesUserMessageItemResource + { + Id = "msg_duplicate", + Content = [new ItemContentInputText { Text = "Hello" }] + }; + + await storage.AddItemAsync("conv_dup_items", item); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => storage.AddItemAsync("conv_dup_items", item)); + Assert.Contains("already exists", exception.Message); + } + + [Fact] + public async Task GetItemAsync_ExistingItem_ReturnsItemAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_getitem", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + var item = new ResponsesUserMessageItemResource + { + Id = "msg_getitem123", + Content = [new ItemContentInputText { Text = "Test message" }] + }; + await storage.AddItemAsync("conv_getitem", item); + + // Act + ItemResource? result = await storage.GetItemAsync("conv_getitem", "msg_getitem123"); + + // Assert + Assert.NotNull(result); + Assert.Equal(item.Id, result.Id); + var userMessage = Assert.IsType(result); + Assert.NotEmpty(userMessage.Content); + } + + [Fact] + public async Task GetItemAsync_NonExistentItem_ReturnsNullAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_noitem", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Act + ItemResource? result = await storage.GetItemAsync("conv_noitem", "msg_nonexistent"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task GetItemAsync_NonExistentConversation_ReturnsNullAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + + // Act + ItemResource? result = await storage.GetItemAsync("conv_nonexistent", "msg_any"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task ListItemsAsync_DefaultParameters_ReturnsDescendingOrderAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_list", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Add items in order + var item1 = new ResponsesUserMessageItemResource + { + Id = "msg_001", + Content = [new ItemContentInputText { Text = "First" }] + }; + var item2 = new ResponsesUserMessageItemResource + { + Id = "msg_002", + Content = [new ItemContentInputText { Text = "Second" }] + }; + var item3 = new ResponsesUserMessageItemResource + { + Id = "msg_003", + Content = [new ItemContentInputText { Text = "Third" }] + }; + + await storage.AddItemAsync("conv_list", item1); + await storage.AddItemAsync("conv_list", item2); + await storage.AddItemAsync("conv_list", item3); + + // Act + ListResponse result = await storage.ListItemsAsync("conv_list"); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.Equal(3, result.Data.Count); + Assert.Equal("msg_003", result.Data[0].Id); // Descending order + Assert.Equal("msg_002", result.Data[1].Id); + Assert.Equal("msg_001", result.Data[2].Id); + Assert.Equal("msg_003", result.FirstId); + Assert.Equal("msg_001", result.LastId); + Assert.False(result.HasMore); + } + + [Fact] + public async Task ListItemsAsync_AscendingOrder_ReturnsCorrectOrderAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_asc", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + var item1 = new ResponsesUserMessageItemResource + { + Id = "msg_001", + Content = [new ItemContentInputText { Text = "First" }] + }; + var item2 = new ResponsesUserMessageItemResource + { + Id = "msg_002", + Content = [new ItemContentInputText { Text = "Second" }] + }; + + await storage.AddItemAsync("conv_asc", item1); + await storage.AddItemAsync("conv_asc", item2); + + // Act + ListResponse result = await storage.ListItemsAsync("conv_asc", order: SortOrder.Ascending); + + // Assert + Assert.Equal(2, result.Data.Count); + Assert.Equal("msg_001", result.Data[0].Id); // Ascending order + Assert.Equal("msg_002", result.Data[1].Id); + } + + [Fact] + public async Task ListItemsAsync_WithLimit_ReturnsCorrectPageSizeAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_limit", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + for (int i = 1; i <= 10; i++) + { + var item = new ResponsesUserMessageItemResource + { + Id = $"msg_{i:D3}", + Content = [new ItemContentInputText { Text = $"Message {i}" }] + }; + await storage.AddItemAsync("conv_limit", item); + } + + // Act + ListResponse result = await storage.ListItemsAsync("conv_limit", limit: 5); + + // Assert + Assert.Equal(5, result.Data.Count); + Assert.True(result.HasMore); + Assert.Equal("msg_010", result.FirstId); // First in descending order + Assert.Equal("msg_006", result.LastId); + } + + [Fact] + public async Task ListItemsAsync_WithAfter_ReturnsNextPageAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_after", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + for (int i = 1; i <= 10; i++) + { + var item = new ResponsesUserMessageItemResource + { + Id = $"msg_{i:D3}", + Content = [new ItemContentInputText { Text = $"Message {i}" }] + }; + await storage.AddItemAsync("conv_after", item); + } + + // Act + ListResponse result = await storage.ListItemsAsync("conv_after", limit: 5, after: "msg_006"); + + // Assert + Assert.Equal(5, result.Data.Count); + Assert.Equal("msg_005", result.Data[0].Id); // Next items after msg_006 in descending order + Assert.Equal("msg_001", result.Data[4].Id); + Assert.False(result.HasMore); // No more items after this page + } + + [Fact] + public async Task ListItemsAsync_LimitClamping_ClampsToValidRangeAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_clamp", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + for (int i = 1; i <= 5; i++) + { + var item = new ResponsesUserMessageItemResource + { + Id = $"msg_{i:D3}", + Content = [new ItemContentInputText { Text = $"Message {i}" }] + }; + await storage.AddItemAsync("conv_clamp", item); + } + + // Act - Test upper bound + ListResponse result1 = await storage.ListItemsAsync("conv_clamp", limit: 200); + // Act - Test lower bound + ListResponse result2 = await storage.ListItemsAsync("conv_clamp", limit: 0); + + // Assert + Assert.Equal(5, result1.Data.Count); // Should return all items (clamped to 100 max, but we only have 5) + Assert.NotNull(result2.Data); + Assert.NotEmpty(result2.Data); + Assert.Single(result2.Data); // Should return at least 1 item (clamped to 1 min) + } + + [Fact] + public async Task ListItemsAsync_EmptyConversation_ReturnsEmptyListAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_empty", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Act + ListResponse result = await storage.ListItemsAsync("conv_empty"); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.Empty(result.Data); + Assert.Null(result.FirstId); + Assert.Null(result.LastId); + Assert.False(result.HasMore); + } + + [Fact] + public async Task ListItemsAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => storage.ListItemsAsync("conv_nonexistent")); + Assert.Contains("not found", exception.Message); + } + + [Fact] + public async Task DeleteItemAsync_ExistingItem_ReturnsTrueAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_delitem", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + var item = new ResponsesUserMessageItemResource + { + Id = "msg_delete", + Content = [new ItemContentInputText { Text = "Delete me" }] + }; + await storage.AddItemAsync("conv_delitem", item); + + // Act + bool result = await storage.DeleteItemAsync("conv_delitem", "msg_delete"); + + // Assert + Assert.True(result); + + // Verify deletion + ItemResource? retrieved = await storage.GetItemAsync("conv_delitem", "msg_delete"); + Assert.Null(retrieved); + } + + [Fact] + public async Task DeleteItemAsync_NonExistentItem_ReturnsFalseAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_delnoitem", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Act + bool result = await storage.DeleteItemAsync("conv_delnoitem", "msg_nonexistent"); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task DeleteItemAsync_NonExistentConversation_ReturnsFalseAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + + // Act + bool result = await storage.DeleteItemAsync("conv_nonexistent", "msg_any"); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task ConcurrentOperations_ThreadSafeAsync() + { + // Arrange + var storage = new InMemoryConversationStorage(); + var conversation = new Conversation + { + Id = "conv_concurrent", + CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Metadata = [] + }; + await storage.CreateConversationAsync(conversation); + + // Act - Add items concurrently + var tasks = new List(); + for (int i = 0; i < 100; i++) + { + int index = i; + tasks.Add(Task.Run(async () => + { + var item = new ResponsesUserMessageItemResource + { + Id = $"msg_{index:D3}", + Content = [new ItemContentInputText { Text = $"Message {index}" }] + }; + await storage.AddItemAsync("conv_concurrent", item); + })); + } + + await Task.WhenAll(tasks); + + // Assert + ListResponse result = await storage.ListItemsAsync("conv_concurrent", limit: 100); + Assert.NotNull(result.Data); + Assert.NotEmpty(result.Data); + Assert.Equal(100, result.Data.Count); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs new file mode 100644 index 0000000000..9854a2181e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs @@ -0,0 +1,1206 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +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; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Conformance tests for OpenAI Conversations API implementation behavior. +/// Tests use real API traces to ensure our implementation produces responses +/// that match OpenAI's wire format when processing actual requests through the server. +/// +public sealed class OpenAIConversationsConformanceTests : IAsyncDisposable +{ + private const string TracesBasePath = "ConformanceTraces/Conversations"; + private WebApplication? _app; + private HttpClient? _httpClient; + + /// + /// Loads a JSON file from the conformance traces directory. + /// + private static string LoadTraceFile(string relativePath) + { + var fullPath = System.IO.Path.Combine(TracesBasePath, relativePath); + + if (!System.IO.File.Exists(fullPath)) + { + throw new System.IO.FileNotFoundException($"Conformance trace file not found: {fullPath}"); + } + + return System.IO.File.ReadAllText(fullPath); + } + + /// + /// Loads a JSON document from the conformance traces directory. + /// + private static JsonDocument LoadTraceDocument(string relativePath) + { + var json = LoadTraceFile(relativePath); + return JsonDocument.Parse(json); + } + + /// + /// Asserts that a JSON element exists (property is present, value can be null). + /// + private static void AssertJsonPropertyExists(JsonElement element, string propertyName) + { + if (!element.TryGetProperty(propertyName, out _)) + { + Assert.Fail($"Expected property '{propertyName}' not found in JSON"); + } + } + + /// + /// Asserts that a JSON element has a specific string value. + /// + private static void AssertJsonPropertyEquals(JsonElement element, string propertyName, string expectedValue) + { + AssertJsonPropertyExists(element, propertyName); + var actualValue = element.GetProperty(propertyName).GetString(); + + if (actualValue != expectedValue) + { + Assert.Fail($"Property '{propertyName}': expected '{expectedValue}', got '{actualValue}'"); + } + } + + /// + /// Asserts that a JSON element has a specific boolean value. + /// + private static void AssertJsonPropertyEquals(JsonElement element, string propertyName, bool expectedValue) + { + AssertJsonPropertyExists(element, propertyName); + var actualValue = element.GetProperty(propertyName).GetBoolean(); + + if (actualValue != expectedValue) + { + Assert.Fail($"Property '{propertyName}': expected {expectedValue}, got {actualValue}"); + } + } + + /// + /// Creates a test server with Conversations API. + /// + private async Task CreateTestServerAsync(string agentName, string instructions, string responseText) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); + builder.Services.AddOpenAIConversations(); + builder.Services.AddOpenAIResponses(); + + this._app = builder.Build(); + AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); + this._app.MapOpenAIConversations(); + this._app.MapOpenAIResponses(agent); + + await this._app.StartAsync(); + + TestServer testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + return this._httpClient; + } + + /// + /// Creates a test server with a stateful mock that returns different responses for each call. + /// + private async Task CreateTestServerWithStatefulMockAsync(string agentName, string instructions, string[] responseTexts) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + IChatClient mockChatClient = new TestHelpers.StatefulMockChatClient(responseTexts); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); + builder.Services.AddOpenAIConversations(); + builder.Services.AddOpenAIResponses(); + + this._app = builder.Build(); + + AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); + this._app.MapOpenAIConversations(); + this._app.MapOpenAIResponses(agent); + + await this._app.StartAsync(); + + TestServer testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + return this._httpClient; + } + + /// + /// Creates a test server with a tool call mock. + /// + private async Task CreateTestServerWithToolCallAsync(string agentName, string instructions, string functionName, string arguments) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + IChatClient mockChatClient = new TestHelpers.ToolCallMockChatClient(functionName, arguments); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); + builder.Services.AddOpenAIConversations(); + builder.Services.AddOpenAIResponses(); + + this._app = builder.Build(); + + AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); + this._app.MapOpenAIConversations(); + this._app.MapOpenAIResponses(agent); + + await this._app.StartAsync(); + + TestServer testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + return this._httpClient; + } + + /// + /// Sends a POST request with JSON content to the test server. + /// + private static async Task SendPostRequestAsync(HttpClient client, string path, string requestJson) + { + StringContent content = new(requestJson, Encoding.UTF8, "application/json"); + return await client.PostAsync(new Uri(path, UriKind.Relative), content); + } + + /// + /// Sends a GET request to the test server. + /// + private static async Task SendGetRequestAsync(HttpClient client, string path) + { + return await client.GetAsync(new Uri(path, UriKind.Relative)); + } + + /// + /// Sends a DELETE request to the test server. + /// + private static async Task SendDeleteRequestAsync(HttpClient client, string path) + { + return await client.DeleteAsync(new Uri(path, UriKind.Relative)); + } + + /// + /// Parses the response JSON and returns a JsonDocument. + /// + private static async Task ParseResponseAsync(HttpResponseMessage response) + { + string responseJson = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(responseJson); + } + + [Fact] + public async Task BasicConversationCreateAsync() + { + // Arrange + string requestJson = LoadTraceFile("basic/create_conversation_request.json"); + using var expectedResponseDoc = LoadTraceDocument("basic/create_conversation_response.json"); + var expectedResponse = expectedResponseDoc.RootElement; + + HttpClient client = await this.CreateTestServerAsync("basic-agent", "You are a helpful assistant.", "The capital of France is Paris."); + + // Act + HttpResponseMessage httpResponse = await SendPostRequestAsync(client, "/v1/conversations", requestJson); + using var responseDoc = await ParseResponseAsync(httpResponse); + var response = responseDoc.RootElement; + + // Parse the request + using var requestDoc = JsonDocument.Parse(requestJson); + var request = requestDoc.RootElement; + + // Assert - Request has metadata + AssertJsonPropertyExists(request, "metadata"); + var requestMetadata = request.GetProperty("metadata"); + Assert.Equal(JsonValueKind.Object, requestMetadata.ValueKind); + + // Assert - Response metadata + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "conversation"); + AssertJsonPropertyExists(response, "created_at"); + var id = response.GetProperty("id").GetString(); + Assert.NotNull(id); + Assert.StartsWith("conv_", id); + var createdAt = response.GetProperty("created_at").GetInt64(); + Assert.True(createdAt > 0, "created_at should be a positive unix timestamp"); + + // Assert - Response preserves metadata + AssertJsonPropertyExists(response, "metadata"); + var responseMetadata = response.GetProperty("metadata"); + Assert.Equal(JsonValueKind.Object, responseMetadata.ValueKind); + } + + [Fact] + public async Task BasicConversationWithMessagesAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("basic/first_message_request.json"); + string secondMessageRequestJson = LoadTraceFile("basic/second_message_request.json"); + using var firstMessageExpectedDoc = LoadTraceDocument("basic/first_message_response.json"); + using var secondMessageExpectedDoc = LoadTraceDocument("basic/second_message_response.json"); + + // Get expected response texts + string firstExpectedText = firstMessageExpectedDoc.RootElement.GetProperty("output")[0] + .GetProperty("content")[0] + .GetProperty("text").GetString()!; + string secondExpectedText = secondMessageExpectedDoc.RootElement.GetProperty("output")[0] + .GetProperty("content")[0] + .GetProperty("text").GetString()!; + + // Create a stateful mock that returns different responses for each call + HttpClient client = await this.CreateTestServerWithStatefulMockAsync( + "basic-agent", + "You are a helpful assistant.", + [firstExpectedText, secondExpectedText]); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - Send first message (using Responses API with conversation parameter) + // Update the request JSON with the actual conversation ID + using var firstMsgDoc = JsonDocument.Parse(firstMessageRequestJson); + var firstMsgRequest = JsonSerializer.Serialize(new + { + model = firstMsgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = firstMsgDoc.RootElement.GetProperty("input").GetString(), + max_output_tokens = firstMsgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + HttpResponseMessage firstMsgResponse = await SendPostRequestAsync(client, "/basic-agent/v1/responses", firstMsgRequest); + using var firstMsgResponseDoc = await ParseResponseAsync(firstMsgResponse); + var firstResponse = firstMsgResponseDoc.RootElement; + + // Assert - First response has conversation reference + AssertJsonPropertyExists(firstResponse, "conversation"); + var conversationRef = firstResponse.GetProperty("conversation"); + + // The conversation reference can be either a string (just the ID) or an object with an id property + if (conversationRef.ValueKind == JsonValueKind.String) + { + var refId = conversationRef.GetString(); + Assert.Equal(conversationId, refId); + } + else if (conversationRef.ValueKind == JsonValueKind.Object) + { + AssertJsonPropertyEquals(conversationRef, "id", conversationId); + } + else + { + Assert.Fail($"Expected conversation to be either a string or an object, but got {conversationRef.ValueKind}"); + } + + // Assert - First response has output + AssertJsonPropertyExists(firstResponse, "output"); + var firstOutput = firstResponse.GetProperty("output"); + Assert.True(firstOutput.GetArrayLength() > 0); + + // Assert - First response status is completed + AssertJsonPropertyEquals(firstResponse, "status", "completed"); + + // Act - Send second message + using var secondMsgDoc = JsonDocument.Parse(secondMessageRequestJson); + var secondMsgRequest = JsonSerializer.Serialize(new + { + model = secondMsgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = secondMsgDoc.RootElement.GetProperty("input").GetString(), + max_output_tokens = secondMsgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + HttpResponseMessage secondMsgResponse = await SendPostRequestAsync(client, "/basic-agent/v1/responses", secondMsgRequest); + using var secondMsgResponseDoc = await ParseResponseAsync(secondMsgResponse); + var secondResponse = secondMsgResponseDoc.RootElement; + + // Assert - Second response has conversation reference + AssertJsonPropertyExists(secondResponse, "conversation"); + var secondConversationRef = secondResponse.GetProperty("conversation"); + + if (secondConversationRef.ValueKind == JsonValueKind.String) + { + var refId = secondConversationRef.GetString(); + Assert.Equal(conversationId, refId); + } + else if (secondConversationRef.ValueKind == JsonValueKind.Object) + { + AssertJsonPropertyEquals(secondConversationRef, "id", conversationId); + } + else + { + Assert.Fail($"Expected conversation to be either a string or an object, but got {secondConversationRef.ValueKind}"); + } + + // Assert - Second response has output + AssertJsonPropertyExists(secondResponse, "output"); + var secondOutput = secondResponse.GetProperty("output"); + Assert.True(secondOutput.GetArrayLength() > 0); + + // Assert - Second response status is completed + AssertJsonPropertyEquals(secondResponse, "status", "completed"); + } + + [Fact] + public async Task CreateConversationWithItemsAsync() + { + // Arrange + string requestJson = LoadTraceFile("create_with_items/create_request.json"); + using var expectedResponseDoc = LoadTraceDocument("create_with_items/create_response.json"); + + HttpClient client = await this.CreateTestServerAsync("items-agent", "You are a helpful assistant.", "Test response"); + + // Act + HttpResponseMessage httpResponse = await SendPostRequestAsync(client, "/v1/conversations", requestJson); + using var responseDoc = await ParseResponseAsync(httpResponse); + var response = responseDoc.RootElement; + + // Parse the request + using var requestDoc = JsonDocument.Parse(requestJson); + var request = requestDoc.RootElement; + + // Assert - Request has items array + AssertJsonPropertyExists(request, "items"); + var requestItems = request.GetProperty("items"); + Assert.Equal(JsonValueKind.Array, requestItems.ValueKind); + Assert.True(requestItems.GetArrayLength() > 0); + + // Assert - Response has conversation structure + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "conversation"); + AssertJsonPropertyExists(response, "created_at"); + AssertJsonPropertyExists(response, "metadata"); + } + + [Fact] + public async Task AddItemsToConversationAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + string addItemsRequestJson = LoadTraceFile("add_items/request.json"); + using var expectedResponseDoc = LoadTraceDocument("add_items/response.json"); + + HttpClient client = await this.CreateTestServerAsync("add-items-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation first + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - Add items + HttpResponseMessage addItemsResponse = await SendPostRequestAsync(client, $"/v1/conversations/{conversationId}/items", addItemsRequestJson); + using var addItemsDoc = await ParseResponseAsync(addItemsResponse); + var response = addItemsDoc.RootElement; + + // Parse the request + using var requestDoc = JsonDocument.Parse(addItemsRequestJson); + var request = requestDoc.RootElement; + + // Assert - Request has items array + AssertJsonPropertyExists(request, "items"); + var requestItems = request.GetProperty("items"); + Assert.Equal(JsonValueKind.Array, requestItems.ValueKind); + var itemCount = requestItems.GetArrayLength(); + Assert.True(itemCount > 0); + + // Assert - Response has data array with created items + AssertJsonPropertyExists(response, "data"); + var responseData = response.GetProperty("data"); + Assert.Equal(JsonValueKind.Array, responseData.ValueKind); + Assert.Equal(itemCount, responseData.GetArrayLength()); + + // Assert - Each item has required fields + foreach (var item in responseData.EnumerateArray()) + { + AssertJsonPropertyExists(item, "id"); + AssertJsonPropertyEquals(item, "type", "message"); + AssertJsonPropertyExists(item, "content"); + AssertJsonPropertyExists(item, "role"); + var itemId = item.GetProperty("id").GetString(); + Assert.NotNull(itemId); + Assert.StartsWith("msg_", itemId); + } + } + + [Fact] + public async Task ListItemsInConversationAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + using var expectedResponseDoc = LoadTraceDocument("list_items/response.json"); + + HttpClient client = await this.CreateTestServerAsync("list-items-agent", "You are a helpful assistant.", "The capital of France is Paris."); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - List items + HttpResponseMessage listResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listDoc = await ParseResponseAsync(listResponse); + var response = listDoc.RootElement; + + // Assert - Response has list structure + AssertJsonPropertyEquals(response, "object", "list"); + AssertJsonPropertyExists(response, "data"); + AssertJsonPropertyExists(response, "first_id"); + AssertJsonPropertyExists(response, "last_id"); + AssertJsonPropertyExists(response, "has_more"); + + var data = response.GetProperty("data"); + Assert.Equal(JsonValueKind.Array, data.ValueKind); + } + + [Fact] + public async Task RetrieveConversationAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + using var expectedResponseDoc = LoadTraceDocument("retrieve_conversation/response.json"); + + HttpClient client = await this.CreateTestServerAsync("retrieve-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var createdConversation = createDoc.RootElement; + string conversationId = createdConversation.GetProperty("id").GetString()!; + + // Act - Retrieve conversation + HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}"); + using var retrieveDoc = await ParseResponseAsync(retrieveResponse); + var response = retrieveDoc.RootElement; + + // Assert - Response has conversation structure + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "conversation"); + AssertJsonPropertyExists(response, "created_at"); + AssertJsonPropertyExists(response, "metadata"); + var id = response.GetProperty("id").GetString(); + Assert.Equal(conversationId, id); + } + + [Fact] + public async Task RetrieveItemAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("create_with_items/create_request.json"); + using var expectedResponseDoc = LoadTraceDocument("retrieve_item/response.json"); + + HttpClient client = await this.CreateTestServerAsync("retrieve-item-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation with items + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - List items to get an item ID + HttpResponseMessage listResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listDoc = await ParseResponseAsync(listResponse); + var listResult = listDoc.RootElement; + var items = listResult.GetProperty("data"); + Assert.True(items.GetArrayLength() > 0, "Should have at least one item"); + string itemId = items[0].GetProperty("id").GetString()!; + + // Act - Retrieve specific item + HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items/{itemId}"); + using var retrieveDoc = await ParseResponseAsync(retrieveResponse); + var response = retrieveDoc.RootElement; + + // Assert - Response has item structure + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "type", "message"); + AssertJsonPropertyExists(response, "content"); + AssertJsonPropertyExists(response, "role"); + var id = response.GetProperty("id").GetString(); + Assert.Equal(itemId, id); + } + + [Fact] + public async Task UpdateConversationAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + string updateRequestJson = LoadTraceFile("update_conversation/request.json"); + using var expectedResponseDoc = LoadTraceDocument("update_conversation/response.json"); + + HttpClient client = await this.CreateTestServerAsync("update-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - Update conversation + HttpResponseMessage updateResponse = await SendPostRequestAsync(client, $"/v1/conversations/{conversationId}", updateRequestJson); + using var updateDoc = await ParseResponseAsync(updateResponse); + var response = updateDoc.RootElement; + + // Parse the request + using var requestDoc = JsonDocument.Parse(updateRequestJson); + var request = requestDoc.RootElement; + + // Assert - Request has metadata + AssertJsonPropertyExists(request, "metadata"); + var requestMetadata = request.GetProperty("metadata"); + + // Assert - Response preserves updated metadata + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "conversation"); + AssertJsonPropertyExists(response, "metadata"); + var responseMetadata = response.GetProperty("metadata"); + + // Verify metadata was updated + foreach (var prop in requestMetadata.EnumerateObject()) + { + Assert.True(responseMetadata.TryGetProperty(prop.Name, out var value)); + Assert.Equal(prop.Value.GetString(), value.GetString()); + } + } + + [Fact] + public async Task DeleteConversationAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + using var expectedResponseDoc = LoadTraceDocument("delete_conversation/response.json"); + + HttpClient client = await this.CreateTestServerAsync("delete-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - Delete conversation + HttpResponseMessage deleteResponse = await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}"); + using var deleteDoc = await ParseResponseAsync(deleteResponse); + var response = deleteDoc.RootElement; + + // Assert - Delete response structure + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "conversation.deleted"); + AssertJsonPropertyEquals(response, "deleted", true); + var id = response.GetProperty("id").GetString(); + Assert.Equal(conversationId, id); + + // Assert - Conversation is actually deleted + HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}"); + Assert.Equal(System.Net.HttpStatusCode.NotFound, retrieveResponse.StatusCode); + } + + [Fact] + public async Task DeleteItemAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("create_with_items/create_request.json"); + using var expectedResponseDoc = LoadTraceDocument("delete_item/response.json"); + + HttpClient client = await this.CreateTestServerAsync("delete-item-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation with items + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - List items to get an item ID + HttpResponseMessage listResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listDoc = await ParseResponseAsync(listResponse); + var listResult = listDoc.RootElement; + var items = listResult.GetProperty("data"); + Assert.True(items.GetArrayLength() > 0, "Should have at least one item"); + string itemId = items[0].GetProperty("id").GetString()!; + + // Act - Delete item + HttpResponseMessage deleteResponse = await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}/items/{itemId}"); + using var deleteDoc = await ParseResponseAsync(deleteResponse); + var response = deleteDoc.RootElement; + + // Assert - Delete response structure + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "conversation.item.deleted"); + AssertJsonPropertyEquals(response, "deleted", true); + var id = response.GetProperty("id").GetString(); + Assert.Equal(itemId, id); + + // Assert - Item is actually deleted + HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items/{itemId}"); + Assert.Equal(System.Net.HttpStatusCode.NotFound, retrieveResponse.StatusCode); + } + + [Fact] + public async Task ErrorConversationNotFoundAsync() + { + // Arrange + using var expectedResponseDoc = LoadTraceDocument("error_conversation_not_found/response.json"); + const string NonExistentConversationId = "conv_nonexistent123456789"; + + HttpClient client = await this.CreateTestServerAsync("error-agent", "You are a helpful assistant.", "Test response"); + + // Act + HttpResponseMessage response = await SendGetRequestAsync(client, $"/v1/conversations/{NonExistentConversationId}"); + using var responseDoc = await ParseResponseAsync(response); + var responseJson = responseDoc.RootElement; + + // Assert - Response is 404 + Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); + + // Assert - Error response structure + AssertJsonPropertyExists(responseJson, "error"); + var error = responseJson.GetProperty("error"); + AssertJsonPropertyExists(error, "message"); + AssertJsonPropertyExists(error, "type"); + var errorMessage = error.GetProperty("message").GetString(); + Assert.NotNull(errorMessage); + Assert.Contains("not found", errorMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ErrorItemNotFoundAsync() + { + // Arrange + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + using var expectedResponseDoc = LoadTraceDocument("error_item_not_found/response.json"); + const string NonExistentItemId = "msg_nonexistent123456789"; + + HttpClient client = await this.CreateTestServerAsync("error-item-agent", "You are a helpful assistant.", "Test response"); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - Try to retrieve non-existent item + HttpResponseMessage response = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items/{NonExistentItemId}"); + using var responseDoc = await ParseResponseAsync(response); + var responseJson = responseDoc.RootElement; + + // Assert - Response is 404 + Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); + + // Assert - Error response structure + AssertJsonPropertyExists(responseJson, "error"); + var error = responseJson.GetProperty("error"); + AssertJsonPropertyExists(error, "message"); + AssertJsonPropertyExists(error, "type"); + var errorMessage = error.GetProperty("message").GetString(); + Assert.NotNull(errorMessage); + Assert.Contains("not found", errorMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ErrorInvalidJsonAsync() + { + // Arrange + string invalidJson = LoadTraceFile("error_invalid_json/request.txt"); + using var expectedResponseDoc = LoadTraceDocument("error_invalid_json/response.json"); + + HttpClient client = await this.CreateTestServerAsync("error-json-agent", "You are a helpful assistant.", "Test response"); + + // Act + StringContent content = new(invalidJson, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await client.PostAsync(new Uri("/v1/conversations", UriKind.Relative), content); + + // Assert - Response is 400 + Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task ErrorDeleteAlreadyDeletedAsync() + { + // Arrange + using var expectedResponseDoc = LoadTraceDocument("error_delete_already_deleted/response.json"); + + HttpClient client = await this.CreateTestServerAsync("delete-twice-agent", "You are a helpful assistant.", "Test response"); + + // Create a conversation + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + string conversationId = createDoc.RootElement.GetProperty("id").GetString()!; + + // Delete the conversation + await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}"); + + // Act - Try to delete again + HttpResponseMessage response = await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}"); + using var responseDoc = await ParseResponseAsync(response); + var responseJson = responseDoc.RootElement; + + // Assert - Should return 404 + Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); + + // Assert - Error response structure + AssertJsonPropertyExists(responseJson, "error"); + var error = responseJson.GetProperty("error"); + AssertJsonPropertyExists(error, "message"); + AssertJsonPropertyExists(error, "type"); + var errorMessage = error.GetProperty("message").GetString(); + Assert.NotNull(errorMessage); + Assert.Contains("not found", errorMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ErrorInvalidLimitAsync() + { + // Arrange + using var expectedResponseDoc = LoadTraceDocument("error_invalid_limit/response.json"); + + HttpClient client = await this.CreateTestServerAsync("invalid-limit-agent", "You are a helpful assistant.", "Test response"); + + // Create a conversation + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + string conversationId = createDoc.RootElement.GetProperty("id").GetString()!; + + // Act - Request items with invalid limit (e.g., negative or too large) + HttpResponseMessage response = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items?limit=-1"); + using var responseDoc = await ParseResponseAsync(response); + var responseJson = responseDoc.RootElement; + + // Assert - Should return 400 + Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode); + + // Assert - Error response structure + AssertJsonPropertyExists(responseJson, "error"); + var error = responseJson.GetProperty("error"); + AssertJsonPropertyExists(error, "message"); + AssertJsonPropertyExists(error, "type"); + var errorMessage = error.GetProperty("message").GetString(); + Assert.NotNull(errorMessage); + } + + [Fact] + public async Task ToolCallFullScenarioAsync() + { + // Arrange - Full test for tool call scenario through Conversations and Responses API + string createRequestJson = LoadTraceFile("tool_call/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("tool_call/first_message_request.json"); + using var messageExpectedDoc = LoadTraceDocument("tool_call/first_message_response.json"); + + // Extract function call details from expected response + var expectedOutput = messageExpectedDoc.RootElement.GetProperty("output")[0]; + string functionName = expectedOutput.GetProperty("name").GetString()!; + string arguments = expectedOutput.GetProperty("arguments").GetString()!; + + // Create server with proper tool call mock + HttpClient client = await this.CreateTestServerWithToolCallAsync("tool-call-agent", "You are a helpful assistant.", functionName, arguments); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Act - Send message with tools through Responses API + using var msgDoc = JsonDocument.Parse(firstMessageRequestJson); + var msgRequest = JsonSerializer.Serialize(new + { + model = msgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = msgDoc.RootElement.GetProperty("input"), + tools = msgDoc.RootElement.GetProperty("tools"), + max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + HttpResponseMessage msgResponse = await SendPostRequestAsync(client, "/tool-call-agent/v1/responses", msgRequest); + using var msgResponseDoc = await ParseResponseAsync(msgResponse); + var response = msgResponseDoc.RootElement; + + // Assert - Response has conversation reference + AssertJsonPropertyExists(response, "conversation"); + AssertJsonPropertyEquals(response, "status", "completed"); + + // Assert - Response has function call output + AssertJsonPropertyExists(response, "output"); + var output = response.GetProperty("output"); + Assert.True(output.GetArrayLength() > 0); + + // Assert - Output contains function call + var outputItem = output[0]; + AssertJsonPropertyEquals(outputItem, "type", "function_call"); + AssertJsonPropertyEquals(outputItem, "name", functionName); + AssertJsonPropertyExists(outputItem, "arguments"); + } + + [Fact] + public async Task ImageInputFullScenarioAsync() + { + // Arrange - Full test for image input scenario through Conversations and Responses API + string createRequestJson = LoadTraceFile("image_input/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("image_input/first_message_request.json"); + using var createExpectedDoc = LoadTraceDocument("image_input/create_conversation_response.json"); + using var messageExpectedDoc = LoadTraceDocument("image_input/first_message_response.json"); + + // Get expected response text + string expectedText = messageExpectedDoc.RootElement.GetProperty("output")[0] + .GetProperty("content")[0] + .GetProperty("text").GetString()!; + + HttpClient client = await this.CreateTestServerAsync("image-input-agent", "You are a helpful assistant.", expectedText); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Parse the image input request to verify structure + using var requestDoc = JsonDocument.Parse(firstMessageRequestJson); + var request = requestDoc.RootElement; + + // Assert - Request structure with image content (validates we're testing the right scenario) + AssertJsonPropertyExists(request, "input"); + var input = request.GetProperty("input"); + Assert.Equal(JsonValueKind.Array, input.ValueKind); + + var message = input[0]; + AssertJsonPropertyExists(message, "content"); + var content = message.GetProperty("content"); + Assert.True(content.GetArrayLength() > 1, "Should have text and image content"); + + // Assert - Has input_image content type + bool hasImage = false; + foreach (var part in content.EnumerateArray()) + { + if (part.GetProperty("type").GetString() == "input_image") + { + hasImage = true; + AssertJsonPropertyExists(part, "image_url"); + break; + } + } + Assert.True(hasImage, "Request should have input_image content"); + + // Act - Send message with image through Responses API + using var msgDoc = JsonDocument.Parse(firstMessageRequestJson); + var msgRequest = JsonSerializer.Serialize(new + { + model = msgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = msgDoc.RootElement.GetProperty("input"), + max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + HttpResponseMessage msgResponse = await SendPostRequestAsync(client, "/image-input-agent/v1/responses", msgRequest); + using var msgResponseDoc = await ParseResponseAsync(msgResponse); + var response = msgResponseDoc.RootElement; + + // Assert - Response has conversation reference (validates integration) + AssertJsonPropertyExists(response, "conversation"); + AssertJsonPropertyEquals(response, "status", "completed"); + + // Assert - Response has output (validates the system processed the request successfully) + AssertJsonPropertyExists(response, "output"); + var output = response.GetProperty("output"); + Assert.True(output.GetArrayLength() > 0); + } + + [Fact] + public async Task ImageInputStreamingScenarioAsync() + { + // Arrange - Test streaming response with image input through Conversations + Responses API + string createRequestJson = LoadTraceFile("image_input_streaming/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("image_input_streaming/first_message_request.json"); + string expectedResponseSse = LoadTraceFile("image_input_streaming/first_message_response.txt"); + + // Extract expected text from SSE events + var expectedEvents = ParseSseEventsFromContent(expectedResponseSse); + var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList(); + string expectedText = string.Concat(deltaEvents.Select(e => e.GetProperty("delta").GetString())); + + HttpClient client = await this.CreateTestServerAsync("image-streaming-agent", "You are a helpful assistant.", expectedText); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Prepare streaming request with conversation + using var msgDoc = JsonDocument.Parse(firstMessageRequestJson); + var msgRequest = JsonSerializer.Serialize(new + { + model = msgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = msgDoc.RootElement.GetProperty("input"), + stream = true, + max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + // Act - Send streaming request + HttpResponseMessage streamResponse = await SendPostRequestAsync(client, "/image-streaming-agent/v1/responses", msgRequest); + + // Assert - Response should be SSE format (validates streaming works with image input) + Assert.Equal("text/event-stream", streamResponse.Content.Headers.ContentType?.MediaType); + + string responseSse = await streamResponse.Content.ReadAsStringAsync(); + var events = ParseSseEventsFromContent(responseSse); + + // Assert - Has expected event types (validates proper streaming event structure) + var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); + Assert.Contains("response.created", eventTypes); + Assert.Contains("response.output_text.delta", eventTypes); + } + + [Fact] + public async Task RefusalStreamingScenarioAsync() + { + // Arrange - Test streaming response with refusal through Conversations + Responses API + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("refusal_streaming/first_message_request.json"); + string expectedResponseSse = LoadTraceFile("refusal_streaming/first_message_response.txt"); + + // Extract expected text from SSE events + var expectedEvents = ParseSseEventsFromContent(expectedResponseSse); + var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList(); + string expectedText = string.Concat(deltaEvents.Select(e => e.GetProperty("delta").GetString())); + + HttpClient client = await this.CreateTestServerAsync("refusal-streaming-agent", "You are a helpful assistant.", expectedText); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Prepare streaming request with conversation + using var msgDoc = JsonDocument.Parse(firstMessageRequestJson); + var msgRequest = JsonSerializer.Serialize(new + { + model = msgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = msgDoc.RootElement.GetProperty("input"), + stream = true, + max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + // Act - Send streaming request + HttpResponseMessage streamResponse = await SendPostRequestAsync(client, "/refusal-streaming-agent/v1/responses", msgRequest); + + // Assert - Response should be SSE format + Assert.Equal("text/event-stream", streamResponse.Content.Headers.ContentType?.MediaType); + + string responseSse = await streamResponse.Content.ReadAsStringAsync(); + var events = ParseSseEventsFromContent(responseSse); + + // Assert - Has expected event types (conformance check) + var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); + Assert.Contains("response.created", eventTypes); + Assert.Contains("response.output_text.delta", eventTypes); + + // Assert - Text contains refusal (validates refusal content is in streaming output) + var doneEvent = events.First(e => e.GetProperty("type").GetString() == "response.output_text.done"); + var finalText = doneEvent.GetProperty("text").GetString(); + Assert.NotNull(finalText); + Assert.Contains("can't assist", finalText, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ToolCallStreamingScenarioAsync() + { + // Arrange - Test streaming response with tool call through Conversations + Responses API + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("tool_call_streaming/first_message_request.json"); + + // Use tool call details from the non-streaming test + using var messageExpectedDoc = LoadTraceDocument("tool_call/first_message_response.json"); + var expectedOutput = messageExpectedDoc.RootElement.GetProperty("output")[0]; + string functionName = expectedOutput.GetProperty("name").GetString()!; + string arguments = expectedOutput.GetProperty("arguments").GetString()!; + + HttpClient client = await this.CreateTestServerWithToolCallAsync("tool-streaming-agent", "You are a helpful assistant.", functionName, arguments); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Prepare streaming request with conversation + using var msgDoc = JsonDocument.Parse(firstMessageRequestJson); + var msgRequest = JsonSerializer.Serialize(new + { + model = msgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = msgDoc.RootElement.GetProperty("input"), + tools = msgDoc.RootElement.GetProperty("tools"), + stream = true, + max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + // Act - Send streaming request + HttpResponseMessage streamResponse = await SendPostRequestAsync(client, "/tool-streaming-agent/v1/responses", msgRequest); + + // Assert - Response should be SSE format + Assert.Equal("text/event-stream", streamResponse.Content.Headers.ContentType?.MediaType); + + string responseSse = await streamResponse.Content.ReadAsStringAsync(); + var events = ParseSseEventsFromContent(responseSse); + + // Assert - Has expected event types for function call streaming + var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); + Assert.Contains("response.created", eventTypes); + } + + [Fact] + public async Task RefusalFullScenarioAsync() + { + // Arrange - Full test for refusal scenario through Conversations and Responses API + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + string firstMessageRequestJson = LoadTraceFile("refusal/first_message_request.json"); + using var createExpectedDoc = LoadTraceDocument("refusal/create_conversation_response.json"); + using var messageExpectedDoc = LoadTraceDocument("refusal/first_message_response.json"); + + // Get expected response text (refusal message) + string expectedText = messageExpectedDoc.RootElement.GetProperty("output")[0] + .GetProperty("content")[0] + .GetProperty("text").GetString()!; + + HttpClient client = await this.CreateTestServerAsync("refusal-agent", "You are a helpful assistant.", expectedText); + + // Act - Create conversation + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + var conversation = createDoc.RootElement; + string conversationId = conversation.GetProperty("id").GetString()!; + + // Parse the refusal request to verify structure + using var requestDoc = JsonDocument.Parse(firstMessageRequestJson); + var request = requestDoc.RootElement; + + // Assert - Request structure (input can be string or array depending on the request format) + AssertJsonPropertyExists(request, "input"); + var input = request.GetProperty("input"); + Assert.True(input.ValueKind is JsonValueKind.String or JsonValueKind.Array); + + // Act - Send message through Responses API + using var msgDoc = JsonDocument.Parse(firstMessageRequestJson); + var msgRequest = JsonSerializer.Serialize(new + { + model = msgDoc.RootElement.GetProperty("model").GetString(), + conversation = conversationId, + input = msgDoc.RootElement.GetProperty("input"), + max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32() + }); + + HttpResponseMessage msgResponse = await SendPostRequestAsync(client, "/refusal-agent/v1/responses", msgRequest); + using var msgResponseDoc = await ParseResponseAsync(msgResponse); + var response = msgResponseDoc.RootElement; + + // Assert - Response has conversation reference (validates integration) + AssertJsonPropertyExists(response, "conversation"); + // Assert - Refusals should be completed, not failed (important behavioral validation) + AssertJsonPropertyEquals(response, "status", "completed"); + + // Assert - Response has output with refusal (validates structure) + AssertJsonPropertyExists(response, "output"); + var output = response.GetProperty("output"); + Assert.True(output.GetArrayLength() > 0); + + var outputMessage = output[0]; + var outputContent = outputMessage.GetProperty("content"); + var textContent = outputContent[0]; + var text = textContent.GetProperty("text").GetString(); + Assert.NotNull(text); + // Validate refusal pattern (confirms we're testing the right scenario) + Assert.Contains("can't assist", text, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ErrorMissingRequiredFieldAsync() + { + // Arrange + string requestJson = LoadTraceFile("error_missing_required_field/request.json"); + + HttpClient client = await this.CreateTestServerAsync("missing-field-agent", "You are a helpful assistant.", "Test response"); + + // Create a conversation first + string createRequestJson = LoadTraceFile("basic/create_conversation_request.json"); + HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson); + using var createDoc = await ParseResponseAsync(createResponse); + string conversationId = createDoc.RootElement.GetProperty("id").GetString()!; + + // Act - Send request with missing required field (role is missing) + HttpResponseMessage response = await SendPostRequestAsync(client, $"/v1/conversations/{conversationId}/items", requestJson); + + // Assert - System should reject the request with a client error status code + // We accept 400 (Bad Request) or 422 (Unprocessable Entity) as both indicate validation failure + Assert.True( + response.StatusCode is System.Net.HttpStatusCode.BadRequest or + System.Net.HttpStatusCode.UnprocessableEntity, + $"Expected 400 or 422 status code for missing required field, but got {(int)response.StatusCode} ({response.StatusCode})"); + } + + public async ValueTask DisposeAsync() + { + this._httpClient?.Dispose(); + if (this._app != null) + { + await this._app.DisposeAsync(); + } + + GC.SuppressFinalize(this); + } + + /// + /// Helper to parse SSE events from streaming response content string. + /// + private static List ParseSseEventsFromContent(string sseContent) + { + var events = new List(); + var lines = sseContent.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].TrimEnd('\r'); + + if (line.StartsWith("event: ", StringComparison.Ordinal)) + { + // Next line should have the data + if (i + 1 < lines.Length) + { + var dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) + { + var jsonData = dataLine.Substring("data: ".Length); + var doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); + } + } + } + } + + return events; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs new file mode 100644 index 0000000000..4e2ae4df03 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs @@ -0,0 +1,596 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Text.Json; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Models; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Tests for OpenAI Conversations API model serialization and deserialization. +/// These tests verify that our models correctly serialize to and deserialize from JSON +/// matching the OpenAI wire format, without testing actual API implementation behavior. +/// +public sealed class OpenAIConversationsSerializationTests +{ + private const string TracesBasePath = "ConformanceTraces/Conversations"; + + /// + /// Loads a JSON file from the conformance traces directory. + /// + private static string LoadTraceFile(string relativePath) + { + var fullPath = System.IO.Path.Combine(TracesBasePath, relativePath); + + if (!System.IO.File.Exists(fullPath)) + { + throw new System.IO.FileNotFoundException($"Conformance trace file not found: {fullPath}"); + } + + return System.IO.File.ReadAllText(fullPath); + } + + #region Request Serialization Tests + + [Fact] + public void Deserialize_CreateConversationRequest_Success() + { + // Arrange + string json = LoadTraceFile("basic/create_conversation_request.json"); + + // Act + CreateConversationRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateConversationRequest); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Metadata); + } + + [Fact] + public void Deserialize_CreateConversationWithItems_Success() + { + // Arrange + string json = LoadTraceFile("create_with_items/create_request.json"); + + // Act + CreateConversationRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateConversationRequest); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Items); + Assert.True(request.Items.Count > 0); + } + + [Fact] + public void Deserialize_CreateItemsRequest_Success() + { + // Arrange + string json = LoadTraceFile("add_items/request.json"); + + // Act + CreateItemsRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateItemsRequest); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Items); + Assert.True(request.Items.Count > 0); + } + + [Fact] + public void Deserialize_UpdateConversationRequest_Success() + { + // Arrange + string json = LoadTraceFile("update_conversation/request.json"); + + // Act + UpdateConversationRequest? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.UpdateConversationRequest); + + // Assert + Assert.NotNull(request); + Assert.NotNull(request.Metadata); + } + + [Fact] + public void Serialize_CreateConversationRequest_MatchesFormat() + { + // Arrange + var request = new CreateConversationRequest + { + Metadata = new System.Collections.Generic.Dictionary + { + ["test_key"] = "test_value" + } + }; + + // Act + string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateConversationRequest); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert + Assert.True(root.TryGetProperty("metadata", out var metadata)); + Assert.Equal(JsonValueKind.Object, metadata.ValueKind); + Assert.Equal("test_value", metadata.GetProperty("test_key").GetString()); + } + + [Fact] + public void Serialize_CreateConversationRequestWithItems_IncludesItems() + { + // Arrange + var request = new CreateConversationRequest + { + Items = + [ + new ResponsesUserMessageItemParam + { + Content = InputMessageContent.FromContents(new ItemContentInputText { Text = "test" }) + } + ], + Metadata = [] + }; + + // Act + string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateConversationRequest); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert + Assert.True(root.TryGetProperty("items", out var items)); + Assert.Equal(JsonValueKind.Array, items.ValueKind); + Assert.Equal(1, items.GetArrayLength()); + } + + [Fact] + public void Serialize_NullableFields_AreOmittedWhenNull() + { + // Arrange + var request = new CreateConversationRequest(); + + // Act + string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateConversationRequest); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert - Optional fields should not be present when null or use null value + // Either the property doesn't exist or it's explicitly null + bool hasItems = root.TryGetProperty("items", out var itemsProp); + if (hasItems) + { + Assert.Equal(JsonValueKind.Null, itemsProp.ValueKind); + } + } + + #endregion + + #region Response Deserialization Tests + + [Fact] + public void Deserialize_Conversation_Success() + { + // Arrange + string json = LoadTraceFile("basic/create_conversation_response.json"); + + // Act + Conversation? conversation = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Conversation); + + // Assert + Assert.NotNull(conversation); + Assert.StartsWith("conv_", conversation.Id); + Assert.Equal("conversation", conversation.Object); + Assert.True(conversation.CreatedAt > 0); + Assert.NotNull(conversation.Metadata); + } + + [Fact] + public void Deserialize_ConversationRoundTrip_PreservesData() + { + // Arrange + string originalJson = LoadTraceFile("basic/create_conversation_response.json"); + + // Act - Deserialize and re-serialize + Conversation? conversation = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.Conversation); + string reserializedJson = JsonSerializer.Serialize(conversation, OpenAIHostingJsonContext.Default.Conversation); + Conversation? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.Conversation); + + // Assert + Assert.NotNull(conversation); + Assert.NotNull(roundtripped); + Assert.Equal(conversation.Id, roundtripped.Id); + Assert.Equal(conversation.CreatedAt, roundtripped.CreatedAt); + Assert.Equal(conversation.Object, roundtripped.Object); + } + + [Fact] + public void Deserialize_ItemListResponse_Success() + { + // Arrange + string json = LoadTraceFile("list_items/response.json"); + + // Act - The list_items response uses ListResponse, not ConversationListResponse + ListResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ListResponseItemResource); + + // Assert + Assert.NotNull(response); + Assert.Equal("list", response.Object); + Assert.NotNull(response.Data); + Assert.NotNull(response.FirstId); + Assert.NotNull(response.LastId); + Assert.False(response.HasMore); + } + + [Fact] + public void Deserialize_ItemResource_Success() + { + // Arrange + string json = LoadTraceFile("retrieve_item/response.json"); + + // Act + ItemResource? item = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ItemResource); + + // Assert + Assert.NotNull(item); + Assert.StartsWith("msg_", item.Id); + Assert.Equal("message", item.Type); + var messageItem = Assert.IsType(item); + Assert.NotNull(messageItem.Content); + Assert.NotEmpty(messageItem.Content); + } + + [Fact] + public void Deserialize_DeleteResponse_Success() + { + // Arrange + string json = LoadTraceFile("delete_conversation/response.json"); + + // Act + DeleteResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.DeleteResponse); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Id); + Assert.Equal("conversation.deleted", response.Object); + Assert.True(response.Deleted); + } + + [Fact] + public void Deserialize_DeleteItemResponse_Success() + { + // Arrange + string json = LoadTraceFile("delete_item/response.json"); + + // Act + DeleteResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.DeleteResponse); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Id); + Assert.Equal("conversation.item.deleted", response.Object); + Assert.True(response.Deleted); + } + + [Fact] + public void Deserialize_ErrorResponse_Success() + { + // Arrange + string json = LoadTraceFile("error_conversation_not_found/response.json"); + + // Act + ErrorResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ErrorResponse); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Error); + Assert.NotNull(response.Error.Message); + Assert.NotNull(response.Error.Type); + } + + [Fact] + public void Deserialize_AllConversationResponses_HaveRequiredFields() + { + // Arrange + string[] responsePaths = + [ + "basic/create_conversation_response.json", + "create_with_items/create_response.json", + "retrieve_conversation/response.json", + "update_conversation/response.json" + ]; + + foreach (var path in responsePaths) + { + string json = LoadTraceFile(path); + + // Act + Conversation? conversation = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Conversation); + + // Assert + Assert.NotNull(conversation); + Assert.NotNull(conversation.Id); + Assert.Equal("conversation", conversation.Object); + Assert.True(conversation.CreatedAt > 0, $"Conversation from {path} should have created_at"); + } + } + + [Fact] + public void Deserialize_AllItemResponses_HaveRequiredFields() + { + // Arrange - Use list_items response which has multiple items + string json = LoadTraceFile("list_items/response.json"); + ListResponse? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.ListResponseItemResource); + Assert.NotNull(response); + Assert.NotNull(response.Data); + + // Act & Assert + foreach (var item in response.Data) + { + Assert.NotNull(item); + Assert.NotNull(item.Id); + Assert.Equal("message", item.Type); + var messageItem = Assert.IsAssignableFrom(item); + // Content is on concrete message types (ResponsesAssistantMessageItemResource, etc.) + // For this test, we just verify the type is correct + Assert.NotNull(messageItem); + } + } + + [Fact] + public void Serialize_Conversation_MatchesFormat() + { + // Arrange + var conversation = new Conversation + { + Id = "conv_test123", + CreatedAt = 1234567890, + Metadata = new System.Collections.Generic.Dictionary + { + ["test_key"] = "test_value" + } + }; + + // Act + string json = JsonSerializer.Serialize(conversation, OpenAIHostingJsonContext.Default.Conversation); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert + Assert.Equal("conv_test123", root.GetProperty("id").GetString()); + Assert.Equal("conversation", root.GetProperty("object").GetString()); + Assert.Equal(1234567890, root.GetProperty("created_at").GetInt64()); + var metadata = root.GetProperty("metadata"); + Assert.Equal("test_value", metadata.GetProperty("test_key").GetString()); + } + + [Fact] + public void Serialize_ConversationListResponse_MatchesFormat() + { + // Arrange + var response = new ListResponse + { + Data = + [ + new() + { + Id = "conv_1", + CreatedAt = 1234567890, + Metadata = [] + } + ], + HasMore = false + }; + + // Act + string json = JsonSerializer.Serialize(response, OpenAIHostingJsonUtilities.DefaultOptions); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert + Assert.Equal("list", root.GetProperty("object").GetString()); + var data = root.GetProperty("data"); + Assert.Equal(JsonValueKind.Array, data.ValueKind); + Assert.Equal(1, data.GetArrayLength()); + Assert.False(root.GetProperty("has_more").GetBoolean()); + } + + [Fact] + public void Serialize_DeleteResponse_MatchesFormat() + { + // Arrange + var response = new DeleteResponse + { + Id = "conv_test123", + Object = "conversation.deleted", + Deleted = true + }; + + // Act + string json = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.DeleteResponse); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert + Assert.Equal("conv_test123", root.GetProperty("id").GetString()); + Assert.Equal("conversation.deleted", root.GetProperty("object").GetString()); + Assert.True(root.GetProperty("deleted").GetBoolean()); + } + + [Fact] + public void Serialize_ErrorResponse_MatchesFormat() + { + // Arrange + var response = new ErrorResponse + { + Error = new ErrorDetails + { + Message = "Conversation not found", + Type = "invalid_request_error" + } + }; + + // Act + string json = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.ErrorResponse); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert + var error = root.GetProperty("error"); + Assert.Equal("Conversation not found", error.GetProperty("message").GetString()); + Assert.Equal("invalid_request_error", error.GetProperty("type").GetString()); + } + + #endregion + + #region Integration with Responses API Tests + + [Fact] + public void Deserialize_ResponsesAPIRequestWithConversation_Success() + { + // Arrange + string json = LoadTraceFile("basic/first_message_request.json"); + + // Act + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert - Verify the request has conversation field + Assert.True(root.TryGetProperty("conversation", out var conversation)); + var conversationId = conversation.GetString(); + Assert.NotNull(conversationId); + Assert.StartsWith("conv_", conversationId); + + // Assert - Has standard Responses API fields + Assert.True(root.TryGetProperty("model", out var model)); + Assert.True(root.TryGetProperty("input", out var input)); + Assert.True(root.TryGetProperty("max_output_tokens", out var maxTokens)); + } + + [Fact] + public void Deserialize_ResponsesAPIResponseWithConversation_Success() + { + // Arrange + string json = LoadTraceFile("basic/first_message_response.json"); + + // Act + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert - Verify the response has conversation field + Assert.True(root.TryGetProperty("conversation", out var conversation)); + Assert.Equal(JsonValueKind.Object, conversation.ValueKind); + Assert.True(conversation.TryGetProperty("id", out var conversationId)); + Assert.NotNull(conversationId.GetString()); + + // Assert - Has standard Responses API fields + Assert.True(root.TryGetProperty("id", out var responseId)); + Assert.True(root.TryGetProperty("object", out var obj)); + Assert.Equal("response", obj.GetString()); + Assert.True(root.TryGetProperty("status", out var status)); + Assert.True(root.TryGetProperty("output", out var output)); + } + + [Fact] + public void Deserialize_StreamingResponseWithConversation_Success() + { + // Arrange + string sseContent = LoadTraceFile("basic_streaming/first_message_response.txt"); + + // Act + var events = ParseSseEventsFromContent(sseContent); + + // Assert - At least one event should be present + Assert.NotEmpty(events); + + // Assert - Check if any event has conversation reference + var createdEvent = events.FirstOrDefault(e => + e.TryGetProperty("type", out var type) && + type.GetString() == "response.created"); + + if (!createdEvent.Equals(default(JsonElement))) + { + Assert.True(createdEvent.TryGetProperty("response", out var response)); + // Conversation field may be in the response object + } + } + + [Fact] + public void Deserialize_ImageInputWithConversation_Success() + { + // Arrange + string json = LoadTraceFile("image_input/first_message_request.json"); + + // Act + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert - Verify has conversation and image input + Assert.True(root.TryGetProperty("conversation", out var conversation)); + Assert.True(root.TryGetProperty("input", out var input)); + Assert.Equal(JsonValueKind.Array, input.ValueKind); + } + + [Fact] + public void Deserialize_ToolCallWithConversation_Success() + { + // Arrange + string json = LoadTraceFile("tool_call/first_message_request.json"); + + // Act + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert - Verify has conversation and tools + Assert.True(root.TryGetProperty("conversation", out var conversation)); + Assert.True(root.TryGetProperty("tools", out var tools)); + Assert.Equal(JsonValueKind.Array, tools.ValueKind); + } + + [Fact] + public void Deserialize_RefusalWithConversation_Success() + { + // Arrange + string json = LoadTraceFile("refusal/first_message_request.json"); + + // Act + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Assert - Verify has conversation + Assert.True(root.TryGetProperty("conversation", out var conversation)); + Assert.NotNull(conversation.GetString()); + } + + /// + /// Helper to parse SSE events from a streaming response content string. + /// + private static System.Collections.Generic.List ParseSseEventsFromContent(string sseContent) + { + var events = new System.Collections.Generic.List(); + var lines = sseContent.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].TrimEnd('\r'); + + if (line.StartsWith("event: ", StringComparison.Ordinal)) + { + // Next line should have the data + if (i + 1 < lines.Length) + { + var dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) + { + var jsonData = dataLine.Substring("data: ".Length); + var doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); + } + } + } + } + + return events; + } + + #endregion +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs new file mode 100644 index 0000000000..aa13258c74 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs @@ -0,0 +1,462 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +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; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Integration tests for the HTTP API with in-memory conversation, response, and agent index storage. +/// Tests create a conversation, create a response, wait for completion, then verify the conversation was updated. +/// +public sealed class OpenAIHttpApiIntegrationTests : IAsyncDisposable +{ + private WebApplication? _app; + private HttpClient? _httpClient; + + [Fact] + public async Task CreateConversationAndResponse_NonStreaming_NonBackground_UpdatesConversationWithOutputAsync() + { + // Arrange + const string AgentName = "test-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "The capital of France is Paris."; + const string UserMessage = "What is the capital of France?"; + + HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse); + + // Act - Create conversation + var createConversationRequest = new { metadata = new { agent_id = AgentName } }; + string createConvJson = JsonSerializer.Serialize(createConversationRequest); + HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson); + using var createConvDoc = await this.ParseResponseAsync(createConvResponse); + string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!; + + // Act - Create response (non-streaming, non-background) + var createResponseRequest = new + { + model = AgentName, + conversation = conversationId, + input = UserMessage, + stream = false + }; + string createRespJson = JsonSerializer.Serialize(createResponseRequest); + HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson); + using var createRespDoc = await this.ParseResponseAsync(createRespResponse); + var response = createRespDoc.RootElement; + + // Assert - Response completed + Assert.Equal("completed", response.GetProperty("status").GetString()); + string responseId = response.GetProperty("id").GetString()!; + Assert.NotNull(responseId); + Assert.StartsWith("resp_", responseId); + + // Assert - Response has output + Assert.True(response.TryGetProperty("output", out var output)); + Assert.True(output.GetArrayLength() > 0); + var outputItem = output[0]; + var content = outputItem.GetProperty("content"); + Assert.True(content.GetArrayLength() > 0); + var textContent = content[0]; + Assert.Equal("output_text", textContent.GetProperty("type").GetString()); + Assert.Equal(ExpectedResponse, textContent.GetProperty("text").GetString()); + + // Act - List conversation items to verify they were updated + HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse); + var itemsList = listItemsDoc.RootElement; + + // Assert - Conversation items were added + Assert.Equal("list", itemsList.GetProperty("object").GetString()); + var items = itemsList.GetProperty("data"); + + // Debug output + Console.WriteLine($"Items count: {items.GetArrayLength()}"); + Console.WriteLine($"Response output count: {output.GetArrayLength()}"); + + Assert.True(items.GetArrayLength() > 0, "Conversation should have items after response completion"); + + // Find the assistant message in the items + bool foundAssistantMessage = false; + foreach (var item in items.EnumerateArray()) + { + if (item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + { + var itemContent = item.GetProperty("content"); + if (itemContent.GetArrayLength() > 0) + { + var firstContent = itemContent[0]; + if (firstContent.GetProperty("type").GetString() == "output_text" && + firstContent.GetProperty("text").GetString() == ExpectedResponse) + { + foundAssistantMessage = true; + break; + } + } + } + } + + Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); + } + + [Fact] + public async Task CreateConversationAndResponse_Streaming_NonBackground_UpdatesConversationWithOutputAsync() + { + // Arrange + const string AgentName = "streaming-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Hello there! How can I help you today?"; + const string UserMessage = "Hello"; + + HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse); + + // Act - Create conversation + var createConversationRequest = new { metadata = new { agent_id = AgentName } }; + string createConvJson = JsonSerializer.Serialize(createConversationRequest); + HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson); + using var createConvDoc = await this.ParseResponseAsync(createConvResponse); + string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!; + + // Act - Create response (streaming, non-background) + var createResponseRequest = new + { + model = AgentName, + conversation = conversationId, + input = UserMessage, + stream = true + }; + string createRespJson = JsonSerializer.Serialize(createResponseRequest); + HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson); + + // Assert - Response is SSE format + Assert.Equal("text/event-stream", createRespResponse.Content.Headers.ContentType?.MediaType); + + // Parse SSE events + string sseContent = await createRespResponse.Content.ReadAsStringAsync(); + var events = this.ParseSseEvents(sseContent); + + // Assert - Has expected event types + var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToList(); + Assert.Contains("response.created", eventTypes); + Assert.Contains("response.completed", eventTypes); + + // Collect the full response text from deltas + var deltaEvents = events.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList(); + string streamedText = string.Concat(deltaEvents.Select(e => e.GetProperty("delta").GetString())); + Assert.Equal(ExpectedResponse, streamedText); + + // Act - List conversation items to verify messages were added + HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse); + var itemsList = listItemsDoc.RootElement; + + // Assert - Conversation items were added + var items = itemsList.GetProperty("data"); + Assert.True(items.GetArrayLength() > 0, "Conversation should have items after streaming response completion"); + + // Find the assistant message in the items + bool foundAssistantMessage = false; + foreach (var item in items.EnumerateArray()) + { + if (item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + { + var itemContent = item.GetProperty("content"); + if (itemContent.GetArrayLength() > 0) + { + var firstContent = itemContent[0]; + if (firstContent.GetProperty("type").GetString() == "output_text" && + firstContent.GetProperty("text").GetString() == ExpectedResponse) + { + foundAssistantMessage = true; + break; + } + } + } + } + + Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); + } + + [Fact] + public async Task CreateConversationAndResponse_NonStreaming_Background_UpdatesConversationWhenCompleteAsync() + { + // Arrange + const string AgentName = "background-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Processing in background..."; + const string UserMessage = "Can you process this?"; + + HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse); + + // Act - Create conversation + var createConversationRequest = new { metadata = new { agent_id = AgentName } }; + string createConvJson = JsonSerializer.Serialize(createConversationRequest); + HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson); + using var createConvDoc = await this.ParseResponseAsync(createConvResponse); + string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!; + + // Act - Create response (non-streaming, background) + var createResponseRequest = new + { + model = AgentName, + conversation = conversationId, + input = UserMessage, + stream = false, + background = true + }; + string createRespJson = JsonSerializer.Serialize(createResponseRequest); + HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson); + using var createRespDoc = await this.ParseResponseAsync(createRespResponse); + var response = createRespDoc.RootElement; + + // Assert - Response is in progress or queued + string status = response.GetProperty("status").GetString()!; + Assert.True(status == "in_progress" || status == "queued" || status == "completed", $"Expected 'in_progress', 'queued', or 'completed', got '{status}'"); + string responseId = response.GetProperty("id").GetString()!; + + // Wait for completion by polling + const int MaxAttempts = 20; + int attempt = 0; + string finalStatus = status; + string? errorMessage = null; + while (finalStatus != "completed" && finalStatus != "failed" && attempt < MaxAttempts) + { + await Task.Delay(100); + HttpResponseMessage getResponseResponse = await this.SendGetRequestAsync(client, $"/{AgentName}/v1/responses/{responseId}"); + using var getRespDoc = await this.ParseResponseAsync(getResponseResponse); + finalStatus = getRespDoc.RootElement.GetProperty("status").GetString()!; + if (getRespDoc.RootElement.TryGetProperty("error", out var error) && + error.ValueKind == System.Text.Json.JsonValueKind.Object && + error.TryGetProperty("message", out var messageElement)) + { + errorMessage = messageElement.GetString(); + } + + attempt++; + } + + // Assert - Response eventually completed + Assert.Equal("completed", finalStatus + (errorMessage != null ? $" Error: {errorMessage}" : "")); + + // Act - List conversation items to verify messages were added + HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse); + var itemsList = listItemsDoc.RootElement; + + // Assert - Conversation items were added + var items = itemsList.GetProperty("data"); + Assert.True(items.GetArrayLength() > 0, "Conversation should have items after background response completion"); + + // Find the assistant message in the items + bool foundAssistantMessage = false; + foreach (var item in items.EnumerateArray()) + { + if (item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + { + var itemContent = item.GetProperty("content"); + if (itemContent.GetArrayLength() > 0) + { + var firstContent = itemContent[0]; + if (firstContent.GetProperty("type").GetString() == "output_text" && + firstContent.GetProperty("text").GetString() == ExpectedResponse) + { + foundAssistantMessage = true; + break; + } + } + } + } + + Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); + } + + [Fact] + public async Task CreateConversationAndResponse_Streaming_Background_UpdatesConversationWhenCompleteAsync() + { + // Arrange + const string AgentName = "streaming-background-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Streaming background response"; + const string UserMessage = "Process this with streaming"; + + HttpClient client = await this.CreateTestServerWithInMemoryStorageAsync(AgentName, Instructions, ExpectedResponse); + + // Act - Create conversation + var createConversationRequest = new { metadata = new { agent_id = AgentName } }; + string createConvJson = JsonSerializer.Serialize(createConversationRequest); + HttpResponseMessage createConvResponse = await this.SendPostRequestAsync(client, "/v1/conversations", createConvJson); + using var createConvDoc = await this.ParseResponseAsync(createConvResponse); + string conversationId = createConvDoc.RootElement.GetProperty("id").GetString()!; + + // Act - Create response (streaming, background) + var createResponseRequest = new + { + model = AgentName, + conversation = conversationId, + input = UserMessage, + stream = true, + background = false // Note: streaming with background=true is typically streaming + }; + string createRespJson = JsonSerializer.Serialize(createResponseRequest); + HttpResponseMessage createRespResponse = await this.SendPostRequestAsync(client, $"/{AgentName}/v1/responses", createRespJson); + + // Assert - Response is SSE format + Assert.Equal("text/event-stream", createRespResponse.Content.Headers.ContentType?.MediaType); + + // Parse SSE events + string sseContent = await createRespResponse.Content.ReadAsStringAsync(); + var events = this.ParseSseEvents(sseContent); + var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToList(); + Assert.Contains("response.created", eventTypes); + Assert.Contains("response.completed", eventTypes); + + // Act - List conversation items to verify messages were added + HttpResponseMessage listItemsResponse = await this.SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items"); + using var listItemsDoc = await this.ParseResponseAsync(listItemsResponse); + var itemsList = listItemsDoc.RootElement; + + // Assert - Conversation items were added + var items = itemsList.GetProperty("data"); + Assert.True(items.GetArrayLength() > 0, "Conversation should have items after streaming response completion"); + + // Find the assistant message in the items + bool foundAssistantMessage = false; + foreach (var item in items.EnumerateArray()) + { + if (item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + { + var itemContent = item.GetProperty("content"); + if (itemContent.GetArrayLength() > 0) + { + var firstContent = itemContent[0]; + if (firstContent.GetProperty("type").GetString() == "output_text") + { + foundAssistantMessage = true; + break; + } + } + } + } + + Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); + } + + /// + /// Creates a test server with in-memory conversation, response, and agent index storage. + /// + private async Task CreateTestServerWithInMemoryStorageAsync(string agentName, string instructions, string responseText) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + // Create mock chat client + IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + + // Add agent + builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); + + // Add in-memory storage for conversations, responses, and agent index + builder.Services.AddOpenAIConversations(); + builder.Services.AddOpenAIResponses(); + + this._app = builder.Build(); + + // Map endpoints + AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); + this._app.MapOpenAIConversations(); + this._app.MapOpenAIResponses(agent); + + await this._app.StartAsync(); + + TestServer testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + this._httpClient = testServer.CreateClient(); + return this._httpClient; + } + + /// + /// Sends a POST request with JSON content to the test server. + /// + private async Task SendPostRequestAsync(HttpClient client, string path, string requestJson) + { + StringContent content = new(requestJson, Encoding.UTF8, "application/json"); + return await client.PostAsync(new Uri(path, UriKind.Relative), content); + } + + /// + /// Sends a GET request to the test server. + /// + private async Task SendGetRequestAsync(HttpClient client, string path) + { + return await client.GetAsync(new Uri(path, UriKind.Relative)); + } + + /// + /// Parses the response JSON and returns a JsonDocument. + /// + private async Task ParseResponseAsync(HttpResponseMessage response) + { + string responseJson = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(responseJson); + } + + /// + /// Parses SSE events from streaming response content string. + /// + private JsonElement[] ParseSseEvents(string sseContent) + { + var events = new System.Collections.Generic.List(); + var lines = sseContent.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].TrimEnd('\r'); + + if (line.StartsWith("event: ", StringComparison.Ordinal)) + { + // Next line should have the data + if (i + 1 < lines.Length) + { + var dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) + { + var jsonData = dataLine.Substring("data: ".Length); + if (!string.IsNullOrWhiteSpace(jsonData)) + { + var doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); + } + } + } + } + } + + return events.ToArray(); + } + + public async ValueTask DisposeAsync() + { + this._httpClient?.Dispose(); + if (this._app != null) + { + await this._app.DisposeAsync(); + } + + GC.SuppressFinalize(this); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs new file mode 100644 index 0000000000..804fc0875a --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs @@ -0,0 +1,438 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +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; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Integration tests for the MapOpenAIResponses variant that resolves agents from the Agent.Name property. +/// These tests validate the agent resolution mechanism using the HostedAgentResponseExecutor. +/// +public sealed class OpenAIResponsesAgentResolutionIntegrationTests : IAsyncDisposable +{ + private WebApplication? _app; + private HttpClient? _httpClient; + + public async ValueTask DisposeAsync() + { + this._httpClient?.Dispose(); + if (this._app != null) + { + await this._app.DisposeAsync(); + } + } + + /// + /// Verifies that agent resolution works using the agent.name property in streaming mode. + /// + [Fact] + public async Task CreateResponseStreaming_WithAgentNameProperty_ResolvesCorrectAgentAsync() + { + // Arrange + const string AgentName = "test-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Hello from agent resolution!"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (AgentName, Instructions, ExpectedResponse)); + + // Act - Use raw HTTP request with agent.name specified + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + agent = new { name = AgentName }, + stream = true, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.True(httpResponse.IsSuccessStatusCode, $"Request failed with status {httpResponse.StatusCode}"); + + string responseText = await httpResponse.Content.ReadAsStringAsync(); + Assert.Contains(ExpectedResponse, responseText); + Assert.Contains("response.created", responseText); + Assert.Contains("response.completed", responseText); + } + + /// + /// Verifies that agent resolution works using the agent.name property in non-streaming mode. + /// + [Fact] + public async Task CreateResponse_WithAgentNameProperty_ResolvesCorrectAgentAsync() + { + // Arrange + const string AgentName = "test-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Hello from agent resolution!"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (AgentName, Instructions, ExpectedResponse)); + + // Act - Use raw HTTP request with agent.name specified + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + agent = new { name = AgentName }, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.True(httpResponse.IsSuccessStatusCode, $"Request failed with status {httpResponse.StatusCode}"); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + using JsonDocument doc = JsonDocument.Parse(responseJson); + JsonElement root = doc.RootElement; + + Assert.Equal("completed", root.GetProperty("status").GetString()); + JsonElement outputArray = root.GetProperty("output"); + Assert.True(outputArray.GetArrayLength() > 0); + + JsonElement firstOutput = outputArray[0]; + JsonElement contentArray = firstOutput.GetProperty("content"); + JsonElement firstContent = contentArray[0]; + string actualResponse = firstContent.GetProperty("text").GetString() ?? string.Empty; + + Assert.Equal(ExpectedResponse, actualResponse); + } + + /// + /// Verifies that agent resolution can distinguish between multiple agents. + /// + [Fact] + public async Task CreateResponse_WithMultipleAgents_ResolvesCorrectAgentAsync() + { + // Arrange + const string Agent1Name = "agent-1"; + const string Agent1Response = "Response from agent 1"; + const string Agent2Name = "agent-2"; + const string Agent2Response = "Response from agent 2"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (Agent1Name, "Agent 1 instructions", Agent1Response), + (Agent2Name, "Agent 2 instructions", Agent2Response)); + + // Act - Create response for agent 1 + using StringContent requestContent1 = new(JsonSerializer.Serialize(new + { + agent = new { name = Agent1Name }, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse1 = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent1); + + // Act - Create response for agent 2 + using StringContent requestContent2 = new(JsonSerializer.Serialize(new + { + agent = new { name = Agent2Name }, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse2 = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent2); + + // Assert + string responseJson1 = await httpResponse1.Content.ReadAsStringAsync(); + string responseJson2 = await httpResponse2.Content.ReadAsStringAsync(); + + using JsonDocument doc1 = JsonDocument.Parse(responseJson1); + using JsonDocument doc2 = JsonDocument.Parse(responseJson2); + + string content1 = doc1.RootElement.GetProperty("output")[0].GetProperty("content")[0].GetProperty("text").GetString() ?? string.Empty; + string content2 = doc2.RootElement.GetProperty("output")[0].GetProperty("content")[0].GetProperty("text").GetString() ?? string.Empty; + + Assert.Equal(Agent1Response, content1); + Assert.Equal(Agent2Response, content2); + } + + /// + /// Verifies that agent resolution using the model property works correctly. + /// + [Fact] + public async Task CreateResponse_WithModelProperty_ResolvesCorrectAgentAsync() + { + // Arrange + const string AgentName = "model-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Response via model property"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (AgentName, Instructions, ExpectedResponse)); + + // Act - Use raw HTTP request to control the model property + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + model = AgentName, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.True(httpResponse.IsSuccessStatusCode, $"Request failed with status {httpResponse.StatusCode}"); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + using JsonDocument doc = JsonDocument.Parse(responseJson); + JsonElement root = doc.RootElement; + + Assert.Equal("completed", root.GetProperty("status").GetString()); + JsonElement outputArray = root.GetProperty("output"); + Assert.True(outputArray.GetArrayLength() > 0); + + JsonElement firstOutput = outputArray[0]; + JsonElement contentArray = firstOutput.GetProperty("content"); + JsonElement firstContent = contentArray[0]; + string actualResponse = firstContent.GetProperty("text").GetString() ?? string.Empty; + + Assert.Equal(ExpectedResponse, actualResponse); + } + + /// + /// Verifies that agent resolution fails gracefully when agent is not found. + /// + [Fact] + public async Task CreateResponse_WithNonExistentAgent_ReturnsNotFoundAsync() + { + // Arrange + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + ("existing-agent", "Instructions", "Response")); + + // Act + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + agent = new { name = "non-existent-agent" }, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.NotFound, httpResponse.StatusCode); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + Assert.Contains("non-existent-agent", responseJson); + Assert.Contains("not found", responseJson, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Verifies that agent resolution fails gracefully when no agent name is provided. + /// + [Fact] + public async Task CreateResponse_WithoutAgentOrModel_ReturnsBadRequestAsync() + { + // Arrange + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + ("test-agent", "Instructions", "Response")); + + // Act - Use raw HTTP request without agent.name or model + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.Equal(System.Net.HttpStatusCode.BadRequest, httpResponse.StatusCode); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + Assert.Contains("agent.name", responseJson, StringComparison.OrdinalIgnoreCase); + Assert.Contains("model", responseJson, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Verifies that agent resolution prioritizes agent.name over model when both are provided. + /// + [Fact] + public async Task CreateResponse_WithBothAgentAndModel_UsesAgentNameAsync() + { + // Arrange + const string Agent1Name = "agent-1"; + const string Agent1Response = "Response from agent 1"; + const string Agent2Name = "agent-2"; + const string Agent2Response = "Response from agent 2"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (Agent1Name, "Agent 1 instructions", Agent1Response), + (Agent2Name, "Agent 2 instructions", Agent2Response)); + + // Act - Use raw HTTP request with both agent.name and model + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + agent = new { name = Agent1Name }, + model = Agent2Name, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.True(httpResponse.IsSuccessStatusCode); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + using JsonDocument doc = JsonDocument.Parse(responseJson); + JsonElement root = doc.RootElement; + + JsonElement outputArray = root.GetProperty("output"); + JsonElement firstOutput = outputArray[0]; + JsonElement contentArray = firstOutput.GetProperty("content"); + JsonElement firstContent = contentArray[0]; + string actualResponse = firstContent.GetProperty("text").GetString() ?? string.Empty; + + // Should use agent.name (Agent1Name) and return Agent1Response + Assert.Equal(Agent1Response, actualResponse); + } + + /// + /// Verifies that streaming and non-streaming work correctly with agent resolution. + /// + [Fact] + public async Task CreateResponse_AgentResolution_StreamingAndNonStreamingBothWorkAsync() + { + // 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.CreateTestServerWithAgentResolutionAsync( + (AgentName, Instructions, ExpectedResponse)); + + // Act - Non-streaming + using StringContent nonStreamingRequest = new(JsonSerializer.Serialize(new + { + agent = new { name = AgentName }, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage nonStreamingHttpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), nonStreamingRequest); + + // Act - Streaming + using StringContent streamingRequest = new(JsonSerializer.Serialize(new + { + agent = new { name = AgentName }, + stream = true, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage streamingHttpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), streamingRequest); + + // Assert non-streaming + string nonStreamingJson = await nonStreamingHttpResponse.Content.ReadAsStringAsync(); + using JsonDocument nonStreamingDoc = JsonDocument.Parse(nonStreamingJson); + string nonStreamingContent = nonStreamingDoc.RootElement.GetProperty("output")[0].GetProperty("content")[0].GetProperty("text").GetString() ?? string.Empty; + + // Assert streaming + string streamingText = await streamingHttpResponse.Content.ReadAsStringAsync(); + + Assert.Equal(ExpectedResponse, nonStreamingContent); + Assert.Contains(ExpectedResponse, streamingText); + } + + /// + /// Verifies that the agent.name field is populated in the response. + /// + [Fact] + public async Task CreateResponse_WithAgentName_ResponseIncludesAgentFieldAsync() + { + // Arrange + const string AgentName = "test-agent"; + const string Instructions = "You are a helpful assistant."; + const string ExpectedResponse = "Hello"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (AgentName, Instructions, ExpectedResponse)); + + // Act + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + agent = new { name = AgentName }, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert + Assert.True(httpResponse.IsSuccessStatusCode); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + using JsonDocument doc = JsonDocument.Parse(responseJson); + JsonElement root = doc.RootElement; + + // Verify the response includes the agent field + if (root.TryGetProperty("agent", out JsonElement agentElement)) + { + string? agentNameInResponse = agentElement.GetProperty("name").GetString(); + Assert.Equal(AgentName, agentNameInResponse); + } + } + + private async Task CreateTestServerWithAgentResolutionAsync( + 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.Services.AddOpenAIResponses(); + + this._app = builder.Build(); + + // Use the agent resolution variant - MapOpenAIResponses() without agent parameter + this._app.MapOpenAIResponses(); + + await this._app.StartAsync(); + + TestServer testServer = this._app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + + return testServer.CreateClient(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesConformanceTests.cs index de05ea666d..dacf67fc18 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesConformanceTests.cs @@ -14,7 +14,6 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; /// Conformance tests for OpenAI Responses API implementation behavior. /// Tests use real API traces to ensure our implementation produces responses /// that match OpenAI's wire format when processing actual requests through the server. -/// For pure serialization/deserialization tests, see OpenAIResponsesSerializationTests. /// public sealed class OpenAIResponsesConformanceTests : ConformanceTestBase { @@ -38,18 +37,6 @@ public async Task BasicRequestResponseAsync() using var responseDoc = await ParseResponseAsync(httpResponse); var response = responseDoc.RootElement; - // Parse the request to verify it was sent correctly - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Verify request was properly formatted (structure check) - AssertJsonPropertyEquals(request, "model", "gpt-4o-mini"); - AssertJsonPropertyExists(request, "input"); - AssertJsonPropertyEquals(request, "max_output_tokens", 100); - var input = request.GetProperty("input"); - Assert.Equal(JsonValueKind.String, input.ValueKind); - Assert.Equal("Hello, how are you?", input.GetString()); - // Assert - Response metadata (IDs and timestamps are dynamic, just verify structure) AssertJsonPropertyExists(response, "id"); AssertJsonPropertyEquals(response, "object", "response"); @@ -150,7 +137,7 @@ public async Task BasicRequestResponseAsync() AssertJsonPropertyExists(response, "service_tier"); var serviceTier = response.GetProperty("service_tier").GetString(); Assert.NotNull(serviceTier); - Assert.True(serviceTier == "default" || serviceTier == "auto", + Assert.True(serviceTier is "default" or "auto", $"service_tier should be 'default' or 'auto', got '{serviceTier}'"); AssertJsonPropertyExists(response, "store"); Assert.Equal(JsonValueKind.True, response.GetProperty("store").ValueKind); @@ -169,43 +156,36 @@ public async Task ConversationRequestResponseAsync() .GetProperty("content")[0] .GetProperty("text").GetString()!; - HttpClient client = await this.CreateTestServerAsync("conversation-agent", "You are a helpful assistant.", expectedText); - - // Act - HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "conversation-agent", requestJson); - using var responseDoc = await ParseResponseAsync(httpResponse); - var response = responseDoc.RootElement; - - // Parse the request + // Parse the request to verify it has previous_response_id using var requestDoc = JsonDocument.Parse(requestJson); var request = requestDoc.RootElement; - - // Assert - Request has previous_response_id (structure verification) - AssertJsonPropertyExists(request, "previous_response_id"); var previousResponseId = request.GetProperty("previous_response_id").GetString(); Assert.NotNull(previousResponseId); - Assert.StartsWith("resp_", previousResponseId); Assert.NotEmpty(previousResponseId); - // Assert - Request structure - AssertJsonPropertyEquals(request, "model", "gpt-4o-mini"); - AssertJsonPropertyExists(request, "input"); - AssertJsonPropertyExists(request, "previous_response_id"); - AssertJsonPropertyExists(request, "max_output_tokens"); - var input = request.GetProperty("input"); - Assert.Equal(JsonValueKind.String, input.ValueKind); + // Use stateful mock that tracks conversation state by returning different responses + // First call (initial message) vs second call (conversation continuation) + HttpClient client = await this.CreateTestServerAsync("conversation-agent", "You are a helpful assistant.", expectedText); + + // Act + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "conversation-agent", requestJson); + using var responseDoc = await ParseResponseAsync(httpResponse); + var response = responseDoc.RootElement; // Assert - Response should have previous_response_id field preserved from request AssertJsonPropertyExists(response, "previous_response_id"); var responsePreviousId = response.GetProperty("previous_response_id").GetString(); Assert.Equal(previousResponseId, responsePreviousId); - // Assert - Response has unique ID + // Assert - Response has unique ID (must be different from previous_response_id) var currentId = response.GetProperty("id").GetString(); Assert.NotNull(currentId); Assert.StartsWith("resp_", currentId); + Assert.NotEqual(previousResponseId, currentId); // Assert - Usage includes context from previous response + // The system should pass accumulated conversation history to the chat client, + // resulting in higher input token counts than a single-message request AssertJsonPropertyExists(response, "usage"); var usage = response.GetProperty("usage"); var inputTokens = usage.GetProperty("input_tokens").GetInt32(); @@ -279,112 +259,62 @@ public async Task ToolCallRequestResponseAsync() string functionName = functionCall.GetProperty("name").GetString()!; string arguments = functionCall.GetProperty("arguments").GetString()!; - HttpClient client = await this.CreateTestServerAsync("tool-agent", "You are a helpful assistant.", functionName); + // Use tool call mock that returns FunctionCallContent from the chat client + // This simulates the chat client (e.g., OpenAI) deciding to call a function + // The test validates that our system correctly processes and serializes + // the function call into the OpenAI Responses API format + HttpClient client = await this.CreateTestServerWithToolCallAsync("tool-agent", "You are a helpful assistant.", functionName, arguments); // Act HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "tool-agent", requestJson); using var responseDoc = await ParseResponseAsync(httpResponse); var response = responseDoc.RootElement; - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has tools array - AssertJsonPropertyExists(request, "tools"); - var requestTools = request.GetProperty("tools"); - Assert.Equal(JsonValueKind.Array, requestTools.ValueKind); - Assert.True(requestTools.GetArrayLength() > 0, "Tools array should not be empty"); - - // Assert - Tool has correct structure - var requestTool = requestTools[0]; - AssertJsonPropertyEquals(requestTool, "type", "function"); - AssertJsonPropertyExists(requestTool, "name"); - AssertJsonPropertyExists(requestTool, "description"); - AssertJsonPropertyExists(requestTool, "parameters"); - var requestToolName = requestTool.GetProperty("name").GetString(); - Assert.Equal("get_weather", requestToolName); - - // Assert - Parameters have JSON Schema - var requestParameters = requestTool.GetProperty("parameters"); - AssertJsonPropertyEquals(requestParameters, "type", "object"); - AssertJsonPropertyExists(requestParameters, "properties"); - AssertJsonPropertyExists(requestParameters, "required"); - var requestProperties = requestParameters.GetProperty("properties"); - Assert.Equal(JsonValueKind.Object, requestProperties.ValueKind); - AssertJsonPropertyExists(requestProperties, "location"); - AssertJsonPropertyExists(requestProperties, "unit"); - - // Assert - Property has type and description - var locationProperty = requestProperties.GetProperty("location"); - AssertJsonPropertyEquals(locationProperty, "type", "string"); - AssertJsonPropertyExists(locationProperty, "description"); - var description = locationProperty.GetProperty("description").GetString(); - Assert.NotNull(description); - Assert.NotEmpty(description); - - // Assert - Required fields is array - var requestRequired = requestParameters.GetProperty("required"); - Assert.Equal(JsonValueKind.Array, requestRequired.ValueKind); - var requestRequiredFields = requestRequired.EnumerateArray().Select(e => e.GetString()).ToList(); - Assert.Contains("location", requestRequiredFields); - - // Assert - Request has tool choice - AssertJsonPropertyExists(request, "tool_choice"); - var toolChoice = request.GetProperty("tool_choice").GetString(); - Assert.Equal("auto", toolChoice); - - // Assert - Response has function call output (or text output depending on implementation) + // Assert - Response has function call output AssertJsonPropertyExists(response, "output"); var output = response.GetProperty("output"); Assert.Equal(JsonValueKind.Array, output.ValueKind); Assert.True(output.GetArrayLength() > 0); var responseItem = output[0]; - // Our implementation may return either function_call or message type + // Assert - Response item type is function_call (system properly converted FunctionCallContent) var itemType = responseItem.GetProperty("type").GetString(); - if (itemType == "function_call") - { - AssertJsonPropertyEquals(responseItem, "type", "function_call"); - - // Assert - Function call has name - AssertJsonPropertyExists(responseItem, "name"); - var funcName = responseItem.GetProperty("name").GetString(); - Assert.Equal("get_weather", funcName); - - // Assert - Function call has arguments - AssertJsonPropertyExists(responseItem, "arguments"); - var argsString = responseItem.GetProperty("arguments").GetString(); - Assert.NotNull(argsString); - Assert.NotEmpty(argsString); - var argsDoc = JsonDocument.Parse(argsString); - var argsRoot = argsDoc.RootElement; - AssertJsonPropertyExists(argsRoot, "location"); - var location = argsRoot.GetProperty("location").GetString(); - Assert.Contains("San Francisco", location); - } - - if (itemType == "function_call") - { - // Assert - Function call has call_id and id - AssertJsonPropertyExists(responseItem, "call_id"); - var callId = responseItem.GetProperty("call_id").GetString(); - Assert.NotNull(callId); - Assert.NotEmpty(callId); - Assert.StartsWith("call_", callId); - AssertJsonPropertyExists(responseItem, "id"); - var itemId = responseItem.GetProperty("id").GetString(); - Assert.NotNull(itemId); - Assert.NotEmpty(itemId); - Assert.StartsWith("fc_", itemId); - - // Assert - Function call has status - AssertJsonPropertyExists(responseItem, "status"); - var itemStatus = responseItem.GetProperty("status").GetString(); - Assert.Equal("completed", itemStatus); - } - - // Assert - Response preserves tool definitions + AssertJsonPropertyEquals(responseItem, "type", "function_call"); + + // Assert - Function call has correct name (from chat client) + AssertJsonPropertyExists(responseItem, "name"); + var funcName = responseItem.GetProperty("name").GetString(); + Assert.Equal("get_weather", funcName); + + // Assert - Function call has arguments (properly serialized from chat client response) + AssertJsonPropertyExists(responseItem, "arguments"); + var argsString = responseItem.GetProperty("arguments").GetString(); + Assert.NotNull(argsString); + Assert.NotEmpty(argsString); + var argsDoc = JsonDocument.Parse(argsString); + var argsRoot = argsDoc.RootElement; + AssertJsonPropertyExists(argsRoot, "location"); + var location = argsRoot.GetProperty("location").GetString(); + Assert.Contains("San Francisco", location); + + // Assert - Function call has call_id and id (system generates these) + AssertJsonPropertyExists(responseItem, "call_id"); + var callId = responseItem.GetProperty("call_id").GetString(); + Assert.NotNull(callId); + Assert.NotEmpty(callId); + Assert.StartsWith("call_", callId); + AssertJsonPropertyExists(responseItem, "id"); + var itemId = responseItem.GetProperty("id").GetString(); + Assert.NotNull(itemId); + Assert.NotEmpty(itemId); + Assert.StartsWith("func_", itemId); + + // Assert - Function call has status + AssertJsonPropertyExists(responseItem, "status"); + var itemStatus = responseItem.GetProperty("status").GetString(); + Assert.Equal("completed", itemStatus); + + // Assert - Response preserves tool definitions from request var responseTools = response.GetProperty("tools"); Assert.Equal(JsonValueKind.Array, responseTools.ValueKind); Assert.True(responseTools.GetArrayLength() > 0); @@ -394,7 +324,7 @@ public async Task ToolCallRequestResponseAsync() AssertJsonPropertyExists(responseTool, "description"); AssertJsonPropertyExists(responseTool, "parameters"); - // Assert - Response has usage statistics + // Assert - Response has usage statistics (includes tool definition overhead) AssertJsonPropertyExists(response, "usage"); var usage = response.GetProperty("usage"); var inputTokens = usage.GetProperty("input_tokens").GetInt32(); @@ -448,13 +378,6 @@ public async Task StreamingRequestResponseAsync() string responseSse = await httpResponse.Content.ReadAsStringAsync(); var events = ParseSseEventsFromContent(responseSse); - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has stream flag - AssertJsonPropertyEquals(request, "stream", true); - // Assert - Response is valid SSE format var lines = responseSse.Split('\n'); Assert.NotEmpty(lines); @@ -504,7 +427,7 @@ public async Task StreamingRequestResponseAsync() Assert.Equal("response.created", eventTypes[0]); Assert.Equal("response.in_progress", eventTypes[1]); var lastEvent = eventTypes[^1]; - Assert.True(lastEvent == "response.completed" || lastEvent == "response.incomplete", + Assert.True(lastEvent is "response.completed" or "response.incomplete", $"Last event should be terminal state, got: {lastEvent}"); // Assert - Created event has response object @@ -577,13 +500,13 @@ public async Task StreamingRequestResponseAsync() var finalEvent = events.FirstOrDefault(e => { var type = e.GetProperty("type").GetString(); - return type == "response.completed" || type == "response.incomplete"; + return type is "response.completed" or "response.incomplete"; }); Assert.False(finalEvent.Equals(default(JsonElement)), "Should have a terminal response event"); AssertJsonPropertyExists(finalEvent, "response"); var finalResponse = finalEvent.GetProperty("response"); var finalStatus = finalResponse.GetProperty("status").GetString(); - Assert.True(finalStatus == "completed" || finalStatus == "incomplete", + Assert.True(finalStatus is "completed" or "incomplete", $"Status should be completed or incomplete, got: {finalStatus}"); AssertJsonPropertyExists(finalResponse, "output"); var finalOutput = finalResponse.GetProperty("output"); @@ -650,39 +573,6 @@ public async Task MetadataRequestResponseAsync() using var responseDoc = await ParseResponseAsync(httpResponse); var response = responseDoc.RootElement; - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has metadata object - AssertJsonPropertyExists(request, "metadata"); - var requestMetadata = request.GetProperty("metadata"); - Assert.Equal(JsonValueKind.Object, requestMetadata.ValueKind); - - // Assert - Request has custom metadata fields - AssertJsonPropertyEquals(requestMetadata, "user_id", "test_user_123"); - AssertJsonPropertyEquals(requestMetadata, "session_id", "session_456"); - AssertJsonPropertyEquals(requestMetadata, "purpose", "conformance_test"); - - // Assert - Request has instructions - AssertJsonPropertyExists(request, "instructions"); - var requestInstructions = request.GetProperty("instructions").GetString(); - Assert.NotNull(requestInstructions); - Assert.NotEmpty(requestInstructions); - Assert.Equal("Respond in a friendly, educational tone.", requestInstructions); - - // Assert - Request has temperature parameter - AssertJsonPropertyExists(request, "temperature"); - var requestTemperature = request.GetProperty("temperature").GetDouble(); - Assert.Equal(0.7, requestTemperature); - Assert.InRange(requestTemperature, 0.0, 2.0); - - // Assert - Request has top_p parameter - AssertJsonPropertyExists(request, "top_p"); - var requestTopP = request.GetProperty("top_p").GetDouble(); - Assert.Equal(0.9, requestTopP); - Assert.InRange(requestTopP, 0.0, 1.0); - // Assert - Response preserves metadata var responseMetadata = response.GetProperty("metadata"); AssertJsonPropertyEquals(responseMetadata, "user_id", "test_user_123"); @@ -690,22 +580,21 @@ public async Task MetadataRequestResponseAsync() AssertJsonPropertyEquals(responseMetadata, "purpose", "conformance_test"); // Assert - Response preserves instructions - var responseInstructions = response.GetProperty("instructions").GetString(); - Assert.Equal(requestInstructions, responseInstructions); + AssertJsonPropertyEquals(response, "instructions", "Respond in a friendly, educational tone."); // Assert - Response preserves temperature var responseTemperature = response.GetProperty("temperature").GetDouble(); - Assert.Equal(requestTemperature, responseTemperature); + Assert.Equal(0.7, responseTemperature); // Assert - Response preserves top_p var responseTopP = response.GetProperty("top_p").GetDouble(); - Assert.Equal(requestTopP, responseTopP); + Assert.Equal(0.9, responseTopP); // Assert - Response status (may be incomplete if max_output_tokens was respected) AssertJsonPropertyExists(response, "status"); var status = response.GetProperty("status").GetString(); // Our implementation may complete even with max_output_tokens if response fits - Assert.True(status == "completed" || status == "incomplete"); + Assert.True(status is "completed" or "incomplete"); // Assert - Response has incomplete_details field AssertJsonPropertyExists(response, "incomplete_details"); @@ -770,25 +659,28 @@ public async Task ReasoningRequestResponseAsync() .GetProperty("content")[0] .GetProperty("text").GetString()!; - HttpClient client = await this.CreateTestServerAsync("reasoning-agent", "You are a helpful assistant.", expectedText); + // Get expected reasoning summary text (if any) + var reasoningSummary = expectedResponse.GetProperty("output")[0].GetProperty("summary"); + string reasoningText = reasoningSummary.GetArrayLength() > 0 + ? reasoningSummary[0].GetProperty("text").GetString()! + : "Thinking about the problem..."; + + // Create a custom content provider that returns reasoning content followed by regular text + HttpClient client = await this.CreateTestServerAsync( + "reasoning-agent", + "You are a helpful assistant.", + expectedText, + contentProvider: _ => + [ + new Extensions.AI.TextReasoningContent(reasoningText), + new Extensions.AI.TextContent(expectedText) + ]); // Act HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "reasoning-agent", requestJson); using var responseDoc = await ParseResponseAsync(httpResponse); var response = responseDoc.RootElement; - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has reasoning configuration - AssertJsonPropertyExists(request, "reasoning"); - var requestReasoning = request.GetProperty("reasoning"); - Assert.Equal(JsonValueKind.Object, requestReasoning.ValueKind); - AssertJsonPropertyExists(requestReasoning, "effort"); - var effort = requestReasoning.GetProperty("effort").GetString(); - Assert.Equal("medium", effort); - // Assert - Response preserves reasoning configuration AssertJsonPropertyExists(response, "reasoning"); var responseReasoning = response.GetProperty("reasoning"); @@ -859,30 +751,6 @@ public async Task JsonOutputRequestResponseAsync() using var responseDoc = await ParseResponseAsync(httpResponse); var response = responseDoc.RootElement; - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has text format with json_schema - AssertJsonPropertyExists(request, "text"); - var requestText = request.GetProperty("text"); - AssertJsonPropertyExists(requestText, "format"); - var format = requestText.GetProperty("format"); - AssertJsonPropertyEquals(format, "type", "json_schema"); - AssertJsonPropertyEquals(format, "name", "person"); - AssertJsonPropertyEquals(format, "strict", true); - - // Assert - Schema has correct structure - AssertJsonPropertyExists(format, "schema"); - var schema = format.GetProperty("schema"); - AssertJsonPropertyEquals(schema, "type", "object"); - AssertJsonPropertyExists(schema, "properties"); - AssertJsonPropertyExists(schema, "required"); - var properties = schema.GetProperty("properties"); - AssertJsonPropertyExists(properties, "name"); - AssertJsonPropertyExists(properties, "age"); - AssertJsonPropertyExists(properties, "occupation"); - // Assert - Response preserves text format configuration AssertJsonPropertyExists(response, "text"); var responseText = response.GetProperty("text"); @@ -903,6 +771,7 @@ public async Task JsonOutputRequestResponseAsync() Assert.Equal(expectedText, text); // Assert - Output text is valid JSON matching schema + // This validates that the mock/system produced well-formed JSON output using var jsonDoc = JsonDocument.Parse(text); var jsonRoot = jsonDoc.RootElement; AssertJsonPropertyExists(jsonRoot, "name"); @@ -1002,38 +871,6 @@ public async Task ImageInputRequestResponseAsync() using var responseDoc = await ParseResponseAsync(httpResponse); var response = responseDoc.RootElement; - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has input array with message - AssertJsonPropertyExists(request, "input"); - var input = request.GetProperty("input"); - Assert.Equal(JsonValueKind.Array, input.ValueKind); - Assert.True(input.GetArrayLength() > 0); - - // Assert - Input message has content with image - var inputMessage = input[0]; - AssertJsonPropertyEquals(inputMessage, "type", "message"); - AssertJsonPropertyEquals(inputMessage, "role", "user"); - AssertJsonPropertyExists(inputMessage, "content"); - var inputContent = inputMessage.GetProperty("content"); - Assert.Equal(JsonValueKind.Array, inputContent.ValueKind); - Assert.True(inputContent.GetArrayLength() >= 2, "Content should have text and image"); - - // Assert - Content has input_text - var textPart = inputContent[0]; - AssertJsonPropertyEquals(textPart, "type", "input_text"); - AssertJsonPropertyExists(textPart, "text"); - - // Assert - Content has input_image - var imagePart = inputContent[1]; - AssertJsonPropertyEquals(imagePart, "type", "input_image"); - AssertJsonPropertyExists(imagePart, "image_url"); - var imageUrl = imagePart.GetProperty("image_url").GetString(); - Assert.NotNull(imageUrl); - Assert.NotEmpty(imageUrl); - // Assert - Response has output AssertJsonPropertyExists(response, "output"); var output = response.GetProperty("output"); @@ -1078,16 +915,6 @@ public async Task ReasoningStreamingRequestResponseAsync() string responseSse = await httpResponse.Content.ReadAsStringAsync(); var events = ParseSseEventsFromContent(responseSse); - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has stream flag and reasoning configuration - AssertJsonPropertyEquals(request, "stream", true); - AssertJsonPropertyExists(request, "reasoning"); - var reasoning = request.GetProperty("reasoning"); - AssertJsonPropertyExists(reasoning, "effort"); - // Assert - Response has event types for reasoning var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); Assert.Contains("response.created", eventTypes); @@ -1120,7 +947,7 @@ public async Task ReasoningStreamingRequestResponseAsync() var finalEvent = events.FirstOrDefault(e => { var type = e.GetProperty("type").GetString(); - return type == "response.completed" || type == "response.incomplete"; + return type is "response.completed" or "response.incomplete"; }); Assert.False(finalEvent.Equals(default(JsonElement))); var finalResponse = finalEvent.GetProperty("response"); @@ -1156,17 +983,6 @@ public async Task JsonOutputStreamingRequestResponseAsync() string responseSse = await httpResponse.Content.ReadAsStringAsync(); var events = ParseSseEventsFromContent(responseSse); - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has stream flag and json_schema format - AssertJsonPropertyEquals(request, "stream", true); - AssertJsonPropertyExists(request, "text"); - var text = request.GetProperty("text"); - var format = text.GetProperty("format"); - AssertJsonPropertyEquals(format, "type", "json_schema"); - // Assert - Response has standard streaming events var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); Assert.Contains("response.created", eventTypes); @@ -1176,7 +992,7 @@ public async Task JsonOutputStreamingRequestResponseAsync() var finalEvent = events.FirstOrDefault(e => { var type = e.GetProperty("type").GetString(); - return type == "response.completed" || type == "response.incomplete"; + return type is "response.completed" or "response.incomplete"; }); Assert.False(finalEvent.Equals(default(JsonElement))); var finalResponse = finalEvent.GetProperty("response"); @@ -1216,13 +1032,6 @@ public async Task RefusalStreamingRequestResponseAsync() string responseSse = await httpResponse.Content.ReadAsStringAsync(); var events = ParseSseEventsFromContent(responseSse); - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has stream flag - AssertJsonPropertyEquals(request, "stream", true); - // Assert - Response has standard streaming events var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); Assert.Contains("response.created", eventTypes); @@ -1232,12 +1041,12 @@ public async Task RefusalStreamingRequestResponseAsync() var finalEvent = events.FirstOrDefault(e => { var type = e.GetProperty("type").GetString(); - return type == "response.completed" || type == "response.incomplete"; + return type is "response.completed" or "response.incomplete"; }); Assert.False(finalEvent.Equals(default(JsonElement))); var finalResponse = finalEvent.GetProperty("response"); var status = finalResponse.GetProperty("status").GetString(); - Assert.True(status == "completed" || status == "incomplete"); + Assert.True(status is "completed" or "incomplete"); // Assert - Text done has refusal content var doneEvent = events.First(e => e.GetProperty("type").GetString() == "response.output_text.done"); @@ -1273,30 +1082,6 @@ public async Task ImageInputStreamingRequestResponseAsync() string responseSse = await httpResponse.Content.ReadAsStringAsync(); var events = ParseSseEventsFromContent(responseSse); - // Parse the request - using var requestDoc = JsonDocument.Parse(requestJson); - var request = requestDoc.RootElement; - - // Assert - Request has stream flag - AssertJsonPropertyEquals(request, "stream", true); - - // Assert - Request has input array with image - AssertJsonPropertyExists(request, "input"); - var input = request.GetProperty("input"); - Assert.Equal(JsonValueKind.Array, input.ValueKind); - var inputMessage = input[0]; - var inputContent = inputMessage.GetProperty("content"); - bool hasImage = false; - foreach (var part in inputContent.EnumerateArray()) - { - if (part.GetProperty("type").GetString() == "input_image") - { - hasImage = true; - break; - } - } - Assert.True(hasImage, "Request should have input_image content"); - // Assert - Response has standard streaming events var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!); Assert.Contains("response.created", eventTypes); @@ -1306,7 +1091,7 @@ public async Task ImageInputStreamingRequestResponseAsync() var finalEvent = events.FirstOrDefault(e => { var type = e.GetProperty("type").GetString(); - return type == "response.completed" || type == "response.incomplete"; + return type is "response.completed" or "response.incomplete"; }); Assert.False(finalEvent.Equals(default(JsonElement))); var finalResponse = finalEvent.GetProperty("response"); @@ -1319,9 +1104,35 @@ public async Task ImageInputStreamingRequestResponseAsync() Assert.NotEmpty(finalText); } - /// - /// Helper to parse SSE events from a streaming response content string. - /// + [Fact] + public async Task MutualExclusiveErrorAsync() + { + // Arrange + string requestJson = LoadTraceFile("mutual_exclusive_error/request.json"); + using var expectedResponseDoc = LoadTraceDocument("mutual_exclusive_error/response.json"); + + HttpClient client = await this.CreateTestServerAsync("mutual-exclusive-agent", "You are a helpful assistant.", "Test response"); + + // Act - Send request with mutually exclusive parameters + HttpResponseMessage httpResponse = await this.SendRequestAsync(client, "mutual-exclusive-agent", requestJson); + using var responseDoc = await ParseResponseAsync(httpResponse); + var response = responseDoc.RootElement; + + // Assert - Should return 400 + Assert.Equal(System.Net.HttpStatusCode.BadRequest, httpResponse.StatusCode); + + // Assert - Error response structure + AssertJsonPropertyExists(response, "error"); + var error = response.GetProperty("error"); + AssertJsonPropertyExists(error, "message"); + AssertJsonPropertyExists(error, "type"); + AssertJsonPropertyExists(error, "code"); + + var errorMessage = error.GetProperty("message").GetString(); + Assert.NotNull(errorMessage); + Assert.Contains("mutually exclusive", errorMessage, StringComparison.OrdinalIgnoreCase); + } + private static List ParseSseEventsFromContent(string sseContent) { var events = new List(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesIntegrationTests.cs index 053387b53b..5e79d5841d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesIntegrationTests.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using OpenAI; using OpenAI.Responses; @@ -1132,7 +1131,7 @@ private async Task CreateTestServerWithCustomClientAsync(string agen builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}"); - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); @@ -1159,7 +1158,7 @@ private async Task CreateTestServerWithMultipleAgentsAsync( builder.AddAIAgent(name, instructions, chatClientServiceKey: $"chat-client-{name}"); } - builder.AddOpenAIResponses(); + builder.Services.AddOpenAIResponses(); this._app = builder.Build(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs index d487450248..bb3a5870d8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs @@ -25,7 +25,7 @@ public void Deserialize_BasicRequest_Success() string json = LoadTraceFile("basic/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -41,9 +41,9 @@ public void Deserialize_BasicRequest_RoundTrip() string originalJson = LoadTraceFile("basic/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.CreateResponse); - string reserializedJson = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); - CreateResponse? roundtripped = JsonSerializer.Deserialize(reserializedJson, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.CreateResponse); + string reserializedJson = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); + CreateResponse? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -59,7 +59,7 @@ public void Deserialize_StreamingRequest_HasStreamFlag() string json = LoadTraceFile("streaming/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -74,7 +74,7 @@ public void Deserialize_ConversationRequest_HasPreviousResponseId() string json = LoadTraceFile("conversation/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -89,7 +89,7 @@ public void Deserialize_MetadataRequest_HasAllParameters() string json = LoadTraceFile("metadata/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -142,7 +142,7 @@ public void Serialize_CreateMinimalRequest_MatchesFormat() }; // Act - string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); + string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; @@ -151,7 +151,7 @@ public void Serialize_CreateMinimalRequest_MatchesFormat() Assert.True(root.TryGetProperty("input", out var input)); // Input can be string or object - verify one exists - Assert.True(input.ValueKind == JsonValueKind.String || input.ValueKind == JsonValueKind.Object); + Assert.True(input.ValueKind is JsonValueKind.String or JsonValueKind.Object); } [Fact] @@ -175,7 +175,7 @@ public void Serialize_CreateRequestWithOptions_IncludesAllFields() }; // Act - string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); + string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; @@ -203,7 +203,7 @@ public void Serialize_NullableFields_AreOmittedWhenNull() }; // Act - string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); + string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; @@ -223,7 +223,7 @@ public void Deserialize_ImageInputRequest_HasImageData() string json = LoadTraceFile("image_input/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -237,7 +237,7 @@ public void Deserialize_ImageInputStreamingRequest_HasStreamAndImage() string json = LoadTraceFile("image_input_streaming/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -252,7 +252,7 @@ public void Deserialize_JsonOutputRequest_HasJsonSchema() string json = LoadTraceFile("json_output/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -263,7 +263,7 @@ public void Deserialize_JsonOutputRequest_HasJsonSchema() var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)request.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); Assert.NotNull(jsonSchemaFormat.Name); - Assert.NotNull(jsonSchemaFormat.Schema); + Assert.NotEqual(default, jsonSchemaFormat.Schema); } [Fact] @@ -273,7 +273,7 @@ public void Deserialize_JsonOutputStreamingRequest_HasJsonSchemaAndStream() string json = LoadTraceFile("json_output_streaming/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -293,7 +293,7 @@ public void Deserialize_ReasoningRequest_HasReasoningConfiguration() string json = LoadTraceFile("reasoning/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -307,7 +307,7 @@ public void Deserialize_ReasoningStreamingRequest_HasReasoningAndStream() string json = LoadTraceFile("reasoning_streaming/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -322,7 +322,7 @@ public void Deserialize_RefusalRequest_CanBeDeserialized() string json = LoadTraceFile("refusal/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -336,7 +336,7 @@ public void Deserialize_RefusalStreamingRequest_HasStream() string json = LoadTraceFile("refusal_streaming/request.json"); // Act - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); @@ -370,7 +370,7 @@ public void Deserialize_AllRequests_CanBeDeserialized() string json = LoadTraceFile(path); // Act & Assert - Should not throw - CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); + CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); Assert.NotNull(request); Assert.NotNull(request.Input); } @@ -387,7 +387,7 @@ public void Deserialize_BasicResponse_Success() string json = LoadTraceFile("basic/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -406,7 +406,7 @@ public void Deserialize_BasicResponse_HasCorrectOutput() string json = LoadTraceFile("basic/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -417,7 +417,7 @@ public void Deserialize_BasicResponse_HasCorrectOutput() Assert.NotNull(outputItem); // Verify it's a message type - using var doc = JsonDocument.Parse(JsonSerializer.Serialize(outputItem, Responses.ResponsesJsonContext.Default.ItemResource)); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(outputItem, OpenAIHostingJsonContext.Default.ItemResource)); var root = doc.RootElement; Assert.Equal("message", root.GetProperty("type").GetString()); } @@ -429,7 +429,7 @@ public void Deserialize_BasicResponse_HasCorrectUsage() string json = LoadTraceFile("basic/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -448,7 +448,7 @@ public void Deserialize_ConversationResponse_HasPreviousResponseId() string json = LoadTraceFile("conversation/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -464,7 +464,7 @@ public void Deserialize_MetadataResponse_PreservesMetadata() string json = LoadTraceFile("metadata/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -481,7 +481,7 @@ public void Deserialize_MetadataResponse_HasIncompleteStatus() string json = LoadTraceFile("metadata/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -497,7 +497,7 @@ public void Deserialize_MetadataResponse_HasInstructions() string json = LoadTraceFile("metadata/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -512,7 +512,7 @@ public void Deserialize_MetadataResponse_HasModelParameters() string json = LoadTraceFile("metadata/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -528,7 +528,7 @@ public void Deserialize_ToolCallResponse_HasFunctionCall() string json = LoadTraceFile("tool_call/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -536,7 +536,7 @@ public void Deserialize_ToolCallResponse_HasFunctionCall() Assert.Single(response.Output); // Verify the output is a function_call type - using var doc = JsonDocument.Parse(JsonSerializer.Serialize(response.Output[0], Responses.ResponsesJsonContext.Default.ItemResource)); + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(response.Output[0], OpenAIHostingJsonContext.Default.ItemResource)); var root = doc.RootElement; Assert.Equal("function_call", root.GetProperty("type").GetString()); Assert.Equal("get_weather", root.GetProperty("name").GetString()); @@ -552,7 +552,7 @@ public void Deserialize_ToolCallResponse_HasToolDefinitions() string json = LoadTraceFile("tool_call/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -576,7 +576,7 @@ public void Deserialize_ImageInputResponse_HasImageInInput() string json = LoadTraceFile("image_input/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -591,7 +591,7 @@ public void Deserialize_JsonOutputResponse_HasStructuredOutput() string json = LoadTraceFile("json_output/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -611,7 +611,7 @@ public void Deserialize_ReasoningResponse_HasReasoningItems() string json = LoadTraceFile("reasoning/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -627,7 +627,7 @@ public void Deserialize_RefusalResponse_HasRefusalContent() string json = LoadTraceFile("refusal/response.json"); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -656,7 +656,7 @@ public void Deserialize_AllResponses_HaveRequiredFields() string json = LoadTraceFile(path); // Act - Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -675,9 +675,9 @@ public void Deserialize_ResponseRoundTrip_PreservesData() string originalJson = LoadTraceFile("basic/response.json"); // Act - Deserialize and re-serialize - Response? response = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.Response); - string reserializedJson = JsonSerializer.Serialize(response, Responses.ResponsesJsonContext.Default.Response); - Response? roundtripped = JsonSerializer.Deserialize(reserializedJson, Responses.ResponsesJsonContext.Default.Response); + Response? response = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.Response); + string reserializedJson = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.Response); + Response? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); @@ -742,7 +742,7 @@ public void ParseStreamingEvents_DeserializeCreatedEvent_Success() // Act string jsonString = createdEventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -764,7 +764,7 @@ public void ParseStreamingEvents_DeserializeInProgressEvent_Success() // Act string jsonString = inProgressEventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -785,7 +785,7 @@ public void ParseStreamingEvents_DeserializeOutputItemAdded_Success() // Act string jsonString = itemAddedJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -805,7 +805,7 @@ public void ParseStreamingEvents_DeserializeContentPartAdded_Success() // Act string jsonString = partAddedJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -827,7 +827,7 @@ public void ParseStreamingEvents_DeserializeTextDelta_Success() // Act string jsonString = textDeltaJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -853,7 +853,7 @@ public void ParseStreamingEvents_AccumulateTextDeltas_MatchesFinalText() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); if (evt is StreamingOutputTextDelta delta) { @@ -885,7 +885,7 @@ public void ParseStreamingEvents_SequenceNumbersAreSequential() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); sequenceNumbers.Add(evt.SequenceNumber); } @@ -910,15 +910,15 @@ public void ParseStreamingEvents_FinalEvent_IsTerminalState() // Act string jsonString = lastEventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); // Should be one of the terminal events - bool isTerminal = evt is StreamingResponseCompleted || - evt is StreamingResponseIncomplete || - evt is StreamingResponseFailed; + bool isTerminal = evt is StreamingResponseCompleted or + StreamingResponseIncomplete or + StreamingResponseFailed; Assert.True(isTerminal, $"Expected terminal event, got: {evt.GetType().Name}"); } @@ -935,7 +935,7 @@ public void ParseStreamingEvents_ImageInputStreaming_HasImageEvents() Assert.NotEmpty(events); Assert.All(events, evt => { - StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } @@ -953,7 +953,7 @@ public void ParseStreamingEvents_JsonOutputStreaming_HasJsonSchemaEvents() Assert.NotEmpty(events); Assert.All(events, evt => { - StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } @@ -974,7 +974,7 @@ public void ParseStreamingEvents_ReasoningStreaming_HasReasoningEvents() Assert.Contains("response.created", eventTypes); Assert.All(events, evt => { - StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } @@ -994,7 +994,7 @@ public void ParseStreamingEvents_RefusalStreaming_HasRefusalEvents() // Should have refusal-related events Assert.All(events, evt => { - StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } @@ -1020,7 +1020,7 @@ public void ParseStreamingEvents_AllStreamingTraces_CanBeDeserialized() foreach (var eventJson in ParseSseEventsFromContent(sseContent)) { // Should not throw - StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); } } @@ -1036,24 +1036,24 @@ public void ParseStreamingEvents_AllEvents_CanBeDeserialized() foreach (var eventJson in ParseSseEventsFromContent(sseContent)) { // Should not throw - StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); // Verify polymorphic deserialization worked Assert.True( - evt is StreamingResponseCreated || - evt is StreamingResponseInProgress || - evt is StreamingResponseCompleted || - evt is StreamingResponseIncomplete || - evt is StreamingResponseFailed || - evt is StreamingOutputItemAdded || - evt is StreamingOutputItemDone || - evt is StreamingContentPartAdded || - evt is StreamingContentPartDone || - evt is StreamingOutputTextDelta || - evt is StreamingOutputTextDone || - evt is StreamingFunctionCallArgumentsDelta || - evt is StreamingFunctionCallArgumentsDone, + evt is StreamingResponseCreated or + StreamingResponseInProgress or + StreamingResponseCompleted or + StreamingResponseIncomplete or + StreamingResponseFailed or + StreamingOutputItemAdded or + StreamingOutputItemDone or + StreamingContentPartAdded or + StreamingContentPartDone or + StreamingOutputTextDelta or + StreamingOutputTextDone or + StreamingFunctionCallArgumentsDelta or + StreamingFunctionCallArgumentsDone, $"Unknown event type: {evt.GetType().Name}"); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/SortOrderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/SortOrderExtensionsTests.cs new file mode 100644 index 0000000000..426c2d2bc9 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/SortOrderExtensionsTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; +using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Unit tests for SortOrderExtensions. +/// +public sealed class SortOrderExtensionsTests +{ + [Fact] + public void ToOrderString_Ascending_ReturnsAsc() + { + // Arrange + const SortOrder Order = SortOrder.Ascending; + + // Act + string result = Order.ToOrderString(); + + // Assert + Assert.Equal("asc", result); + } + + [Fact] + public void ToOrderString_Descending_ReturnsDesc() + { + // Arrange + const SortOrder Order = SortOrder.Descending; + + // Act + string result = Order.ToOrderString(); + + // Assert + Assert.Equal("desc", result); + } + + [Fact] + public void IsAscending_Ascending_ReturnsTrue() + { + // Arrange + const SortOrder Order = SortOrder.Ascending; + + // Act + bool result = Order.IsAscending(); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsAscending_Descending_ReturnsFalse() + { + // Arrange + const SortOrder Order = SortOrder.Descending; + + // Act + bool result = Order.IsAscending(); + + // Assert + Assert.False(result); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/StreamingEventConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/StreamingEventConformanceTests.cs index 22e3838a67..2358fd0174 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/StreamingEventConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/StreamingEventConformanceTests.cs @@ -108,9 +108,9 @@ public async Task ParseStreamingEvents_HasCorrectEventTypesAsync() // Assert - Last event should be a terminal state string lastEventType = eventTypes[^1]; Assert.True( - lastEventType == "response.completed" || - lastEventType == "response.incomplete" || - lastEventType == "response.failed", + lastEventType is "response.completed" or + "response.incomplete" or + "response.failed", $"Last event should be a terminal state, got: {lastEventType}"); } @@ -135,7 +135,7 @@ public async Task ParseStreamingEvents_DeserializeCreatedEvent_SuccessAsync() // Act string jsonString = createdEventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -168,7 +168,7 @@ public async Task ParseStreamingEvents_DeserializeInProgressEvent_SuccessAsync() // Act string jsonString = inProgressEventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -200,7 +200,7 @@ public async Task ParseStreamingEvents_DeserializeOutputItemAdded_SuccessAsync() // Act string jsonString = itemAddedJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -231,7 +231,7 @@ public async Task ParseStreamingEvents_DeserializeContentPartAdded_SuccessAsync( // Act string jsonString = partAddedJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -264,7 +264,7 @@ public async Task ParseStreamingEvents_DeserializeTextDelta_SuccessAsync() // Act string jsonString = textDeltaJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); @@ -301,7 +301,7 @@ public async Task ParseStreamingEvents_AccumulateTextDeltas_MatchesFinalTextAsyn foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); if (evt is StreamingOutputTextDelta delta) { @@ -344,7 +344,7 @@ public async Task ParseStreamingEvents_SequenceNumbersAreSequentialAsync() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); sequenceNumbers.Add(evt.SequenceNumber); } @@ -380,15 +380,15 @@ public async Task ParseStreamingEvents_FinalEvent_IsTerminalStateAsync() // Act string jsonString = lastEventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); // Should be one of the terminal events - bool isTerminal = evt is StreamingResponseCompleted || - evt is StreamingResponseIncomplete || - evt is StreamingResponseFailed; + bool isTerminal = evt is StreamingResponseCompleted or + StreamingResponseIncomplete or + StreamingResponseFailed; Assert.True(isTerminal, $"Expected terminal event, got: {evt.GetType().Name}"); } @@ -413,24 +413,24 @@ public async Task ParseStreamingEvents_AllEvents_CanBeDeserializedAsync() foreach (var eventJson in ParseSseEvents(sseContent)) { // Should not throw - StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); // Verify polymorphic deserialization worked Assert.True( - evt is StreamingResponseCreated || - evt is StreamingResponseInProgress || - evt is StreamingResponseCompleted || - evt is StreamingResponseIncomplete || - evt is StreamingResponseFailed || - evt is StreamingOutputItemAdded || - evt is StreamingOutputItemDone || - evt is StreamingContentPartAdded || - evt is StreamingContentPartDone || - evt is StreamingOutputTextDelta || - evt is StreamingOutputTextDone || - evt is StreamingFunctionCallArgumentsDelta || - evt is StreamingFunctionCallArgumentsDone, + evt is StreamingResponseCreated or + StreamingResponseInProgress or + StreamingResponseCompleted or + StreamingResponseIncomplete or + StreamingResponseFailed or + StreamingOutputItemAdded or + StreamingOutputItemDone or + StreamingContentPartAdded or + StreamingContentPartDone or + StreamingOutputTextDelta or + StreamingOutputTextDone or + StreamingFunctionCallArgumentsDelta or + StreamingFunctionCallArgumentsDone, $"Unknown event type: {evt.GetType().Name}"); } } @@ -459,7 +459,7 @@ public async Task ParseStreamingEvents_IdConsistency_ValidAsync() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); string? responseId = null; @@ -499,7 +499,7 @@ public async Task ParseStreamingEvents_IdConsistency_ValidAsync() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); string? itemId = evt switch { @@ -545,7 +545,7 @@ public async Task ParseStreamingEvents_IndexConsistency_ValidAsync() // Assert - All events with output_index should have valid values foreach (var eventJson in ParseSseEvents(await httpResponse.Content.ReadAsStringAsync())) { - StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); if (evt is StreamingOutputItemAdded or StreamingOutputItemDone or StreamingContentPartAdded or StreamingContentPartDone or @@ -607,7 +607,7 @@ public async Task ParseStreamingEvents_ResponseObjectEvolution_ValidAsync() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); if (evt is StreamingResponseCreated created) @@ -722,7 +722,7 @@ public async Task ParseStreamingEvents_EventPairing_ValidAsync() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); switch (evt) @@ -774,7 +774,7 @@ public async Task ParseStreamingEvents_NoDuplicateSequenceNumbers_ValidAsync() foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); - StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); + StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); Assert.True(sequenceNumbers.Add(evt.SequenceNumber), diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs index 53d19574f0..831ab9a2d8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs @@ -92,6 +92,102 @@ 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. /// @@ -256,7 +352,7 @@ internal sealed class FunctionCallMockChatClient : IChatClient public FunctionCallMockChatClient(string functionName = "test_function", string arguments = "{\"param\":\"value\"}") { this._functionName = functionName; - this._arguments = System.Text.Json.JsonSerializer.Deserialize>(arguments) ?? new Dictionary(); + this._arguments = System.Text.Json.JsonSerializer.Deserialize>(arguments) ?? []; } public ChatClientMetadata Metadata { get; } = new("Test", new Uri("https://test.example.com"), "test-model"); @@ -408,7 +504,89 @@ public void Dispose() } /// - /// Mock implementation of IChatClient that returns custom content based on a provider function. + /// 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 = new Dictionary(); + 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() + { + } + } + + /// + /// Custom content mock implementation of IChatClient that returns custom content based on a provider function. /// internal sealed class CustomContentMockChatClient : IChatClient { @@ -448,12 +626,19 @@ public async IAsyncEnumerable GetStreamingResponseAsync( ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + Console.WriteLine("CustomContentMockChatClient.GetStreamingResponseAsync called!"); await Task.Delay(1, cancellationToken); ChatMessage lastMessage = messages.Last(); IEnumerable contents = this._contentProvider(lastMessage); List contentList = contents.ToList(); + Console.WriteLine($"Content provider returned {contentList.Count} content items"); + foreach (var content in contentList) + { + Console.WriteLine($"Content type: {content.GetType().Name}"); + } + // Stream each content item separately for (int i = 0; i < contentList.Count; i++) { @@ -470,6 +655,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( })); } + Console.WriteLine($"Yielding update {i} with {updateContents.Count} contents"); yield return new ChatResponseUpdate { Contents = updateContents, diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs index ffc4d0ba35..7d4cf87dee 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/07_GroupChat_Workflow_HostAsAgent.cs @@ -22,10 +22,16 @@ public static async ValueTask RunAsync(TextWriter writer, IWorkflowExecutionEnvi AgentThread thread = agent.GetNewThread(); await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(thread).ConfigureAwait(false)) { + if (update.RawRepresentation is WorkflowEvent) + { + // Skip workflow status updates + continue; + } + string updateText = $"{update.AuthorName - ?? update.AgentId - ?? update.Role.ToString() - ?? ChatRole.Assistant.ToString()}: {update.Text}"; + ?? update.AgentId + ?? update.Role.ToString() + ?? ChatRole.Assistant.ToString()}: {update.Text}"; writer.WriteLine(updateText); } } From db0afee06954207e784ab045c09c6261b9d65e20 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 11:43:18 -0800 Subject: [PATCH 02/13] dotnet format --- .../ServiceCollectionExtensions.Responses.cs | 2 +- .../OpenAIHttpApiIntegrationTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs index 72243286db..b0fea1846d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs @@ -46,7 +46,7 @@ public static IServiceCollection AddOpenAIResponses(this IServiceCollection serv { // Inject IConversationStorage if it's available (though executors no longer use it directly) var conversationStorage = sp.GetService(); - var logger = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); return new HostedAgentResponseExecutor(sp, logger, conversationStorage); }); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs index aa13258c74..845b44fb83 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; @@ -237,7 +237,7 @@ public async Task CreateConversationAndResponse_NonStreaming_Background_UpdatesC using var getRespDoc = await this.ParseResponseAsync(getResponseResponse); finalStatus = getRespDoc.RootElement.GetProperty("status").GetString()!; if (getRespDoc.RootElement.TryGetProperty("error", out var error) && - error.ValueKind == System.Text.Json.JsonValueKind.Object && + error.ValueKind == JsonValueKind.Object && error.TryGetProperty("message", out var messageElement)) { errorMessage = messageElement.GetString(); From fa4552abb6c574d58551ba85de8990e24ce92151 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 11:50:25 -0800 Subject: [PATCH 03/13] Clean up --- ...ndpointRouteBuilderExtensions.Responses.cs | 17 +++++---------- .../Responses/AIAgentResponseExecutor.cs | 4 +--- .../Responses/HostedAgentResponseExecutor.cs | 21 +++---------------- .../ServiceCollectionExtensions.Responses.cs | 8 +------ 4 files changed, 10 insertions(+), 40 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs index 8bdf8a7749..f4159011cf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs @@ -43,18 +43,11 @@ public static IEndpointConventionBuilder MapOpenAIResponses( responsesPath ??= $"/{agent.Name}/v1/responses"; - // Get or create an executor for this specific agent - IResponseExecutor executor; - InMemoryResponsesService responsesService; - - // Try to get conversation storage from DI if available - IConversationStorage? conversationStorage = endpoints.ServiceProvider.GetService(); - - executor = new AIAgentResponseExecutor(agent, conversationStorage); - - // Try to get storage options from DI, or create default - InMemoryStorageOptions storageOptions = endpoints.ServiceProvider.GetService() ?? new InMemoryStorageOptions(); - responsesService = new InMemoryResponsesService(executor, storageOptions, conversationStorage); + // Create an executor for this agent + var executor = new AIAgentResponseExecutor(agent); + var storageOptions = endpoints.ServiceProvider.GetService() ?? new InMemoryStorageOptions(); + var conversationStorage = endpoints.ServiceProvider.GetService(); + var responsesService = new InMemoryResponsesService(executor, storageOptions, conversationStorage); var handlers = new ResponsesHttpHandler(responsesService); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs index bdc6be7bb2..f71b853c50 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs @@ -18,13 +18,11 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses; internal sealed class AIAgentResponseExecutor : IResponseExecutor { private readonly AIAgent _agent; - private readonly IConversationStorage? _conversationStorage; - public AIAgentResponseExecutor(AIAgent agent, IConversationStorage? conversationStorage = null) + public AIAgentResponseExecutor(AIAgent agent) { ArgumentNullException.ThrowIfNull(agent); this._agent = agent; - this._conversationStorage = conversationStorage; } public async IAsyncEnumerable ExecuteAsync( diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs index 2162bd8969..94ea81223c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -21,46 +20,32 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private readonly IConversationStorage? _conversationStorage; /// /// Initializes a new instance of the class. /// /// The service provider used to resolve hosted agents. /// The logger instance. - /// Optional conversation storage for reading and updating conversation history. public HostedAgentResponseExecutor( IServiceProvider serviceProvider, - ILogger logger, - IConversationStorage? conversationStorage = null) + ILogger logger) { ArgumentNullException.ThrowIfNull(serviceProvider); ArgumentNullException.ThrowIfNull(logger); this._serviceProvider = serviceProvider; this._logger = logger; - this._conversationStorage = conversationStorage; } /// - public IAsyncEnumerable ExecuteAsync( + public async IAsyncEnumerable ExecuteAsync( AgentInvocationContext context, CreateResponse request, - CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Validate and resolve agent synchronously to ensure validation errors are thrown immediately AIAgent agent = this.ResolveAgent(request); - // Return the actual async enumerable implementation - return this.ExecuteAsyncCoreAsync(agent, context, request, cancellationToken); - } - - private async IAsyncEnumerable ExecuteAsyncCoreAsync( - AIAgent agent, - AgentInvocationContext context, - CreateResponse request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { // Create options with properties from the request var chatOptions = new ChatOptions { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs index b0fea1846d..4e83d0b570 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.Responses.cs @@ -42,13 +42,7 @@ public static IServiceCollection AddOpenAIResponses(this IServiceCollection serv var conversationStorage = sp.GetService(); return new InMemoryResponsesService(executor, options, conversationStorage); }); - services.TryAddSingleton(sp => - { - // Inject IConversationStorage if it's available (though executors no longer use it directly) - var conversationStorage = sp.GetService(); - var logger = sp.GetRequiredService>(); - return new HostedAgentResponseExecutor(sp, logger, conversationStorage); - }); + services.TryAddSingleton(); return services; } From d67c202259b8fadb5d5e0b44d090a64893728516 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:09:06 -0800 Subject: [PATCH 04/13] Review feedback --- .../Responses/InMemoryResponsesService.cs | 3 +-- .../OpenAIConversationsConformanceTests.cs | 2 -- .../OpenAIHttpApiIntegrationTests.cs | 4 ---- .../TestHelpers.cs | 8 -------- 4 files changed, 1 insertion(+), 16 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs index 8e948104a1..2ad0fd1bf0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs @@ -399,7 +399,7 @@ private async Task ExecuteResponseAsync(string responseId, ResponseState state, { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var request = state.Request!; - var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, state.CancellationTokenSource!.Token); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, state.CancellationTokenSource!.Token); try { @@ -503,7 +503,6 @@ private async Task ExecuteResponseAsync(string responseId, ResponseState state, { // Signal one final time to unblock any waiting consumers state.SignalUpdate(); - linkedCts.Dispose(); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs index 9854a2181e..f464db6b55 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs @@ -214,8 +214,6 @@ public async Task BasicConversationCreateAsync() { // Arrange string requestJson = LoadTraceFile("basic/create_conversation_request.json"); - using var expectedResponseDoc = LoadTraceDocument("basic/create_conversation_response.json"); - var expectedResponse = expectedResponseDoc.RootElement; HttpClient client = await this.CreateTestServerAsync("basic-agent", "You are a helpful assistant.", "The capital of France is Paris."); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs index 845b44fb83..c1c8c4d1b1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs @@ -79,10 +79,6 @@ public async Task CreateConversationAndResponse_NonStreaming_NonBackground_Updat Assert.Equal("list", itemsList.GetProperty("object").GetString()); var items = itemsList.GetProperty("data"); - // Debug output - Console.WriteLine($"Items count: {items.GetArrayLength()}"); - Console.WriteLine($"Response output count: {output.GetArrayLength()}"); - Assert.True(items.GetArrayLength() > 0, "Conversation should have items after response completion"); // Find the assistant message in the items diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs index 831ab9a2d8..11a0c1940d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/TestHelpers.cs @@ -626,19 +626,12 @@ public async IAsyncEnumerable GetStreamingResponseAsync( ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - Console.WriteLine("CustomContentMockChatClient.GetStreamingResponseAsync called!"); await Task.Delay(1, cancellationToken); ChatMessage lastMessage = messages.Last(); IEnumerable contents = this._contentProvider(lastMessage); List contentList = contents.ToList(); - Console.WriteLine($"Content provider returned {contentList.Count} content items"); - foreach (var content in contentList) - { - Console.WriteLine($"Content type: {content.GetType().Name}"); - } - // Stream each content item separately for (int i = 0; i < contentList.Count; i++) { @@ -655,7 +648,6 @@ public async IAsyncEnumerable GetStreamingResponseAsync( })); } - Console.WriteLine($"Yielding update {i} with {updateContents.Count} contents"); yield return new ChatResponseUpdate { Contents = updateContents, From 5e1b2b6797de222daf10e681639f0085c267a1ef Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:20:05 -0800 Subject: [PATCH 05/13] Review feedback --- .../OpenAIConversationsConformanceTests.cs | 19 ++-- .../OpenAIHttpApiIntegrationTests.cs | 100 +++++++----------- 2 files changed, 49 insertions(+), 70 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs index f464db6b55..bad224195d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs @@ -180,7 +180,7 @@ private async Task CreateTestServerWithToolCallAsync(string agentNam /// private static async Task SendPostRequestAsync(HttpClient client, string path, string requestJson) { - StringContent content = new(requestJson, Encoding.UTF8, "application/json"); + using StringContent content = new(requestJson, Encoding.UTF8, "application/json"); return await client.PostAsync(new Uri(path, UriKind.Relative), content); } @@ -725,7 +725,7 @@ public async Task ErrorInvalidJsonAsync() HttpClient client = await this.CreateTestServerAsync("error-json-agent", "You are a helpful assistant.", "Test response"); // Act - StringContent content = new(invalidJson, Encoding.UTF8, "application/json"); + using StringContent content = new(invalidJson, Encoding.UTF8, "application/json"); HttpResponseMessage response = await client.PostAsync(new Uri("/v1/conversations", UriKind.Relative), content); // Assert - Response is 400 @@ -888,15 +888,14 @@ public async Task ImageInputFullScenarioAsync() Assert.True(content.GetArrayLength() > 1, "Should have text and image content"); // Assert - Has input_image content type - bool hasImage = false; - foreach (var part in content.EnumerateArray()) + JsonElement? imagePart = content.EnumerateArray() + .Where(part => part.GetProperty("type").GetString() == "input_image") + .Cast() + .FirstOrDefault(); + bool hasImage = imagePart.HasValue; + if (hasImage) { - if (part.GetProperty("type").GetString() == "input_image") - { - hasImage = true; - AssertJsonPropertyExists(part, "image_url"); - break; - } + AssertJsonPropertyExists(imagePart!.Value, "image_url"); } Assert.True(hasImage, "Request should have input_image content"); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs index c1c8c4d1b1..4484a733d8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs @@ -82,25 +82,20 @@ public async Task CreateConversationAndResponse_NonStreaming_NonBackground_Updat Assert.True(items.GetArrayLength() > 0, "Conversation should have items after response completion"); // Find the assistant message in the items - bool foundAssistantMessage = false; - foreach (var item in items.EnumerateArray()) - { - if (item.GetProperty("type").GetString() == "message" && - item.GetProperty("role").GetString() == "assistant") + bool foundAssistantMessage = items.EnumerateArray() + .Where(item => item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + .Any(item => { - var itemContent = item.GetProperty("content"); + JsonElement itemContent = item.GetProperty("content"); if (itemContent.GetArrayLength() > 0) { - var firstContent = itemContent[0]; - if (firstContent.GetProperty("type").GetString() == "output_text" && - firstContent.GetProperty("text").GetString() == ExpectedResponse) - { - foundAssistantMessage = true; - break; - } + JsonElement firstContent = itemContent[0]; + return firstContent.GetProperty("type").GetString() == "output_text" && + firstContent.GetProperty("text").GetString() == ExpectedResponse; } - } - } + return false; + }); Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); } @@ -161,25 +156,20 @@ public async Task CreateConversationAndResponse_Streaming_NonBackground_UpdatesC Assert.True(items.GetArrayLength() > 0, "Conversation should have items after streaming response completion"); // Find the assistant message in the items - bool foundAssistantMessage = false; - foreach (var item in items.EnumerateArray()) - { - if (item.GetProperty("type").GetString() == "message" && - item.GetProperty("role").GetString() == "assistant") + bool foundAssistantMessage = items.EnumerateArray() + .Where(item => item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + .Any(item => { - var itemContent = item.GetProperty("content"); + JsonElement itemContent = item.GetProperty("content"); if (itemContent.GetArrayLength() > 0) { - var firstContent = itemContent[0]; - if (firstContent.GetProperty("type").GetString() == "output_text" && - firstContent.GetProperty("text").GetString() == ExpectedResponse) - { - foundAssistantMessage = true; - break; - } + JsonElement firstContent = itemContent[0]; + return firstContent.GetProperty("type").GetString() == "output_text" && + firstContent.GetProperty("text").GetString() == ExpectedResponse; } - } - } + return false; + }); Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); } @@ -255,25 +245,20 @@ public async Task CreateConversationAndResponse_NonStreaming_Background_UpdatesC Assert.True(items.GetArrayLength() > 0, "Conversation should have items after background response completion"); // Find the assistant message in the items - bool foundAssistantMessage = false; - foreach (var item in items.EnumerateArray()) - { - if (item.GetProperty("type").GetString() == "message" && - item.GetProperty("role").GetString() == "assistant") + bool foundAssistantMessage = items.EnumerateArray() + .Where(item => item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + .Any(item => { - var itemContent = item.GetProperty("content"); + JsonElement itemContent = item.GetProperty("content"); if (itemContent.GetArrayLength() > 0) { - var firstContent = itemContent[0]; - if (firstContent.GetProperty("type").GetString() == "output_text" && - firstContent.GetProperty("text").GetString() == ExpectedResponse) - { - foundAssistantMessage = true; - break; - } + JsonElement firstContent = itemContent[0]; + return firstContent.GetProperty("type").GetString() == "output_text" && + firstContent.GetProperty("text").GetString() == ExpectedResponse; } - } - } + return false; + }); Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); } @@ -328,24 +313,19 @@ public async Task CreateConversationAndResponse_Streaming_Background_UpdatesConv Assert.True(items.GetArrayLength() > 0, "Conversation should have items after streaming response completion"); // Find the assistant message in the items - bool foundAssistantMessage = false; - foreach (var item in items.EnumerateArray()) - { - if (item.GetProperty("type").GetString() == "message" && - item.GetProperty("role").GetString() == "assistant") + bool foundAssistantMessage = items.EnumerateArray() + .Where(item => item.GetProperty("type").GetString() == "message" && + item.GetProperty("role").GetString() == "assistant") + .Any(item => { - var itemContent = item.GetProperty("content"); + JsonElement itemContent = item.GetProperty("content"); if (itemContent.GetArrayLength() > 0) { - var firstContent = itemContent[0]; - if (firstContent.GetProperty("type").GetString() == "output_text") - { - foundAssistantMessage = true; - break; - } + JsonElement firstContent = itemContent[0]; + return firstContent.GetProperty("type").GetString() == "output_text"; } - } - } + return false; + }); Assert.True(foundAssistantMessage, "Conversation should contain the assistant's response message"); } @@ -390,7 +370,7 @@ private async Task CreateTestServerWithInMemoryStorageAsync(string a /// private async Task SendPostRequestAsync(HttpClient client, string path, string requestJson) { - StringContent content = new(requestJson, Encoding.UTF8, "application/json"); + using StringContent content = new(requestJson, Encoding.UTF8, "application/json"); return await client.PostAsync(new Uri(path, UriKind.Relative), content); } From b368b42b3e32740a4d989e025631057fa56902a1 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:25:38 -0800 Subject: [PATCH 06/13] Review feedback --- .../Responses/ResponsesHttpHandler.cs | 105 ++++++------------ 1 file changed, 32 insertions(+), 73 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs index 893f9fd917..d9b4a43aeb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesHttpHandler.cs @@ -95,13 +95,6 @@ public async Task CreateResponseAsync( } }); } - catch (Exception ex) - { - return Results.Problem( - detail: ex.Message, - statusCode: StatusCodes.Status500InternalServerError, - title: "Error creating response"); - } } /// @@ -114,42 +107,32 @@ public async Task GetResponseAsync( [FromQuery] int? starting_after, CancellationToken cancellationToken) { - try + // If streaming is requested, return SSE stream + if (stream == true) { - // If streaming is requested, return SSE stream - if (stream == true) - { - var streamingResponse = this._responsesService.GetResponseStreamingAsync( - responseId, - startingAfter: starting_after, - cancellationToken: cancellationToken); + var streamingResponse = this._responsesService.GetResponseStreamingAsync( + responseId, + startingAfter: starting_after, + cancellationToken: cancellationToken); - return new SseJsonResult( - streamingResponse, - static evt => evt.Type, - OpenAIHostingJsonContext.Default.StreamingResponseEvent); - } + return new SseJsonResult( + streamingResponse, + static evt => evt.Type, + OpenAIHostingJsonContext.Default.StreamingResponseEvent); + } - // Non-streaming: return the response object - var response = await this._responsesService.GetResponseAsync(responseId, cancellationToken).ConfigureAwait(false); - return response is not null - ? Results.Ok(response) - : Results.NotFound(new ErrorResponse + // Non-streaming: return the response object + var response = await this._responsesService.GetResponseAsync(responseId, cancellationToken).ConfigureAwait(false); + return response is not null + ? Results.Ok(response) + : Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails { - Error = new ErrorDetails - { - Message = $"Response '{responseId}' not found.", - Type = "invalid_request_error" - } - }); - } - catch (Exception ex) - { - return Results.Problem( - detail: ex.Message, - statusCode: StatusCodes.Status404NotFound, - title: "Response not found"); - } + Message = $"Response '{responseId}' not found.", + Type = "invalid_request_error" + } + }); } /// @@ -175,13 +158,6 @@ public async Task CancelResponseAsync( } }); } - catch (Exception ex) - { - return Results.Problem( - detail: ex.Message, - statusCode: StatusCodes.Status500InternalServerError, - title: "Error cancelling response"); - } } /// @@ -191,27 +167,17 @@ public async Task DeleteResponseAsync( string responseId, CancellationToken cancellationToken) { - try - { - var deleted = await this._responsesService.DeleteResponseAsync(responseId, cancellationToken).ConfigureAwait(false); - return deleted - ? Results.Ok(new DeleteResponse { Id = responseId, Object = "response", Deleted = true }) - : Results.NotFound(new ErrorResponse + var deleted = await this._responsesService.DeleteResponseAsync(responseId, cancellationToken).ConfigureAwait(false); + return deleted + ? Results.Ok(new DeleteResponse { Id = responseId, Object = "response", Deleted = true }) + : Results.NotFound(new ErrorResponse + { + Error = new ErrorDetails { - Error = new ErrorDetails - { - Message = $"Response '{responseId}' not found.", - Type = "invalid_request_error" - } - }); - } - catch (Exception ex) - { - return Results.Problem( - detail: ex.Message, - statusCode: StatusCodes.Status500InternalServerError, - title: "Error deleting response"); - } + Message = $"Response '{responseId}' not found.", + Type = "invalid_request_error" + } + }); } /// @@ -248,12 +214,5 @@ public async Task ListResponseInputItemsAsync( } }); } - catch (Exception ex) - { - return Results.Problem( - detail: ex.Message, - statusCode: StatusCodes.Status500InternalServerError, - title: "Error listing input items"); - } } } From f7be2dcbc790a625a3bfcbc6b20bfc8c7ebc07dc Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:31:24 -0800 Subject: [PATCH 07/13] Review feedback --- .../InMemoryAgentConversationIndex.cs | 12 +++--------- .../OpenAIConversationsConformanceTests.cs | 16 ++++++---------- .../OpenAIConversationsSerializationTests.cs | 16 ++++++---------- .../OpenAIHttpApiIntegrationTests.cs | 18 +++++++----------- 4 files changed, 22 insertions(+), 40 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs index 1cfc9e59f2..abf8585797 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryAgentConversationIndex.cs @@ -101,15 +101,9 @@ public async Task> GetConversationIdsAsync(string agentId, { ArgumentException.ThrowIfNullOrEmpty(agentId); - string[] conversations; - if (this._cache.TryGetValue(agentId, out ConversationSet? conversationSet) && conversationSet is not null) - { - conversations = conversationSet.GetAll(); - } - else - { - conversations = []; - } + string[] conversations = (this._cache.TryGetValue(agentId, out ConversationSet? conversationSet) && conversationSet is not null) + ? conversationSet.GetAll() + : []; return new ListResponse { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs index bad224195d..93fe1391fc 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs @@ -1182,18 +1182,14 @@ private static List ParseSseEventsFromContent(string sseContent) { var line = lines[i].TrimEnd('\r'); - if (line.StartsWith("event: ", StringComparison.Ordinal)) + if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length) { - // Next line should have the data - if (i + 1 < lines.Length) + var dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) { - var dataLine = lines[i + 1].TrimEnd('\r'); - if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) - { - var jsonData = dataLine.Substring("data: ".Length); - var doc = JsonDocument.Parse(jsonData); - events.Add(doc.RootElement.Clone()); - } + var jsonData = dataLine.Substring("data: ".Length); + var doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); } } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs index 4e2ae4df03..ecbdba4a53 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsSerializationTests.cs @@ -573,18 +573,14 @@ private static System.Collections.Generic.List ParseSseEventsFromCo { var line = lines[i].TrimEnd('\r'); - if (line.StartsWith("event: ", StringComparison.Ordinal)) + if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length) { - // Next line should have the data - if (i + 1 < lines.Length) + var dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) { - var dataLine = lines[i + 1].TrimEnd('\r'); - if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) - { - var jsonData = dataLine.Substring("data: ".Length); - var doc = JsonDocument.Parse(jsonData); - events.Add(doc.RootElement.Clone()); - } + var jsonData = dataLine.Substring("data: ".Length); + var doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); } } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs index 4484a733d8..600f3a61dd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIHttpApiIntegrationTests.cs @@ -403,20 +403,16 @@ private JsonElement[] ParseSseEvents(string sseContent) { var line = lines[i].TrimEnd('\r'); - if (line.StartsWith("event: ", StringComparison.Ordinal)) + if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length) { - // Next line should have the data - if (i + 1 < lines.Length) + var dataLine = lines[i + 1].TrimEnd('\r'); + if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) { - var dataLine = lines[i + 1].TrimEnd('\r'); - if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) + var jsonData = dataLine.Substring("data: ".Length); + if (!string.IsNullOrWhiteSpace(jsonData)) { - var jsonData = dataLine.Substring("data: ".Length); - if (!string.IsNullOrWhiteSpace(jsonData)) - { - var doc = JsonDocument.Parse(jsonData); - events.Add(doc.RootElement.Clone()); - } + var doc = JsonDocument.Parse(jsonData); + events.Add(doc.RootElement.Clone()); } } } From 87ba166a89606fe4f88ab8b783bfcb9d536a045e Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:32:52 -0800 Subject: [PATCH 08/13] Review feedback --- .../OpenAIConversationsConformanceTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs index 93fe1391fc..2d510b86a4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIConversationsConformanceTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Text; @@ -31,14 +32,14 @@ public sealed class OpenAIConversationsConformanceTests : IAsyncDisposable /// private static string LoadTraceFile(string relativePath) { - var fullPath = System.IO.Path.Combine(TracesBasePath, relativePath); + var fullPath = Path.Combine(TracesBasePath, relativePath); - if (!System.IO.File.Exists(fullPath)) + if (!File.Exists(fullPath)) { - throw new System.IO.FileNotFoundException($"Conformance trace file not found: {fullPath}"); + throw new FileNotFoundException($"Conformance trace file not found: {fullPath}"); } - return System.IO.File.ReadAllText(fullPath); + return File.ReadAllText(fullPath); } /// From 7150d60f126705ec38f6ec789aaf7eda764681d5 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:41:38 -0800 Subject: [PATCH 09/13] Review feedback --- .../Responses/InMemoryResponsesService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs index 2ad0fd1bf0..99bb446bce 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs @@ -471,7 +471,7 @@ private async Task ExecuteResponseAsync(string responseId, ResponseState state, state.AddStreamingEvent(cancelledEvent); } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException) { // Determine error code based on exception message // Azure OpenAI returns HTTP 400 with "content_filter" in the error message From 70f9464af8a71bd1b4d77ba4888f940a8a36f590 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 12:43:26 -0800 Subject: [PATCH 10/13] Review feedback --- .../Responses/InMemoryResponsesService.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs index 99bb446bce..b8a0cc6083 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs @@ -473,19 +473,13 @@ private async Task ExecuteResponseAsync(string responseId, ResponseState state, } catch (Exception ex) when (ex is not OperationCanceledException) { - // Determine error code based on exception message - // Azure OpenAI returns HTTP 400 with "content_filter" in the error message - string errorCode = ex.Message.Contains("content_filter", StringComparison.OrdinalIgnoreCase) - ? "content_filter" - : "execution_error"; - // Update response status to failed state.Response = state.Response! with { Status = ResponseStatus.Failed, Error = new ResponseError { - Code = errorCode, + Code = "execution_error", Message = ex.Message } }; From d3e976d27346c832b5f69d8134f00ee2c92082b2 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 13:44:44 -0800 Subject: [PATCH 11/13] Review feedback --- .../Conversations/ConversationsHttpHandler.cs | 17 +++----- .../Conversations/IConversationStorage.cs | 10 ----- .../InMemoryConversationStorage.cs | 17 -------- .../Responses/AIAgentResponseExecutor.cs | 1 - .../InMemoryConversationStorageTests.cs | 39 ++++++++++--------- 5 files changed, 25 insertions(+), 59 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs index a72111cec3..e0a1da1c2d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/ConversationsHttpHandler.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; @@ -99,11 +100,8 @@ public async Task CreateConversationAsync( // Add initial items if provided if (request.Items is { Count: > 0 }) { - foreach (ItemParam itemParam in request.Items) - { - ItemResource itemToAdd = itemParam.ToItemResource(); - await this._storage.AddItemAsync(created.Id, itemToAdd, cancellationToken).ConfigureAwait(false); - } + List itemsToAdd = [.. request.Items.Select(itemParam => itemParam.ToItemResource())]; + await this._storage.AddItemsAsync(created.Id, itemsToAdd, cancellationToken).ConfigureAwait(false); } // Add to conversation index if available and agent_id is provided in metadata @@ -224,13 +222,8 @@ public async Task CreateItemsAsync( }); } - var createdItems = new List(); - foreach (ItemParam itemParam in request.Items) - { - ItemResource itemToAdd = itemParam.ToItemResource(); - ItemResource created = await this._storage.AddItemAsync(conversationId, itemToAdd, cancellationToken).ConfigureAwait(false); - createdItems.Add(created); - } + List createdItems = [.. request.Items.Select(itemParam => itemParam.ToItemResource())]; + await this._storage.AddItemsAsync(conversationId, createdItems, cancellationToken).ConfigureAwait(false); return Results.Ok(new ListResponse { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs index 4547385760..8e850de9b2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/IConversationStorage.cs @@ -50,16 +50,6 @@ internal interface IConversationStorage // Item operations - /// - /// Adds an item (message, function call, etc.) to a conversation. - /// Items are ItemResource objects from the Responses API. - /// - /// The conversation ID to add the item to. - /// The item to add. - /// Cancellation token. - /// The created item. - Task AddItemAsync(string conversationId, ItemResource item, CancellationToken cancellationToken = default); - /// /// Adds multiple items to a conversation atomically. /// Items are ItemResource objects from the Responses API. diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs index f6d58ea929..29734b13c3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Conversations/InMemoryConversationStorage.cs @@ -86,23 +86,6 @@ public Task DeleteConversationAsync(string conversationId, CancellationTok return Task.FromResult(false); } - /// - public Task AddItemAsync(string conversationId, ItemResource item, CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrEmpty(conversationId, nameof(conversationId)); - - if (!this._cache.TryGetValue(conversationId, out ConversationState? state) || state is null) - { - throw new InvalidOperationException($"Conversation '{conversationId}' not found."); - } - - state.AddItem(item); - // Touch the cache entry to reset expiration - var entryOptions = this._options.ToMemoryCacheEntryOptions(); - this._cache.Set(conversationId, state, entryOptions); - return Task.FromResult(item); - } - /// public Task AddItemsAsync(string conversationId, IEnumerable items, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs index f71b853c50..714ad49cf4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponseExecutor.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; using Microsoft.Extensions.AI; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs index 35ef217484..540d1dfc3b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -184,7 +184,7 @@ public async Task DeleteConversationAsync_NonExistentConversation_ReturnsFalseAs } [Fact] - public async Task AddItemAsync_SuccessAsync() + public async Task AddItemsAsync_SuccessAsync() { // Arrange var storage = new InMemoryConversationStorage(); @@ -203,15 +203,16 @@ public async Task AddItemAsync_SuccessAsync() }; // Act - ItemResource result = await storage.AddItemAsync("conv_items123", item); + await storage.AddItemsAsync("conv_items123", [item]); // Assert + ItemResource? result = await storage.GetItemAsync("conv_items123", item.Id); Assert.NotNull(result); Assert.Equal(item.Id, result.Id); } [Fact] - public async Task AddItemAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync() + public async Task AddItemsAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync() { // Arrange var storage = new InMemoryConversationStorage(); @@ -223,12 +224,12 @@ public async Task AddItemAsync_NonExistentConversation_ThrowsInvalidOperationExc // Act & Assert var exception = await Assert.ThrowsAsync( - () => storage.AddItemAsync("conv_nonexistent", item)); + () => storage.AddItemsAsync("conv_nonexistent", [item])); Assert.Contains("not found", exception.Message); } [Fact] - public async Task AddItemAsync_DuplicateItemId_ThrowsInvalidOperationExceptionAsync() + public async Task AddItemsAsync_DuplicateItemId_ThrowsInvalidOperationExceptionAsync() { // Arrange var storage = new InMemoryConversationStorage(); @@ -246,11 +247,11 @@ public async Task AddItemAsync_DuplicateItemId_ThrowsInvalidOperationExceptionAs Content = [new ItemContentInputText { Text = "Hello" }] }; - await storage.AddItemAsync("conv_dup_items", item); + await storage.AddItemsAsync("conv_dup_items", [item]); // Act & Assert var exception = await Assert.ThrowsAsync( - () => storage.AddItemAsync("conv_dup_items", item)); + () => storage.AddItemsAsync("conv_dup_items", [item])); Assert.Contains("already exists", exception.Message); } @@ -272,7 +273,7 @@ public async Task GetItemAsync_ExistingItem_ReturnsItemAsync() Id = "msg_getitem123", Content = [new ItemContentInputText { Text = "Test message" }] }; - await storage.AddItemAsync("conv_getitem", item); + await storage.AddItemsAsync("conv_getitem", [item]); // Act ItemResource? result = await storage.GetItemAsync("conv_getitem", "msg_getitem123"); @@ -347,9 +348,9 @@ public async Task ListItemsAsync_DefaultParameters_ReturnsDescendingOrderAsync() Content = [new ItemContentInputText { Text = "Third" }] }; - await storage.AddItemAsync("conv_list", item1); - await storage.AddItemAsync("conv_list", item2); - await storage.AddItemAsync("conv_list", item3); + await storage.AddItemsAsync("conv_list", [item1]); + await storage.AddItemsAsync("conv_list", [item2]); + await storage.AddItemsAsync("conv_list", [item3]); // Act ListResponse result = await storage.ListItemsAsync("conv_list"); @@ -390,8 +391,8 @@ public async Task ListItemsAsync_AscendingOrder_ReturnsCorrectOrderAsync() Content = [new ItemContentInputText { Text = "Second" }] }; - await storage.AddItemAsync("conv_asc", item1); - await storage.AddItemAsync("conv_asc", item2); + await storage.AddItemsAsync("conv_asc", [item1]); + await storage.AddItemsAsync("conv_asc", [item2]); // Act ListResponse result = await storage.ListItemsAsync("conv_asc", order: SortOrder.Ascending); @@ -422,7 +423,7 @@ public async Task ListItemsAsync_WithLimit_ReturnsCorrectPageSizeAsync() Id = $"msg_{i:D3}", Content = [new ItemContentInputText { Text = $"Message {i}" }] }; - await storage.AddItemAsync("conv_limit", item); + await storage.AddItemsAsync("conv_limit", [item]); } // Act @@ -455,7 +456,7 @@ public async Task ListItemsAsync_WithAfter_ReturnsNextPageAsync() Id = $"msg_{i:D3}", Content = [new ItemContentInputText { Text = $"Message {i}" }] }; - await storage.AddItemAsync("conv_after", item); + await storage.AddItemsAsync("conv_after", [item]); } // Act @@ -488,7 +489,7 @@ public async Task ListItemsAsync_LimitClamping_ClampsToValidRangeAsync() Id = $"msg_{i:D3}", Content = [new ItemContentInputText { Text = $"Message {i}" }] }; - await storage.AddItemAsync("conv_clamp", item); + await storage.AddItemsAsync("conv_clamp", [item]); } // Act - Test upper bound @@ -558,7 +559,7 @@ public async Task DeleteItemAsync_ExistingItem_ReturnsTrueAsync() Id = "msg_delete", Content = [new ItemContentInputText { Text = "Delete me" }] }; - await storage.AddItemAsync("conv_delitem", item); + await storage.AddItemsAsync("conv_delitem", [item]); // Act bool result = await storage.DeleteItemAsync("conv_delitem", "msg_delete"); @@ -629,7 +630,7 @@ public async Task ConcurrentOperations_ThreadSafeAsync() Id = $"msg_{index:D3}", Content = [new ItemContentInputText { Text = $"Message {index}" }] }; - await storage.AddItemAsync("conv_concurrent", item); + await storage.AddItemsAsync("conv_concurrent", [item]); })); } From 0476d82adfeb680c994dde48b428e8459011b842 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 14:10:39 -0800 Subject: [PATCH 12/13] dotnet format --- .../InMemoryConversationStorageTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs index 540d1dfc3b..0ed93b7a93 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/InMemoryConversationStorageTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; From 866fefb5b22c068fef9698e1a80bab32f5f25f99 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Tue, 4 Nov 2025 11:30:05 -0800 Subject: [PATCH 13/13] Implement DevUI --- .github/.linkspector.yml | 2 + .github/workflows/dotnet-build-and-test.yml | 1 + dotnet/agent-framework-dotnet.slnx | 5 + .../DevUI_Step01_BasicUsage.csproj | 25 + .../DevUI/DevUI_Step01_BasicUsage/Program.cs | 79 + .../DevUI/DevUI_Step01_BasicUsage/README.md | 81 + dotnet/samples/GettingStarted/DevUI/README.md | 57 + .../DevUIExtensions.cs | 53 + .../DevUIMiddleware.cs | 236 + .../Entities/EntitiesJsonContext.cs | 24 + .../Entities/EntityInfo.cs | 83 + .../WorkflowSerializationExtensions.cs | 193 + .../EntitiesApiExtensions.cs | 244 + ...Microsoft.Agents.AI.DevUI.Frontend.targets | 68 + .../Microsoft.Agents.AI.DevUI.csproj | 31 + .../Properties/launchSettings.json | 12 + .../src/Microsoft.Agents.AI.DevUI/README.md | 53 + .../wwwroot/index.html | 14 + .../WorkflowThread.cs | 14 + .../ui/assets/index-CE4pGoXh.css | 1 - .../ui/assets/index-D_Y1oSGu.js | 577 --- .../agent_framework_devui/ui/assets/index.css | 1 + .../agent_framework_devui/ui/assets/index.js | 576 +++ .../devui/agent_framework_devui/ui/index.html | 6 +- .../devui/agent_framework_devui/ui/vite.svg | 1 - python/packages/devui/frontend/index.html | 4 +- .../packages/devui/frontend/package-lock.json | 4215 +++++++++++++++++ .../packages/devui/frontend/public/vite.svg | 1 - .../components/features/agent/agent-view.tsx | 395 +- .../OpenAIContentRenderer.tsx | 4 +- .../features/workflow/workflow-view.tsx | 4 + python/packages/devui/frontend/src/main.tsx | 4 + .../devui/frontend/src/services/api.ts | 431 +- .../frontend/src/services/streaming-state.ts | 207 + .../devui/frontend/src/types/openai.ts | 14 + python/packages/devui/frontend/vite.config.ts | 9 + python/packages/devui/frontend/yarn.lock | 441 +- 37 files changed, 6992 insertions(+), 1174 deletions(-) create mode 100644 dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj create mode 100644 dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs create mode 100644 dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md create mode 100644 dotnet/samples/GettingStarted/DevUI/README.md create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.Frontend.targets create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/Properties/launchSettings.json create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/README.md create mode 100644 dotnet/src/Microsoft.Agents.AI.DevUI/wwwroot/index.html delete mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-CE4pGoXh.css delete mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-D_Y1oSGu.js create mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index.css create mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index.js delete mode 100644 python/packages/devui/agent_framework_devui/ui/vite.svg create mode 100644 python/packages/devui/frontend/package-lock.json delete mode 100644 python/packages/devui/frontend/public/vite.svg create mode 100644 python/packages/devui/frontend/src/services/streaming-state.ts diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml index 24fee6edce..eb365c2982 100644 --- a/.github/.linkspector.yml +++ b/.github/.linkspector.yml @@ -12,6 +12,8 @@ ignorePatterns: - pattern: "https:\/\/platform.openai.com" - pattern: "http:\/\/localhost" - pattern: "http:\/\/127.0.0.1" + - pattern: "https:\/\/localhost" + - pattern: "https:\/\/127.0.0.1" - pattern: "0001-spec.md" - pattern: "0001-madr-architecture-decisions.md" - pattern: "https://api.powerplatform.com/.default" diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 1a51f94119..5abfe2a879 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -74,6 +74,7 @@ jobs: . .github dotnet + python workflow-samples - name: Setup dotnet diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index de8aef42fc..ae2a92d590 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -62,6 +62,10 @@ + + + + @@ -273,6 +277,7 @@ + diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj new file mode 100644 index 0000000000..8ae36b52e0 --- /dev/null +++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + enable + enable + DevUI_Step01_BasicUsage + true + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs new file mode 100644 index 0000000000..88397252d0 --- /dev/null +++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents. + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI.DevUI; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Extensions.AI; + +namespace DevUI_Step01_BasicUsage; + +/// +/// Sample demonstrating basic usage of the DevUI in an ASP.NET Core application. +/// +/// +/// This sample shows how to: +/// 1. Set up Azure OpenAI as the chat client +/// 2. Register agents and workflows using the hosting packages +/// 3. Map the DevUI endpoint which automatically configures the middleware +/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility +/// 5. Access the DevUI in a web browser +/// +/// The DevUI provides an interactive web interface for testing and debugging AI agents. +/// DevUI assets are served from embedded resources within the assembly. +/// Simply call MapDevUI() to set up everything needed. +/// +/// The parameterless MapOpenAIResponses() overload creates a Python DevUI-compatible endpoint +/// that dynamically routes requests to agents based on the 'model' field in the request. +/// +internal static class Program +{ + /// + /// Entry point that starts an ASP.NET Core web server with the DevUI. + /// + /// Command line arguments. + private static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Set up the Azure OpenAI client + var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); + var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4o-mini"; + + var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); + + builder.Services.AddChatClient(chatClient); + + // Register sample agents + builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately."); + builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry."); + builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples."); + + // Register sample workflows + var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow."); + var reviewerBuilder = builder.AddAIAgent("workflow-reviewer", "You are a reviewer. Review and critique the previous response."); + builder.AddSequentialWorkflow( + "review-workflow", + [assistantBuilder, reviewerBuilder]) + .AddAsAIAgent(); + + builder.AddDevUI(); + + var app = builder.Build(); + + app.MapDevUI(); + app.MapEntities(); + app.MapOpenAIResponses(); + app.MapOpenAIConversations(); + + Console.WriteLine("DevUI is available at: https://localhost:50516/devui"); + Console.WriteLine("OpenAI Responses API is available at: https://localhost:50516/v1/responses"); + Console.WriteLine("Press Ctrl+C to stop the server."); + + app.Run(); + } +} diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md new file mode 100644 index 0000000000..2b6cc28644 --- /dev/null +++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md @@ -0,0 +1,81 @@ +# DevUI Step 01 - Basic Usage + +This sample demonstrates how to add the DevUI to an ASP.NET Core application with AI agents. + +## What is DevUI? + +The DevUI provides an interactive web interface for testing and debugging AI agents during development. + +## Configuration + +Set the following environment variables: + +- `AZURE_OPENAI_ENDPOINT` - Your Azure OpenAI endpoint URL (required) +- `AZURE_OPENAI_DEPLOYMENT_NAME` - Your deployment name (defaults to "gpt-4o-mini") + +## Running the Sample + +1. Set your Azure OpenAI credentials as environment variables +2. Run the application: + ```bash + dotnet run + ``` +3. Open your browser to https://localhost:50516/devui +4. Select an agent or workflow from the dropdown and start chatting! + +## Sample Agents and Workflows + +This sample includes: + +**Agents:** +- **assistant** - A helpful assistant +- **poet** - A creative poet +- **coder** - An expert programmer + +**Workflows:** +- **review-workflow** - A sequential workflow that generates a response and then reviews it + +## Adding DevUI to Your Own Project + +To add DevUI to your ASP.NET Core application: + +1. Add the DevUI package and hosting packages: + ```bash + dotnet add package Microsoft.Agents.AI.DevUI + dotnet add package Microsoft.Agents.AI.Hosting + dotnet add package Microsoft.Agents.AI.Hosting.OpenAI + ``` + +2. Register your agents and workflows: + ```csharp + var builder = WebApplication.CreateBuilder(args); + + // Set up your chat client + builder.Services.AddChatClient(chatClient); + + // Register agents + builder.AddAIAgent("assistant", "You are a helpful assistant."); + + // Register workflows + var agent1Builder = builder.AddAIAgent("workflow-agent1", "You are agent 1."); + var agent2Builder = builder.AddAIAgent("workflow-agent2", "You are agent 2."); + builder.AddSequentialWorkflow("my-workflow", [agent1Builder, agent2Builder]) + .AddAsAIAgent(); + ``` + +3. Add DevUI services and map the endpoint: + ```csharp + builder.AddDevUI(); + var app = builder.Build(); + + app.MapDevUI(); + + // Add required endpoints + app.MapEntities(); + app.MapOpenAIResponses(); + app.MapOpenAIConversations(); + + app.Run(); + ``` + +4. Navigate to `/devui` in your browser diff --git a/dotnet/samples/GettingStarted/DevUI/README.md b/dotnet/samples/GettingStarted/DevUI/README.md new file mode 100644 index 0000000000..155d3f2b9d --- /dev/null +++ b/dotnet/samples/GettingStarted/DevUI/README.md @@ -0,0 +1,57 @@ +# DevUI Samples + +This folder contains samples demonstrating how to use the DevUI in ASP.NET Core applications. + +## What is DevUI? + +The DevUI provides an interactive web interface for testing and debugging AI agents during development. + +## Samples + +### [DevUI_Step01_BasicUsage](./DevUI_Step01_BasicUsage) + +Shows how to add DevUI to an ASP.NET Core application with multiple agents and workflows. + +**Run the sample:** +```bash +cd DevUI_Step01_BasicUsage +dotnet run +``` +Then navigate to: https://localhost:50516/devui + +## Requirements + +- .NET 8.0 or later +- ASP.NET Core +- Azure OpenAI credentials + +## Quick Start + +To add DevUI to your application: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Set up the chat client +builder.Services.AddChatClient(chatClient); + +// Register your agents +builder.AddAIAgent("my-agent", "You are a helpful assistant."); + +// Add DevUI services +builder.AddDevUI(); + +var app = builder.Build(); + +// Map the DevUI endpoint +app.MapDevUI(); + +// Add required endpoints +app.MapEntities(); +app.MapOpenAIResponses(); +app.MapOpenAIConversations(); + +app.Run(); +``` + +Then navigate to `/devui` in your browser. diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs new file mode 100644 index 0000000000..36daa2e8bf --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Agents.AI.DevUI; + +/// +/// Provides helper methods for configuring the Microsoft Agents AI DevUI in ASP.NET applications. +/// +public static class DevUIExtensions +{ + /// + /// Adds the necessary services for the DevUI to the application builder. + /// + public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.AddOpenAIConversations(); + builder.Services.AddOpenAIResponses(); + + return builder; + } + + /// + /// Maps an endpoint that serves the DevUI. + /// + /// The to add the endpoint to. + /// + /// The route pattern for the endpoint (e.g., "/devui", "/agent-ui"). + /// Defaults to "/devui" if not specified. This is the path where DevUI will be accessible. + /// + /// A that can be used to add authorization or other endpoint configuration. + /// Thrown when is null. + /// Thrown when is null or whitespace. + public static IEndpointConventionBuilder MapDevUI( + this IEndpointRouteBuilder endpoints, + [StringSyntax("Route")] string pattern = "/devui") + { + ArgumentNullException.ThrowIfNull(endpoints); + ArgumentException.ThrowIfNullOrWhiteSpace(pattern); + + // Ensure the pattern doesn't end with a slash for consistency + var cleanPattern = pattern.TrimEnd('/'); + + // Create the DevUI handler + var logger = endpoints.ServiceProvider.GetRequiredService>(); + var devUIHandler = new DevUIMiddleware(logger, cleanPattern); + + return endpoints.MapGet($"{cleanPattern}/{{*path}}", devUIHandler.HandleRequestAsync) + .WithName($"DevUI at {cleanPattern}") + .WithDescription("Interactive developer interface for Microsoft Agent Framework"); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs new file mode 100644 index 0000000000..fc6dd512ec --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Frozen; +using System.IO.Compression; +using System.Reflection; +using System.Security.Cryptography; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.Agents.AI.DevUI; + +/// +/// Handler that serves embedded DevUI resource files from the 'resources' directory. +/// +internal sealed class DevUIMiddleware +{ + private const string GZipEncodingValue = "gzip"; + private static readonly StringValues s_gzipEncodingHeader = new(GZipEncodingValue); + private static readonly Assembly s_assembly = typeof(DevUIMiddleware).Assembly; + private static readonly FileExtensionContentTypeProvider s_contentTypeProvider = new(); + private static readonly StringValues s_cacheControl = new(new CacheControlHeaderValue() + { + NoCache = true, + NoStore = true, + }.ToString()); + + private readonly ILogger _logger; + private readonly FrozenDictionary _resourceCache; + private readonly string _basePath; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + /// The base path where DevUI is mounted. + public DevUIMiddleware(ILogger logger, string basePath) + { + ArgumentNullException.ThrowIfNull(logger); + ArgumentException.ThrowIfNullOrEmpty(basePath); + this._logger = logger; + this._basePath = basePath.TrimEnd('/'); + + // Build resource cache + var resourceNamePrefix = $"{s_assembly.GetName().Name}.resources."; + this._resourceCache = s_assembly + .GetManifestResourceNames() + .Where(p => p.StartsWith(resourceNamePrefix, StringComparison.Ordinal)) + .ToFrozenDictionary( + p => p[resourceNamePrefix.Length..].Replace('.', '/'), + CreateResourceEntry, + StringComparer.OrdinalIgnoreCase); + } + + /// + /// Handles an HTTP request for DevUI resources. + /// + /// The HTTP context. + public async Task HandleRequestAsync(HttpContext context) + { + var path = context.Request.Path.Value; + + if (path == null) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + // If requesting the base path without a trailing slash, redirect to include it + // This ensures relative URLs in the HTML work correctly + if (string.Equals(path, this._basePath, StringComparison.OrdinalIgnoreCase) && !path.EndsWith('/')) + { + var redirectUrl = $"{path}/"; + if (context.Request.QueryString.HasValue) + { + redirectUrl += context.Request.QueryString.Value; + } + + context.Response.StatusCode = StatusCodes.Status301MovedPermanently; + context.Response.Headers.Location = redirectUrl; + this._logger.LogDebug("Redirecting {OriginalPath} to {RedirectUrl}", path, redirectUrl); + return; + } + + // Remove the base path to get the resource path + var resourcePath = path.StartsWith(this._basePath, StringComparison.OrdinalIgnoreCase) + ? path.Substring(this._basePath.Length).TrimStart('/') + : path.TrimStart('/'); + + // If requesting the base path, serve index.html + if (string.IsNullOrEmpty(resourcePath)) + { + resourcePath = "index.html"; + } + + // Try to serve the embedded resource + if (await this.TryServeResourceAsync(context, resourcePath).ConfigureAwait(false)) + { + return; + } + + // If resource not found, try serving index.html for client-side routing + if (!resourcePath.Contains('.', StringComparison.Ordinal) || resourcePath.EndsWith('/')) + { + if (await this.TryServeResourceAsync(context, "index.html").ConfigureAwait(false)) + { + return; + } + } + + // Resource not found + context.Response.StatusCode = StatusCodes.Status404NotFound; + } + + private async Task TryServeResourceAsync(HttpContext context, string resourcePath) + { + try + { + if (!this._resourceCache.TryGetValue(resourcePath.Replace('.', '/'), out var cacheEntry)) + { + this._logger.LogDebug("Embedded resource not found: {ResourcePath}", resourcePath); + return false; + } + + var response = context.Response; + + // Check if client has cached version + if (context.Request.Headers.IfNoneMatch == cacheEntry.ETag) + { + response.StatusCode = StatusCodes.Status304NotModified; + this._logger.LogDebug("Resource not modified (304): {ResourcePath}", resourcePath); + return true; + } + + var responseHeaders = response.Headers; + + byte[] content; + bool serveCompressed; + if (cacheEntry.CompressedContent is not null && IsGZipAccepted(context.Request)) + { + serveCompressed = true; + responseHeaders.ContentEncoding = s_gzipEncodingHeader; + responseHeaders.ContentLength = cacheEntry.CompressedContent.Length; + content = cacheEntry.CompressedContent; + } + else + { + serveCompressed = false; + responseHeaders.ContentLength = cacheEntry.DecompressedContent!.Length; + content = cacheEntry.DecompressedContent; + } + + responseHeaders.CacheControl = s_cacheControl; + responseHeaders.ContentType = cacheEntry.ContentType; + responseHeaders.ETag = cacheEntry.ETag; + + await response.Body.WriteAsync(content, context.RequestAborted).ConfigureAwait(false); + + this._logger.LogDebug("Served embedded resource: {ResourcePath} (compressed: {Compressed})", resourcePath, serveCompressed); + return true; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Error serving embedded resource: {ResourcePath}", resourcePath); + return false; + } + } + + private static bool IsGZipAccepted(HttpRequest httpRequest) + { + if (httpRequest.GetTypedHeaders().AcceptEncoding is not { Count: > 0 } acceptEncoding) + { + return false; + } + + for (int i = 0; i < acceptEncoding.Count; i++) + { + var encoding = acceptEncoding[i]; + + if (encoding.Quality is not 0 && + string.Equals(encoding.Value.Value, GZipEncodingValue, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static ResourceEntry CreateResourceEntry(string resourceName) + { + using var resourceStream = s_assembly.GetManifestResourceStream(resourceName)!; + using var decompressedContent = new MemoryStream(); + + // Read and cache the original resource content + resourceStream.CopyTo(decompressedContent); + var decompressedArray = decompressedContent.ToArray(); + + // Compress the content + using var compressedContent = new MemoryStream(); + using (var gzip = new GZipStream(compressedContent, CompressionMode.Compress, leaveOpen: true)) + { + // This is a synchronous write to a memory stream. + // There is no benefit to asynchrony here. + gzip.Write(decompressedArray); + } + + // Only use compression if it actually reduces size + byte[]? compressedArray = compressedContent.Length < decompressedArray.Length + ? compressedContent.ToArray() + : null; + + var hash = SHA256.HashData(compressedArray ?? decompressedArray); + var eTag = $"\"{Convert.ToBase64String(hash)}\""; + + // Determine content type from resource name + var contentType = s_contentTypeProvider.TryGetContentType(resourceName, out var ct) + ? ct + : "application/octet-stream"; + + return new ResourceEntry(resourceName, decompressedArray, compressedArray, eTag, contentType); + } + + private sealed class ResourceEntry(string resourceName, byte[] decompressedContent, byte[]? compressedContent, string eTag, string contentType) + { + public byte[]? CompressedContent { get; } = compressedContent; + + public string ContentType { get; } = contentType; + + public byte[] DecompressedContent { get; } = decompressedContent; + + public string ETag { get; } = eTag; + + public string ResourceName { get; } = resourceName; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs new file mode 100644 index 0000000000..fc8bbe3864 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.DevUI.Entities; + +/// +/// JSON serialization context for entity-related types. +/// Enables AOT-compatible JSON serialization using source generators. +/// +[JsonSourceGenerationOptions( + JsonSerializerDefaults.Web, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(EntityInfo))] +[JsonSerializable(typeof(DiscoveryResponse))] +[JsonSerializable(typeof(EnvVarRequirement))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(JsonElement))] +[ExcludeFromCodeCoverage] +internal sealed partial class EntitiesJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs new file mode 100644 index 0000000000..8b5e4e5492 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Agents.AI.DevUI.Entities; + +/// +/// Information about an environment variable required by an entity. +/// +internal sealed record EnvVarRequirement( + [property: JsonPropertyName("name")] + string Name, + + [property: JsonPropertyName("description")] + string? Description = null, + + [property: JsonPropertyName("required")] + bool Required = true, + + [property: JsonPropertyName("example")] + string? Example = null +); + +/// +/// Information about an entity (agent or workflow). +/// +internal sealed record EntityInfo( + [property: JsonPropertyName("id")] + string Id, + + [property: JsonPropertyName("type")] + string Type, + + [property: JsonPropertyName("name")] + string Name, + + [property: JsonPropertyName("description")] + string? Description = null, + + [property: JsonPropertyName("framework")] + string Framework = "dotnet", + + [property: JsonPropertyName("tools")] + List? Tools = null, + + [property: JsonPropertyName("metadata")] + Dictionary? Metadata = null +) +{ + [JsonPropertyName("source")] + public string? Source { get; init; } = "di"; + + [JsonPropertyName("original_url")] + public string? OriginalUrl { get; init; } + + // Workflow-specific fields + [JsonPropertyName("required_env_vars")] + public List? RequiredEnvVars { get; init; } + + [JsonPropertyName("executors")] + public List? Executors { get; init; } + + [JsonPropertyName("workflow_dump")] + public JsonElement? WorkflowDump { get; init; } + + [JsonPropertyName("input_schema")] + public JsonElement? InputSchema { get; init; } + + [JsonPropertyName("input_type_name")] + public string? InputTypeName { get; init; } + + [JsonPropertyName("start_executor_id")] + public string? StartExecutorId { get; init; } +}; + +/// +/// Response containing a list of discovered entities. +/// +internal sealed record DiscoveryResponse( + [property: JsonPropertyName("entities")] + List Entities +); diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs new file mode 100644 index 0000000000..81ce6182d1 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows; +using Microsoft.Agents.AI.Workflows.Checkpointing; + +namespace Microsoft.Agents.AI.DevUI.Entities; + +/// +/// Extension methods for serializing workflows to DevUI-compatible format +/// +internal static class WorkflowSerializationExtensions +{ + // The frontend max iterations default value expected by the DevUI frontend + private const int MaxIterationsDefault = 100; + + /// + /// Converts a workflow to a dictionary representation compatible with DevUI frontend. + /// This matches the Python workflow.to_dict() format expected by the UI. + /// + public static Dictionary ToDevUIDict(this Workflow workflow) + { + var result = new Dictionary + { + ["id"] = workflow.Name ?? Guid.NewGuid().ToString(), + ["start_executor_id"] = workflow.StartExecutorId, + ["max_iterations"] = MaxIterationsDefault + }; + + // Add optional fields + if (!string.IsNullOrEmpty(workflow.Name)) + { + result["name"] = workflow.Name; + } + + if (!string.IsNullOrEmpty(workflow.Description)) + { + result["description"] = workflow.Description; + } + + // Convert executors to Python-compatible format + result["executors"] = ConvertExecutorsToDict(workflow); + + // Convert edges to edge_groups format + result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow); + + return result; + } + + /// + /// Converts workflow executors to a dictionary format compatible with Python + /// + private static Dictionary ConvertExecutorsToDict(Workflow workflow) + { + var executors = new Dictionary(); + + // Extract executor IDs from edges and start executor + // (Registrations is internal, so we infer executors from the graph structure) + var executorIds = new HashSet { workflow.StartExecutorId }; + + var reflectedEdges = workflow.ReflectEdges(); + foreach (var (sourceId, edgeSet) in reflectedEdges) + { + executorIds.Add(sourceId); + foreach (var edge in edgeSet) + { + foreach (var sinkId in edge.Connection.SinkIds) + { + executorIds.Add(sinkId); + } + } + } + + // Create executor entries (we can't access internal Registrations for type info) + foreach (var executorId in executorIds) + { + executors[executorId] = new Dictionary + { + ["id"] = executorId, + ["type"] = "Executor" + }; + } + + return executors; + } + + /// + /// Converts workflow edges to edge_groups format expected by the UI + /// + private static List ConvertEdgesToEdgeGroups(Workflow workflow) + { + var edgeGroups = new List(); + var edgeGroupId = 0; + + // Get edges using the public ReflectEdges method + var reflectedEdges = workflow.ReflectEdges(); + + foreach (var (sourceId, edgeSet) in reflectedEdges) + { + foreach (var edgeInfo in edgeSet) + { + if (edgeInfo is DirectEdgeInfo directEdge) + { + // Single edge group for direct edges + var edges = new List(); + + foreach (var source in directEdge.Connection.SourceIds) + { + foreach (var sink in directEdge.Connection.SinkIds) + { + var edge = new Dictionary + { + ["source_id"] = source, + ["target_id"] = sink + }; + + // Add condition name if this is a conditional edge + if (directEdge.HasCondition) + { + edge["condition_name"] = "predicate"; + } + + edges.Add(edge); + } + } + + edgeGroups.Add(new Dictionary + { + ["id"] = $"edge_group_{edgeGroupId++}", + ["type"] = "SingleEdgeGroup", + ["edges"] = edges + }); + } + else if (edgeInfo is FanOutEdgeInfo fanOutEdge) + { + // FanOut edge group + var edges = new List(); + + foreach (var source in fanOutEdge.Connection.SourceIds) + { + foreach (var sink in fanOutEdge.Connection.SinkIds) + { + edges.Add(new Dictionary + { + ["source_id"] = source, + ["target_id"] = sink + }); + } + } + + var fanOutGroup = new Dictionary + { + ["id"] = $"edge_group_{edgeGroupId++}", + ["type"] = "FanOutEdgeGroup", + ["edges"] = edges + }; + + if (fanOutEdge.HasAssigner) + { + fanOutGroup["selection_func_name"] = "selector"; + } + + edgeGroups.Add(fanOutGroup); + } + else if (edgeInfo is FanInEdgeInfo fanInEdge) + { + // FanIn edge group + var edges = new List(); + + foreach (var source in fanInEdge.Connection.SourceIds) + { + foreach (var sink in fanInEdge.Connection.SinkIds) + { + edges.Add(new Dictionary + { + ["source_id"] = source, + ["target_id"] = sink + }); + } + } + + edgeGroups.Add(new Dictionary + { + ["id"] = $"edge_group_{edgeGroupId++}", + ["type"] = "FanInEdgeGroup", + ["edges"] = edges + }); + } + } + } + + return edgeGroups; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs new file mode 100644 index 0000000000..ef52c15bf4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; + +using Microsoft.Agents.AI.DevUI.Entities; +using Microsoft.Agents.AI.Hosting; + +namespace Microsoft.Agents.AI.DevUI; + +/// +/// Provides extension methods for mapping entity discovery and management endpoints to an . +/// +public static class EntitiesApiExtensions +{ + /// + /// Maps HTTP API endpoints for entity discovery and management. + /// + /// The to add the routes to. + /// The for method chaining. + /// + /// This extension method registers the following endpoints: + /// + /// GET /v1/entities - List all registered entities (agents and workflows) + /// GET /v1/entities/{entityId}/info - Get detailed information about a specific entity + /// + /// The endpoints are compatible with the Python DevUI frontend and automatically discover entities + /// from the registered and services. + /// + public static IEndpointConventionBuilder MapEntities(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/v1/entities") + .WithTags("Entities"); + + // List all entities + group.MapGet("", ListEntitiesAsync) + .WithName("ListEntities") + .WithSummary("List all registered entities (agents and workflows)") + .Produces(StatusCodes.Status200OK, contentType: "application/json"); + + // Get detailed entity information + group.MapGet("{entityId}/info", GetEntityInfoAsync) + .WithName("GetEntityInfo") + .WithSummary("Get detailed information about a specific entity") + .Produces(StatusCodes.Status200OK, contentType: "application/json") + .Produces(StatusCodes.Status404NotFound); + + return group; + } + + private static async Task ListEntitiesAsync( + AgentCatalog? agentCatalog, + WorkflowCatalog? workflowCatalog, + CancellationToken cancellationToken) + { + try + { + var entities = new List(); + + // Discover agents from the agent catalog + if (agentCatalog is not null) + { + await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false)) + { + if (agent.GetType().Name == "WorkflowHostAgent") + { + // HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows, + // and workflows are handled below. + continue; + } + + entities.Add(new EntityInfo( + Id: agent.Name ?? agent.Id, + Type: "agent", + Name: agent.Name ?? agent.Id, + Description: agent.Description, + Framework: "agent-framework", + Tools: null, + Metadata: [] + ) + { + Source = "in_memory" + }); + } + } + + // Discover workflows from the workflow catalog + if (workflowCatalog is not null) + { + await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false)) + { + // Extract executor IDs from the workflow structure + var executorIds = new HashSet { workflow.StartExecutorId }; + var reflectedEdges = workflow.ReflectEdges(); + foreach (var (sourceId, edgeSet) in reflectedEdges) + { + executorIds.Add(sourceId); + foreach (var edge in edgeSet) + { + foreach (var sinkId in edge.Connection.SinkIds) + { + executorIds.Add(sinkId); + } + } + } + + // Create a default input schema (string type) + var defaultInputSchema = new Dictionary + { + ["type"] = "string" + }; + + entities.Add(new EntityInfo( + Id: workflow.Name ?? workflow.StartExecutorId, + Type: "workflow", + Name: workflow.Name ?? workflow.StartExecutorId, + Description: workflow.Description, + Framework: "agent-framework", + Tools: [.. executorIds], + Metadata: [] + ) + { + Source = "in_memory", + WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()), + InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema), + InputTypeName = "string", + StartExecutorId = workflow.StartExecutorId + }); + } + } + + return Results.Json(new DiscoveryResponse(entities), EntitiesJsonContext.Default.DiscoveryResponse); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status500InternalServerError, + title: "Error listing entities"); + } + } + + private static async Task GetEntityInfoAsync( + string entityId, + AgentCatalog? agentCatalog, + WorkflowCatalog? workflowCatalog, + CancellationToken cancellationToken) + { + try + { + // Try to find the entity among discovered agents + if (agentCatalog is not null) + { + await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false)) + { + if (agent.GetType().Name == "WorkflowHostAgent") + { + // HACK: ignore WorkflowHostAgent instances as they are just wrappers around workflows, + // and workflows are handled below. + continue; + } + + if (string.Equals(agent.Name, entityId, StringComparison.OrdinalIgnoreCase) || + string.Equals(agent.Id, entityId, StringComparison.OrdinalIgnoreCase)) + { + var entityInfo = new EntityInfo( + Id: agent.Name ?? agent.Id, + Type: "agent", + Name: agent.Name ?? agent.Id, + Description: agent.Description, + Framework: "agent-framework", + Tools: null, + Metadata: [] + ) + { + Source = "in_memory" + }; + + return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo); + } + } + } + + // Try to find the entity among discovered workflows + if (workflowCatalog is not null) + { + await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false)) + { + var workflowId = workflow.Name ?? workflow.StartExecutorId; + if (string.Equals(workflowId, entityId, StringComparison.OrdinalIgnoreCase)) + { + // Extract executor IDs from the workflow structure + var executorIds = new HashSet { workflow.StartExecutorId }; + var reflectedEdges = workflow.ReflectEdges(); + foreach (var (sourceId, edgeSet) in reflectedEdges) + { + executorIds.Add(sourceId); + foreach (var edge in edgeSet) + { + foreach (var sinkId in edge.Connection.SinkIds) + { + executorIds.Add(sinkId); + } + } + } + + // Create a default input schema (string type) + var defaultInputSchema = new Dictionary + { + ["type"] = "string" + }; + + var entityInfo = new EntityInfo( + Id: workflowId, + Type: "workflow", + Name: workflow.Name ?? workflow.StartExecutorId, + Description: workflow.Description, + Framework: "agent-framework", + Tools: [.. executorIds], + Metadata: [] + ) + { + Source = "in_memory", + WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()), + InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema), + InputTypeName = "Input", + StartExecutorId = workflow.StartExecutorId + }; + + return Results.Json(entityInfo, EntitiesJsonContext.Default.EntityInfo); + } + } + } + + return Results.NotFound(new { error = new { message = $"Entity '{entityId}' not found.", type = "invalid_request_error" } }); + } + catch (Exception ex) + { + return Results.Problem( + detail: ex.Message, + statusCode: StatusCodes.Status500InternalServerError, + title: "Error getting entity info"); + } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.Frontend.targets b/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.Frontend.targets new file mode 100644 index 0000000000..f62a92e28d --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.Frontend.targets @@ -0,0 +1,68 @@ + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..\python\packages\devui\frontend')) + $([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..\..\python\packages\devui\agent_framework_devui\ui')) + $(FrontendRoot)\package.json + $(FrontendRoot)\node_modules + + + + + + + + + + + + + + + + + + + + + + + + $(BaseIntermediateOutputPath)\frontend.build.marker + + + + + + + + + + + + + + resources\$([MSBuild]::MakeRelative('$(FrontendBuildOutput)', '%(Identity)')) + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj b/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj new file mode 100644 index 0000000000..43f5b54eac --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + enable + enable + Microsoft.Agents.AI.DevUI + Library + Microsoft Agent Framework Developer UI + Provides Microsoft Agent Framework support for developer UI. + true + + $(NoWarn);CS1591;CA1852;CA1050;RCS1037;RCS1036;RCS1124;RCS1021;RCS1146;RCS1211;CA2007;CA1308;IL2026;IL3050;CA1812 + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Properties/launchSettings.json b/dotnet/src/Microsoft.Agents.AI.DevUI/Properties/launchSettings.json new file mode 100644 index 0000000000..32587da80b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.Agents.AI.DevUI": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57966;http://localhost:57967" + } + } +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/README.md b/dotnet/src/Microsoft.Agents.AI.DevUI/README.md new file mode 100644 index 0000000000..dbc4b328ed --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/README.md @@ -0,0 +1,53 @@ +# Microsoft.Agents.AI.DevUI + +This package provides a web interface for testing and debugging AI agents during development. + +## Installation + +```bash +dotnet add package Microsoft.Agents.AI.DevUI +dotnet add package Microsoft.Agents.AI.Hosting +dotnet add package Microsoft.Agents.AI.Hosting.OpenAI +``` + +## Usage + +Add DevUI services and map the endpoint in your ASP.NET Core application: + +```csharp +using Microsoft.Agents.AI.DevUI; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Agents.AI.Hosting.OpenAI; + +var builder = WebApplication.CreateBuilder(args); + +// Register your agents +builder.AddAIAgent("assistant", "You are a helpful assistant."); + +// Add DevUI services +builder.AddDevUI(); + +var app = builder.Build(); + +// Map DevUI endpoint +app.MapDevUI(); // Available at /devui + +// Map required endpoints +app.MapEntities(); +app.MapOpenAIResponses(); +app.MapOpenAIConversations(); + +app.Run(); +``` + +### Custom Path + +```csharp +app.MapDevUI("/agent-ui"); +``` + +### With Authorization + +```csharp +app.MapDevUI().RequireAuthorization("Developer"); +``` \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/wwwroot/index.html b/dotnet/src/Microsoft.Agents.AI.DevUI/wwwroot/index.html new file mode 100644 index 0000000000..ff0ead674e --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/wwwroot/index.html @@ -0,0 +1,14 @@ + + + + + + + Agent Framework Dev UI + + + + +
+ + diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs index 7866df5ec6..ffa044791f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs @@ -153,13 +153,27 @@ IAsyncEnumerable InvokeStageAsync( case AgentRunUpdateEvent agentUpdate: yield return agentUpdate.Update; break; + case RequestInfoEvent requestInfo: FunctionCallContent fcContent = requestInfo.Request.ToFunctionCall(); AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, fcContent); yield return update; break; + case SuperStepCompletedEvent stepCompleted: this.LastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint; + goto default; + + default: + // Emit all other workflow events for observability (DevUI, logging, etc.) + yield return new AgentRunResponseUpdate(ChatRole.Assistant, []) + { + CreatedAt = DateTimeOffset.UtcNow, + MessageId = Guid.NewGuid().ToString("N"), + Role = ChatRole.Assistant, + ResponseId = this.LastResponseId, + RawRepresentation = evt + }; break; } } diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index-CE4pGoXh.css b/python/packages/devui/agent_framework_devui/ui/assets/index-CE4pGoXh.css deleted file mode 100644 index c86e173c41..0000000000 --- a/python/packages/devui/agent_framework_devui/ui/assets/index-CE4pGoXh.css +++ /dev/null @@ -1 +0,0 @@ -/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--drop-shadow-lg:0 4px 4px #00000026;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);color:var(--foreground)}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-2{inset:calc(var(--spacing)*2)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.-top-1{top:calc(var(--spacing)*-1)}.-top-2{top:calc(var(--spacing)*-2)}.top-1{top:calc(var(--spacing)*1)}.top-2{top:calc(var(--spacing)*2)}.top-4{top:calc(var(--spacing)*4)}.-right-1{right:calc(var(--spacing)*-1)}.-right-2{right:calc(var(--spacing)*-2)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.-bottom-2{bottom:calc(var(--spacing)*-2)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-3{bottom:calc(var(--spacing)*3)}.bottom-14{bottom:calc(var(--spacing)*14)}.bottom-24{bottom:calc(var(--spacing)*24)}.-left-2{left:calc(var(--spacing)*-2)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.container\!{width:100%!important}@media (min-width:40rem){.container\!{max-width:40rem!important}}@media (min-width:48rem){.container\!{max-width:48rem!important}}@media (min-width:64rem){.container\!{max-width:64rem!important}}@media (min-width:80rem){.container\!{max-width:80rem!important}}@media (min-width:96rem){.container\!{max-width:96rem!important}}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing)*1)}.my-2{margin-block:calc(var(--spacing)*2)}.my-3{margin-block:calc(var(--spacing)*3)}.my-4{margin-block:calc(var(--spacing)*4)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-12{margin-top:calc(var(--spacing)*12)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-1\.5{margin-right:calc(var(--spacing)*1.5)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-1\.5{margin-left:calc(var(--spacing)*1.5)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-5{margin-left:calc(var(--spacing)*5)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.field-sizing-content{field-sizing:content}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-3\.5{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-9{width:calc(var(--spacing)*9);height:calc(var(--spacing)*9)}.\!h-2{height:calc(var(--spacing)*2)!important}.h-0{height:calc(var(--spacing)*0)}.h-0\.5{height:calc(var(--spacing)*.5)}.h-1{height:calc(var(--spacing)*1)}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-16{height:calc(var(--spacing)*16)}.h-32{height:calc(var(--spacing)*32)}.h-96{height:calc(var(--spacing)*96)}.h-\[1\.2rem\]{height:1.2rem}.h-\[500px\]{height:500px}.h-\[calc\(100vh-3\.5rem\)\]{height:calc(100vh - 3.5rem)}.h-\[calc\(100vh-3\.7rem\)\]{height:calc(100vh - 3.7rem)}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\(--radix-dropdown-menu-content-available-height\){max-height:var(--radix-dropdown-menu-content-available-height)}.max-h-\(--radix-select-content-available-height\){max-height:var(--radix-select-content-available-height)}.max-h-20{max-height:calc(var(--spacing)*20)}.max-h-24{max-height:calc(var(--spacing)*24)}.max-h-32{max-height:calc(var(--spacing)*32)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-\[90vh\]{max-height:90vh}.max-h-\[200px\]{max-height:200px}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.\!min-h-0{min-height:calc(var(--spacing)*0)!important}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-16{min-height:calc(var(--spacing)*16)}.min-h-\[36px\]{min-height:36px}.min-h-\[40px\]{min-height:40px}.min-h-\[50vh\]{min-height:50vh}.min-h-\[240px\]{min-height:240px}.min-h-\[400px\]{min-height:400px}.min-h-screen{min-height:100vh}.\!w-2{width:calc(var(--spacing)*2)!important}.w-1{width:calc(var(--spacing)*1)}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-56{width:calc(var(--spacing)*56)}.w-64{width:calc(var(--spacing)*64)}.w-80{width:calc(var(--spacing)*80)}.w-\[1\.2rem\]{width:1.2rem}.w-\[600px\]{width:600px}.w-\[800px\]{width:800px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[80\%\]{max-width:80%}.max-w-\[90vw\]{max-width:90vw}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.\!min-w-0{min-width:calc(var(--spacing)*0)!important}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[300px\]{min-width:300px}.min-w-\[400px\]{min-width:400px}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.origin-\(--radix-dropdown-menu-content-transform-origin\){transform-origin:var(--radix-dropdown-menu-content-transform-origin)}.origin-\(--radix-select-content-transform-origin\){transform-origin:var(--radix-select-content-transform-origin)}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.rotate-0{rotate:none}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.scroll-my-1{scroll-margin-block:calc(var(--spacing)*1)}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.flex-col{flex-direction:column}.flex-row-reverse{flex-direction:row-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.\!rounded-full{border-radius:3.40282e38px!important}.rounded{border-radius:.25rem}.rounded-\[4px\]{border-radius:4px}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-none{border-radius:0}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.\!border{border-style:var(--tw-border-style)!important;border-width:1px!important}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-gray-600{border-color:var(--color-gray-600)!important}.border-\[\#643FB2\]{border-color:#643fb2}.border-\[\#643FB2\]\/30{border-color:#643fb24d}.border-\[\#643FB2\]\/40{border-color:#643fb266}.border-amber-200{border-color:var(--color-amber-200)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500\/30{border-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/30{border-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.border-blue-500\/40{border-color:#3080ff66}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/40{border-color:color-mix(in oklab,var(--color-blue-500)40%,transparent)}}.border-border,.border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/50{border-color:color-mix(in oklab,var(--border)50%,transparent)}}.border-current\/30{border-color:currentColor}@supports (color:color-mix(in lab,red,red)){.border-current\/30{border-color:color-mix(in oklab,currentcolor 30%,transparent)}}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/30{border-color:color-mix(in oklab,var(--destructive)30%,transparent)}}.border-destructive\/50{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/50{border-color:color-mix(in oklab,var(--destructive)50%,transparent)}}.border-destructive\/70{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/70{border-color:color-mix(in oklab,var(--destructive)70%,transparent)}}.border-emerald-300{border-color:var(--color-emerald-300)}.border-foreground\/5{border-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.border-foreground\/5{border-color:color-mix(in oklab,var(--foreground)5%,transparent)}}.border-foreground\/10{border-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.border-foreground\/10{border-color:color-mix(in oklab,var(--foreground)10%,transparent)}}.border-foreground\/20{border-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.border-foreground\/20{border-color:color-mix(in oklab,var(--foreground)20%,transparent)}}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-green-500{border-color:var(--color-green-500)}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab,red,red)){.border-green-500\/30{border-color:color-mix(in oklab,var(--color-green-500)30%,transparent)}}.border-green-500\/40{border-color:#00c75866}@supports (color:color-mix(in lab,red,red)){.border-green-500\/40{border-color:color-mix(in oklab,var(--color-green-500)40%,transparent)}}.border-input{border-color:var(--input)}.border-muted{border-color:var(--muted)}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-500{border-color:var(--color-orange-500)}.border-primary,.border-primary\/20{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/20{border-color:color-mix(in oklab,var(--primary)20%,transparent)}}.border-red-200{border-color:var(--color-red-200)}.border-red-500{border-color:var(--color-red-500)}.border-transparent{border-color:#0000}.border-yellow-200{border-color:var(--color-yellow-200)}.border-t-transparent{border-top-color:#0000}.border-l-transparent{border-left-color:#0000}.bg-\[\#643FB2\]{background-color:#643fb2}.bg-\[\#643FB2\]\/5{background-color:#643fb20d}.bg-\[\#643FB2\]\/10{background-color:#643fb21a}.bg-accent\/10{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--accent)10%,transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-background{background-color:var(--background)}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/80{background-color:#eff6ffcc}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/80{background-color:color-mix(in oklab,var(--color-blue-50)80%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/5{background-color:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/5{background-color:color-mix(in oklab,var(--color-blue-500)5%,transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-current{background-color:currentColor}.bg-destructive,.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/5{background-color:color-mix(in oklab,var(--destructive)5%,transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.bg-destructive\/80{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/80{background-color:color-mix(in oklab,var(--destructive)80%,transparent)}}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-foreground\/5{background-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.bg-foreground\/5{background-color:color-mix(in oklab,var(--foreground)5%,transparent)}}.bg-foreground\/10{background-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.bg-foreground\/10{background-color:color-mix(in oklab,var(--foreground)10%,transparent)}}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-900\/90{background-color:#101828e6}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/90{background-color:color-mix(in oklab,var(--color-gray-900)90%,transparent)}}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-500\/5{background-color:#00c7580d}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/5{background-color:color-mix(in oklab,var(--color-green-500)5%,transparent)}}.bg-green-500\/10{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/10{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.bg-muted,.bg-muted\/30{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/30{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-popover{background-color:var(--popover)}.bg-primary,.bg-primary\/10{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.bg-primary\/30{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/30{background-color:color-mix(in oklab,var(--primary)30%,transparent)}}.bg-primary\/40{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/40{background-color:color-mix(in oklab,var(--primary)40%,transparent)}}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-500{background-color:var(--color-red-500)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-100{background-color:var(--color-yellow-100)}.fill-current{fill:currentColor}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-\[1px\]{padding:1px}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-6{padding-top:calc(var(--spacing)*6)}.pt-8{padding-top:calc(var(--spacing)*8)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-4{padding-right:calc(var(--spacing)*4)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-8{padding-left:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#643FB2\]{color:#643fb2}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-card-foreground{color:var(--card-foreground)}.text-current{color:currentColor}.text-destructive{color:var(--destructive)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-foreground{color:var(--foreground)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-muted-foreground,.text-muted-foreground\/80{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/80{color:color-mix(in oklab,var(--muted-foreground)80%,transparent)}}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-800{color:var(--color-orange-800)}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[\#643FB2\]\/20{--tw-shadow-color:#643fb233}@supports (color:color-mix(in lab,red,red)){.shadow-\[\#643FB2\]\/20{--tw-shadow-color:color-mix(in oklab,oklab(47.4316% .069152 -.159147/.2) var(--tw-shadow-alpha),transparent)}}.shadow-green-500\/20{--tw-shadow-color:#00c75833}@supports (color:color-mix(in lab,red,red)){.shadow-green-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-green-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-orange-500\/20{--tw-shadow-color:#fe6e0033}@supports (color:color-mix(in lab,red,red)){.shadow-orange-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-orange-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-primary\/25{--tw-shadow-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.shadow-primary\/25{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--primary)25%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-red-500\/20{--tw-shadow-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.shadow-red-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-red-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[animation-delay\:-0\.3s\]{animation-delay:-.3s}.\[animation-delay\:-0\.15s\]{animation-delay:-.15s}.fade-in{--tw-enter-opacity:0}.running{animation-play-state:running}.slide-in-from-bottom-2{--tw-enter-translate-y:calc(2*var(--spacing))}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}@media (hover:hover){.group-hover\:bg-primary:is(:where(.group):hover *){background-color:var(--primary)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\:shadow-md:is(:where(.group):hover *){--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-hover\:shadow-primary\/20:is(:where(.group):hover *){--tw-shadow-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.group-hover\:shadow-primary\/20:is(:where(.group):hover *){--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--primary)20%,transparent)var(--tw-shadow-alpha),transparent)}}}.group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.group)[data-disabled=true] *){pointer-events:none}.group-data-\[disabled\=true\]\:opacity-50:is(:where(.group)[data-disabled=true] *){opacity:.5}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.selection\:bg-primary ::selection{background-color:var(--primary)}.selection\:bg-primary::selection{background-color:var(--primary)}.selection\:text-primary-foreground ::selection{color:var(--primary-foreground)}.selection\:text-primary-foreground::selection{color:var(--primary-foreground)}.file\:inline-flex::file-selector-button{display:inline-flex}.file\:h-7::file-selector-button{height:calc(var(--spacing)*7)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.first\:mt-0:first-child{margin-top:calc(var(--spacing)*0)}.last\:border-r-0:last-child{border-right-style:var(--tw-border-style);border-right-width:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-\[\#643FB2\]\/10:hover{background-color:#643fb21a}.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-blue-500\/10:hover{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-blue-500\/10:hover{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.hover\:bg-destructive\/20:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/20:hover{background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.hover\:bg-destructive\/80:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/80:hover{background-color:color-mix(in oklab,var(--destructive)80%,transparent)}}.hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.hover\:bg-green-500\/10:hover{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/10:hover{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.hover\:bg-muted:hover,.hover\:bg-muted\/30:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/30:hover{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.hover\:bg-primary\/20:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/20:hover{background-color:color-mix(in oklab,var(--primary)20%,transparent)}}.hover\:bg-primary\/80:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab,var(--primary)80%,transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--spacing)*2.5)}.has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--spacing)*3)}.has-\[\>svg\]\:px-4:has(>svg){padding-inline:calc(var(--spacing)*4)}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[inset\]\:pl-8[data-inset]{padding-left:calc(var(--spacing)*8)}.data-\[placeholder\]\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.data-\[size\=default\]\:h-9[data-size=default]{height:calc(var(--spacing)*9)}.data-\[size\=sm\]\:h-8[data-size=sm]{height:calc(var(--spacing)*8)}:is(.\*\:data-\[slot\=select-value\]\:line-clamp-1>*)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.\*\:data-\[slot\=select-value\]\:flex>*)[data-slot=select-value]{display:flex}:is(.\*\:data-\[slot\=select-value\]\:items-center>*)[data-slot=select-value]{align-items:center}:is(.\*\:data-\[slot\=select-value\]\:gap-2>*)[data-slot=select-value]{gap:calc(var(--spacing)*2)}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--primary)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.data-\[state\=open\]\:text-accent-foreground[data-state=open]{color:var(--accent-foreground)}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.data-\[variant\=destructive\]\:text-destructive[data-variant=destructive]{color:var(--destructive)}.data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.data-\[variant\=destructive\]\:focus\:text-destructive[data-variant=destructive]:focus{color:var(--destructive)}@media (min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:max-w-lg{max-width:var(--container-lg)}.sm\:flex-none{flex:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:col-start-2{grid-column-start:2}.md\:inline{display:inline}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:gap-8{gap:calc(var(--spacing)*8)}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:col-span-3{grid-column:span 3/span 3}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}}@media (min-width:80rem){.xl\:col-span-2{grid-column:span 2/span 2}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:max-w-5xl{max-width:var(--container-5xl)}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.dark\:scale-0:is(.dark *){--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.dark\:scale-100:is(.dark *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.dark\:-rotate-90:is(.dark *){rotate:-90deg}.dark\:rotate-0:is(.dark *){rotate:none}.dark\:\!border-gray-500:is(.dark *){border-color:var(--color-gray-500)!important}.dark\:\!border-gray-600:is(.dark *){border-color:var(--color-gray-600)!important}.dark\:border-\[\#8B5CF6\]:is(.dark *){border-color:#8b5cf6}.dark\:border-\[\#8B5CF6\]\/30:is(.dark *){border-color:#8b5cf64d}.dark\:border-\[\#8B5CF6\]\/40:is(.dark *){border-color:#8b5cf666}.dark\:border-amber-800:is(.dark *){border-color:var(--color-amber-800)}.dark\:border-amber-900:is(.dark *){border-color:var(--color-amber-900)}.dark\:border-blue-500:is(.dark *){border-color:var(--color-blue-500)}.dark\:border-blue-500\/30:is(.dark *){border-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.dark\:border-blue-500\/30:is(.dark *){border-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.dark\:border-blue-500\/40:is(.dark *){border-color:#3080ff66}@supports (color:color-mix(in lab,red,red)){.dark\:border-blue-500\/40:is(.dark *){border-color:color-mix(in oklab,var(--color-blue-500)40%,transparent)}}.dark\:border-blue-600:is(.dark *){border-color:var(--color-blue-600)}.dark\:border-blue-800:is(.dark *){border-color:var(--color-blue-800)}.dark\:border-emerald-600:is(.dark *){border-color:var(--color-emerald-600)}.dark\:border-gray-600:is(.dark *){border-color:var(--color-gray-600)}.dark\:border-gray-700:is(.dark *){border-color:var(--color-gray-700)}.dark\:border-green-400:is(.dark *){border-color:var(--color-green-400)}.dark\:border-green-400\/30:is(.dark *){border-color:#05df724d}@supports (color:color-mix(in lab,red,red)){.dark\:border-green-400\/30:is(.dark *){border-color:color-mix(in oklab,var(--color-green-400)30%,transparent)}}.dark\:border-green-400\/40:is(.dark *){border-color:#05df7266}@supports (color:color-mix(in lab,red,red)){.dark\:border-green-400\/40:is(.dark *){border-color:color-mix(in oklab,var(--color-green-400)40%,transparent)}}.dark\:border-green-800:is(.dark *){border-color:var(--color-green-800)}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:border-orange-400:is(.dark *){border-color:var(--color-orange-400)}.dark\:border-orange-800:is(.dark *){border-color:var(--color-orange-800)}.dark\:border-red-400:is(.dark *){border-color:var(--color-red-400)}.dark\:border-red-800:is(.dark *){border-color:var(--color-red-800)}.dark\:\!bg-gray-800\/90:is(.dark *){background-color:#1e2939e6!important}@supports (color:color-mix(in lab,red,red)){.dark\:\!bg-gray-800\/90:is(.dark *){background-color:color-mix(in oklab,var(--color-gray-800)90%,transparent)!important}}.dark\:bg-\[\#8B5CF6\]:is(.dark *){background-color:#8b5cf6}.dark\:bg-\[\#8B5CF6\]\/5:is(.dark *){background-color:#8b5cf60d}.dark\:bg-\[\#8B5CF6\]\/10:is(.dark *){background-color:#8b5cf61a}.dark\:bg-amber-950\/20:is(.dark *){background-color:#46190133}@supports (color:color-mix(in lab,red,red)){.dark\:bg-amber-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-amber-950)20%,transparent)}}.dark\:bg-amber-950\/50:is(.dark *){background-color:#46190180}@supports (color:color-mix(in lab,red,red)){.dark\:bg-amber-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-amber-950)50%,transparent)}}.dark\:bg-background:is(.dark *){background-color:var(--background)}.dark\:bg-blue-500\/5:is(.dark *){background-color:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-500\/5:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-500)5%,transparent)}}.dark\:bg-blue-500\/10:is(.dark *){background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-500\/10:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.dark\:bg-blue-900:is(.dark *){background-color:var(--color-blue-900)}.dark\:bg-blue-900\/50:is(.dark *){background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-900\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark\:bg-blue-950\/20:is(.dark *){background-color:#16245633}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-950)20%,transparent)}}.dark\:bg-blue-950\/40:is(.dark *){background-color:#16245666}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/40:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-950)40%,transparent)}}.dark\:bg-blue-950\/50:is(.dark *){background-color:#16245680}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-950)50%,transparent)}}.dark\:bg-card:is(.dark *){background-color:var(--card)}.dark\:bg-destructive\/20:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/20:is(.dark *){background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.dark\:bg-destructive\/60:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/60:is(.dark *){background-color:color-mix(in oklab,var(--destructive)60%,transparent)}}.dark\:bg-emerald-900\/50:is(.dark *){background-color:#004e3b80}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-900\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-emerald-900)50%,transparent)}}.dark\:bg-emerald-950\/50:is(.dark *){background-color:#002c2280}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-emerald-950)50%,transparent)}}.dark\:bg-foreground\/10:is(.dark *){background-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-foreground\/10:is(.dark *){background-color:color-mix(in oklab,var(--foreground)10%,transparent)}}.dark\:bg-gray-500:is(.dark *){background-color:var(--color-gray-500)}.dark\:bg-gray-800:is(.dark *){background-color:var(--color-gray-800)}.dark\:bg-gray-800\/90:is(.dark *){background-color:#1e2939e6}@supports (color:color-mix(in lab,red,red)){.dark\:bg-gray-800\/90:is(.dark *){background-color:color-mix(in oklab,var(--color-gray-800)90%,transparent)}}.dark\:bg-gray-900:is(.dark *){background-color:var(--color-gray-900)}.dark\:bg-green-400:is(.dark *){background-color:var(--color-green-400)}.dark\:bg-green-400\/5:is(.dark *){background-color:#05df720d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-400\/5:is(.dark *){background-color:color-mix(in oklab,var(--color-green-400)5%,transparent)}}.dark\:bg-green-400\/10:is(.dark *){background-color:#05df721a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-400\/10:is(.dark *){background-color:color-mix(in oklab,var(--color-green-400)10%,transparent)}}.dark\:bg-green-900:is(.dark *){background-color:var(--color-green-900)}.dark\:bg-green-950:is(.dark *){background-color:var(--color-green-950)}.dark\:bg-green-950\/20:is(.dark *){background-color:#032e1533}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-green-950)20%,transparent)}}.dark\:bg-green-950\/50:is(.dark *){background-color:#032e1580}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-green-950)50%,transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab,var(--input)30%,transparent)}}.dark\:bg-orange-400:is(.dark *){background-color:var(--color-orange-400)}.dark\:bg-orange-900:is(.dark *){background-color:var(--color-orange-900)}.dark\:bg-orange-950:is(.dark *){background-color:var(--color-orange-950)}.dark\:bg-orange-950\/50:is(.dark *){background-color:#44130680}@supports (color:color-mix(in lab,red,red)){.dark\:bg-orange-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-orange-950)50%,transparent)}}.dark\:bg-purple-900:is(.dark *){background-color:var(--color-purple-900)}.dark\:bg-red-400:is(.dark *){background-color:var(--color-red-400)}.dark\:bg-red-900:is(.dark *){background-color:var(--color-red-900)}.dark\:bg-red-950:is(.dark *){background-color:var(--color-red-950)}.dark\:bg-red-950\/20:is(.dark *){background-color:#46080933}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-red-950)20%,transparent)}}.dark\:text-\[\#8B5CF6\]:is(.dark *){color:#8b5cf6}.dark\:text-amber-100:is(.dark *){color:var(--color-amber-100)}.dark\:text-amber-200:is(.dark *){color:var(--color-amber-200)}.dark\:text-amber-300:is(.dark *){color:var(--color-amber-300)}.dark\:text-amber-400:is(.dark *){color:var(--color-amber-400)}.dark\:text-amber-500:is(.dark *){color:var(--color-amber-500)}.dark\:text-blue-200:is(.dark *){color:var(--color-blue-200)}.dark\:text-blue-300:is(.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:is(.dark *){color:var(--color-blue-400)}.dark\:text-blue-500:is(.dark *){color:var(--color-blue-500)}.dark\:text-emerald-200:is(.dark *){color:var(--color-emerald-200)}.dark\:text-emerald-300:is(.dark *){color:var(--color-emerald-300)}.dark\:text-emerald-400:is(.dark *){color:var(--color-emerald-400)}.dark\:text-gray-100:is(.dark *){color:var(--color-gray-100)}.dark\:text-gray-300:is(.dark *){color:var(--color-gray-300)}.dark\:text-gray-400:is(.dark *){color:var(--color-gray-400)}.dark\:text-green-200:is(.dark *){color:var(--color-green-200)}.dark\:text-green-300:is(.dark *){color:var(--color-green-300)}.dark\:text-green-400:is(.dark *){color:var(--color-green-400)}.dark\:text-orange-200:is(.dark *){color:var(--color-orange-200)}.dark\:text-orange-400:is(.dark *){color:var(--color-orange-400)}.dark\:text-purple-400:is(.dark *){color:var(--color-purple-400)}.dark\:text-red-200:is(.dark *){color:var(--color-red-200)}.dark\:text-red-400:is(.dark *){color:var(--color-red-400)}.dark\:text-yellow-400:is(.dark *){color:var(--color-yellow-400)}.dark\:opacity-30:is(.dark *){opacity:.3}@media (hover:hover){.dark\:hover\:bg-\[\#8B5CF6\]\/10:is(.dark *):hover{background-color:#8b5cf61a}.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.dark\:hover\:bg-amber-950\/30:is(.dark *):hover{background-color:#4619014d}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-amber-950\/30:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-amber-950)30%,transparent)}}.dark\:hover\:bg-blue-500\/10:is(.dark *):hover{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-blue-500\/10:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:color-mix(in oklab,var(--destructive)30%,transparent)}}.dark\:hover\:bg-gray-800:is(.dark *):hover{background-color:var(--color-gray-800)}.dark\:hover\:bg-green-400\/10:is(.dark *):hover{background-color:#05df721a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-green-400\/10:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-green-400)10%,transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--input)50%,transparent)}}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:data-\[state\=checked\]\:bg-primary:is(.dark *)[data-state=checked]{background-color:var(--primary)}.dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.\[\&_p\]\:leading-relaxed p{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_svg\:not\(\[class\*\=\'text-\'\]\)\]\:text-muted-foreground svg:not([class*=text-]){color:var(--muted-foreground)}.\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--spacing)*6)}.\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--spacing)*6)}:is(.\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--spacing)*2)}:is(.data-\[variant\=destructive\]\:\*\:\[svg\]\:\!text-destructive[data-variant=destructive]>*):is(svg){color:var(--destructive)!important}.\[\&\>svg\]\:absolute>svg{position:absolute}.\[\&\>svg\]\:top-4>svg{top:calc(var(--spacing)*4)}.\[\&\>svg\]\:left-4>svg{left:calc(var(--spacing)*4)}.\[\&\>svg\]\:text-foreground>svg{color:var(--foreground)}.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div{--tw-translate-y:-3px;translate:var(--tw-translate-x)var(--tw-translate-y)}.\[\&\>svg\~\*\]\:pl-7>svg~*{padding-left:calc(var(--spacing)*7)}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(48% .18 290);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0);--chart-1:oklch(64.6% .222 41.116);--chart-2:oklch(60% .118 184.704);--chart-3:oklch(39.8% .07 227.392);--chart-4:oklch(82.8% .189 84.429);--chart-5:oklch(76.9% .188 70.08);--sidebar:oklch(98.5% 0 0);--sidebar-foreground:oklch(14.5% 0 0);--sidebar-primary:oklch(20.5% 0 0);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(97% 0 0);--sidebar-accent-foreground:oklch(20.5% 0 0);--sidebar-border:oklch(92.2% 0 0);--sidebar-ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(62% .2 290);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0);--chart-1:oklch(48.8% .243 264.376);--chart-2:oklch(69.6% .17 162.48);--chart-3:oklch(76.9% .188 70.08);--chart-4:oklch(62.7% .265 303.9);--chart-5:oklch(64.5% .246 16.439);--sidebar:oklch(20.5% 0 0);--sidebar-foreground:oklch(98.5% 0 0);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(26.9% 0 0);--sidebar-accent-foreground:oklch(98.5% 0 0);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.6% 0 0)}.workflow-chat-view .border-green-200{border-color:var(--color-emerald-200)}.workflow-chat-view .bg-green-50{background-color:var(--color-emerald-50)}.workflow-chat-view .bg-green-100{background-color:var(--color-emerald-100)}.workflow-chat-view .text-green-600{color:var(--color-emerald-600)}.workflow-chat-view .text-green-700{color:var(--color-emerald-700)}.workflow-chat-view .text-green-800{color:var(--color-emerald-800)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))} diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index-D_Y1oSGu.js b/python/packages/devui/agent_framework_devui/ui/assets/index-D_Y1oSGu.js deleted file mode 100644 index 85924eb971..0000000000 --- a/python/packages/devui/agent_framework_devui/ui/assets/index-D_Y1oSGu.js +++ /dev/null @@ -1,577 +0,0 @@ -function BE(e,r){for(var o=0;os[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))s(l);new MutationObserver(l=>{for(const u of l)if(u.type==="childList")for(const d of u.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&s(d)}).observe(document,{childList:!0,subtree:!0});function o(l){const u={};return l.integrity&&(u.integrity=l.integrity),l.referrerPolicy&&(u.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?u.credentials="include":l.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function s(l){if(l.ep)return;l.ep=!0;const u=o(l);fetch(l.href,u)}})();function qh(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var _m={exports:{}},wi={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var bv;function UE(){if(bv)return wi;bv=1;var e=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function o(s,l,u){var d=null;if(u!==void 0&&(d=""+u),l.key!==void 0&&(d=""+l.key),"key"in l){u={};for(var f in l)f!=="key"&&(u[f]=l[f])}else u=l;return l=u.ref,{$$typeof:e,type:s,key:d,ref:l!==void 0?l:null,props:u}}return wi.Fragment=r,wi.jsx=o,wi.jsxs=o,wi}var wv;function PE(){return wv||(wv=1,_m.exports=UE()),_m.exports}var i=PE(),Em={exports:{}},Le={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Nv;function $E(){if(Nv)return Le;Nv=1;var e=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),l=Symbol.for("react.profiler"),u=Symbol.for("react.consumer"),d=Symbol.for("react.context"),f=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),y=Symbol.iterator;function v(T){return T===null||typeof T!="object"?null:(T=y&&T[y]||T["@@iterator"],typeof T=="function"?T:null)}var b={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},S=Object.assign,w={};function j(T,U,X){this.props=T,this.context=U,this.refs=w,this.updater=X||b}j.prototype.isReactComponent={},j.prototype.setState=function(T,U){if(typeof T!="object"&&typeof T!="function"&&T!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,T,U,"setState")},j.prototype.forceUpdate=function(T){this.updater.enqueueForceUpdate(this,T,"forceUpdate")};function k(){}k.prototype=j.prototype;function M(T,U,X){this.props=T,this.context=U,this.refs=w,this.updater=X||b}var E=M.prototype=new k;E.constructor=M,S(E,j.prototype),E.isPureReactComponent=!0;var A=Array.isArray,D={H:null,A:null,T:null,S:null,V:null},L=Object.prototype.hasOwnProperty;function H(T,U,X,ee,se,he){return X=he.ref,{$$typeof:e,type:T,key:U,ref:X!==void 0?X:null,props:he}}function B(T,U){return H(T.type,U,void 0,void 0,void 0,T.props)}function q(T){return typeof T=="object"&&T!==null&&T.$$typeof===e}function F(T){var U={"=":"=0",":":"=2"};return"$"+T.replace(/[=:]/g,function(X){return U[X]})}var K=/\/+/g;function G(T,U){return typeof T=="object"&&T!==null&&T.key!=null?F(""+T.key):U.toString(36)}function te(){}function I(T){switch(T.status){case"fulfilled":return T.value;case"rejected":throw T.reason;default:switch(typeof T.status=="string"?T.then(te,te):(T.status="pending",T.then(function(U){T.status==="pending"&&(T.status="fulfilled",T.value=U)},function(U){T.status==="pending"&&(T.status="rejected",T.reason=U)})),T.status){case"fulfilled":return T.value;case"rejected":throw T.reason}}throw T}function V(T,U,X,ee,se){var he=typeof T;(he==="undefined"||he==="boolean")&&(T=null);var fe=!1;if(T===null)fe=!0;else switch(he){case"bigint":case"string":case"number":fe=!0;break;case"object":switch(T.$$typeof){case e:case r:fe=!0;break;case g:return fe=T._init,V(fe(T._payload),U,X,ee,se)}}if(fe)return se=se(T),fe=ee===""?"."+G(T,0):ee,A(se)?(X="",fe!=null&&(X=fe.replace(K,"$&/")+"/"),V(se,U,X,"",function(xe){return xe})):se!=null&&(q(se)&&(se=B(se,X+(se.key==null||T&&T.key===se.key?"":(""+se.key).replace(K,"$&/")+"/")+fe)),U.push(se)),1;fe=0;var Q=ee===""?".":ee+":";if(A(T))for(var ae=0;ae>>1,T=_[P];if(0>>1;Pl(ee,z))sel(he,ee)?(_[P]=he,_[se]=z,P=se):(_[P]=ee,_[X]=z,P=X);else if(sel(he,z))_[P]=he,_[se]=z,P=se;else break e}}return O}function l(_,O){var z=_.sortIndex-O.sortIndex;return z!==0?z:_.id-O.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var u=performance;e.unstable_now=function(){return u.now()}}else{var d=Date,f=d.now();e.unstable_now=function(){return d.now()-f}}var h=[],p=[],g=1,y=null,v=3,b=!1,S=!1,w=!1,j=!1,k=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,E=typeof setImmediate<"u"?setImmediate:null;function A(_){for(var O=o(p);O!==null;){if(O.callback===null)s(p);else if(O.startTime<=_)s(p),O.sortIndex=O.expirationTime,r(h,O);else break;O=o(p)}}function D(_){if(w=!1,A(_),!S)if(o(h)!==null)S=!0,L||(L=!0,G());else{var O=o(p);O!==null&&V(D,O.startTime-_)}}var L=!1,H=-1,B=5,q=-1;function F(){return j?!0:!(e.unstable_now()-q_&&F());){var P=y.callback;if(typeof P=="function"){y.callback=null,v=y.priorityLevel;var T=P(y.expirationTime<=_);if(_=e.unstable_now(),typeof T=="function"){y.callback=T,A(_),O=!0;break t}y===o(h)&&s(h),A(_)}else s(h);y=o(h)}if(y!==null)O=!0;else{var U=o(p);U!==null&&V(D,U.startTime-_),O=!1}}break e}finally{y=null,v=z,b=!1}O=void 0}}finally{O?G():L=!1}}}var G;if(typeof E=="function")G=function(){E(K)};else if(typeof MessageChannel<"u"){var te=new MessageChannel,I=te.port2;te.port1.onmessage=K,G=function(){I.postMessage(null)}}else G=function(){k(K,0)};function V(_,O){H=k(function(){_(e.unstable_now())},O)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(_){_.callback=null},e.unstable_forceFrameRate=function(_){0>_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):B=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return v},e.unstable_next=function(_){switch(v){case 1:case 2:case 3:var O=3;break;default:O=v}var z=v;v=O;try{return _()}finally{v=z}},e.unstable_requestPaint=function(){j=!0},e.unstable_runWithPriority=function(_,O){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var z=v;v=_;try{return O()}finally{v=z}},e.unstable_scheduleCallback=function(_,O,z){var P=e.unstable_now();switch(typeof z=="object"&&z!==null?(z=z.delay,z=typeof z=="number"&&0P?(_.sortIndex=z,r(p,_),o(h)===null&&_===o(p)&&(w?(M(H),H=-1):w=!0,V(D,z-P))):(_.sortIndex=T,r(h,_),S||b||(S=!0,L||(L=!0,G()))),_},e.unstable_shouldYield=F,e.unstable_wrapCallback=function(_){var O=v;return function(){var z=v;v=O;try{return _.apply(this,arguments)}finally{v=z}}}})(km)),km}var Ev;function qE(){return Ev||(Ev=1,Cm.exports=VE()),Cm.exports}var Am={exports:{}},Lt={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var jv;function YE(){if(jv)return Lt;jv=1;var e=Wi();function r(h){var p="https://react.dev/errors/"+h;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(r){console.error(r)}}return e(),Am.exports=YE(),Am.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var kv;function GE(){if(kv)return Ni;kv=1;var e=qE(),r=Wi(),o=Eb();function s(t){var n="https://react.dev/errors/"+t;if(1T||(t.current=P[T],P[T]=null,T--)}function ee(t,n){T++,P[T]=t.current,t.current=n}var se=U(null),he=U(null),fe=U(null),Q=U(null);function ae(t,n){switch(ee(fe,n),ee(he,t),ee(se,null),n.nodeType){case 9:case 11:t=(t=n.documentElement)&&(t=t.namespaceURI)?F0(t):0;break;default:if(t=n.tagName,n=n.namespaceURI)n=F0(n),t=Z0(n,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}X(se),ee(se,t)}function xe(){X(se),X(he),X(fe)}function le(t){t.memoizedState!==null&&ee(Q,t);var n=se.current,a=Z0(n,t.type);n!==a&&(ee(he,t),ee(se,a))}function ce(t){he.current===t&&(X(se),X(he)),Q.current===t&&(X(Q),gi._currentValue=z)}var ue=Object.prototype.hasOwnProperty,ge=e.unstable_scheduleCallback,pe=e.unstable_cancelCallback,Be=e.unstable_shouldYield,st=e.unstable_requestPaint,re=e.unstable_now,ve=e.unstable_getCurrentPriorityLevel,ke=e.unstable_ImmediatePriority,De=e.unstable_UserBlockingPriority,be=e.unstable_NormalPriority,Te=e.unstable_LowPriority,Ye=e.unstable_IdlePriority,it=e.log,Tn=e.unstable_setDisableYieldValue,Fe=null,Ue=null;function Qe(t){if(typeof it=="function"&&Tn(t),Ue&&typeof Ue.setStrictMode=="function")try{Ue.setStrictMode(Fe,t)}catch{}}var ht=Math.clz32?Math.clz32:dd,Ft=Math.log,ga=Math.LN2;function dd(t){return t>>>=0,t===0?32:31-(Ft(t)/ga|0)|0}var ns=256,rs=4194304;function Wn(t){var n=t&42;if(n!==0)return n;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function os(t,n,a){var c=t.pendingLanes;if(c===0)return 0;var m=0,x=t.suspendedLanes,C=t.pingedLanes;t=t.warmLanes;var R=c&134217727;return R!==0?(c=R&~x,c!==0?m=Wn(c):(C&=R,C!==0?m=Wn(C):a||(a=R&~t,a!==0&&(m=Wn(a))))):(R=c&~x,R!==0?m=Wn(R):C!==0?m=Wn(C):a||(a=c&~t,a!==0&&(m=Wn(a)))),m===0?0:n!==0&&n!==m&&(n&x)===0&&(x=m&-m,a=n&-n,x>=a||x===32&&(a&4194048)!==0)?n:m}function xo(t,n){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&n)===0}function fd(t,n){switch(t){case 1:case 2:case 4:case 8:case 64:return n+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return n+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function dl(){var t=ns;return ns<<=1,(ns&4194048)===0&&(ns=256),t}function fl(){var t=rs;return rs<<=1,(rs&62914560)===0&&(rs=4194304),t}function xa(t){for(var n=[],a=0;31>a;a++)n.push(t);return n}function vo(t,n){t.pendingLanes|=n,n!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function md(t,n,a,c,m,x){var C=t.pendingLanes;t.pendingLanes=a,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=a,t.entangledLanes&=a,t.errorRecoveryDisabledLanes&=a,t.shellSuspendCounter=0;var R=t.entanglements,$=t.expirationTimes,J=t.hiddenUpdates;for(a=C&~a;0)":-1m||$[c]!==J[m]){var ie=` -`+$[c].replace(" at new "," at ");return t.displayName&&ie.includes("")&&(ie=ie.replace("",t.displayName)),ie}while(1<=c&&0<=m);break}}}finally{Ea=!1,Error.prepareStackTrace=a}return(a=t?t.displayName||t.name:"")?tr(a):""}function yd(t){switch(t.tag){case 26:case 27:case 5:return tr(t.type);case 16:return tr("Lazy");case 13:return tr("Suspense");case 19:return tr("SuspenseList");case 0:case 15:return ja(t.type,!1);case 11:return ja(t.type.render,!1);case 1:return ja(t.type,!0);case 31:return tr("Activity");default:return""}}function bl(t){try{var n="";do n+=yd(t),t=t.return;while(t);return n}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}function Bt(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function wl(t){var n=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function bd(t){var n=wl(t)?"checked":"value",a=Object.getOwnPropertyDescriptor(t.constructor.prototype,n),c=""+t[n];if(!t.hasOwnProperty(n)&&typeof a<"u"&&typeof a.get=="function"&&typeof a.set=="function"){var m=a.get,x=a.set;return Object.defineProperty(t,n,{configurable:!0,get:function(){return m.call(this)},set:function(C){c=""+C,x.call(this,C)}}),Object.defineProperty(t,n,{enumerable:a.enumerable}),{getValue:function(){return c},setValue:function(C){c=""+C},stopTracking:function(){t._valueTracker=null,delete t[n]}}}}function is(t){t._valueTracker||(t._valueTracker=bd(t))}function Ca(t){if(!t)return!1;var n=t._valueTracker;if(!n)return!0;var a=n.getValue(),c="";return t&&(c=wl(t)?t.checked?"true":"false":t.value),t=c,t!==a?(n.setValue(t),!0):!1}function ls(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var wd=/[\n"\\]/g;function Ut(t){return t.replace(wd,function(n){return"\\"+n.charCodeAt(0).toString(16)+" "})}function bo(t,n,a,c,m,x,C,R){t.name="",C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"?t.type=C:t.removeAttribute("type"),n!=null?C==="number"?(n===0&&t.value===""||t.value!=n)&&(t.value=""+Bt(n)):t.value!==""+Bt(n)&&(t.value=""+Bt(n)):C!=="submit"&&C!=="reset"||t.removeAttribute("value"),n!=null?ka(t,C,Bt(n)):a!=null?ka(t,C,Bt(a)):c!=null&&t.removeAttribute("value"),m==null&&x!=null&&(t.defaultChecked=!!x),m!=null&&(t.checked=m&&typeof m!="function"&&typeof m!="symbol"),R!=null&&typeof R!="function"&&typeof R!="symbol"&&typeof R!="boolean"?t.name=""+Bt(R):t.removeAttribute("name")}function Nl(t,n,a,c,m,x,C,R){if(x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"&&(t.type=x),n!=null||a!=null){if(!(x!=="submit"&&x!=="reset"||n!=null))return;a=a!=null?""+Bt(a):"",n=n!=null?""+Bt(n):a,R||n===t.value||(t.value=n),t.defaultValue=n}c=c??m,c=typeof c!="function"&&typeof c!="symbol"&&!!c,t.checked=R?t.checked:!!c,t.defaultChecked=!!c,C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"&&(t.name=C)}function ka(t,n,a){n==="number"&&ls(t.ownerDocument)===t||t.defaultValue===""+a||(t.defaultValue=""+a)}function nr(t,n,a,c){if(t=t.options,n){n={};for(var m=0;m"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),jd=!1;if(rr)try{var Ma={};Object.defineProperty(Ma,"passive",{get:function(){jd=!0}}),window.addEventListener("test",Ma,Ma),window.removeEventListener("test",Ma,Ma)}catch{jd=!1}var Or=null,Cd=null,_l=null;function Qp(){if(_l)return _l;var t,n=Cd,a=n.length,c,m="value"in Or?Or.value:Or.textContent,x=m.length;for(t=0;t=Da),og=" ",sg=!1;function ag(t,n){switch(t){case"keyup":return c_.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ig(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var fs=!1;function d_(t,n){switch(t){case"compositionend":return ig(n);case"keypress":return n.which!==32?null:(sg=!0,og);case"textInput":return t=n.data,t===og&&sg?null:t;default:return null}}function f_(t,n){if(fs)return t==="compositionend"||!Rd&&ag(t,n)?(t=Qp(),_l=Cd=Or=null,fs=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:a,offset:n-t};t=c}e:{for(;a;){if(a.nextSibling){a=a.nextSibling;break e}a=a.parentNode}a=void 0}a=pg(a)}}function xg(t,n){return t&&n?t===n?!0:t&&t.nodeType===3?!1:n&&n.nodeType===3?xg(t,n.parentNode):"contains"in t?t.contains(n):t.compareDocumentPosition?!!(t.compareDocumentPosition(n)&16):!1:!1}function vg(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var n=ls(t.document);n instanceof t.HTMLIFrameElement;){try{var a=typeof n.contentWindow.location.href=="string"}catch{a=!1}if(a)t=n.contentWindow;else break;n=ls(t.document)}return n}function zd(t){var n=t&&t.nodeName&&t.nodeName.toLowerCase();return n&&(n==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||n==="textarea"||t.contentEditable==="true")}var b_=rr&&"documentMode"in document&&11>=document.documentMode,ms=null,Ld=null,Ia=null,Id=!1;function yg(t,n,a){var c=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;Id||ms==null||ms!==ls(c)||(c=ms,"selectionStart"in c&&zd(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset}),Ia&&La(Ia,c)||(Ia=c,c=hc(Ld,"onSelect"),0>=C,m-=C,sr=1<<32-ht(n)+m|a<x?x:8;var C=_.T,R={};_.T=R,Sf(t,!1,n,a);try{var $=m(),J=_.S;if(J!==null&&J(R,$),$!==null&&typeof $=="object"&&typeof $.then=="function"){var ie=A_($,c);Qa(t,n,ie,en(t))}else Qa(t,n,c,en(t))}catch(me){Qa(t,n,{then:function(){},status:"rejected",reason:me},en())}finally{O.p=x,_.T=C}}function O_(){}function wf(t,n,a,c){if(t.tag!==5)throw Error(s(476));var m=bx(t).queue;yx(t,m,n,z,a===null?O_:function(){return wx(t),a(c)})}function bx(t){var n=t.memoizedState;if(n!==null)return n;n={memoizedState:z,baseState:z,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:cr,lastRenderedState:z},next:null};var a={};return n.next={memoizedState:a,baseState:a,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:cr,lastRenderedState:a},next:null},t.memoizedState=n,t=t.alternate,t!==null&&(t.memoizedState=n),n}function wx(t){var n=bx(t).next.queue;Qa(t,n,{},en())}function Nf(){return zt(gi)}function Nx(){return yt().memoizedState}function Sx(){return yt().memoizedState}function z_(t){for(var n=t.return;n!==null;){switch(n.tag){case 24:case 3:var a=en();t=Ir(a);var c=Hr(n,t,a);c!==null&&(tn(c,n,a),Ga(c,n,a)),n={cache:Wd()},t.payload=n;return}n=n.return}}function L_(t,n,a){var c=en();a={lane:c,revertLane:0,action:a,hasEagerState:!1,eagerState:null,next:null},Fl(t)?Ex(n,a):(a=Pd(t,n,a,c),a!==null&&(tn(a,t,c),jx(a,n,c)))}function _x(t,n,a){var c=en();Qa(t,n,a,c)}function Qa(t,n,a,c){var m={lane:c,revertLane:0,action:a,hasEagerState:!1,eagerState:null,next:null};if(Fl(t))Ex(n,m);else{var x=t.alternate;if(t.lanes===0&&(x===null||x.lanes===0)&&(x=n.lastRenderedReducer,x!==null))try{var C=n.lastRenderedState,R=x(C,a);if(m.hasEagerState=!0,m.eagerState=R,Zt(R,C))return Tl(t,n,m,0),at===null&&Ml(),!1}catch{}finally{}if(a=Pd(t,n,m,c),a!==null)return tn(a,t,c),jx(a,n,c),!0}return!1}function Sf(t,n,a,c){if(c={lane:2,revertLane:tm(),action:c,hasEagerState:!1,eagerState:null,next:null},Fl(t)){if(n)throw Error(s(479))}else n=Pd(t,a,c,2),n!==null&&tn(n,t,2)}function Fl(t){var n=t.alternate;return t===Ie||n!==null&&n===Ie}function Ex(t,n){Ss=$l=!0;var a=t.pending;a===null?n.next=n:(n.next=a.next,a.next=n),t.pending=n}function jx(t,n,a){if((a&4194048)!==0){var c=n.lanes;c&=t.pendingLanes,a|=c,n.lanes=a,va(t,a)}}var Zl={readContext:zt,use:ql,useCallback:pt,useContext:pt,useEffect:pt,useImperativeHandle:pt,useLayoutEffect:pt,useInsertionEffect:pt,useMemo:pt,useReducer:pt,useRef:pt,useState:pt,useDebugValue:pt,useDeferredValue:pt,useTransition:pt,useSyncExternalStore:pt,useId:pt,useHostTransitionStatus:pt,useFormState:pt,useActionState:pt,useOptimistic:pt,useMemoCache:pt,useCacheRefresh:pt},Cx={readContext:zt,use:ql,useCallback:function(t,n){return $t().memoizedState=[t,n===void 0?null:n],t},useContext:zt,useEffect:ux,useImperativeHandle:function(t,n,a){a=a!=null?a.concat([t]):null,Xl(4194308,4,hx.bind(null,n,t),a)},useLayoutEffect:function(t,n){return Xl(4194308,4,t,n)},useInsertionEffect:function(t,n){Xl(4,2,t,n)},useMemo:function(t,n){var a=$t();n=n===void 0?null:n;var c=t();if(Ro){Qe(!0);try{t()}finally{Qe(!1)}}return a.memoizedState=[c,n],c},useReducer:function(t,n,a){var c=$t();if(a!==void 0){var m=a(n);if(Ro){Qe(!0);try{a(n)}finally{Qe(!1)}}}else m=n;return c.memoizedState=c.baseState=m,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:m},c.queue=t,t=t.dispatch=L_.bind(null,Ie,t),[c.memoizedState,t]},useRef:function(t){var n=$t();return t={current:t},n.memoizedState=t},useState:function(t){t=xf(t);var n=t.queue,a=_x.bind(null,Ie,n);return n.dispatch=a,[t.memoizedState,a]},useDebugValue:yf,useDeferredValue:function(t,n){var a=$t();return bf(a,t,n)},useTransition:function(){var t=xf(!1);return t=yx.bind(null,Ie,t.queue,!0,!1),$t().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,n,a){var c=Ie,m=$t();if(We){if(a===void 0)throw Error(s(407));a=a()}else{if(a=n(),at===null)throw Error(s(349));(Ge&124)!==0||Fg(c,n,a)}m.memoizedState=a;var x={value:a,getSnapshot:n};return m.queue=x,ux(Kg.bind(null,c,x,t),[t]),c.flags|=2048,Es(9,Gl(),Zg.bind(null,c,x,a,n),null),a},useId:function(){var t=$t(),n=at.identifierPrefix;if(We){var a=ar,c=sr;a=(c&~(1<<32-ht(c)-1)).toString(32)+a,n="«"+n+"R"+a,a=Vl++,0Me?(Ct=je,je=null):Ct=je.sibling;var Ze=ne(Z,je,W[Me],de);if(Ze===null){je===null&&(je=Ct);break}t&&je&&Ze.alternate===null&&n(Z,je),Y=x(Ze,Y,Me),Pe===null?we=Ze:Pe.sibling=Ze,Pe=Ze,je=Ct}if(Me===W.length)return a(Z,je),We&&jo(Z,Me),we;if(je===null){for(;MeMe?(Ct=je,je=null):Ct=je.sibling;var to=ne(Z,je,Ze.value,de);if(to===null){je===null&&(je=Ct);break}t&&je&&to.alternate===null&&n(Z,je),Y=x(to,Y,Me),Pe===null?we=to:Pe.sibling=to,Pe=to,je=Ct}if(Ze.done)return a(Z,je),We&&jo(Z,Me),we;if(je===null){for(;!Ze.done;Me++,Ze=W.next())Ze=me(Z,Ze.value,de),Ze!==null&&(Y=x(Ze,Y,Me),Pe===null?we=Ze:Pe.sibling=Ze,Pe=Ze);return We&&jo(Z,Me),we}for(je=c(je);!Ze.done;Me++,Ze=W.next())Ze=oe(je,Z,Me,Ze.value,de),Ze!==null&&(t&&Ze.alternate!==null&&je.delete(Ze.key===null?Me:Ze.key),Y=x(Ze,Y,Me),Pe===null?we=Ze:Pe.sibling=Ze,Pe=Ze);return t&&je.forEach(function(HE){return n(Z,HE)}),We&&jo(Z,Me),we}function rt(Z,Y,W,de){if(typeof W=="object"&&W!==null&&W.type===S&&W.key===null&&(W=W.props.children),typeof W=="object"&&W!==null){switch(W.$$typeof){case v:e:{for(var we=W.key;Y!==null;){if(Y.key===we){if(we=W.type,we===S){if(Y.tag===7){a(Z,Y.sibling),de=m(Y,W.props.children),de.return=Z,Z=de;break e}}else if(Y.elementType===we||typeof we=="object"&&we!==null&&we.$$typeof===B&&Ax(we)===Y.type){a(Z,Y.sibling),de=m(Y,W.props),ei(de,W),de.return=Z,Z=de;break e}a(Z,Y);break}else n(Z,Y);Y=Y.sibling}W.type===S?(de=_o(W.props.children,Z.mode,de,W.key),de.return=Z,Z=de):(de=Dl(W.type,W.key,W.props,null,Z.mode,de),ei(de,W),de.return=Z,Z=de)}return C(Z);case b:e:{for(we=W.key;Y!==null;){if(Y.key===we)if(Y.tag===4&&Y.stateNode.containerInfo===W.containerInfo&&Y.stateNode.implementation===W.implementation){a(Z,Y.sibling),de=m(Y,W.children||[]),de.return=Z,Z=de;break e}else{a(Z,Y);break}else n(Z,Y);Y=Y.sibling}de=qd(W,Z.mode,de),de.return=Z,Z=de}return C(Z);case B:return we=W._init,W=we(W._payload),rt(Z,Y,W,de)}if(V(W))return Re(Z,Y,W,de);if(G(W)){if(we=G(W),typeof we!="function")throw Error(s(150));return W=we.call(W),Ae(Z,Y,W,de)}if(typeof W.then=="function")return rt(Z,Y,Kl(W),de);if(W.$$typeof===E)return rt(Z,Y,Il(Z,W),de);Wl(Z,W)}return typeof W=="string"&&W!==""||typeof W=="number"||typeof W=="bigint"?(W=""+W,Y!==null&&Y.tag===6?(a(Z,Y.sibling),de=m(Y,W),de.return=Z,Z=de):(a(Z,Y),de=Vd(W,Z.mode,de),de.return=Z,Z=de),C(Z)):a(Z,Y)}return function(Z,Y,W,de){try{Ja=0;var we=rt(Z,Y,W,de);return js=null,we}catch(je){if(je===qa||je===Bl)throw je;var Pe=Kt(29,je,null,Z.mode);return Pe.lanes=de,Pe.return=Z,Pe}finally{}}}var Cs=Mx(!0),Tx=Mx(!1),mn=U(null),On=null;function Ur(t){var n=t.alternate;ee(Nt,Nt.current&1),ee(mn,t),On===null&&(n===null||Ns.current!==null||n.memoizedState!==null)&&(On=t)}function Rx(t){if(t.tag===22){if(ee(Nt,Nt.current),ee(mn,t),On===null){var n=t.alternate;n!==null&&n.memoizedState!==null&&(On=t)}}else Pr()}function Pr(){ee(Nt,Nt.current),ee(mn,mn.current)}function ur(t){X(mn),On===t&&(On=null),X(Nt)}var Nt=U(0);function Ql(t){for(var n=t;n!==null;){if(n.tag===13){var a=n.memoizedState;if(a!==null&&(a=a.dehydrated,a===null||a.data==="$?"||mm(a)))return n}else if(n.tag===19&&n.memoizedProps.revealOrder!==void 0){if((n.flags&128)!==0)return n}else if(n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return null;n=n.return}n.sibling.return=n.return,n=n.sibling}return null}function _f(t,n,a,c){n=t.memoizedState,a=a(c,n),a=a==null?n:g({},n,a),t.memoizedState=a,t.lanes===0&&(t.updateQueue.baseState=a)}var Ef={enqueueSetState:function(t,n,a){t=t._reactInternals;var c=en(),m=Ir(c);m.payload=n,a!=null&&(m.callback=a),n=Hr(t,m,c),n!==null&&(tn(n,t,c),Ga(n,t,c))},enqueueReplaceState:function(t,n,a){t=t._reactInternals;var c=en(),m=Ir(c);m.tag=1,m.payload=n,a!=null&&(m.callback=a),n=Hr(t,m,c),n!==null&&(tn(n,t,c),Ga(n,t,c))},enqueueForceUpdate:function(t,n){t=t._reactInternals;var a=en(),c=Ir(a);c.tag=2,n!=null&&(c.callback=n),n=Hr(t,c,a),n!==null&&(tn(n,t,a),Ga(n,t,a))}};function Dx(t,n,a,c,m,x,C){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(c,x,C):n.prototype&&n.prototype.isPureReactComponent?!La(a,c)||!La(m,x):!0}function Ox(t,n,a,c){t=n.state,typeof n.componentWillReceiveProps=="function"&&n.componentWillReceiveProps(a,c),typeof n.UNSAFE_componentWillReceiveProps=="function"&&n.UNSAFE_componentWillReceiveProps(a,c),n.state!==t&&Ef.enqueueReplaceState(n,n.state,null)}function Do(t,n){var a=n;if("ref"in n){a={};for(var c in n)c!=="ref"&&(a[c]=n[c])}if(t=t.defaultProps){a===n&&(a=g({},a));for(var m in t)a[m]===void 0&&(a[m]=t[m])}return a}var Jl=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var n=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(n))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)};function zx(t){Jl(t)}function Lx(t){console.error(t)}function Ix(t){Jl(t)}function ec(t,n){try{var a=t.onUncaughtError;a(n.value,{componentStack:n.stack})}catch(c){setTimeout(function(){throw c})}}function Hx(t,n,a){try{var c=t.onCaughtError;c(a.value,{componentStack:a.stack,errorBoundary:n.tag===1?n.stateNode:null})}catch(m){setTimeout(function(){throw m})}}function jf(t,n,a){return a=Ir(a),a.tag=3,a.payload={element:null},a.callback=function(){ec(t,n)},a}function Bx(t){return t=Ir(t),t.tag=3,t}function Ux(t,n,a,c){var m=a.type.getDerivedStateFromError;if(typeof m=="function"){var x=c.value;t.payload=function(){return m(x)},t.callback=function(){Hx(n,a,c)}}var C=a.stateNode;C!==null&&typeof C.componentDidCatch=="function"&&(t.callback=function(){Hx(n,a,c),typeof m!="function"&&(Xr===null?Xr=new Set([this]):Xr.add(this));var R=c.stack;this.componentDidCatch(c.value,{componentStack:R!==null?R:""})})}function H_(t,n,a,c,m){if(a.flags|=32768,c!==null&&typeof c=="object"&&typeof c.then=="function"){if(n=a.alternate,n!==null&&Pa(n,a,m,!0),a=mn.current,a!==null){switch(a.tag){case 13:return On===null?Kf():a.alternate===null&&mt===0&&(mt=3),a.flags&=-257,a.flags|=65536,a.lanes=m,c===ef?a.flags|=16384:(n=a.updateQueue,n===null?a.updateQueue=new Set([c]):n.add(c),Qf(t,c,m)),!1;case 22:return a.flags|=65536,c===ef?a.flags|=16384:(n=a.updateQueue,n===null?(n={transitions:null,markerInstances:null,retryQueue:new Set([c])},a.updateQueue=n):(a=n.retryQueue,a===null?n.retryQueue=new Set([c]):a.add(c)),Qf(t,c,m)),!1}throw Error(s(435,a.tag))}return Qf(t,c,m),Kf(),!1}if(We)return n=mn.current,n!==null?((n.flags&65536)===0&&(n.flags|=256),n.flags|=65536,n.lanes=m,c!==Xd&&(t=Error(s(422),{cause:c}),Ua(cn(t,a)))):(c!==Xd&&(n=Error(s(423),{cause:c}),Ua(cn(n,a))),t=t.current.alternate,t.flags|=65536,m&=-m,t.lanes|=m,c=cn(c,a),m=jf(t.stateNode,c,m),rf(t,m),mt!==4&&(mt=2)),!1;var x=Error(s(520),{cause:c});if(x=cn(x,a),ii===null?ii=[x]:ii.push(x),mt!==4&&(mt=2),n===null)return!0;c=cn(c,a),a=n;do{switch(a.tag){case 3:return a.flags|=65536,t=m&-m,a.lanes|=t,t=jf(a.stateNode,c,t),rf(a,t),!1;case 1:if(n=a.type,x=a.stateNode,(a.flags&128)===0&&(typeof n.getDerivedStateFromError=="function"||x!==null&&typeof x.componentDidCatch=="function"&&(Xr===null||!Xr.has(x))))return a.flags|=65536,m&=-m,a.lanes|=m,m=Bx(m),Ux(m,t,a,c),rf(a,m),!1}a=a.return}while(a!==null);return!1}var Px=Error(s(461)),Et=!1;function kt(t,n,a,c){n.child=t===null?Tx(n,null,a,c):Cs(n,t.child,a,c)}function $x(t,n,a,c,m){a=a.render;var x=n.ref;if("ref"in c){var C={};for(var R in c)R!=="ref"&&(C[R]=c[R])}else C=c;return Mo(n),c=cf(t,n,a,C,x,m),R=uf(),t!==null&&!Et?(df(t,n,m),dr(t,n,m)):(We&&R&&Yd(n),n.flags|=1,kt(t,n,c,m),n.child)}function Vx(t,n,a,c,m){if(t===null){var x=a.type;return typeof x=="function"&&!$d(x)&&x.defaultProps===void 0&&a.compare===null?(n.tag=15,n.type=x,qx(t,n,x,c,m)):(t=Dl(a.type,null,c,n,n.mode,m),t.ref=n.ref,t.return=n,n.child=t)}if(x=t.child,!Of(t,m)){var C=x.memoizedProps;if(a=a.compare,a=a!==null?a:La,a(C,c)&&t.ref===n.ref)return dr(t,n,m)}return n.flags|=1,t=or(x,c),t.ref=n.ref,t.return=n,n.child=t}function qx(t,n,a,c,m){if(t!==null){var x=t.memoizedProps;if(La(x,c)&&t.ref===n.ref)if(Et=!1,n.pendingProps=c=x,Of(t,m))(t.flags&131072)!==0&&(Et=!0);else return n.lanes=t.lanes,dr(t,n,m)}return Cf(t,n,a,c,m)}function Yx(t,n,a){var c=n.pendingProps,m=c.children,x=t!==null?t.memoizedState:null;if(c.mode==="hidden"){if((n.flags&128)!==0){if(c=x!==null?x.baseLanes|a:a,t!==null){for(m=n.child=t.child,x=0;m!==null;)x=x|m.lanes|m.childLanes,m=m.sibling;n.childLanes=x&~c}else n.childLanes=0,n.child=null;return Gx(t,n,c,a)}if((a&536870912)!==0)n.memoizedState={baseLanes:0,cachePool:null},t!==null&&Hl(n,x!==null?x.cachePool:null),x!==null?qg(n,x):sf(),Rx(n);else return n.lanes=n.childLanes=536870912,Gx(t,n,x!==null?x.baseLanes|a:a,a)}else x!==null?(Hl(n,x.cachePool),qg(n,x),Pr(),n.memoizedState=null):(t!==null&&Hl(n,null),sf(),Pr());return kt(t,n,m,a),n.child}function Gx(t,n,a,c){var m=Jd();return m=m===null?null:{parent:wt._currentValue,pool:m},n.memoizedState={baseLanes:a,cachePool:m},t!==null&&Hl(n,null),sf(),Rx(n),t!==null&&Pa(t,n,c,!0),null}function tc(t,n){var a=n.ref;if(a===null)t!==null&&t.ref!==null&&(n.flags|=4194816);else{if(typeof a!="function"&&typeof a!="object")throw Error(s(284));(t===null||t.ref!==a)&&(n.flags|=4194816)}}function Cf(t,n,a,c,m){return Mo(n),a=cf(t,n,a,c,void 0,m),c=uf(),t!==null&&!Et?(df(t,n,m),dr(t,n,m)):(We&&c&&Yd(n),n.flags|=1,kt(t,n,a,m),n.child)}function Xx(t,n,a,c,m,x){return Mo(n),n.updateQueue=null,a=Gg(n,c,a,m),Yg(t),c=uf(),t!==null&&!Et?(df(t,n,x),dr(t,n,x)):(We&&c&&Yd(n),n.flags|=1,kt(t,n,a,x),n.child)}function Fx(t,n,a,c,m){if(Mo(n),n.stateNode===null){var x=xs,C=a.contextType;typeof C=="object"&&C!==null&&(x=zt(C)),x=new a(c,x),n.memoizedState=x.state!==null&&x.state!==void 0?x.state:null,x.updater=Ef,n.stateNode=x,x._reactInternals=n,x=n.stateNode,x.props=c,x.state=n.memoizedState,x.refs={},tf(n),C=a.contextType,x.context=typeof C=="object"&&C!==null?zt(C):xs,x.state=n.memoizedState,C=a.getDerivedStateFromProps,typeof C=="function"&&(_f(n,a,C,c),x.state=n.memoizedState),typeof a.getDerivedStateFromProps=="function"||typeof x.getSnapshotBeforeUpdate=="function"||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(C=x.state,typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount(),C!==x.state&&Ef.enqueueReplaceState(x,x.state,null),Fa(n,c,x,m),Xa(),x.state=n.memoizedState),typeof x.componentDidMount=="function"&&(n.flags|=4194308),c=!0}else if(t===null){x=n.stateNode;var R=n.memoizedProps,$=Do(a,R);x.props=$;var J=x.context,ie=a.contextType;C=xs,typeof ie=="object"&&ie!==null&&(C=zt(ie));var me=a.getDerivedStateFromProps;ie=typeof me=="function"||typeof x.getSnapshotBeforeUpdate=="function",R=n.pendingProps!==R,ie||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(R||J!==C)&&Ox(n,x,c,C),Lr=!1;var ne=n.memoizedState;x.state=ne,Fa(n,c,x,m),Xa(),J=n.memoizedState,R||ne!==J||Lr?(typeof me=="function"&&(_f(n,a,me,c),J=n.memoizedState),($=Lr||Dx(n,a,$,c,ne,J,C))?(ie||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount()),typeof x.componentDidMount=="function"&&(n.flags|=4194308)):(typeof x.componentDidMount=="function"&&(n.flags|=4194308),n.memoizedProps=c,n.memoizedState=J),x.props=c,x.state=J,x.context=C,c=$):(typeof x.componentDidMount=="function"&&(n.flags|=4194308),c=!1)}else{x=n.stateNode,nf(t,n),C=n.memoizedProps,ie=Do(a,C),x.props=ie,me=n.pendingProps,ne=x.context,J=a.contextType,$=xs,typeof J=="object"&&J!==null&&($=zt(J)),R=a.getDerivedStateFromProps,(J=typeof R=="function"||typeof x.getSnapshotBeforeUpdate=="function")||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(C!==me||ne!==$)&&Ox(n,x,c,$),Lr=!1,ne=n.memoizedState,x.state=ne,Fa(n,c,x,m),Xa();var oe=n.memoizedState;C!==me||ne!==oe||Lr||t!==null&&t.dependencies!==null&&Ll(t.dependencies)?(typeof R=="function"&&(_f(n,a,R,c),oe=n.memoizedState),(ie=Lr||Dx(n,a,ie,c,ne,oe,$)||t!==null&&t.dependencies!==null&&Ll(t.dependencies))?(J||typeof x.UNSAFE_componentWillUpdate!="function"&&typeof x.componentWillUpdate!="function"||(typeof x.componentWillUpdate=="function"&&x.componentWillUpdate(c,oe,$),typeof x.UNSAFE_componentWillUpdate=="function"&&x.UNSAFE_componentWillUpdate(c,oe,$)),typeof x.componentDidUpdate=="function"&&(n.flags|=4),typeof x.getSnapshotBeforeUpdate=="function"&&(n.flags|=1024)):(typeof x.componentDidUpdate!="function"||C===t.memoizedProps&&ne===t.memoizedState||(n.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||C===t.memoizedProps&&ne===t.memoizedState||(n.flags|=1024),n.memoizedProps=c,n.memoizedState=oe),x.props=c,x.state=oe,x.context=$,c=ie):(typeof x.componentDidUpdate!="function"||C===t.memoizedProps&&ne===t.memoizedState||(n.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||C===t.memoizedProps&&ne===t.memoizedState||(n.flags|=1024),c=!1)}return x=c,tc(t,n),c=(n.flags&128)!==0,x||c?(x=n.stateNode,a=c&&typeof a.getDerivedStateFromError!="function"?null:x.render(),n.flags|=1,t!==null&&c?(n.child=Cs(n,t.child,null,m),n.child=Cs(n,null,a,m)):kt(t,n,a,m),n.memoizedState=x.state,t=n.child):t=dr(t,n,m),t}function Zx(t,n,a,c){return Ba(),n.flags|=256,kt(t,n,a,c),n.child}var kf={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Af(t){return{baseLanes:t,cachePool:Lg()}}function Mf(t,n,a){return t=t!==null?t.childLanes&~a:0,n&&(t|=hn),t}function Kx(t,n,a){var c=n.pendingProps,m=!1,x=(n.flags&128)!==0,C;if((C=x)||(C=t!==null&&t.memoizedState===null?!1:(Nt.current&2)!==0),C&&(m=!0,n.flags&=-129),C=(n.flags&32)!==0,n.flags&=-33,t===null){if(We){if(m?Ur(n):Pr(),We){var R=ft,$;if($=R){e:{for($=R,R=Dn;$.nodeType!==8;){if(!R){R=null;break e}if($=wn($.nextSibling),$===null){R=null;break e}}R=$}R!==null?(n.memoizedState={dehydrated:R,treeContext:Eo!==null?{id:sr,overflow:ar}:null,retryLane:536870912,hydrationErrors:null},$=Kt(18,null,null,0),$.stateNode=R,$.return=n,n.child=$,It=n,ft=null,$=!0):$=!1}$||ko(n)}if(R=n.memoizedState,R!==null&&(R=R.dehydrated,R!==null))return mm(R)?n.lanes=32:n.lanes=536870912,null;ur(n)}return R=c.children,c=c.fallback,m?(Pr(),m=n.mode,R=nc({mode:"hidden",children:R},m),c=_o(c,m,a,null),R.return=n,c.return=n,R.sibling=c,n.child=R,m=n.child,m.memoizedState=Af(a),m.childLanes=Mf(t,C,a),n.memoizedState=kf,c):(Ur(n),Tf(n,R))}if($=t.memoizedState,$!==null&&(R=$.dehydrated,R!==null)){if(x)n.flags&256?(Ur(n),n.flags&=-257,n=Rf(t,n,a)):n.memoizedState!==null?(Pr(),n.child=t.child,n.flags|=128,n=null):(Pr(),m=c.fallback,R=n.mode,c=nc({mode:"visible",children:c.children},R),m=_o(m,R,a,null),m.flags|=2,c.return=n,m.return=n,c.sibling=m,n.child=c,Cs(n,t.child,null,a),c=n.child,c.memoizedState=Af(a),c.childLanes=Mf(t,C,a),n.memoizedState=kf,n=m);else if(Ur(n),mm(R)){if(C=R.nextSibling&&R.nextSibling.dataset,C)var J=C.dgst;C=J,c=Error(s(419)),c.stack="",c.digest=C,Ua({value:c,source:null,stack:null}),n=Rf(t,n,a)}else if(Et||Pa(t,n,a,!1),C=(a&t.childLanes)!==0,Et||C){if(C=at,C!==null&&(c=a&-a,c=(c&42)!==0?1:ya(c),c=(c&(C.suspendedLanes|a))!==0?0:c,c!==0&&c!==$.retryLane))throw $.retryLane=c,gs(t,c),tn(C,t,c),Px;R.data==="$?"||Kf(),n=Rf(t,n,a)}else R.data==="$?"?(n.flags|=192,n.child=t.child,n=null):(t=$.treeContext,ft=wn(R.nextSibling),It=n,We=!0,Co=null,Dn=!1,t!==null&&(dn[fn++]=sr,dn[fn++]=ar,dn[fn++]=Eo,sr=t.id,ar=t.overflow,Eo=n),n=Tf(n,c.children),n.flags|=4096);return n}return m?(Pr(),m=c.fallback,R=n.mode,$=t.child,J=$.sibling,c=or($,{mode:"hidden",children:c.children}),c.subtreeFlags=$.subtreeFlags&65011712,J!==null?m=or(J,m):(m=_o(m,R,a,null),m.flags|=2),m.return=n,c.return=n,c.sibling=m,n.child=c,c=m,m=n.child,R=t.child.memoizedState,R===null?R=Af(a):($=R.cachePool,$!==null?(J=wt._currentValue,$=$.parent!==J?{parent:J,pool:J}:$):$=Lg(),R={baseLanes:R.baseLanes|a,cachePool:$}),m.memoizedState=R,m.childLanes=Mf(t,C,a),n.memoizedState=kf,c):(Ur(n),a=t.child,t=a.sibling,a=or(a,{mode:"visible",children:c.children}),a.return=n,a.sibling=null,t!==null&&(C=n.deletions,C===null?(n.deletions=[t],n.flags|=16):C.push(t)),n.child=a,n.memoizedState=null,a)}function Tf(t,n){return n=nc({mode:"visible",children:n},t.mode),n.return=t,t.child=n}function nc(t,n){return t=Kt(22,t,null,n),t.lanes=0,t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},t}function Rf(t,n,a){return Cs(n,t.child,null,a),t=Tf(n,n.pendingProps.children),t.flags|=2,n.memoizedState=null,t}function Wx(t,n,a){t.lanes|=n;var c=t.alternate;c!==null&&(c.lanes|=n),Zd(t.return,n,a)}function Df(t,n,a,c,m){var x=t.memoizedState;x===null?t.memoizedState={isBackwards:n,rendering:null,renderingStartTime:0,last:c,tail:a,tailMode:m}:(x.isBackwards=n,x.rendering=null,x.renderingStartTime=0,x.last=c,x.tail=a,x.tailMode=m)}function Qx(t,n,a){var c=n.pendingProps,m=c.revealOrder,x=c.tail;if(kt(t,n,c.children,a),c=Nt.current,(c&2)!==0)c=c&1|2,n.flags|=128;else{if(t!==null&&(t.flags&128)!==0)e:for(t=n.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&Wx(t,a,n);else if(t.tag===19)Wx(t,a,n);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===n)break e;for(;t.sibling===null;){if(t.return===null||t.return===n)break e;t=t.return}t.sibling.return=t.return,t=t.sibling}c&=1}switch(ee(Nt,c),m){case"forwards":for(a=n.child,m=null;a!==null;)t=a.alternate,t!==null&&Ql(t)===null&&(m=a),a=a.sibling;a=m,a===null?(m=n.child,n.child=null):(m=a.sibling,a.sibling=null),Df(n,!1,m,a,x);break;case"backwards":for(a=null,m=n.child,n.child=null;m!==null;){if(t=m.alternate,t!==null&&Ql(t)===null){n.child=m;break}t=m.sibling,m.sibling=a,a=m,m=t}Df(n,!0,a,null,x);break;case"together":Df(n,!1,null,null,void 0);break;default:n.memoizedState=null}return n.child}function dr(t,n,a){if(t!==null&&(n.dependencies=t.dependencies),Gr|=n.lanes,(a&n.childLanes)===0)if(t!==null){if(Pa(t,n,a,!1),(a&n.childLanes)===0)return null}else return null;if(t!==null&&n.child!==t.child)throw Error(s(153));if(n.child!==null){for(t=n.child,a=or(t,t.pendingProps),n.child=a,a.return=n;t.sibling!==null;)t=t.sibling,a=a.sibling=or(t,t.pendingProps),a.return=n;a.sibling=null}return n.child}function Of(t,n){return(t.lanes&n)!==0?!0:(t=t.dependencies,!!(t!==null&&Ll(t)))}function B_(t,n,a){switch(n.tag){case 3:ae(n,n.stateNode.containerInfo),zr(n,wt,t.memoizedState.cache),Ba();break;case 27:case 5:le(n);break;case 4:ae(n,n.stateNode.containerInfo);break;case 10:zr(n,n.type,n.memoizedProps.value);break;case 13:var c=n.memoizedState;if(c!==null)return c.dehydrated!==null?(Ur(n),n.flags|=128,null):(a&n.child.childLanes)!==0?Kx(t,n,a):(Ur(n),t=dr(t,n,a),t!==null?t.sibling:null);Ur(n);break;case 19:var m=(t.flags&128)!==0;if(c=(a&n.childLanes)!==0,c||(Pa(t,n,a,!1),c=(a&n.childLanes)!==0),m){if(c)return Qx(t,n,a);n.flags|=128}if(m=n.memoizedState,m!==null&&(m.rendering=null,m.tail=null,m.lastEffect=null),ee(Nt,Nt.current),c)break;return null;case 22:case 23:return n.lanes=0,Yx(t,n,a);case 24:zr(n,wt,t.memoizedState.cache)}return dr(t,n,a)}function Jx(t,n,a){if(t!==null)if(t.memoizedProps!==n.pendingProps)Et=!0;else{if(!Of(t,a)&&(n.flags&128)===0)return Et=!1,B_(t,n,a);Et=(t.flags&131072)!==0}else Et=!1,We&&(n.flags&1048576)!==0&&Ag(n,zl,n.index);switch(n.lanes=0,n.tag){case 16:e:{t=n.pendingProps;var c=n.elementType,m=c._init;if(c=m(c._payload),n.type=c,typeof c=="function")$d(c)?(t=Do(c,t),n.tag=1,n=Fx(null,n,c,t,a)):(n.tag=0,n=Cf(null,n,c,t,a));else{if(c!=null){if(m=c.$$typeof,m===A){n.tag=11,n=$x(null,n,c,t,a);break e}else if(m===H){n.tag=14,n=Vx(null,n,c,t,a);break e}}throw n=I(c)||c,Error(s(306,n,""))}}return n;case 0:return Cf(t,n,n.type,n.pendingProps,a);case 1:return c=n.type,m=Do(c,n.pendingProps),Fx(t,n,c,m,a);case 3:e:{if(ae(n,n.stateNode.containerInfo),t===null)throw Error(s(387));c=n.pendingProps;var x=n.memoizedState;m=x.element,nf(t,n),Fa(n,c,null,a);var C=n.memoizedState;if(c=C.cache,zr(n,wt,c),c!==x.cache&&Kd(n,[wt],a,!0),Xa(),c=C.element,x.isDehydrated)if(x={element:c,isDehydrated:!1,cache:C.cache},n.updateQueue.baseState=x,n.memoizedState=x,n.flags&256){n=Zx(t,n,c,a);break e}else if(c!==m){m=cn(Error(s(424)),n),Ua(m),n=Zx(t,n,c,a);break e}else{switch(t=n.stateNode.containerInfo,t.nodeType){case 9:t=t.body;break;default:t=t.nodeName==="HTML"?t.ownerDocument.body:t}for(ft=wn(t.firstChild),It=n,We=!0,Co=null,Dn=!0,a=Tx(n,null,c,a),n.child=a;a;)a.flags=a.flags&-3|4096,a=a.sibling}else{if(Ba(),c===m){n=dr(t,n,a);break e}kt(t,n,c,a)}n=n.child}return n;case 26:return tc(t,n),t===null?(a=rv(n.type,null,n.pendingProps,null))?n.memoizedState=a:We||(a=n.type,t=n.pendingProps,c=gc(fe.current).createElement(a),c[_t]=n,c[Ot]=t,Mt(c,a,t),xt(c),n.stateNode=c):n.memoizedState=rv(n.type,t.memoizedProps,n.pendingProps,t.memoizedState),null;case 27:return le(n),t===null&&We&&(c=n.stateNode=ev(n.type,n.pendingProps,fe.current),It=n,Dn=!0,m=ft,Kr(n.type)?(hm=m,ft=wn(c.firstChild)):ft=m),kt(t,n,n.pendingProps.children,a),tc(t,n),t===null&&(n.flags|=4194304),n.child;case 5:return t===null&&We&&((m=c=ft)&&(c=mE(c,n.type,n.pendingProps,Dn),c!==null?(n.stateNode=c,It=n,ft=wn(c.firstChild),Dn=!1,m=!0):m=!1),m||ko(n)),le(n),m=n.type,x=n.pendingProps,C=t!==null?t.memoizedProps:null,c=x.children,um(m,x)?c=null:C!==null&&um(m,C)&&(n.flags|=32),n.memoizedState!==null&&(m=cf(t,n,T_,null,null,a),gi._currentValue=m),tc(t,n),kt(t,n,c,a),n.child;case 6:return t===null&&We&&((t=a=ft)&&(a=hE(a,n.pendingProps,Dn),a!==null?(n.stateNode=a,It=n,ft=null,t=!0):t=!1),t||ko(n)),null;case 13:return Kx(t,n,a);case 4:return ae(n,n.stateNode.containerInfo),c=n.pendingProps,t===null?n.child=Cs(n,null,c,a):kt(t,n,c,a),n.child;case 11:return $x(t,n,n.type,n.pendingProps,a);case 7:return kt(t,n,n.pendingProps,a),n.child;case 8:return kt(t,n,n.pendingProps.children,a),n.child;case 12:return kt(t,n,n.pendingProps.children,a),n.child;case 10:return c=n.pendingProps,zr(n,n.type,c.value),kt(t,n,c.children,a),n.child;case 9:return m=n.type._context,c=n.pendingProps.children,Mo(n),m=zt(m),c=c(m),n.flags|=1,kt(t,n,c,a),n.child;case 14:return Vx(t,n,n.type,n.pendingProps,a);case 15:return qx(t,n,n.type,n.pendingProps,a);case 19:return Qx(t,n,a);case 31:return c=n.pendingProps,a=n.mode,c={mode:c.mode,children:c.children},t===null?(a=nc(c,a),a.ref=n.ref,n.child=a,a.return=n,n=a):(a=or(t.child,c),a.ref=n.ref,n.child=a,a.return=n,n=a),n;case 22:return Yx(t,n,a);case 24:return Mo(n),c=zt(wt),t===null?(m=Jd(),m===null&&(m=at,x=Wd(),m.pooledCache=x,x.refCount++,x!==null&&(m.pooledCacheLanes|=a),m=x),n.memoizedState={parent:c,cache:m},tf(n),zr(n,wt,m)):((t.lanes&a)!==0&&(nf(t,n),Fa(n,null,null,a),Xa()),m=t.memoizedState,x=n.memoizedState,m.parent!==c?(m={parent:c,cache:c},n.memoizedState=m,n.lanes===0&&(n.memoizedState=n.updateQueue.baseState=m),zr(n,wt,c)):(c=x.cache,zr(n,wt,c),c!==m.cache&&Kd(n,[wt],a,!0))),kt(t,n,n.pendingProps.children,a),n.child;case 29:throw n.pendingProps}throw Error(s(156,n.tag))}function fr(t){t.flags|=4}function e0(t,n){if(n.type!=="stylesheet"||(n.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!lv(n)){if(n=mn.current,n!==null&&((Ge&4194048)===Ge?On!==null:(Ge&62914560)!==Ge&&(Ge&536870912)===0||n!==On))throw Ya=ef,Ig;t.flags|=8192}}function rc(t,n){n!==null&&(t.flags|=4),t.flags&16384&&(n=t.tag!==22?fl():536870912,t.lanes|=n,Ts|=n)}function ti(t,n){if(!We)switch(t.tailMode){case"hidden":n=t.tail;for(var a=null;n!==null;)n.alternate!==null&&(a=n),n=n.sibling;a===null?t.tail=null:a.sibling=null;break;case"collapsed":a=t.tail;for(var c=null;a!==null;)a.alternate!==null&&(c=a),a=a.sibling;c===null?n||t.tail===null?t.tail=null:t.tail.sibling=null:c.sibling=null}}function ut(t){var n=t.alternate!==null&&t.alternate.child===t.child,a=0,c=0;if(n)for(var m=t.child;m!==null;)a|=m.lanes|m.childLanes,c|=m.subtreeFlags&65011712,c|=m.flags&65011712,m.return=t,m=m.sibling;else for(m=t.child;m!==null;)a|=m.lanes|m.childLanes,c|=m.subtreeFlags,c|=m.flags,m.return=t,m=m.sibling;return t.subtreeFlags|=c,t.childLanes=a,n}function U_(t,n,a){var c=n.pendingProps;switch(Gd(n),n.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return ut(n),null;case 1:return ut(n),null;case 3:return a=n.stateNode,c=null,t!==null&&(c=t.memoizedState.cache),n.memoizedState.cache!==c&&(n.flags|=2048),lr(wt),xe(),a.pendingContext&&(a.context=a.pendingContext,a.pendingContext=null),(t===null||t.child===null)&&(Ha(n)?fr(n):t===null||t.memoizedState.isDehydrated&&(n.flags&256)===0||(n.flags|=1024,Rg())),ut(n),null;case 26:return a=n.memoizedState,t===null?(fr(n),a!==null?(ut(n),e0(n,a)):(ut(n),n.flags&=-16777217)):a?a!==t.memoizedState?(fr(n),ut(n),e0(n,a)):(ut(n),n.flags&=-16777217):(t.memoizedProps!==c&&fr(n),ut(n),n.flags&=-16777217),null;case 27:ce(n),a=fe.current;var m=n.type;if(t!==null&&n.stateNode!=null)t.memoizedProps!==c&&fr(n);else{if(!c){if(n.stateNode===null)throw Error(s(166));return ut(n),null}t=se.current,Ha(n)?Mg(n):(t=ev(m,c,a),n.stateNode=t,fr(n))}return ut(n),null;case 5:if(ce(n),a=n.type,t!==null&&n.stateNode!=null)t.memoizedProps!==c&&fr(n);else{if(!c){if(n.stateNode===null)throw Error(s(166));return ut(n),null}if(t=se.current,Ha(n))Mg(n);else{switch(m=gc(fe.current),t){case 1:t=m.createElementNS("http://www.w3.org/2000/svg",a);break;case 2:t=m.createElementNS("http://www.w3.org/1998/Math/MathML",a);break;default:switch(a){case"svg":t=m.createElementNS("http://www.w3.org/2000/svg",a);break;case"math":t=m.createElementNS("http://www.w3.org/1998/Math/MathML",a);break;case"script":t=m.createElement("div"),t.innerHTML=" - + +
diff --git a/python/packages/devui/agent_framework_devui/ui/vite.svg b/python/packages/devui/agent_framework_devui/ui/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/python/packages/devui/agent_framework_devui/ui/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/packages/devui/frontend/index.html b/python/packages/devui/frontend/index.html index 5290b853eb..e957a1547f 100644 --- a/python/packages/devui/frontend/index.html +++ b/python/packages/devui/frontend/index.html @@ -2,12 +2,12 @@ - + Agent Framework Dev UI
- + diff --git a/python/packages/devui/frontend/package-lock.json b/python/packages/devui/frontend/package-lock.json new file mode 100644 index 0000000000..db895c9af3 --- /dev/null +++ b/python/packages/devui/frontend/package-lock.json @@ -0,0 +1,4215 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/vite": "^4.1.12", + "@xyflow/react": "^12.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.540.0", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.12", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/node": "^24.3.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "tw-animate-css": "^1.3.7", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.11" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.32", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz", + "integrity": "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.47.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz", + "integrity": "sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz", + "integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "tailwindcss": "4.1.12" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.40.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.1.tgz", + "integrity": "sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.3", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.32", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.8.4", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz", + "integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.68", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.68", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz", + "integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001735", + "electron-to-chromium": "^1.5.204", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001736", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz", + "integrity": "sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.208", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz", + "integrity": "sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.540.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.540.0.tgz", + "integrity": "sha512-armkCAqQvO62EIX4Hq7hqX/q11WSZu0Jd23cnnqx0/49yIxGXyL/zyZfBxNN9YDx0ensPTb4L+DjTh3yQXUxtQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.47.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.47.1.tgz", + "integrity": "sha512-iasGAQoZ5dWDzULEUX3jiW0oB1qyFOepSyDyoU6S/OhVlDIwj5knI5QBa5RRQ0sK7OE0v+8VIi2JuV+G+3tfNg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.47.1", + "@rollup/rollup-android-arm64": "4.47.1", + "@rollup/rollup-darwin-arm64": "4.47.1", + "@rollup/rollup-darwin-x64": "4.47.1", + "@rollup/rollup-freebsd-arm64": "4.47.1", + "@rollup/rollup-freebsd-x64": "4.47.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.47.1", + "@rollup/rollup-linux-arm-musleabihf": "4.47.1", + "@rollup/rollup-linux-arm64-gnu": "4.47.1", + "@rollup/rollup-linux-arm64-musl": "4.47.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.47.1", + "@rollup/rollup-linux-ppc64-gnu": "4.47.1", + "@rollup/rollup-linux-riscv64-gnu": "4.47.1", + "@rollup/rollup-linux-riscv64-musl": "4.47.1", + "@rollup/rollup-linux-s390x-gnu": "4.47.1", + "@rollup/rollup-linux-x64-gnu": "4.47.1", + "@rollup/rollup-linux-x64-musl": "4.47.1", + "@rollup/rollup-win32-arm64-msvc": "4.47.1", + "@rollup/rollup-win32-ia32-msvc": "4.47.1", + "@rollup/rollup-win32-x64-msvc": "4.47.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.7.tgz", + "integrity": "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", + "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.40.0", + "@typescript-eslint/parser": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/python/packages/devui/frontend/public/vite.svg b/python/packages/devui/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/python/packages/devui/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/features/agent/agent-view.tsx b/python/packages/devui/frontend/src/components/features/agent/agent-view.tsx index dc7f59ec62..d21468dffb 100644 --- a/python/packages/devui/frontend/src/components/features/agent/agent-view.tsx +++ b/python/packages/devui/frontend/src/components/features/agent/agent-view.tsx @@ -45,6 +45,7 @@ import type { ExtendedResponseStreamEvent, } from "@/types"; import { useDevUIStore } from "@/stores"; +import { loadStreamingState } from "@/services/streaming-state"; type DebugEventHandler = (event: ExtendedResponseStreamEvent | "clear") => void; @@ -229,7 +230,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { const scrollAreaRef = useRef(null); const messagesEndRef = useRef(null); - const accumulatedText = useRef(""); const textareaRef = useRef(null); const currentMessageUsage = useRef<{ total_tokens: number; @@ -237,6 +237,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { output_tokens: number; } | null>(null); const userJustSentMessage = useRef(false); + const accumulatedTextRef = useRef(""); // Auto-scroll to bottom when new items arrive useEffect(() => { @@ -281,33 +282,289 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // Load conversations when agent changes useEffect(() => { + // Resume streaming after page refresh + const resumeStreaming = async ( + assistantMessage: import("@/types/openai").ConversationMessage, + conversation: Conversation, + agent: AgentInfo + ) => { + console.log(`[AgentView] resumeStreaming called for conversation ${conversation.id} with agent ${agent.id}`); + + // Load the stored state to get the response ID + const storedState = loadStreamingState(conversation.id); + if (!storedState || !storedState.responseId) { + console.error(`[AgentView] Cannot resume - no stored response ID for conversation ${conversation.id}`); + setIsStreaming(false); + return; + } + + console.log(`[AgentView] Resuming stream with response ID: ${storedState.responseId}`); + + try { + // Use the stored responseId to resume the stream via GET /v1/responses/{responseId} + const openAIRequest: import("@/types/agent-framework").AgentFrameworkRequest = { + model: agent.id, + input: [], // Not needed for resume (using GET) + stream: true, + conversation: conversation.id, + }; + + // Pass the response ID explicitly to trigger GET request + const streamGenerator = apiClient.streamAgentExecutionOpenAIDirect( + agent.id, + openAIRequest, + conversation.id, + storedState.responseId // Pass response ID for resume + ); + + for await (const openAIEvent of streamGenerator) { + // Pass all events to debug panel + onDebugEvent(openAIEvent); + + // Handle response.completed event + if (openAIEvent.type === "response.completed") { + const completedEvent = openAIEvent as import("@/types/openai").ResponseCompletedEvent; + const usage = completedEvent.response?.usage; + + if (usage) { + currentMessageUsage.current = { + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + total_tokens: usage.total_tokens, + }; + } + continue; + } + + // Handle response.failed event + if (openAIEvent.type === "response.failed") { + const failedEvent = openAIEvent as import("@/types/openai").ResponseFailedEvent; + const error = failedEvent.response?.error; + const errorMessage = error + ? typeof error === "object" && "message" in error + ? (error as any).message + : JSON.stringify(error) + : "Request failed"; + + const currentItems = useDevUIStore.getState().chatItems; + setChatItems(currentItems.map((item) => + item.id === assistantMessage.id && item.type === "message" + ? { + ...item, + content: [ + { + type: "text", + text: accumulatedTextRef.current || errorMessage, + } as import("@/types/openai").MessageTextContent, + ], + status: "incomplete" as const, + } + : item + )); + setIsStreaming(false); + return; + } + + // Handle function approval request events + if (openAIEvent.type === "response.function_approval.requested") { + const approvalEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRequestedEvent; + setPendingApprovals([ + ...useDevUIStore.getState().pendingApprovals, + { + request_id: approvalEvent.request_id, + function_call: approvalEvent.function_call, + }, + ]); + continue; + } + + // Handle function approval response events + if (openAIEvent.type === "response.function_approval.responded") { + const responseEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRespondedEvent; + setPendingApprovals( + useDevUIStore.getState().pendingApprovals.filter((a) => a.request_id !== responseEvent.request_id) + ); + continue; + } + + // Handle error events + if (openAIEvent.type === "error") { + const errorEvent = openAIEvent as ExtendedResponseStreamEvent & { message?: string }; + const errorMessage = errorEvent.message || "An error occurred"; + + const currentItems = useDevUIStore.getState().chatItems; + setChatItems(currentItems.map((item) => + item.id === assistantMessage.id && item.type === "message" + ? { + ...item, + content: [ + { + type: "text", + text: accumulatedTextRef.current || errorMessage, + } as import("@/types/openai").MessageTextContent, + ], + status: "incomplete" as const, + } + : item + )); + setIsStreaming(false); + return; + } + + // Handle text delta events + if ( + openAIEvent.type === "response.output_text.delta" && + "delta" in openAIEvent && + openAIEvent.delta + ) { + accumulatedTextRef.current += openAIEvent.delta; + + const currentItems = useDevUIStore.getState().chatItems; + setChatItems(currentItems.map((item) => + item.id === assistantMessage.id && item.type === "message" + ? { + ...item, + content: [ + { + type: "text", + text: accumulatedTextRef.current, + } as import("@/types/openai").MessageTextContent, + ], + status: "in_progress" as const, + } + : item + )); + } + } + + // Stream ended - mark as complete + const finalUsage = currentMessageUsage.current; + + const currentItems = useDevUIStore.getState().chatItems; + setChatItems(currentItems.map((item) => + item.id === assistantMessage.id && item.type === "message" + ? { + ...item, + status: "completed" as const, + usage: finalUsage || undefined, + } + : item + )); + setIsStreaming(false); + + if (finalUsage) { + updateConversationUsage(finalUsage.total_tokens); + } + + currentMessageUsage.current = null; + } catch (error) { + const currentItems = useDevUIStore.getState().chatItems; + setChatItems(currentItems.map((item) => + item.id === assistantMessage.id && item.type === "message" + ? { + ...item, + content: [ + { + type: "text", + text: `Error resuming stream: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + } as import("@/types/openai").MessageTextContent, + ], + status: "incomplete" as const, + } + : item + )); + setIsStreaming(false); + } + }; + const loadConversations = async () => { if (!selectedAgent) return; + console.log(`[AgentView] loadConversations called for agent ${selectedAgent.id}`); setLoadingConversations(true); try { - // Step 1: Try to list conversations from backend (DevUI extension) - // This works with DevUI backend but fails with OpenAI/Azure (they don't have list endpoint) + // Step 1: Always try to list conversations from backend first + // This ensures we get the latest data from the server try { const { data: conversations } = await apiClient.listConversations( selectedAgent.id ); + // Backend successfully returned conversations list + setAvailableConversations(conversations); + if (conversations.length > 0) { // Found conversations on backend - use most recent const mostRecent = conversations[0]; - setAvailableConversations(conversations); setCurrentConversation(mostRecent); // Load conversation items from backend try { - const { data: items } = await apiClient.listConversationItems( - mostRecent.id - ); + // Load all conversation items with pagination + let allItems: unknown[] = []; + let hasMore = true; + let after: string | undefined = undefined; + + while (hasMore) { + const result = await apiClient.listConversationItems( + mostRecent.id, + { order: "asc", after } // Load in chronological order (oldest first) + ); + allItems = allItems.concat(result.data); + hasMore = result.has_more; + + // Get the last item's ID for pagination + if (hasMore && result.data.length > 0) { + const lastItem = result.data[result.data.length - 1] as { id?: string }; + after = lastItem.id; + } + } // Use OpenAI ConversationItems directly (no conversion!) - setChatItems(items as import("@/types/openai").ConversationItem[]); + setChatItems(allItems as import("@/types/openai").ConversationItem[]); setIsStreaming(false); + + // Check for incomplete stream and resume if needed + console.log(`[AgentView] Checking for incomplete stream for conversation ${mostRecent.id}`); + const state = loadStreamingState(mostRecent.id); + console.log(`[AgentView] Loaded streaming state:`, state); + + if (state && !state.completed) { + console.log(`[AgentView] Found incomplete stream, preparing to resume`); + accumulatedTextRef.current = state.accumulatedText || ""; + // Add assistant message with resumed text + const assistantMsg: import("@/types/openai").ConversationMessage = { + id: state.lastMessageId || `assistant-${Date.now()}`, + type: "message", + role: "assistant", + content: state.accumulatedText ? [{ type: "text", text: state.accumulatedText }] : [], + status: "in_progress", + }; + setChatItems([...allItems as import("@/types/openai").ConversationItem[], assistantMsg]); + setIsStreaming(true); + + // Resume streaming from where we left off + console.log(`[AgentView] Scheduling resumeStreaming in 100ms`); + setTimeout(() => { + console.log(`[AgentView] setTimeout callback executing, calling resumeStreaming`); + try { + resumeStreaming(assistantMsg, mostRecent, selectedAgent); + } catch (error) { + console.error(`[AgentView] Error calling resumeStreaming:`, error); + } + }, 100); + } else if (state && state.completed) { + console.log(`[AgentView] Stream already completed, not resuming`); + } else { + console.log(`[AgentView] No streaming state found`); + } + + // Scroll to bottom after loading conversation + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); } catch { // 404 means conversation exists but has no items yet (newly created) // This is normal - just start with empty chat @@ -316,11 +573,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setIsStreaming(false); } - // Cache to localStorage for faster future loads - localStorage.setItem( - `devui_convs_${selectedAgent.id}`, - JSON.stringify(conversations) - ); return; } } catch { @@ -371,9 +623,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setAvailableConversations([newConversation]); setChatItems([]); setIsStreaming(false); - - // Save to localStorage - localStorage.setItem(cachedKey, JSON.stringify([newConversation])); } catch { setAvailableConversations([]); setChatItems([]); @@ -387,10 +636,12 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setChatItems([]); setIsStreaming(false); setCurrentConversation(undefined); - accumulatedText.current = ""; + accumulatedTextRef.current = ""; loadConversations(); - }, [selectedAgent, setLoadingConversations, setAvailableConversations, setCurrentConversation, setChatItems, setIsStreaming]); + // currentConversation is intentionally excluded - this effect should only run when agent changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedAgent, onDebugEvent, setChatItems, setIsStreaming, setLoadingConversations, setAvailableConversations, setCurrentConversation, setPendingApprovals, updateConversationUsage]); // Handle file uploads const handleFilesSelected = async (files: File[]) => { @@ -626,16 +877,11 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setIsStreaming(false); // Reset conversation usage by setting it to initial state useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } }); - accumulatedText.current = ""; - - // Update localStorage cache with new conversation - const cachedKey = `devui_convs_${selectedAgent.id}`; - const updated = [newConversation, ...availableConversations]; - localStorage.setItem(cachedKey, JSON.stringify(updated)); + accumulatedTextRef.current = ""; } catch { // Failed to create conversation } - }, [selectedAgent, availableConversations, setCurrentConversation, setAvailableConversations, setChatItems, setIsStreaming]); + }, [selectedAgent, setCurrentConversation, setAvailableConversations, setChatItems, setIsStreaming]); // Handle conversation deletion const handleDeleteConversation = useCallback( @@ -660,15 +906,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { ); setAvailableConversations(updatedConversations); - // Update localStorage cache - if (selectedAgent) { - const cachedKey = `devui_convs_${selectedAgent.id}`; - localStorage.setItem( - cachedKey, - JSON.stringify(updatedConversations) - ); - } - // If deleted conversation was selected, switch to another conversation or clear chat if (currentConversation?.id === conversationId) { if (updatedConversations.length > 0) { @@ -683,7 +920,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setChatItems([]); setIsStreaming(false); useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } }); - accumulatedText.current = ""; + accumulatedTextRef.current = ""; } } @@ -694,7 +931,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { alert("Failed to delete conversation. Please try again."); } }, - [availableConversations, currentConversation, selectedAgent, onDebugEvent, setAvailableConversations, setCurrentConversation, setChatItems, setIsStreaming] + [availableConversations, currentConversation, onDebugEvent, setAvailableConversations, setCurrentConversation, setChatItems, setIsStreaming] ); // Handle conversation selection @@ -711,11 +948,28 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { onDebugEvent("clear"); try { - // Load conversation history from backend - const result = await apiClient.listConversationItems(conversationId); + // Load conversation history from backend with pagination + let allItems: unknown[] = []; + let hasMore = true; + let after: string | undefined = undefined; + + while (hasMore) { + const result = await apiClient.listConversationItems(conversationId, { + order: "asc", // Load in chronological order (oldest first) + after, + }); + allItems = allItems.concat(result.data); + hasMore = result.has_more; + + // Get the last item's ID for pagination + if (hasMore && result.data.length > 0) { + const lastItem = result.data[result.data.length - 1] as { id?: string }; + after = lastItem.id; + } + } // Use OpenAI ConversationItems directly (no conversion!) - const items = result.data as import("@/types/openai").ConversationItem[]; + const items = allItems as import("@/types/openai").ConversationItem[]; setChatItems(items); setIsStreaming(false); @@ -727,6 +981,27 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { message_count: items.length, } }); + + // Check for incomplete stream and restore accumulated text + const state = loadStreamingState(conversationId); + if (state?.accumulatedText) { + accumulatedTextRef.current = state.accumulatedText; + // Add assistant message with resumed text - streaming will continue automatically + const assistantMsg: import("@/types/openai").ConversationMessage = { + id: `assistant-${Date.now()}`, + type: "message", + role: "assistant", + content: [{ type: "output_text", text: state.accumulatedText }], + status: "in_progress", + }; + setChatItems([...items, assistantMsg]); + setIsStreaming(true); + } + + // Scroll to bottom after loading conversation + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, 100); } catch { // 404 means conversation doesn't exist or has no items yet // This can happen if server restarted (in-memory store cleared) @@ -736,7 +1011,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } }); } - accumulatedText.current = ""; + accumulatedTextRef.current = ""; }, [availableConversations, onDebugEvent, setCurrentConversation, setChatItems, setIsStreaming] ); @@ -851,13 +1126,18 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { } } + // Clear any previous streaming state for this conversation before starting new message + if (conversationToUse?.id) { + apiClient.clearStreamingState(conversationToUse.id); + } + const apiRequest = { input: request.input, conversation_id: conversationToUse?.id, }; // Clear text accumulator for new response - accumulatedText.current = ""; + accumulatedTextRef.current = ""; // Use OpenAI-compatible API streaming - direct event handling const streamGenerator = apiClient.streamAgentExecutionOpenAI( @@ -884,6 +1164,35 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { continue; // Continue processing other events } + // Handle response.failed event + if (openAIEvent.type === "response.failed") { + const failedEvent = openAIEvent as import("@/types/openai").ResponseFailedEvent; + const error = failedEvent.response?.error; + const errorMessage = error + ? typeof error === "object" && "message" in error + ? (error as any).message + : JSON.stringify(error) + : "Request failed"; + + const currentItems = useDevUIStore.getState().chatItems; + setChatItems(currentItems.map((item) => + item.id === assistantMessage.id && item.type === "message" + ? { + ...item, + content: [ + { + type: "text", + text: accumulatedTextRef.current || errorMessage, + } as import("@/types/openai").MessageTextContent, + ], + status: "incomplete" as const, + } + : item + )); + setIsStreaming(false); + return; + } + // Handle function approval request events if (openAIEvent.type === "response.function_approval.requested") { const approvalEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRequestedEvent; @@ -943,7 +1252,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { "delta" in openAIEvent && openAIEvent.delta ) { - accumulatedText.current += openAIEvent.delta; + accumulatedTextRef.current += openAIEvent.delta; // Update assistant message with accumulated content const currentItems = useDevUIStore.getState().chatItems; @@ -954,7 +1263,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { content: [ { type: "text", - text: accumulatedText.current, + text: accumulatedTextRef.current, } as import("@/types/openai").MessageTextContent, ], status: "in_progress" as const, diff --git a/python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIContentRenderer.tsx b/python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIContentRenderer.tsx index 8d266be40f..3871376945 100644 --- a/python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIContentRenderer.tsx +++ b/python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIContentRenderer.tsx @@ -23,7 +23,7 @@ interface ContentRendererProps { // Text content renderer function TextContentRenderer({ content, className, isStreaming }: ContentRendererProps) { - if (content.type !== "text") return null; + if (content.type !== "text" && content.type !== "input_text" && content.type !== "output_text") return null; const text = content.text; @@ -160,6 +160,8 @@ function FileContentRenderer({ content, className }: ContentRendererProps) { export function OpenAIContentRenderer({ content, className, isStreaming }: ContentRendererProps) { switch (content.type) { case "text": + case "input_text": + case "output_text": return ; case "input_image": return ; diff --git a/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx b/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx index 43cf1ac509..1ca95048db 100644 --- a/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx +++ b/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx @@ -554,6 +554,10 @@ export function WorkflowView({ try { const request = { input_data: inputData }; + // Clear any previous streaming state before starting new workflow execution + // Note: Workflows don't use conversation IDs, so we use workflow ID as the key + apiClient.clearStreamingState(selectedWorkflow.id); + // Use OpenAI-compatible API streaming - direct event handling const streamGenerator = apiClient.streamWorkflowExecutionOpenAI( selectedWorkflow.id, diff --git a/python/packages/devui/frontend/src/main.tsx b/python/packages/devui/frontend/src/main.tsx index 27db649a06..572a406006 100644 --- a/python/packages/devui/frontend/src/main.tsx +++ b/python/packages/devui/frontend/src/main.tsx @@ -3,6 +3,10 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' import { ThemeProvider } from "./components/theme-provider" +import { initStreamingState } from "./services/api" + +// Initialize streaming state management (clears expired states) +initStreamingState(); createRoot(document.getElementById('root')!).render( diff --git a/python/packages/devui/frontend/src/services/api.ts b/python/packages/devui/frontend/src/services/api.ts index 8a685cf5aa..9b8c160133 100644 --- a/python/packages/devui/frontend/src/services/api.ts +++ b/python/packages/devui/frontend/src/services/api.ts @@ -14,6 +14,12 @@ import type { } from "@/types"; import type { AgentFrameworkRequest } from "@/types/agent-framework"; import type { ExtendedResponseStreamEvent } from "@/types/openai"; +import { + loadStreamingState, + updateStreamingState, + markStreamingCompleted, + clearStreamingState, +} from "./streaming-state"; // Backend API response type - polymorphic entity that can be agent or workflow // This matches the Python Pydantic EntityInfo model which has all fields optional @@ -57,9 +63,27 @@ const DEFAULT_API_BASE_URL = ? import.meta.env.VITE_API_BASE_URL : "http://localhost:8080"; +// Retry configuration for streaming +const RETRY_INTERVAL_MS = 1000; // Retry every second +const MAX_RETRY_ATTEMPTS = 600; // Max 600 retries (10 minutes total) + // Get backend URL from localStorage or default function getBackendUrl(): string { - return localStorage.getItem("devui_backend_url") || DEFAULT_API_BASE_URL; + const stored = localStorage.getItem("devui_backend_url"); + if (stored) return stored; + + // If VITE_API_BASE_URL is explicitly set to empty string, use relative path + // This allows the frontend to call the same host it's served from + if (import.meta.env.VITE_API_BASE_URL === "") { + return ""; + } + + return DEFAULT_API_BASE_URL; +} + +// Helper to sleep for a given duration +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); } class ApiClient { @@ -272,6 +296,8 @@ class ApiClient { await this.request(`/v1/conversations/${conversationId}`, { method: "DELETE", }); + // Clear streaming state when conversation is deleted + clearStreamingState(conversationId); return true; } catch { return false; @@ -297,99 +323,280 @@ class ApiClient { // OpenAI-compatible streaming methods using /v1/responses endpoint - // Stream agent execution using OpenAI format with simplified routing - async *streamAgentExecutionOpenAI( - agentId: string, - request: RunAgentRequest - ): AsyncGenerator { - const openAIRequest: AgentFrameworkRequest = { - model: agentId, // Model IS the entity_id (simplified routing!) - input: request.input, // Direct OpenAI ResponseInputParam - stream: true, - conversation: request.conversation_id, // OpenAI standard conversation param - }; - - return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest); - } - - // Stream agent execution using direct OpenAI format - async *streamAgentExecutionOpenAIDirect( - _agentId: string, - openAIRequest: AgentFrameworkRequest + // Private helper method that handles the actual streaming with retry logic + private async *streamOpenAIResponse( + openAIRequest: AgentFrameworkRequest, + conversationId?: string, + resumeResponseId?: string ): AsyncGenerator { - - const response = await fetch(`${this.baseUrl}/v1/responses`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify(openAIRequest), - }); - - if (!response.ok) { - // Try to extract detailed error message from response body - let errorMessage = `Request failed with status ${response.status}`; - try { - const errorBody = await response.json(); - if (errorBody.error && errorBody.error.message) { - errorMessage = errorBody.error.message; - } else if (errorBody.detail) { - errorMessage = errorBody.detail; + let lastSequenceNumber = -1; + let retryCount = 0; + let hasYieldedAnyEvent = false; + let currentResponseId: string | undefined = resumeResponseId; + let lastMessageId: string | undefined = undefined; + + // Try to resume from stored state if conversation ID is provided + if (conversationId) { + const storedState = loadStreamingState(conversationId); + if (storedState) { + console.log( + `[Stream Resume] Found stored state: responseId=${storedState.responseId}, ` + + `lastSeq=${storedState.lastSequenceNumber}, events=${storedState.events.length}, ` + + `completed=${storedState.completed}` + ); + + // Use stored response ID if no explicit one provided + if (!resumeResponseId) { + currentResponseId = storedState.responseId; + } + + lastSequenceNumber = storedState.lastSequenceNumber; + lastMessageId = storedState.lastMessageId; + + // Replay stored events only if we're not explicitly resuming + // (explicit resume means the caller already has the events) + if (!resumeResponseId) { + for (const event of storedState.events) { + hasYieldedAnyEvent = true; + yield event; + } + } else { + // Mark that we've already seen events up to this sequence number + hasYieldedAnyEvent = storedState.events.length > 0; + } + } else { + console.log(`[Stream Resume] No stored state found for conversation ${conversationId}`); + if (resumeResponseId) { + console.log(`[Stream Resume] Resuming with explicit response ID but no stored state: ${resumeResponseId}`); } - } catch { - // Fallback to generic message if parsing fails } - throw new Error(errorMessage); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("Response body is not readable"); + } else if (resumeResponseId) { + console.log(`[Stream Resume] Resuming with explicit response ID (no conversation ID): ${resumeResponseId}`); } - const decoder = new TextDecoder(); - let buffer = ""; - - try { - while (true) { - const { done, value } = await reader.read(); + while (retryCount <= MAX_RETRY_ATTEMPTS) { + try { + // If we have a response_id from a previous attempt, use GET endpoint to resume + // Otherwise, use POST to create a new response + let response: Response; + if (currentResponseId) { + const params = new URLSearchParams(); + params.set("stream", "true"); + if (lastSequenceNumber >= 0) { + params.set("starting_after", lastSequenceNumber.toString()); + } + const url = `${this.baseUrl}/v1/responses/${currentResponseId}?${params.toString()}`; + console.log(`[Stream Resume] Using GET to resume: ${url}`); + response = await fetch(url, { + method: "GET", + headers: { + Accept: "text/event-stream", + }, + }); + } else { + const url = `${this.baseUrl}/v1/responses`; + console.log(`[Stream Start] Using POST to create new response: ${url}`); + response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify(openAIRequest), + }); + } - if (done) { - break; + if (!response.ok) { + // Try to extract detailed error message from response body + let errorMessage = `Request failed with status ${response.status}`; + try { + const errorBody = await response.json(); + if (errorBody.error && errorBody.error.message) { + errorMessage = errorBody.error.message; + } else if (errorBody.detail) { + errorMessage = errorBody.detail; + } + } catch { + // Fallback to generic message if parsing fails + } + throw new Error(errorMessage); } - buffer += decoder.decode(value, { stream: true }); + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Response body is not readable"); + } - // Parse SSE events - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; // Keep incomplete line in buffer + const decoder = new TextDecoder(); + let buffer = ""; - for (const line of lines) { - if (line.startsWith("data: ")) { - const dataStr = line.slice(6); + try { + while (true) { + const { done, value } = await reader.read(); - // Handle [DONE] signal - if (dataStr === "[DONE]") { + if (done) { + // Stream completed successfully + if (conversationId) { + markStreamingCompleted(conversationId); + } return; } - try { - const openAIEvent: ExtendedResponseStreamEvent = - JSON.parse(dataStr); - yield openAIEvent; // Direct pass-through - no conversion! - } catch (e) { - console.error("Failed to parse OpenAI SSE event:", e); + buffer += decoder.decode(value, { stream: true }); + + // Parse SSE events + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.startsWith("data: ")) { + const dataStr = line.slice(6); + + // Handle [DONE] signal + if (dataStr === "[DONE]") { + if (conversationId) { + markStreamingCompleted(conversationId); + } + return; + } + + try { + const openAIEvent: ExtendedResponseStreamEvent = + JSON.parse(dataStr); + + // Capture response_id if present in the event for use in retries + if ("response" in openAIEvent && openAIEvent.response && typeof openAIEvent.response === "object" && "id" in openAIEvent.response) { + const newResponseId = openAIEvent.response.id as string; + if (!currentResponseId || currentResponseId !== newResponseId) { + console.log(`[Stream] Captured response ID from event.response.id: ${newResponseId}`); + currentResponseId = newResponseId; + } + } else if ("id" in openAIEvent && typeof openAIEvent.id === "string" && openAIEvent.id.startsWith("resp_")) { + const newResponseId = openAIEvent.id; + if (!currentResponseId || currentResponseId !== newResponseId) { + console.log(`[Stream] Captured response ID from event.id: ${newResponseId}`); + currentResponseId = newResponseId; + } + } + + // Track last message ID if present (for user/assistant messages) + if ("item_id" in openAIEvent && openAIEvent.item_id) { + lastMessageId = openAIEvent.item_id; + } + + // Check for sequence number restart (server restarted response) + const eventSeq = "sequence_number" in openAIEvent ? openAIEvent.sequence_number : undefined; + if (eventSeq !== undefined) { + // If we've received events before and sequence restarted from 0/1 + if (hasYieldedAnyEvent && eventSeq <= 1 && lastSequenceNumber > 1) { + // Server restarted the response - clear old state and start fresh + if (conversationId) { + clearStreamingState(conversationId); + } + yield { + type: "error", + message: "Connection lost - previous response failed. Starting new response.", + } as ExtendedResponseStreamEvent; + lastSequenceNumber = eventSeq; + hasYieldedAnyEvent = true; + + // Save new event to storage + if (conversationId && currentResponseId) { + updateStreamingState(conversationId, openAIEvent, currentResponseId, lastMessageId); + } else if (conversationId && !currentResponseId) { + console.warn(`[Stream] Cannot save state - missing response ID for event type: ${openAIEvent.type}`); + } + + yield openAIEvent; + } + // Skip events we've already seen (resume from last position) + else if (eventSeq <= lastSequenceNumber) { + continue; // Skip duplicate event + } else { + lastSequenceNumber = eventSeq; + hasYieldedAnyEvent = true; + + // Save event to storage before yielding + if (conversationId && currentResponseId) { + updateStreamingState(conversationId, openAIEvent, currentResponseId, lastMessageId); + } else if (conversationId && !currentResponseId) { + console.warn(`[Stream] Cannot save state - missing response ID for event type: ${openAIEvent.type}`); + } + + yield openAIEvent; + } + } else { + // No sequence number - just yield the event + hasYieldedAnyEvent = true; + + // Still save to storage if we have conversation context + if (conversationId && currentResponseId) { + updateStreamingState(conversationId, openAIEvent, currentResponseId, lastMessageId); + } else if (conversationId && !currentResponseId) { + console.warn(`[Stream] Cannot save state - missing response ID for event type: ${openAIEvent.type}`); + } + + yield openAIEvent; + } + } catch (e) { + console.error("Failed to parse OpenAI SSE event:", e); + } + } } } + } finally { + reader.releaseLock(); } + } catch (error) { + // Network error occurred - prepare to retry + retryCount++; + + if (retryCount > MAX_RETRY_ATTEMPTS) { + // Max retries exceeded - give up + throw new Error( + `Connection failed after ${MAX_RETRY_ATTEMPTS} retry attempts: ${error instanceof Error ? error.message : String(error)}` + ); + } + + // Wait before retrying + const method = currentResponseId ? `GET /v1/responses/${currentResponseId}` : "POST /v1/responses"; + console.warn( + `Stream connection lost (attempt ${retryCount}/${MAX_RETRY_ATTEMPTS}). Retrying with ${method} in ${RETRY_INTERVAL_MS}ms...`, + error + ); + await sleep(RETRY_INTERVAL_MS); + // Loop will retry with GET if we have response_id, otherwise POST } - } finally { - reader.releaseLock(); } } - // Stream workflow execution using OpenAI format - direct event pass-through + // Stream agent execution using OpenAI format with simplified routing + async *streamAgentExecutionOpenAI( + agentId: string, + request: RunAgentRequest, + resumeResponseId?: string + ): AsyncGenerator { + const openAIRequest: AgentFrameworkRequest = { + model: agentId, // Model IS the entity_id (simplified routing!) + input: request.input, // Direct OpenAI ResponseInputParam + stream: true, + conversation: request.conversation_id, // OpenAI standard conversation param + }; + + return yield* this.streamAgentExecutionOpenAIDirect(agentId, openAIRequest, request.conversation_id, resumeResponseId); + } + + // Stream agent execution using direct OpenAI format + async *streamAgentExecutionOpenAIDirect( + _agentId: string, + openAIRequest: AgentFrameworkRequest, + conversationId?: string, + resumeResponseId?: string + ): AsyncGenerator { + yield* this.streamOpenAIResponse(openAIRequest, conversationId, resumeResponseId); + } + + // Stream workflow execution using OpenAI format async *streamWorkflowExecutionOpenAI( workflowId: string, request: RunWorkflowRequest @@ -402,75 +609,7 @@ class ApiClient { conversation: request.conversation_id, // Include conversation if present }; - const response = await fetch(`${this.baseUrl}/v1/responses`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify(openAIRequest), - }); - - if (!response.ok) { - // Try to extract detailed error message from response body - let errorMessage = `Request failed with status ${response.status}`; - try { - const errorBody = await response.json(); - if (errorBody.error && errorBody.error.message) { - errorMessage = errorBody.error.message; - } else if (errorBody.detail) { - errorMessage = errorBody.detail; - } - } catch { - // Fallback to generic message if parsing fails - } - throw new Error(errorMessage); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("Response body is not readable"); - } - - const decoder = new TextDecoder(); - let buffer = ""; - - try { - while (true) { - const { done, value } = await reader.read(); - - if (done) { - break; - } - - buffer += decoder.decode(value, { stream: true }); - - // Parse SSE events - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; // Keep incomplete line in buffer - - for (const line of lines) { - if (line.startsWith("data: ")) { - const dataStr = line.slice(6); - - // Handle [DONE] signal - if (dataStr === "[DONE]") { - return; - } - - try { - const openAIEvent: ExtendedResponseStreamEvent = - JSON.parse(dataStr); - yield openAIEvent; // Direct pass-through - no conversion! - } catch (e) { - console.error("Failed to parse OpenAI SSE event:", e); - } - } - } - } - } finally { - reader.releaseLock(); - } + yield* this.streamOpenAIResponse(openAIRequest, request.conversation_id); } // REMOVED: Legacy streaming methods - use streamAgentExecutionOpenAI and streamWorkflowExecutionOpenAI instead @@ -503,8 +642,16 @@ class ApiClient { body: JSON.stringify(request), }); } + + // Clear streaming state for a conversation (e.g., when starting a new message) + clearStreamingState(conversationId: string): void { + clearStreamingState(conversationId); + } } // Export singleton instance export const apiClient = new ApiClient(); export { ApiClient }; + +// Export streaming state init function +export { initStreamingState } from "./streaming-state"; diff --git a/python/packages/devui/frontend/src/services/streaming-state.ts b/python/packages/devui/frontend/src/services/streaming-state.ts new file mode 100644 index 0000000000..1aa2586d75 --- /dev/null +++ b/python/packages/devui/frontend/src/services/streaming-state.ts @@ -0,0 +1,207 @@ +/** + * Streaming State Persistence + * + * Manages browser storage of streaming response state to enable: + * - Resume interrupted streams after page refresh + * - Replay cached events before fetching new ones + * - Graceful recovery from network disconnections + */ + +import type { ExtendedResponseStreamEvent } from "@/types/openai"; + +export interface StreamingState { + conversationId: string; + responseId: string; + lastMessageId?: string; + lastSequenceNumber: number; + events: ExtendedResponseStreamEvent[]; + timestamp: number; // When this state was last updated + completed: boolean; // Whether the stream completed successfully + accumulatedText?: string; // Accumulated text content for quick restoration +} + +const STORAGE_KEY_PREFIX = "devui_streaming_state_"; +const STATE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours + +/** + * Storage key for a specific conversation + */ +function getStorageKey(conversationId: string): string { + return `${STORAGE_KEY_PREFIX}${conversationId}`; +} + +/** + * Extract accumulated text from events (for quick restoration) + */ +function extractAccumulatedText(events: ExtendedResponseStreamEvent[]): string { + let text = ""; + for (const event of events) { + if (event.type === "response.output_text.delta" && "delta" in event) { + text += event.delta; + } + } + return text; +} + +/** + * Save streaming state to browser storage + */ +export function saveStreamingState(state: StreamingState): void { + try { + const key = getStorageKey(state.conversationId); + const data = JSON.stringify(state); + localStorage.setItem(key, data); + } catch (error) { + console.error("Failed to save streaming state:", error); + // If storage is full, try to clear old states + try { + clearExpiredStreamingStates(); + // Try again + const key = getStorageKey(state.conversationId); + const data = JSON.stringify(state); + localStorage.setItem(key, data); + } catch { + console.error("Failed to save streaming state even after cleanup"); + } + } +} + +/** + * Load streaming state from browser storage + */ +export function loadStreamingState(conversationId: string): StreamingState | null { + try { + console.log(`[StreamingState] Loading state for conversation ${conversationId}`); + const key = getStorageKey(conversationId); + const data = localStorage.getItem(key); + + if (!data) { + console.log(`[StreamingState] No data found in localStorage for key ${key}`); + return null; + } + + const state: StreamingState = JSON.parse(data); + console.log(`[StreamingState] Parsed state:`, state); + + // Check if state has expired + const age = Date.now() - state.timestamp; + if (age > STATE_EXPIRY_MS) { + console.log(`[StreamingState] State expired (age: ${age}ms > ${STATE_EXPIRY_MS}ms)`); + clearStreamingState(conversationId); + return null; + } + + // If stream was completed, no need to resume + if (state.completed) { + console.log(`[StreamingState] Stream already completed`); + return null; + } + + console.log(`[StreamingState] Returning valid incomplete state`); + return state; + } catch (error) { + console.error("Failed to load streaming state:", error); + return null; + } +} + +/** + * Update streaming state with a new event + */ +export function updateStreamingState( + conversationId: string, + event: ExtendedResponseStreamEvent, + responseId: string, + lastMessageId?: string +): void { + try { + console.log(`[StreamingState] Updating state for conversation ${conversationId}, responseId: ${responseId}, event type: ${event.type}`); + const existing = loadStreamingState(conversationId); + const sequenceNumber = "sequence_number" in event ? event.sequence_number : undefined; + + const newEvents = existing ? [...existing.events, event] : [event]; + + const state: StreamingState = { + conversationId, + responseId, + lastMessageId, + lastSequenceNumber: sequenceNumber ?? (existing?.lastSequenceNumber ?? -1), + events: newEvents, + timestamp: Date.now(), + completed: event.type === "response.completed" || event.type === "response.failed", + accumulatedText: extractAccumulatedText(newEvents), + }; + + saveStreamingState(state); + console.log(`[StreamingState] Saved state with ${newEvents.length} events, completed: ${state.completed}`); + } catch (error) { + console.error("Failed to update streaming state:", error); + } +} + +/** + * Mark streaming state as completed + */ +export function markStreamingCompleted(conversationId: string): void { + try { + const existing = loadStreamingState(conversationId); + if (existing) { + existing.completed = true; + existing.timestamp = Date.now(); + saveStreamingState(existing); + } + } catch (error) { + console.error("Failed to mark streaming as completed:", error); + } +} + +/** + * Clear streaming state for a conversation + */ +export function clearStreamingState(conversationId: string): void { + try { + const key = getStorageKey(conversationId); + localStorage.removeItem(key); + } catch (error) { + console.error("Failed to clear streaming state:", error); + } +} + +/** + * Clear all expired streaming states + */ +export function clearExpiredStreamingStates(): void { + try { + const keys = Object.keys(localStorage); + const now = Date.now(); + + for (const key of keys) { + if (key.startsWith(STORAGE_KEY_PREFIX)) { + try { + const data = localStorage.getItem(key); + if (data) { + const state: StreamingState = JSON.parse(data); + const age = now - state.timestamp; + + if (age > STATE_EXPIRY_MS || state.completed) { + localStorage.removeItem(key); + } + } + } catch { + // Invalid state, remove it + localStorage.removeItem(key); + } + } + } + } catch (error) { + console.error("Failed to clear expired streaming states:", error); + } +} + +/** + * Initialize streaming state management (call on app startup) + */ +export function initStreamingState(): void { + // Clear expired states on startup + clearExpiredStreamingStates(); +} diff --git a/python/packages/devui/frontend/src/types/openai.ts b/python/packages/devui/frontend/src/types/openai.ts index e4df1a1bf8..ad6e613dbc 100644 --- a/python/packages/devui/frontend/src/types/openai.ts +++ b/python/packages/devui/frontend/src/types/openai.ts @@ -347,6 +347,18 @@ export interface MessageTextContent { text: string; } +export interface MessageInputTextContent { + type: "input_text"; + text: string; +} + +export interface MessageOutputTextContent { + type: "output_text"; + text: string; + annotations?: any[]; + logprobs?: any[]; +} + export interface MessageInputImage { type: "input_image"; image_url: string; @@ -376,6 +388,8 @@ export interface MessageFunctionApprovalResponseContent { export type MessageContent = | MessageTextContent + | MessageInputTextContent + | MessageOutputTextContent | MessageInputImage | MessageInputFile | MessageFunctionApprovalResponseContent; diff --git a/python/packages/devui/frontend/vite.config.ts b/python/packages/devui/frontend/vite.config.ts index 010f098001..49a54b477d 100644 --- a/python/packages/devui/frontend/vite.config.ts +++ b/python/packages/devui/frontend/vite.config.ts @@ -5,6 +5,7 @@ import path from "path"; // https://vite.dev/config/ export default defineConfig({ + base: "", plugins: [react(), tailwindcss()], resolve: { alias: { @@ -12,12 +13,20 @@ export default defineConfig({ }, }, build: { + commonjsOptions: { + // Enable deterministic builds, as per https://github.com/vitejs/vite/issues/13672#issuecomment-1784110536 + strictRequires: true, + }, outDir: "../agent_framework_devui/ui", emptyOutDir: true, rollupOptions: { output: { manualChunks: undefined, inlineDynamicImports: true, + // Use static filenames instead of content hashes + entryFileNames: "assets/index.js", + chunkFileNames: "assets/[name].js", + assetFileNames: "assets/[name].[ext]", }, }, }, diff --git a/python/packages/devui/frontend/yarn.lock b/python/packages/devui/frontend/yarn.lock index 00efde8b6f..a6793bb713 100644 --- a/python/packages/devui/frontend/yarn.lock +++ b/python/packages/devui/frontend/yarn.lock @@ -24,7 +24,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== -"@babel/core@^7.28.3": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.3": version "7.28.3" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz" integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== @@ -168,156 +168,9 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" -"@emnapi/core@^1.4.3", "@emnapi/core@^1.4.5": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0" - integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg== - dependencies: - "@emnapi/wasi-threads" "1.1.0" - tslib "^2.4.0" - -"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.4.5": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73" - integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ== - dependencies: - tslib "^2.4.0" - -"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.4": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" - integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== - dependencies: - tslib "^2.4.0" - -"@esbuild/aix-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" - integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== - -"@esbuild/android-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c" - integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== - -"@esbuild/android-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419" - integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== - -"@esbuild/android-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683" - integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== - -"@esbuild/darwin-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz" - integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== - -"@esbuild/darwin-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be" - integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== - -"@esbuild/freebsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca" - integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== - -"@esbuild/freebsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab" - integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== - -"@esbuild/linux-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b" - integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== - -"@esbuild/linux-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37" - integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== - -"@esbuild/linux-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4" - integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== - -"@esbuild/linux-loong64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0" - integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== - -"@esbuild/linux-mips64el@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5" - integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== - -"@esbuild/linux-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db" - integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== - -"@esbuild/linux-riscv64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547" - integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== - -"@esbuild/linux-s390x@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830" - integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== - -"@esbuild/linux-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f" - integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== - -"@esbuild/netbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548" - integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== - -"@esbuild/netbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52" - integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== - -"@esbuild/openbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935" - integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== - -"@esbuild/openbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf" - integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== - -"@esbuild/openharmony-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314" - integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== - -"@esbuild/sunos-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e" - integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== - -"@esbuild/win32-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b" - integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== - -"@esbuild/win32-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3" - integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== - "@esbuild/win32-x64@0.25.9": version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz" integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": @@ -368,7 +221,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.33.0", "@eslint/js@^9.33.0": +"@eslint/js@^9.33.0", "@eslint/js@9.33.0": version "9.33.0" resolved "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz" integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A== @@ -482,15 +335,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@napi-rs/wasm-runtime@^0.2.12": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" - integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== - dependencies: - "@emnapi/core" "^1.4.3" - "@emnapi/runtime" "^1.4.3" - "@tybys/wasm-util" "^0.10.0" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -499,7 +343,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -740,7 +584,7 @@ aria-hidden "^1.2.4" react-remove-scroll "^2.6.3" -"@radix-ui/react-slot@1.2.3", "@radix-ui/react-slot@^1.2.3": +"@radix-ui/react-slot@^1.2.3", "@radix-ui/react-slot@1.2.3": version "1.2.3" resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz" integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== @@ -829,104 +673,9 @@ resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz" integrity sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g== -"@rollup/rollup-android-arm-eabi@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz#6e236cd2fd29bb01a300ad4ff6ed0f1a17550e69" - integrity sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g== - -"@rollup/rollup-android-arm64@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz#808f2c9c7e68161add613ebcb0eac5a058a0df3c" - integrity sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA== - -"@rollup/rollup-darwin-arm64@4.47.1": - version "4.47.1" - resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz" - integrity sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ== - -"@rollup/rollup-darwin-x64@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz#9aac64e886435493f2e3a0aa5e4aad098a90814c" - integrity sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ== - -"@rollup/rollup-freebsd-arm64@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz#9fc804264f7b7a7cdad3747950299f990163be1f" - integrity sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw== - -"@rollup/rollup-freebsd-x64@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz#933feaff864feb03bbbcd0c18ea351ade957cf79" - integrity sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA== - -"@rollup/rollup-linux-arm-gnueabihf@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz#02915e6b2c55fe5961c27404aba2d9c8ef48ac6c" - integrity sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A== - -"@rollup/rollup-linux-arm-musleabihf@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz#1afef33191b26e76ae7f0d0dc767efc6be1285ce" - integrity sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q== - -"@rollup/rollup-linux-arm64-gnu@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz#6e7f38fb99d14143de3ce33204e6cd61e1c2c780" - integrity sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA== - -"@rollup/rollup-linux-arm64-musl@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz#25ab09f14bbcba85a604bcee2962d2486db90794" - integrity sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA== - -"@rollup/rollup-linux-loongarch64-gnu@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz#d3e3a3fd61e21b2753094391dee9b515a2bc9ecd" - integrity sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg== - -"@rollup/rollup-linux-ppc64-gnu@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz#6b44445e2bd5866692010de241bf18d2ae8b0cb8" - integrity sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ== - -"@rollup/rollup-linux-riscv64-gnu@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz#3ff412d20d3b157e6aadabf84788e8c5cb221ba7" - integrity sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow== - -"@rollup/rollup-linux-riscv64-musl@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz#104f451497d53d82a49c6d08c13c59f5f30eed57" - integrity sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw== - -"@rollup/rollup-linux-s390x-gnu@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz#d04de7b21d181f30750760cb3553946306506172" - integrity sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ== - -"@rollup/rollup-linux-x64-gnu@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz#a6ba88ff7480940a435b1e67ddbb3f207a7ae02f" - integrity sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g== - -"@rollup/rollup-linux-x64-musl@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz#c912c8ffa0c242ed3175cd91cdeaef98109afa54" - integrity sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ== - -"@rollup/rollup-win32-arm64-msvc@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz#ca5eaae89443554b461bb359112a056528cfdac0" - integrity sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA== - -"@rollup/rollup-win32-ia32-msvc@4.47.1": - version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz#34e76172515fb4b374eb990d59f54faff938246e" - integrity sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ== - "@rollup/rollup-win32-x64-msvc@4.47.1": version "4.47.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz#e5e0a0bae2c9d4858cc9b8dc508b2e10d7f0df8b" + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz" integrity sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA== "@tailwindcss/node@4.1.12": @@ -942,71 +691,9 @@ source-map-js "^1.2.1" tailwindcss "4.1.12" -"@tailwindcss/oxide-android-arm64@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz#27920fe61fa2743afe8a8ca296fa640b609d17d5" - integrity sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ== - -"@tailwindcss/oxide-darwin-arm64@4.1.12": - version "4.1.12" - resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz" - integrity sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw== - -"@tailwindcss/oxide-darwin-x64@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz#8ddb7e5ddfd9b049ec84a2bda99f2b04a86859f5" - integrity sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg== - -"@tailwindcss/oxide-freebsd-x64@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz#da1c0b16b7a5f95a1e400f299a3ec94fb6fd40ac" - integrity sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww== - -"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz#34e558aa6e869c6fe9867cb78ed7ba651b9fcaa4" - integrity sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ== - -"@tailwindcss/oxide-linux-arm64-gnu@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz#0a00a8146ab6215f81b2d385056c991441bf390e" - integrity sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g== - -"@tailwindcss/oxide-linux-arm64-musl@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz#b138f494068884ae0d8c343dc1904b22f5e98dc6" - integrity sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA== - -"@tailwindcss/oxide-linux-x64-gnu@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz#5b9d5f23b15cdb714639f5b9741c0df5d610f794" - integrity sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q== - -"@tailwindcss/oxide-linux-x64-musl@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz#f68ec530d3ca6875ea9015bcd5dd0762ee5e2f5d" - integrity sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A== - -"@tailwindcss/oxide-wasm32-wasi@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz#9fd15a1ebde6076c42c445c5e305c31673ead965" - integrity sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg== - dependencies: - "@emnapi/core" "^1.4.5" - "@emnapi/runtime" "^1.4.5" - "@emnapi/wasi-threads" "^1.0.4" - "@napi-rs/wasm-runtime" "^0.2.12" - "@tybys/wasm-util" "^0.10.0" - tslib "^2.8.0" - -"@tailwindcss/oxide-win32-arm64-msvc@4.1.12": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz#938bcc6a82e1120ea4fe2ce94be0a8cdf3ae92c7" - integrity sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg== - "@tailwindcss/oxide-win32-x64-msvc@4.1.12": version "4.1.12" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz#b1ee2ed0ef2c4095ddec3684a1987e2b3613af36" + resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz" integrity sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA== "@tailwindcss/oxide@4.1.12": @@ -1039,13 +726,6 @@ "@tailwindcss/oxide" "4.1.12" tailwindcss "4.1.12" -"@tybys/wasm-util@^0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369" - integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ== - dependencies: - tslib "^2.4.0" - "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" @@ -1118,7 +798,7 @@ "@types/d3-interpolate" "*" "@types/d3-selection" "*" -"@types/estree@1.0.8", "@types/estree@^1.0.6": +"@types/estree@^1.0.6", "@types/estree@1.0.8": version "1.0.8" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -1128,19 +808,19 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@^24.3.0": +"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0": version "24.3.0" resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz" integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== dependencies: undici-types "~7.10.0" -"@types/react-dom@^19.1.7": +"@types/react-dom@*", "@types/react-dom@^19.1.7": version "19.1.7" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz" integrity sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw== -"@types/react@^19.1.10": +"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.10", "@types/react@>=16.8", "@types/react@>=18.0.0": version "19.1.10" resolved "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz" integrity sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg== @@ -1162,7 +842,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.40.0": +"@typescript-eslint/parser@^8.40.0", "@typescript-eslint/parser@8.40.0": version "8.40.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz" integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw== @@ -1190,7 +870,7 @@ "@typescript-eslint/types" "8.40.0" "@typescript-eslint/visitor-keys" "8.40.0" -"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0": +"@typescript-eslint/tsconfig-utils@^8.40.0", "@typescript-eslint/tsconfig-utils@8.40.0": version "8.40.0" resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz" integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw== @@ -1206,7 +886,7 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0": +"@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@8.40.0": version "8.40.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz" integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg== @@ -1259,7 +939,7 @@ "@xyflow/react@^12.8.4": version "12.8.4" - resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.8.4.tgz#db0eabd9e356c25f5ebf427413a8c5dd46113394" + resolved "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz" integrity sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w== dependencies: "@xyflow/system" "0.0.68" @@ -1286,7 +966,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.15.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -1347,7 +1027,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0: +browserslist@^4.24.0, "browserslist@>= 4.21.0": version "4.25.3" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz" integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ== @@ -1443,7 +1123,7 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== -"d3-drag@2 - 3", d3-drag@^3.0.0: +d3-drag@^3.0.0, "d3-drag@2 - 3": version "3.0.0" resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== @@ -1456,14 +1136,14 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== -"d3-interpolate@1 - 3", d3-interpolate@^3.0.1: +d3-interpolate@^3.0.1, "d3-interpolate@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== dependencies: d3-color "1 - 3" -"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: +d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3: version "3.0.0" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== @@ -1600,7 +1280,7 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@^9.33.0: +"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.33.0, eslint@>=8.40: version "9.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz" integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA== @@ -1846,7 +1526,7 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jiti@^2.5.1: +jiti@*, jiti@^2.5.1, jiti@>=1.21.0: version "2.5.1" resolved "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz" integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w== @@ -1903,57 +1583,12 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lightningcss-darwin-arm64@1.30.1: - version "1.30.1" - resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz" - integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== - -lightningcss-darwin-x64@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" - integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== - -lightningcss-freebsd-x64@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" - integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== - -lightningcss-linux-arm-gnueabihf@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" - integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== - -lightningcss-linux-arm64-gnu@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" - integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== - -lightningcss-linux-arm64-musl@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" - integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== - -lightningcss-linux-x64-gnu@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" - integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== - -lightningcss-linux-x64-musl@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" - integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== - -lightningcss-win32-arm64-msvc@1.30.1: - version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" - integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== - lightningcss-win32-x64-msvc@1.30.1: version "1.30.1" - resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" + resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz" integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== -lightningcss@1.30.1: +lightningcss@^1.21.0, lightningcss@1.30.1: version "1.30.1" resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== @@ -2063,7 +1698,7 @@ natural-compare@^1.4.0: next-themes@^0.4.6: version "0.4.6" - resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6" + resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz" integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA== node-releases@^2.0.19: @@ -2124,7 +1759,7 @@ picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.3: +"picomatch@^3 || ^4", picomatch@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== @@ -2153,7 +1788,7 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-dom@^19.1.1: +"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react-dom@^19.1.1, react-dom@>=16.8.0, react-dom@>=17: version "19.1.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz" integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw== @@ -2192,7 +1827,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: get-nonce "^1.0.0" tslib "^2.0.0" -react@^19.1.1: +"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^19.1.1, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18.0.0: version "19.1.1" resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz" integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== @@ -2292,7 +1927,7 @@ tailwind-merge@^3.3.1: resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz" integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== -tailwindcss@4.1.12, tailwindcss@^4.1.12: +tailwindcss@^4.1.12, tailwindcss@4.1.12: version "4.1.12" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz" integrity sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA== @@ -2316,7 +1951,7 @@ tar@^7.4.3: tinyglobby@^0.2.15: version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== dependencies: fdir "^6.5.0" @@ -2334,7 +1969,7 @@ ts-api-utils@^2.1.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0: +tslib@^2.0.0, tslib@^2.1.0: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -2361,7 +1996,7 @@ typescript-eslint@^8.39.1: "@typescript-eslint/typescript-estree" "8.40.0" "@typescript-eslint/utils" "8.40.0" -typescript@~5.8.3: +typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@~5.8.3: version "5.8.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== @@ -2401,15 +2036,15 @@ use-sidecar@^1.1.3: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.2.2: +use-sync-external-store@^1.2.2, use-sync-external-store@>=1.2.0: version "1.5.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== -vite@^7.1.11: - version "7.1.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.11.tgz#4d006746112fee056df64985191e846ebfb6007e" - integrity sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg== +"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.1.11: + version "7.1.12" + resolved "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz" + integrity sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug== dependencies: esbuild "^0.25.0" fdir "^6.5.0" @@ -2456,5 +2091,5 @@ zustand@^4.4.0: zustand@^5.0.8: version "5.0.8" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.8.tgz#b998a0c088c7027a20f2709141a91cb07ac57f8a" + resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz" integrity sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==