diff --git a/python/packages/devui/README.md b/python/packages/devui/README.md index d9a17392b2..30e807e341 100644 --- a/python/packages/devui/README.md +++ b/python/packages/devui/README.md @@ -49,6 +49,19 @@ devui ./agents --port 8080 When DevUI starts with no discovered entities, it displays a **sample entity gallery** with curated examples from the Agent Framework repository. You can download these samples, review them, and run them locally to get started quickly. +## Using MCP Tools + +**Important:** Don't use `async with` context managers when creating agents with MCP tools for DevUI - connections will close before execution. + +```python +# ✅ Correct - DevUI handles cleanup automatically +mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", chat_client=chat_client) +agent = ChatAgent(tools=mcp_tool) +serve(entities=[agent]) +``` + +MCP tools use lazy initialization and connect automatically on first use. DevUI attempts to clean up connections on shutdown + ## Directory Structure For your agents to be discovered by the DevUI, they must be organized in a directory structure like below. Each agent/workflow must have an `__init__.py` that exports the required variable (`agent` or `workflow`). @@ -157,42 +170,62 @@ Options: Given that DevUI offers an OpenAI Responses API, it internally maps messages and events from Agent Framework to OpenAI Responses API events (in `_mapper.py`). For transparency, this mapping is shown below: -| Agent Framework Content | OpenAI Event/Type | Status | -| ------------------------------- | ---------------------------------------- | -------- | -| `TextContent` | `response.output_text.delta` | Standard | -| `TextReasoningContent` | `response.reasoning_text.delta` | Standard | -| `FunctionCallContent` (initial) | `response.output_item.added` | Standard | -| `FunctionCallContent` (args) | `response.function_call_arguments.delta` | Standard | -| `FunctionResultContent` | `response.function_result.complete` | DevUI | -| `FunctionApprovalRequestContent`| `response.function_approval.requested` | DevUI | -| `FunctionApprovalResponseContent`| `response.function_approval.responded` | DevUI | -| `ErrorContent` | `error` | Standard | -| `UsageContent` | Final `Response.usage` field (not streamed) | Standard | -| `WorkflowEvent` | `response.workflow_event.complete` | DevUI | -| `DataContent` | `response.trace.complete` | DevUI | -| `UriContent` | `response.trace.complete` | DevUI | -| `HostedFileContent` | `response.trace.complete` | DevUI | -| `HostedVectorStoreContent` | `response.trace.complete` | DevUI | - -- **Standard** = OpenAI Responses API spec -- **DevUI** = Custom extensions for Agent Framework features (workflows, traces, function approvals) +| OpenAI Event/Type | Agent Framework Content | Status | +| ------------------------------------------------------------ | --------------------------------- | -------- | +| | **Lifecycle Events** | | +| `response.created` + `response.in_progress` | `AgentStartedEvent` | OpenAI | +| `response.completed` | `AgentCompletedEvent` | OpenAI | +| `response.failed` | `AgentFailedEvent` | OpenAI | +| `response.created` + `response.in_progress` | `WorkflowStartedEvent` | OpenAI | +| `response.completed` | `WorkflowCompletedEvent` | OpenAI | +| `response.failed` | `WorkflowFailedEvent` | OpenAI | +| | **Content Types** | | +| `response.content_part.added` + `response.output_text.delta` | `TextContent` | OpenAI | +| `response.reasoning_text.delta` | `TextReasoningContent` | OpenAI | +| `response.output_item.added` | `FunctionCallContent` (initial) | OpenAI | +| `response.function_call_arguments.delta` | `FunctionCallContent` (args) | OpenAI | +| `response.function_result.complete` | `FunctionResultContent` | DevUI | +| `response.function_approval.requested` | `FunctionApprovalRequestContent` | DevUI | +| `response.function_approval.responded` | `FunctionApprovalResponseContent` | DevUI | +| `error` | `ErrorContent` | OpenAI | +| Final `Response.usage` field (not streamed) | `UsageContent` | OpenAI | +| | **Workflow Events** | | +| `response.output_item.added` (ExecutorActionItem)* | `ExecutorInvokedEvent` | OpenAI | +| `response.output_item.done` (ExecutorActionItem)* | `ExecutorCompletedEvent` | OpenAI | +| `response.output_item.done` (ExecutorActionItem with error)* | `ExecutorFailedEvent` | OpenAI | +| `response.workflow_event.complete` | `WorkflowEvent` (other) | DevUI | +| `response.trace.complete` | `WorkflowStatusEvent` | DevUI | +| `response.trace.complete` | `WorkflowWarningEvent` | DevUI | +| | **Trace Content** | | +| `response.trace.complete` | `DataContent` | DevUI | +| `response.trace.complete` | `UriContent` | DevUI | +| `response.trace.complete` | `HostedFileContent` | DevUI | +| `response.trace.complete` | `HostedVectorStoreContent` | DevUI | + +\*Uses standard OpenAI event structure but carries DevUI-specific `ExecutorActionItem` payload + +- **OpenAI** = Standard OpenAI Responses API event types +- **DevUI** = Custom event types specific to Agent Framework (e.g., workflows, traces, function approvals) ### OpenAI Responses API Compliance DevUI follows the OpenAI Responses API specification for maximum compatibility: -**Standard OpenAI Types Used:** +**OpenAI Standard Event Types Used:** + - `ResponseOutputItemAddedEvent` - Output item notifications (function calls and results) +- `ResponseOutputItemDoneEvent` - Output item completion notifications - `Response.usage` - Token usage (in final response, not streamed) - All standard text, reasoning, and function call events **Custom DevUI Extensions:** + - `response.function_approval.requested` - Function approval requests (for interactive approval workflows) - `response.function_approval.responded` - Function approval responses (user approval/rejection) - `response.workflow_event.complete` - Agent Framework workflow events - `response.trace.complete` - Execution traces and internal content (DataContent, UriContent, hosted files/stores) -These custom extensions are clearly namespaced and can be safely ignored by standard OpenAI clients. +These custom extensions are clearly namespaced and can be safely ignored by standard OpenAI clients. Note that DevUI also uses standard OpenAI events with custom payloads (e.g., `ExecutorActionItem` within `response.output_item.added`). ### Entity Management @@ -224,12 +257,14 @@ These custom extensions are clearly namespaced and can be safely ignored by stan DevUI is designed as a **sample application for local development** and should not be exposed to untrusted networks or used in production environments. **Security features:** + - Only loads entities from local directories or in-memory registration - No remote code execution capabilities - Binds to localhost (127.0.0.1) by default - All samples must be manually downloaded and reviewed before running **Best practices:** + - Never expose DevUI to the internet - Review all agent/workflow code before running - Only load entities from trusted sources diff --git a/python/packages/devui/agent_framework_devui/_discovery.py b/python/packages/devui/agent_framework_devui/_discovery.py index 175109c7a0..213af1f0e1 100644 --- a/python/packages/devui/agent_framework_devui/_discovery.py +++ b/python/packages/devui/agent_framework_devui/_discovery.py @@ -127,7 +127,7 @@ async def load_entity(self, entity_id: str) -> Any: # Cache the loaded object self._loaded_objects[entity_id] = entity_obj - logger.info(f"✅ Successfully loaded entity: {entity_id} (type: {enriched_info.type})") + logger.info(f"Successfully loaded entity: {entity_id} (type: {enriched_info.type})") return entity_obj @@ -217,7 +217,7 @@ def invalidate_entity(self, entity_id: str) -> None: if entity_info and "lazy_loaded" in entity_info.metadata: entity_info.metadata["lazy_loaded"] = False - logger.info(f"♻️ Entity invalidated: {entity_id} (will reload on next access)") + logger.info(f"Entity invalidated: {entity_id} (will reload on next access)") def invalidate_all(self) -> None: """Invalidate all cached entities. diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index 6c02d91cf0..16d57c94b5 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -217,6 +217,11 @@ async def _execute_agent( Agent update events and trace events """ try: + # Emit agent lifecycle start event + from .models._openai_custom import AgentStartedEvent + + yield AgentStartedEvent() + # Convert input to proper ChatMessage or string user_message = self._convert_input_to_chat_message(request.input) @@ -266,8 +271,19 @@ async def _execute_agent( else: raise ValueError("Agent must implement either run() or run_stream() method") + # Emit agent lifecycle completion event + from .models._openai_custom import AgentCompletedEvent + + yield AgentCompletedEvent() + except Exception as e: logger.error(f"Error in agent execution: {e}") + # Emit agent lifecycle failure event + from .models._openai_custom import AgentFailedEvent + + yield AgentFailedEvent(error=e) + + # Still yield the error for backward compatibility yield {"type": "error", "message": f"Agent execution error: {e!s}"} async def _execute_workflow( @@ -284,14 +300,9 @@ async def _execute_workflow( Workflow events and trace events """ try: - # Get input data - prefer structured data from extra_body - input_data: str | list[Any] | dict[str, Any] - if request.extra_body and isinstance(request.extra_body, dict) and request.extra_body.get("input_data"): - input_data = request.extra_body.get("input_data") # type: ignore - logger.debug(f"Using structured input_data from extra_body: {type(input_data)}") - else: - input_data = request.input - logger.debug(f"Using input field as fallback: {type(input_data)}") + # Get input data directly from request.input field + input_data = request.input + logger.debug(f"Using input field: {type(input_data)}") # Parse input based on workflow's expected input type parsed_input = await self._parse_workflow_input(workflow, input_data) diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py index 488b1be10b..af127e6d3d 100644 --- a/python/packages/devui/agent_framework_devui/_mapper.py +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -4,17 +4,32 @@ import json import logging +import time import uuid from collections import OrderedDict from collections.abc import Sequence from datetime import datetime from typing import Any, Union +from uuid import uuid4 + +from openai.types.responses import ( + Response, + ResponseContentPartAddedEvent, + ResponseCreatedEvent, + ResponseError, + ResponseFailedEvent, + ResponseInProgressEvent, +) from .models import ( AgentFrameworkRequest, + CustomResponseOutputItemAddedEvent, + CustomResponseOutputItemDoneEvent, + ExecutorActionItem, InputTokensDetails, OpenAIResponse, OutputTokensDetails, + ResponseCompletedEvent, ResponseErrorEvent, ResponseFunctionCallArgumentsDeltaEvent, ResponseFunctionResultComplete, @@ -41,6 +56,56 @@ ] +def _serialize_content_recursive(value: Any) -> Any: + """Recursively serialize Agent Framework Content objects to JSON-compatible values. + + This handles nested Content objects (like TextContent inside FunctionResultContent.result) + that can't be directly serialized by json.dumps(). + + Args: + value: Value to serialize (can be Content object, dict, list, primitive, etc.) + + Returns: + JSON-serializable version with all Content objects converted to dicts/primitives + """ + # Handle None and basic JSON-serializable types + if value is None or isinstance(value, (str, int, float, bool)): + return value + + # Check if it's a SerializationMixin (includes all Content types) + # Content objects have to_dict() method + if hasattr(value, "to_dict") and callable(getattr(value, "to_dict", None)): + try: + return value.to_dict() + except Exception as e: + # If to_dict() fails, fall through to other methods + logger.debug(f"Failed to serialize with to_dict(): {e}") + + # Handle dictionaries - recursively process values + if isinstance(value, dict): + return {key: _serialize_content_recursive(val) for key, val in value.items()} + + # Handle lists and tuples - recursively process elements + if isinstance(value, (list, tuple)): + serialized = [_serialize_content_recursive(item) for item in value] + # For single-item lists containing text Content, extract just the text + # This handles the MCP case where result = [TextContent(text="Hello")] + # and we want output = "Hello" not output = '[{"type": "text", "text": "Hello"}]' + if len(serialized) == 1 and isinstance(serialized[0], dict) and serialized[0].get("type") == "text": + return serialized[0].get("text", "") + return serialized + + # For other objects with model_dump(), try that + if hasattr(value, "model_dump") and callable(getattr(value, "model_dump", None)): + try: + return value.model_dump() + except Exception as e: + logger.debug(f"Failed to serialize with model_dump(): {e}") + + # Return as-is and let json.dumps handle it (may raise TypeError for non-serializable types) + return value + + class MessageMapper: """Maps Agent Framework messages/responses to OpenAI format.""" @@ -102,6 +167,12 @@ async def convert_event(self, raw_event: Any, request: AgentFrameworkRequest) -> ) ] + # Handle Agent lifecycle events first + from .models._openai_custom import AgentCompletedEvent, AgentFailedEvent, AgentStartedEvent + + if isinstance(raw_event, (AgentStartedEvent, AgentCompletedEvent, AgentFailedEvent)): + return await self._convert_agent_lifecycle_event(raw_event, context) + # Import Agent Framework types for proper isinstance checks try: from agent_framework import AgentRunResponse, AgentRunResponseUpdate, WorkflowEvent @@ -245,6 +316,7 @@ def _get_or_create_context(self, request: AgentFrameworkRequest) -> dict[str, An "content_index": 0, "output_index": 0, "request_id": str(request_key), # For usage accumulation + "request": request, # Store the request for model name access # Track active function calls: {call_id: {name, item_id, args_chunks}} "active_function_calls": {}, } @@ -267,7 +339,7 @@ def _next_sequence(self, context: dict[str, Any]) -> int: return int(context["sequence_counter"]) async def _convert_agent_update(self, update: Any, context: dict[str, Any]) -> Sequence[Any]: - """Convert AgentRunResponseUpdate to OpenAI events using comprehensive content mapping. + """Convert agent text updates to proper content part events. Args: update: Agent run response update @@ -283,10 +355,60 @@ async def _convert_agent_update(self, update: Any, context: dict[str, Any]) -> S if not hasattr(update, "contents") or not update.contents: return events + # Check if we're streaming text content + has_text_content = any(content.__class__.__name__ == "TextContent" for content in update.contents) + + # If we have text content and haven't created a message yet, create one + if has_text_content and "current_message_id" not in context: + message_id = f"msg_{uuid4().hex[:8]}" + context["current_message_id"] = message_id + context["output_index"] = context.get("output_index", -1) + 1 + + # Add message output item + events.append( + ResponseOutputItemAddedEvent( + type="response.output_item.added", + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + item=ResponseOutputMessage( + type="message", id=message_id, role="assistant", content=[], status="in_progress" + ), + ) + ) + + # Add content part for text + context["content_index"] = 0 + events.append( + ResponseContentPartAddedEvent( + type="response.content_part.added", + output_index=context["output_index"], + content_index=context["content_index"], + item_id=message_id, + sequence_number=self._next_sequence(context), + part=ResponseOutputText(type="output_text", text="", annotations=[]), + ) + ) + + # Process each content item for content in update.contents: content_type = content.__class__.__name__ - if content_type in self.content_mappers: + # Special handling for TextContent to use proper delta events + if content_type == "TextContent" and "current_message_id" in context: + # Stream text content via proper delta events + events.append( + ResponseTextDeltaEvent( + type="response.output_text.delta", + output_index=context["output_index"], + content_index=context.get("content_index", 0), + item_id=context["current_message_id"], + delta=content.text, + logprobs=[], # We don't have logprobs from Agent Framework + sequence_number=self._next_sequence(context), + ) + ) + elif content_type in self.content_mappers: + # Use existing mappers for other content types mapped_events = await self.content_mappers[content_type](content, context) if mapped_events is not None: # Handle None returns (e.g., UsageContent) if isinstance(mapped_events, list): @@ -297,7 +419,9 @@ async def _convert_agent_update(self, update: Any, context: dict[str, Any]) -> S # Graceful fallback for unknown content types events.append(await self._create_unknown_content_event(content, context)) - context["content_index"] += 1 + # Don't increment content_index for text deltas within the same part + if content_type != "TextContent": + context["content_index"] = context.get("content_index", 0) + 1 except Exception as e: logger.warning(f"Error converting agent update: {e}") @@ -358,8 +482,105 @@ async def _convert_agent_response(self, response: Any, context: dict[str, Any]) return events + async def _convert_agent_lifecycle_event(self, event: Any, context: dict[str, Any]) -> Sequence[Any]: + """Convert agent lifecycle events to OpenAI response events. + + Args: + event: AgentStartedEvent, AgentCompletedEvent, or AgentFailedEvent + context: Conversion context + + Returns: + List of OpenAI response stream events + """ + from .models._openai_custom import AgentCompletedEvent, AgentFailedEvent, AgentStartedEvent + + try: + # Get model name from context (the agent name) + model_name = context.get("request", {}).model if context.get("request") else "agent" + + if isinstance(event, AgentStartedEvent): + execution_id = f"agent_{uuid4().hex[:12]}" + context["execution_id"] = execution_id + + # Create Response object + response_obj = Response( + id=f"resp_{execution_id}", + object="response", + created_at=float(time.time()), + model=model_name, + output=[], + status="in_progress", + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + # Emit both created and in_progress events + return [ + ResponseCreatedEvent( + type="response.created", sequence_number=self._next_sequence(context), response=response_obj + ), + ResponseInProgressEvent( + type="response.in_progress", sequence_number=self._next_sequence(context), response=response_obj + ), + ] + + if isinstance(event, AgentCompletedEvent): + execution_id = context.get("execution_id", f"agent_{uuid4().hex[:12]}") + + response_obj = Response( + id=f"resp_{execution_id}", + object="response", + created_at=float(time.time()), + model=model_name, + output=[], + status="completed", + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + return [ + ResponseCompletedEvent( + type="response.completed", sequence_number=self._next_sequence(context), response=response_obj + ) + ] + + if isinstance(event, AgentFailedEvent): + execution_id = context.get("execution_id", f"agent_{uuid4().hex[:12]}") + + # Create error object + response_error = ResponseError( + message=str(event.error) if event.error else "Unknown error", code="server_error" + ) + + response_obj = Response( + id=f"resp_{execution_id}", + object="response", + created_at=float(time.time()), + model=model_name, + output=[], + status="failed", + error=response_error, + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + return [ + ResponseFailedEvent( + type="response.failed", sequence_number=self._next_sequence(context), response=response_obj + ) + ] + + return [] + + except Exception as e: + logger.warning(f"Error converting agent lifecycle event: {e}") + return [await self._create_error_event(str(e), context)] + async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) -> Sequence[Any]: - """Convert workflow event to structured OpenAI events. + """Convert workflow events to standard OpenAI event objects. Args: event: Workflow event @@ -369,22 +590,247 @@ async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) -> List of OpenAI response stream events """ try: + event_class = event.__class__.__name__ + + # Response-level events - construct proper OpenAI objects + if event_class == "WorkflowStartedEvent": + workflow_id = getattr(event, "workflow_id", str(uuid4())) + context["workflow_id"] = workflow_id + + # Import Response type for proper construction + from openai.types.responses import Response + + # Return proper OpenAI event objects + events: list[Any] = [] + + # Determine the model name - use request model or default to "workflow" + # The request model will be the agent name for agents, workflow name for workflows + model_name = context.get("request", {}).model if context.get("request") else "workflow" + + # Create a full Response object with all required fields + response_obj = Response( + id=f"resp_{workflow_id}", + object="response", + created_at=float(time.time()), + model=model_name, # Use the actual model/agent name + output=[], # Empty output list initially + status="in_progress", + # Required fields with safe defaults + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + # First emit response.created + events.append( + ResponseCreatedEvent( + type="response.created", sequence_number=self._next_sequence(context), response=response_obj + ) + ) + + # Then emit response.in_progress (reuse same response object) + events.append( + ResponseInProgressEvent( + type="response.in_progress", sequence_number=self._next_sequence(context), response=response_obj + ) + ) + + return events + + if event_class in ["WorkflowCompletedEvent", "WorkflowOutputEvent"]: + workflow_id = context.get("workflow_id", str(uuid4())) + + # Import Response type for proper construction + from openai.types.responses import Response + + # Get model name from context + model_name = context.get("request", {}).model if context.get("request") else "workflow" + + # Create a full Response object for completed state + response_obj = Response( + id=f"resp_{workflow_id}", + object="response", + created_at=float(time.time()), + model=model_name, + output=[], # Output should be populated by this point from text streaming + status="completed", + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + return [ + ResponseCompletedEvent( + type="response.completed", sequence_number=self._next_sequence(context), response=response_obj + ) + ] + + if event_class == "WorkflowFailedEvent": + workflow_id = context.get("workflow_id", str(uuid4())) + error_info = getattr(event, "error", None) + + # Import Response and ResponseError types + from openai.types.responses import Response, ResponseError + + # Get model name from context + model_name = context.get("request", {}).model if context.get("request") else "workflow" + + # Create error object + error_message = str(error_info) if error_info else "Unknown error" + + # Create ResponseError object (code must be one of the allowed values) + response_error = ResponseError( + message=error_message, + code="server_error", # Use generic server_error code for workflow failures + ) + + # Create a full Response object for failed state + response_obj = Response( + id=f"resp_{workflow_id}", + object="response", + created_at=float(time.time()), + model=model_name, + output=[], + status="failed", + error=response_error, + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + return [ + ResponseFailedEvent( + type="response.failed", sequence_number=self._next_sequence(context), response=response_obj + ) + ] + + # Executor-level events (output items) + if event_class == "ExecutorInvokedEvent": + executor_id = getattr(event, "executor_id", "unknown") + item_id = f"exec_{executor_id}_{uuid4().hex[:8]}" + context[f"exec_item_{executor_id}"] = item_id + context["output_index"] = context.get("output_index", -1) + 1 + + # Create ExecutorActionItem with proper type + executor_item = ExecutorActionItem( + type="executor_action", + id=item_id, + executor_id=executor_id, + status="in_progress", + metadata=getattr(event, "metadata", {}), + ) + + # Use our custom event type that accepts ExecutorActionItem + return [ + CustomResponseOutputItemAddedEvent( + type="response.output_item.added", + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + item=executor_item, + ) + ] + + if event_class == "ExecutorCompletedEvent": + executor_id = getattr(event, "executor_id", "unknown") + item_id = context.get(f"exec_item_{executor_id}", f"exec_{executor_id}_unknown") + + # Create ExecutorActionItem with completed status + # ExecutorCompletedEvent uses 'data' field, not 'result' + executor_item = ExecutorActionItem( + type="executor_action", + id=item_id, + executor_id=executor_id, + status="completed", + result=getattr(event, "data", None), + ) + + # Use our custom event type + return [ + CustomResponseOutputItemDoneEvent( + type="response.output_item.done", + output_index=context.get("output_index", 0), + sequence_number=self._next_sequence(context), + item=executor_item, + ) + ] + + if event_class == "ExecutorFailedEvent": + executor_id = getattr(event, "executor_id", "unknown") + item_id = context.get(f"exec_item_{executor_id}", f"exec_{executor_id}_unknown") + error_info = getattr(event, "error", None) + + # Create ExecutorActionItem with failed status + executor_item = ExecutorActionItem( + type="executor_action", + id=item_id, + executor_id=executor_id, + status="failed", + error={"message": str(error_info)} if error_info else None, + ) + + # Use our custom event type + return [ + CustomResponseOutputItemDoneEvent( + type="response.output_item.done", + output_index=context.get("output_index", 0), + sequence_number=self._next_sequence(context), + item=executor_item, + ) + ] + + # Handle informational workflow events (status, warnings, errors) + if event_class in ["WorkflowStatusEvent", "WorkflowWarningEvent", "WorkflowErrorEvent", "RequestInfoEvent"]: + # These are informational events that don't map to OpenAI lifecycle events + # Convert them to trace events for debugging visibility + event_data: dict[str, Any] = {} + + # Extract relevant data based on event type + if event_class == "WorkflowStatusEvent": + event_data["state"] = str(getattr(event, "state", "unknown")) + elif event_class == "WorkflowWarningEvent": + event_data["message"] = str(getattr(event, "message", "")) + elif event_class == "WorkflowErrorEvent": + event_data["message"] = str(getattr(event, "message", "")) + event_data["error"] = str(getattr(event, "error", "")) + elif event_class == "RequestInfoEvent": + request_info = getattr(event, "data", {}) + event_data["request_info"] = request_info if isinstance(request_info, dict) else str(request_info) + + # Create a trace event for debugging + trace_event = ResponseTraceEventComplete( + type="response.trace.complete", + data={ + "trace_type": "workflow_info", + "event_type": event_class, + "data": event_data, + "timestamp": datetime.now().isoformat(), + }, + span_id=f"workflow_info_{uuid4().hex[:8]}", + item_id=context["item_id"], + output_index=context.get("output_index", 0), + sequence_number=self._next_sequence(context), + ) + + return [trace_event] + + # For unknown/legacy events, still emit as workflow event for backward compatibility # Get event data and serialize if it's a SerializationMixin - event_data = getattr(event, "data", None) - if event_data is not None and hasattr(event_data, "to_dict"): + raw_event_data = getattr(event, "data", None) + serialized_event_data: dict[str, Any] | str | None = raw_event_data + if raw_event_data is not None and hasattr(raw_event_data, "to_dict"): # SerializationMixin objects - convert to dict for JSON serialization try: - event_data = event_data.to_dict() + serialized_event_data = raw_event_data.to_dict() except Exception as e: logger.debug(f"Failed to serialize event data with to_dict(): {e}") - event_data = str(event_data) + serialized_event_data = str(raw_event_data) - # Create structured workflow event + # Create structured workflow event (keeping for backward compatibility) workflow_event = ResponseWorkflowEventComplete( type="response.workflow_event.complete", data={ "event_type": event.__class__.__name__, - "data": event_data, + "data": serialized_event_data, "executor_id": getattr(event, "executor_id", None), "timestamp": datetime.now().isoformat(), }, @@ -394,6 +840,7 @@ async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) -> sequence_number=self._next_sequence(context), ) + logger.debug(f"Unhandled workflow event type: {event_class}, emitting as legacy workflow event") return [workflow_event] except Exception as e: @@ -538,8 +985,16 @@ async def _map_function_result_content( result = getattr(content, "result", None) exception = getattr(content, "exception", None) - # Convert result to string - output = result if isinstance(result, str) else json.dumps(result) if result is not None else "" + # Convert result to string, handling nested Content objects from MCP tools + if isinstance(result, str): + output = result + elif result is not None: + # Recursively serialize any nested Content objects (e.g., from MCP tools) + serialized = _serialize_content_recursive(result) + # Convert to JSON string if still not a string + output = serialized if isinstance(serialized, str) else json.dumps(serialized) + else: + output = "" # Determine status based on exception status = "incomplete" if exception else "completed" @@ -556,6 +1011,7 @@ async def _map_function_result_content( item_id=item_id, output_index=context["output_index"], sequence_number=self._next_sequence(context), + timestamp=datetime.now().isoformat(), ) async def _map_error_content(self, content: Any, context: dict[str, Any]) -> ResponseErrorEvent: @@ -723,7 +1179,7 @@ async def _create_unknown_event(self, event_data: Any, context: dict[str, Any]) async def _create_unknown_content_event(self, content: Any, context: dict[str, Any]) -> ResponseStreamEvent: """Create event for unknown content types.""" content_type = content.__class__.__name__ - text = f"⚠️ Unknown content type: {content_type}\n" + text = f"Warning: Unknown content type: {content_type}\n" return self._create_text_delta_event(text, context) async def _create_error_response(self, error_message: str, request: AgentFrameworkRequest) -> OpenAIResponse: diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index e6fd871ca2..3e3c538098 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -85,19 +85,25 @@ async def _ensure_executor(self) -> AgentFrameworkExecutor: return self.executor async def _cleanup_entities(self) -> None: - """Cleanup entity resources (close clients, credentials, etc.).""" + """Cleanup entity resources (close clients, MCP tools, credentials, etc.).""" if not self.executor: return logger.info("Cleaning up entity resources...") entities = self.executor.entity_discovery.list_entities() closed_count = 0 + mcp_tools_closed = 0 + credentials_closed = 0 for entity_info in entities: try: entity_obj = self.executor.entity_discovery.get_entity_object(entity_info.id) + + # Close chat clients and their credentials if entity_obj and hasattr(entity_obj, "chat_client"): client = entity_obj.chat_client + + # Close the chat client itself if hasattr(client, "close") and callable(client.close): if inspect.iscoroutinefunction(client.close): await client.close() @@ -105,11 +111,47 @@ async def _cleanup_entities(self) -> None: client.close() closed_count += 1 logger.debug(f"Closed client for entity: {entity_info.id}") + + # Close credentials attached to chat clients (e.g., AzureCliCredential) + credential_attrs = ["credential", "async_credential", "_credential", "_async_credential"] + for attr in credential_attrs: + if hasattr(client, attr): + cred = getattr(client, attr) + if cred and hasattr(cred, "close") and callable(cred.close): + try: + if inspect.iscoroutinefunction(cred.close): + await cred.close() + else: + cred.close() + credentials_closed += 1 + logger.debug(f"Closed credential for entity: {entity_info.id}") + except Exception as e: + logger.warning(f"Error closing credential for {entity_info.id}: {e}") + + # Close MCP tools (framework tracks them in _local_mcp_tools) + if entity_obj and hasattr(entity_obj, "_local_mcp_tools"): + for mcp_tool in entity_obj._local_mcp_tools: + if hasattr(mcp_tool, "close") and callable(mcp_tool.close): + try: + if inspect.iscoroutinefunction(mcp_tool.close): + await mcp_tool.close() + else: + mcp_tool.close() + mcp_tools_closed += 1 + tool_name = getattr(mcp_tool, "name", "unknown") + logger.debug(f"Closed MCP tool '{tool_name}' for entity: {entity_info.id}") + except Exception as e: + logger.warning(f"Error closing MCP tool for {entity_info.id}: {e}") + except Exception as e: logger.warning(f"Error closing entity {entity_info.id}: {e}") if closed_count > 0: logger.info(f"Closed {closed_count} entity client(s)") + if credentials_closed > 0: + logger.info(f"Closed {credentials_closed} credential(s)") + if mcp_tools_closed > 0: + logger.info(f"Closed {mcp_tools_closed} MCP tool(s)") def create_app(self) -> FastAPI: """Create the FastAPI application.""" diff --git a/python/packages/devui/agent_framework_devui/models/__init__.py b/python/packages/devui/agent_framework_devui/models/__init__.py index 3db699beff..254bb4e4af 100644 --- a/python/packages/devui/agent_framework_devui/models/__init__.py +++ b/python/packages/devui/agent_framework_devui/models/__init__.py @@ -30,6 +30,9 @@ from ._discovery_models import DiscoveryResponse, EntityInfo from ._openai_custom import ( AgentFrameworkRequest, + CustomResponseOutputItemAddedEvent, + CustomResponseOutputItemDoneEvent, + ExecutorActionItem, OpenAIError, ResponseFunctionResultComplete, ResponseTraceEvent, @@ -46,8 +49,11 @@ "Conversation", "ConversationDeletedResource", "ConversationItem", + "CustomResponseOutputItemAddedEvent", + "CustomResponseOutputItemDoneEvent", "DiscoveryResponse", "EntityInfo", + "ExecutorActionItem", "InputTokensDetails", "Metadata", "OpenAIError", diff --git a/python/packages/devui/agent_framework_devui/models/_openai_custom.py b/python/packages/devui/agent_framework_devui/models/_openai_custom.py index f07f9c7b9c..d4506c7b4c 100644 --- a/python/packages/devui/agent_framework_devui/models/_openai_custom.py +++ b/python/packages/devui/agent_framework_devui/models/_openai_custom.py @@ -8,6 +8,7 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any, Literal from pydantic import BaseModel, ConfigDict @@ -15,6 +16,69 @@ # Custom Agent Framework OpenAI event types for structured data +# Agent lifecycle events - simple and clear +class AgentStartedEvent: + """Event emitted when an agent starts execution.""" + + pass + + +class AgentCompletedEvent: + """Event emitted when an agent completes execution successfully.""" + + pass + + +@dataclass +class AgentFailedEvent: + """Event emitted when an agent fails during execution.""" + + error: Exception | None = None + + +class ExecutorActionItem(BaseModel): + """Custom item type for workflow executor actions. + + This is a DevUI-specific extension to represent workflow executors as output items. + Since OpenAI's ResponseOutputItemAddedEvent only accepts specific item types, + and executor actions are not part of the standard, we need this custom type. + """ + + type: Literal["executor_action"] = "executor_action" + id: str + executor_id: str + status: Literal["in_progress", "completed", "failed", "cancelled"] = "in_progress" + metadata: dict[str, Any] | None = None + result: Any | None = None + error: dict[str, Any] | None = None + + +class CustomResponseOutputItemAddedEvent(BaseModel): + """Custom version of ResponseOutputItemAddedEvent that accepts any item type. + + This allows us to emit executor action items while maintaining the same + event structure as OpenAI's standard. + """ + + type: Literal["response.output_item.added"] = "response.output_item.added" + output_index: int + sequence_number: int + item: dict[str, Any] | ExecutorActionItem | Any # Flexible item type + + +class CustomResponseOutputItemDoneEvent(BaseModel): + """Custom version of ResponseOutputItemDoneEvent that accepts any item type. + + This allows us to emit executor action items while maintaining the same + event structure as OpenAI's standard. + """ + + type: Literal["response.output_item.done"] = "response.output_item.done" + output_index: int + sequence_number: int + item: dict[str, Any] | ExecutorActionItem | Any # Flexible item type + + class ResponseWorkflowEventComplete(BaseModel): """Complete workflow event data.""" @@ -57,6 +121,7 @@ class ResponseFunctionResultComplete(BaseModel): item_id: str output_index: int = 0 sequence_number: int + timestamp: str | None = None # Optional timestamp for UI display # Agent Framework extension fields @@ -64,7 +129,7 @@ class AgentFrameworkExtraBody(BaseModel): """Agent Framework specific routing fields for OpenAI requests.""" entity_id: str - input_data: dict[str, Any] | None = None + # input_data removed - now using standard input field for all data model_config = ConfigDict(extra="allow") @@ -80,7 +145,7 @@ class AgentFrameworkRequest(BaseModel): # All OpenAI fields from ResponseCreateParams model: str # Used as entity_id in DevUI! - input: str | list[Any] # ResponseInputParam + input: str | list[Any] | dict[str, Any] # ResponseInputParam + dict for workflow structured input stream: bool | None = False # OpenAI conversation parameter (standard!) 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 new file mode 100644 index 0000000000..85924eb971 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/ui/assets/index-D_Y1oSGu.js @@ -0,0 +1,577 @@ +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/frontend/src/App.tsx b/python/packages/devui/frontend/src/App.tsx index caddc4ffe4..f8473e9c60 100644 --- a/python/packages/devui/frontend/src/App.tsx +++ b/python/packages/devui/frontend/src/App.tsx @@ -241,6 +241,8 @@ export default function App() { // Show error state if loading failed if (entityError) { + const currentBackendUrl = apiClient.getBaseUrl(); + return (
Default:{" "} - http://localhost:8080 + {currentBackendUrl}

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 eb45753665..43cf1ac509 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 @@ -408,46 +408,82 @@ export function WorkflowView({ // This preserves the workflow's final output for display }; - // Extract workflow events from OpenAI events for executor tracking + // Extract workflow and output item events from OpenAI events for executor tracking const workflowEvents = useMemo(() => { return openAIEvents.filter( - (event) => event.type === "response.workflow_event.complete" + (event) => + event.type === "response.output_item.added" || + event.type === "response.output_item.done" || + event.type === "response.created" || + event.type === "response.in_progress" || + event.type === "response.completed" || + event.type === "response.failed" || + // Keep legacy support for older backends + event.type === "response.workflow_event.complete" ); }, [openAIEvents]); // Extract executor history from workflow events (filter out workflow-level events) const executorHistory = useMemo(() => { - return workflowEvents - .filter((event) => { - if ("data" in event && event.data && typeof event.data === "object") { - const data = event.data as Record; - // Filter out workflow-level events (those without executor_id) - // These include: WorkflowStartedEvent, WorkflowOutputEvent, WorkflowStatusEvent, etc. - return data.executor_id != null; + const history: Array<{ + executorId: string; + message: string; + timestamp: string; + status: "running" | "completed" | "error"; + }> = []; + + workflowEvents.forEach((event) => { + // Handle new standard OpenAI events + if ( + event.type === "response.output_item.added" || + event.type === "response.output_item.done" + ) { + const item = (event as any).item; + if (item && item.type === "executor_action" && item.executor_id) { + history.push({ + executorId: item.executor_id, + message: + event.type === "response.output_item.added" + ? "Executor started" + : item.status === "completed" + ? "Executor completed" + : item.status === "failed" + ? "Executor failed" + : "Executor processing", + timestamp: new Date().toISOString(), + status: + item.status === "completed" + ? "completed" + : item.status === "failed" + ? "error" + : "running", + }); } - return false; - }) - .map((event) => { - if ("data" in event && event.data && typeof event.data === "object") { - const data = event.data as Record; - return { + } + // Legacy support for older backends + else if ( + event.type === "response.workflow_event.complete" && + "data" in event && + event.data && + typeof event.data === "object" + ) { + const data = event.data as Record; + if (data.executor_id != null) { + history.push({ executorId: String(data.executor_id), message: String(data.event_type || "Processing"), timestamp: String(data.timestamp || new Date().toISOString()), status: String(data.event_type || "").includes("Completed") - ? ("completed" as const) + ? "completed" : String(data.event_type || "").includes("Error") - ? ("error" as const) - : ("running" as const), - }; + ? "error" + : "running", + }); } - return { - executorId: "unknown", - message: "Processing", - timestamp: new Date().toISOString(), - status: "running" as const, - }; - }); + } + }); + + return history; }, [workflowEvents]); // Track active executors @@ -525,16 +561,51 @@ export function WorkflowView({ ); for await (const openAIEvent of streamGenerator) { - // Only store workflow events in state for performance - // Text deltas are processed directly without state updates - if (openAIEvent.type === "response.workflow_event.complete") { + // Store workflow-related events for tracking + if ( + openAIEvent.type === "response.output_item.added" || + openAIEvent.type === "response.output_item.done" || + openAIEvent.type === "response.created" || + openAIEvent.type === "response.in_progress" || + openAIEvent.type === "response.completed" || + openAIEvent.type === "response.failed" || + openAIEvent.type === "response.workflow_event.complete" // Legacy + ) { setOpenAIEvents((prev) => [...prev, openAIEvent]); } // Pass to debug panel onDebugEvent(openAIEvent); - // Handle workflow events to track current executor + // Handle new standard OpenAI events + if (openAIEvent.type === "response.output_item.added") { + const item = (openAIEvent as any).item; + if (item && item.type === "executor_action" && item.executor_id) { + currentStreamingExecutor.current = item.executor_id; + // Initialize output for this executor if not exists + if (!executorOutputs.current[item.executor_id]) { + executorOutputs.current[item.executor_id] = ""; + } + } + } + + // Handle workflow completion + if (openAIEvent.type === "response.completed") { + // Workflow completed successfully + // Final output is already in workflowResult from text streaming + } + + // Handle workflow failure + if (openAIEvent.type === "response.failed") { + const error = (openAIEvent as any).response?.error; + if (error) { + setWorkflowError( + typeof error === "string" ? error : JSON.stringify(error) + ); + } + } + + // Legacy support for older backends if ( openAIEvent.type === "response.workflow_event.complete" && "data" in openAIEvent && diff --git a/python/packages/devui/frontend/src/components/layout/debug-panel.tsx b/python/packages/devui/frontend/src/components/layout/debug-panel.tsx index 64866ae728..b96fb185e6 100644 --- a/python/packages/devui/frontend/src/components/layout/debug-panel.tsx +++ b/python/packages/devui/frontend/src/components/layout/debug-panel.tsx @@ -116,6 +116,39 @@ function getFunctionResultFromEvent(event: ExtendedResponseStreamEvent): { return null; } +// Helper to get a stable timestamp for an event +// Uses event's own timestamp fields if available +function getEventTimestamp(event: ExtendedResponseStreamEvent): string { + // Priority 1: Check for top-level timestamp (DevUI custom events like function_result.complete) + if ('timestamp' in event && typeof event.timestamp === 'string') { + return new Date(event.timestamp).toLocaleTimeString(); + } + + // Priority 2: Check for nested data.timestamp (workflow/trace events) + if ('data' in event && event.data && typeof event.data === 'object' && 'timestamp' in event.data) { + const dataTimestamp = (event.data as any).timestamp; + if (typeof dataTimestamp === 'string') { + return new Date(dataTimestamp).toLocaleTimeString(); + } + } + + // Priority 3: Check for created_at in response object (lifecycle events) + if ('response' in event && event.response && typeof event.response === 'object' && 'created_at' in event.response) { + const createdAt = (event.response as any).created_at; + if (typeof createdAt === 'number') { + return new Date(createdAt * 1000).toLocaleTimeString(); + } + } + + // Fallback: use sequence number as label (better than showing same time for all) + if ('sequence_number' in event && typeof event.sequence_number === 'number') { + return `#${event.sequence_number}`; + } + + // Last resort: hide timestamp by returning empty string + return ''; +} + // Helper function to accumulate OpenAI events into meaningful units function processEventsForDisplay( events: ExtendedResponseStreamEvent[] @@ -551,7 +584,7 @@ function EventItem({ event }: EventItemProps) { const [isExpanded, setIsExpanded] = useState(false); const Icon = getEventIcon(event.type); const colorClass = getEventColor(event.type); - const timestamp = new Date().toLocaleTimeString(); + const timestamp = getEventTimestamp(event); const summary = getEventSummary(event); // Determine if this event has expandable content @@ -1487,7 +1520,7 @@ function ToolsTab({ events }: { events: ExtendedResponseStreamEvent[] }) { } function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) { - const timestamp = new Date().toLocaleTimeString(); + const timestamp = getEventTimestamp(event); // Check if this is a function call or result event const isFunctionCall = event.type === "response.function_call.complete"; diff --git a/python/packages/devui/frontend/src/components/ui/markdown-renderer.tsx b/python/packages/devui/frontend/src/components/ui/markdown-renderer.tsx index 952dbc1aae..375f3a010c 100644 --- a/python/packages/devui/frontend/src/components/ui/markdown-renderer.tsx +++ b/python/packages/devui/frontend/src/components/ui/markdown-renderer.tsx @@ -18,7 +18,7 @@ * - Horizontal rules (---) */ -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; interface MarkdownRendererProps { content: string; @@ -35,10 +35,10 @@ interface CodeBlockProps { */ function CodeBlock({ code, language }: CodeBlockProps) { const [copied, setCopied] = useState(false); - const timeoutRef = React.useRef(null); + const timeoutRef = useRef(null); // Cleanup timeout on unmount - React.useEffect(() => { + useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); diff --git a/python/packages/devui/frontend/src/services/api.ts b/python/packages/devui/frontend/src/services/api.ts index c94920222e..8a685cf5aa 100644 --- a/python/packages/devui/frontend/src/services/api.ts +++ b/python/packages/devui/frontend/src/services/api.ts @@ -397,9 +397,7 @@ class ApiClient { // Convert to OpenAI format - use model field for entity_id (same as agents) const openAIRequest: AgentFrameworkRequest = { model: workflowId, // Use workflow ID in model field (matches agent pattern) - input: typeof request.input_data === 'string' - ? request.input_data - : JSON.stringify(request.input_data || ""), // Convert input_data to string + input: request.input_data || "", // Send dict directly, no stringification needed stream: true, conversation: request.conversation_id, // Include conversation if present }; diff --git a/python/packages/devui/frontend/src/types/agent-framework.ts b/python/packages/devui/frontend/src/types/agent-framework.ts index 95cd0f261c..c90c8beb91 100644 --- a/python/packages/devui/frontend/src/types/agent-framework.ts +++ b/python/packages/devui/frontend/src/types/agent-framework.ts @@ -68,13 +68,13 @@ export type ResponseInputParam = ResponseInputItem[]; // Agent Framework extension fields (matches backend AgentFrameworkExtraBody) export interface AgentFrameworkExtraBody { entity_id: string; - input_data?: Record; + // input_data removed - now using standard input field for all data } // Agent Framework Request - OpenAI ResponseCreateParams with extensions export interface AgentFrameworkRequest { model: string; - input: string | ResponseInputParam; // Union type matching OpenAI + input: string | ResponseInputParam | Record; // Union type matching OpenAI + dict for workflows stream?: boolean; // OpenAI conversation parameter (standard!) diff --git a/python/packages/devui/frontend/src/types/index.ts b/python/packages/devui/frontend/src/types/index.ts index 6d1a494cf5..6435a91654 100644 --- a/python/packages/devui/frontend/src/types/index.ts +++ b/python/packages/devui/frontend/src/types/index.ts @@ -104,11 +104,19 @@ export type { ResponseWorkflowEventComplete, ResponseTraceEventComplete, ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseCreatedEvent, + ResponseInProgressEvent, ResponseCompletedEvent, + ResponseFailedEvent, ResponseFunctionResultComplete, StructuredEvent, + WorkflowItem, + ExecutorActionItem, } from "./openai"; +export { isExecutorAction } from "./openai"; + // Re-export Agent Framework types export type { AgentFrameworkRequest, diff --git a/python/packages/devui/frontend/src/types/openai.ts b/python/packages/devui/frontend/src/types/openai.ts index 9842e37bcc..e4df1a1bf8 100644 --- a/python/packages/devui/frontend/src/types/openai.ts +++ b/python/packages/devui/frontend/src/types/openai.ts @@ -21,6 +21,48 @@ export interface ResponseStreamEvent { created_at?: number; } +// Standard OpenAI Response Lifecycle Events +export interface ResponseCreatedEvent { + type: "response.created"; + response: { + id: string; + status: "in_progress"; + created_at: number; + output?: any[]; + }; + sequence_number?: number; +} + +export interface ResponseInProgressEvent { + type: "response.in_progress"; + response: { + id: string; + status: "in_progress"; + }; + sequence_number?: number; +} + +export interface ResponseCompletedEvent { + type: "response.completed"; + response: { + id: string; + status: "completed"; + usage?: any; // Optional usage information + model?: string; // Optional model information + }; + sequence_number?: number; +} + +export interface ResponseFailedEvent { + type: "response.failed"; + response: { + id: string; + status: "failed"; + error?: any; + }; + sequence_number?: number; +} + // Custom Agent Framework OpenAI event types with structured data export interface ResponseWorkflowEventComplete { type: "response.workflow_event.complete"; @@ -83,13 +125,41 @@ export interface ResponseFunctionToolCall { status?: "in_progress" | "completed" | "incomplete"; } -// OpenAI Responses API - Output Item Added Event -// OpenAI standard: Output item added event +// Workflow Item Types - flexible interface for any workflow item +export interface WorkflowItem { + type: string; // "executor_action", "workflow_action", "message", or any future type + id: string; + status?: "in_progress" | "completed" | "failed" | "cancelled"; + [key: string]: any; // Allow any additional fields +} + +// Executor Action Item (DevUI specific) +export interface ExecutorActionItem extends WorkflowItem { + type: "executor_action"; + executor_id: string; + metadata?: Record; + result?: any; + error?: any; +} + +// Type guard for executor actions +export function isExecutorAction(item: WorkflowItem): item is ExecutorActionItem { + return item.type === "executor_action" && "executor_id" in item; +} + +// OpenAI Responses API - Output Item Events export interface ResponseOutputItemAddedEvent { type: "response.output_item.added"; - item: ResponseFunctionToolCall; + item: WorkflowItem | ResponseFunctionToolCall | any; // Flexible to support various item types output_index: number; - sequence_number: number; + sequence_number?: number; +} + +export interface ResponseOutputItemDoneEvent { + type: "response.output_item.done"; + item: WorkflowItem | ResponseFunctionToolCall | any; + output_index: number; + sequence_number?: number; } // Trace event - matching actual backend output @@ -171,6 +241,7 @@ export interface ResponseFunctionResultComplete { item_id: string; output_index: number; sequence_number: number; + timestamp?: string; // Optional ISO timestamp for UI display } // DevUI Extension: Turn Separator (UI-only event for grouping) @@ -182,11 +253,15 @@ export interface TurnSeparatorEvent { // Union type for all structured events export type StructuredEvent = + | ResponseCreatedEvent + | ResponseInProgressEvent | ResponseCompletedEvent + | ResponseFailedEvent | ResponseWorkflowEventComplete | ResponseTraceEventComplete | ResponseTraceComplete | ResponseOutputItemAddedEvent + | ResponseOutputItemDoneEvent | ResponseFunctionCallComplete | ResponseFunctionCallDelta | ResponseFunctionCallArgumentsDelta @@ -249,12 +324,6 @@ export interface ResponseUsage { }; } -// OpenAI standard: response.completed event -export interface ResponseCompletedEvent { - type: "response.completed"; - response: OpenAIResponse; - sequence_number: number; -} // Request format for Agent Framework // AgentFrameworkRequest moved to agent-framework.ts to avoid conflicts diff --git a/python/packages/devui/frontend/src/utils/workflow-utils.ts b/python/packages/devui/frontend/src/utils/workflow-utils.ts index 0663b6a376..5e7c01bfb1 100644 --- a/python/packages/devui/frontend/src/utils/workflow-utils.ts +++ b/python/packages/devui/frontend/src/utils/workflow-utils.ts @@ -307,6 +307,7 @@ export function applyDagreLayout( /** * Process workflow events and extract node updates + * Handles both new standard OpenAI events and legacy workflow events */ export function processWorkflowEvents( events: ExtendedResponseStreamEvent[], @@ -316,7 +317,43 @@ export function processWorkflowEvents( let hasWorkflowStarted = false; events.forEach((event) => { - if ( + // Handle new standard OpenAI events + if (event.type === "response.output_item.added" || event.type === "response.output_item.done") { + const item = (event as any).item; + if (item && item.type === "executor_action" && item.executor_id) { + const executorId = item.executor_id; + + let state: ExecutorState = "pending"; + let error: string | undefined; + + if (event.type === "response.output_item.added") { + state = "running"; + } else if (event.type === "response.output_item.done") { + if (item.status === "completed") { + state = "completed"; + } else if (item.status === "failed") { + state = "failed"; + error = item.error ? (typeof item.error === "string" ? item.error : JSON.stringify(item.error)) : "Execution failed"; + } else if (item.status === "cancelled") { + state = "cancelled"; + } + } + + nodeUpdates[executorId] = { + nodeId: executorId, + state, + data: item.result, + error, + timestamp: new Date().toISOString(), + }; + } + } + // Handle workflow lifecycle events + else if (event.type === "response.created" || event.type === "response.in_progress") { + hasWorkflowStarted = true; + } + // Legacy support for older backends + else if ( event.type === "response.workflow_event.complete" && "data" in event && event.data @@ -417,7 +454,20 @@ export function getCurrentlyExecutingExecutors( // Process events to find the most recent event for each executor events.forEach((event) => { - if ( + // Handle new standard OpenAI events + if (event.type === "response.output_item.added" || event.type === "response.output_item.done") { + const item = (event as any).item; + if (item && item.type === "executor_action" && item.executor_id) { + const executorId = item.executor_id; + + executorTimeline[executorId] = { + lastEvent: event.type === "response.output_item.added" ? "ExecutorInvokedEvent" : "ExecutorCompletedEvent", + timestamp: new Date().toISOString(), + }; + } + } + // Legacy support for older backends + else if ( event.type === "response.workflow_event.complete" && "data" in event && event.data diff --git a/python/packages/devui/tests/test_mapper.py b/python/packages/devui/tests/test_mapper.py index 7eb57a8c99..dfa8ac9d6d 100644 --- a/python/packages/devui/tests/test_mapper.py +++ b/python/packages/devui/tests/test_mapper.py @@ -13,7 +13,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "main")) # Import Agent Framework types (assuming they are always available) -from agent_framework._types import AgentRunResponseUpdate, ErrorContent, FunctionCallContent, Role, TextContent +from agent_framework._types import ( + AgentRunResponseUpdate, + ErrorContent, + FunctionCallContent, + FunctionResultContent, + Role, + TextContent, +) from agent_framework_devui._mapper import MessageMapper from agent_framework_devui.models._openai_custom import AgentFrameworkRequest @@ -79,15 +86,30 @@ async def test_critical_isinstance_bug_detection(mapper: MessageMapper, test_req async def test_text_content_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: - """Test TextContent mapping.""" + """Test TextContent mapping with proper OpenAI event hierarchy.""" content = create_test_content("text", text="Hello, clean test!") update = create_test_agent_update([content]) events = await mapper.convert_event(update, test_request) - assert len(events) == 1 - assert events[0].type == "response.output_text.delta" - assert events[0].delta == "Hello, clean test!" + # With proper OpenAI hierarchy, we expect 3 events: + # 1. response.output_item.added (message) + # 2. response.content_part.added (text part) + # 3. response.output_text.delta (actual text) + assert len(events) == 3 + + # Check message output item + assert events[0].type == "response.output_item.added" + assert events[0].item.type == "message" + assert events[0].item.role == "assistant" + + # Check content part + assert events[1].type == "response.content_part.added" + assert events[1].part.type == "output_text" + + # Check text delta + assert events[2].type == "response.output_text.delta" + assert events[2].delta == "Hello, clean test!" async def test_function_call_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: @@ -108,6 +130,83 @@ async def test_function_call_mapping(mapper: MessageMapper, test_request: AgentF assert "TestCity" in full_json +async def test_function_result_content_with_string_result( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """Test FunctionResultContent with plain string result (regular tools).""" + content = FunctionResultContent( + call_id="test_call_123", + result="Hello, World!", # Plain string like regular Python function tools + ) + update = create_test_agent_update([content]) + + events = await mapper.convert_event(update, test_request) + + # Should produce response.function_result.complete event + assert len(events) >= 1 + result_events = [e for e in events if e.type == "response.function_result.complete"] + assert len(result_events) == 1 + assert result_events[0].output == "Hello, World!" + assert result_events[0].call_id == "test_call_123" + assert result_events[0].status == "completed" + + +async def test_function_result_content_with_nested_content_objects( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """Test FunctionResultContent with nested Content objects (MCP tools case). + + This tests the issue from GitHub #1476 where MCP tools return FunctionResultContent + with nested TextContent objects that fail to serialize properly. + """ + # This is what MCP tools return - result contains nested Content objects + content = FunctionResultContent( + call_id="mcp_call_456", + result=[TextContent(text="Hello from MCP!")], # List containing TextContent object + ) + update = create_test_agent_update([content]) + + events = await mapper.convert_event(update, test_request) + + # Should successfully serialize the nested Content object + assert len(events) >= 1 + result_events = [e for e in events if e.type == "response.function_result.complete"] + assert len(result_events) == 1 + + # The output should contain the text from the nested TextContent + # Should not have TypeError or empty output + assert result_events[0].output != "" + assert "Hello from MCP!" in result_events[0].output + assert result_events[0].call_id == "mcp_call_456" + + +async def test_function_result_content_with_multiple_nested_content_objects( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """Test FunctionResultContent with multiple nested Content objects.""" + # MCP tools can return multiple Content objects + content = FunctionResultContent( + call_id="mcp_call_789", + result=[ + TextContent(text="First result"), + TextContent(text="Second result"), + ], + ) + update = create_test_agent_update([content]) + + events = await mapper.convert_event(update, test_request) + + assert len(events) >= 1 + result_events = [e for e in events if e.type == "response.function_result.complete"] + assert len(result_events) == 1 + + # Should serialize all nested Content objects + output = result_events[0].output + assert output != "" + assert "First result" in output + assert "Second result" in output + + async def test_error_content_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: """Test ErrorContent mapping.""" content = create_test_content("error", message="Test error", code="test_code") @@ -182,6 +281,140 @@ async def test_agent_run_response_mapping(mapper: MessageMapper, test_request: A assert text_events[0].delta == "Complete response from run()" +async def test_agent_lifecycle_events(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: + """Test that agent lifecycle events are properly converted to OpenAI format.""" + from agent_framework_devui.models._openai_custom import AgentCompletedEvent, AgentFailedEvent, AgentStartedEvent + + # Test AgentStartedEvent + start_event = AgentStartedEvent() + events = await mapper.convert_event(start_event, test_request) + + assert len(events) == 2 # Should emit response.created and response.in_progress + assert events[0].type == "response.created" + assert events[1].type == "response.in_progress" + assert events[0].response.model == "test_agent" # Should use model from request + assert events[0].response.status == "in_progress" + + # Test AgentCompletedEvent + complete_event = AgentCompletedEvent() + events = await mapper.convert_event(complete_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.completed" + assert events[0].response.status == "completed" + + # Test AgentFailedEvent + error = Exception("Test error") + failed_event = AgentFailedEvent(error=error) + events = await mapper.convert_event(failed_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.failed" + assert events[0].response.status == "failed" + assert events[0].response.error.message == "Test error" + assert events[0].response.error.code == "server_error" + + +@pytest.mark.skip(reason="Workflow events need real classes from agent_framework.workflows") +async def test_workflow_lifecycle_events(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: + """Test that workflow lifecycle events are properly converted to OpenAI format.""" + + # Create mock workflow events (since we don't have access to the real ones in tests) + class WorkflowStartedEvent: # noqa: B903 + def __init__(self, workflow_id: str): + self.workflow_id = workflow_id + + class WorkflowCompletedEvent: # noqa: B903 + def __init__(self, workflow_id: str): + self.workflow_id = workflow_id + + class WorkflowFailedEvent: # noqa: B903 + def __init__(self, workflow_id: str, error_info: dict | None = None): + self.workflow_id = workflow_id + self.error_info = error_info + + # Test WorkflowStartedEvent + start_event = WorkflowStartedEvent(workflow_id="test_workflow_123") + events = await mapper.convert_event(start_event, test_request) + + assert len(events) == 2 # Should emit response.created and response.in_progress + assert events[0].type == "response.created" + assert events[1].type == "response.in_progress" + assert events[0].response.model == "test_agent" # Should use model from request + assert events[0].response.status == "in_progress" + + # Test WorkflowCompletedEvent + complete_event = WorkflowCompletedEvent(workflow_id="test_workflow_123") + events = await mapper.convert_event(complete_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.completed" + assert events[0].response.status == "completed" + + # Test WorkflowFailedEvent with error info + failed_event = WorkflowFailedEvent(workflow_id="test_workflow_123", error_info={"message": "Workflow failed"}) + events = await mapper.convert_event(failed_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.failed" + assert events[0].response.status == "failed" + assert events[0].response.error.message == "{'message': 'Workflow failed'}" + assert events[0].response.error.code == "server_error" + + +@pytest.mark.skip(reason="Executor events need real classes from agent_framework.workflows") +async def test_executor_action_events(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: + """Test that workflow executor events are properly converted to custom output item events.""" + + # Create mock executor events (since we don't have access to the real ones in tests) + class ExecutorInvokedEvent: # noqa: B903 + def __init__(self, executor_id: str, executor_type: str = "test"): + self.executor_id = executor_id + self.executor_type = executor_type + + class ExecutorCompletedEvent: # noqa: B903 + def __init__(self, executor_id: str, result: Any = None): + self.executor_id = executor_id + self.result = result + + class ExecutorFailedEvent: # noqa: B903 + def __init__(self, executor_id: str, error: Exception | None = None): + self.executor_id = executor_id + self.error = error + + # Test ExecutorInvokedEvent + invoked_event = ExecutorInvokedEvent(executor_id="exec_123", executor_type="test_executor") + events = await mapper.convert_event(invoked_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.output_item.added" + assert events[0].item["type"] == "executor_action" + assert events[0].item["executor_id"] == "exec_123" + assert events[0].item["status"] == "in_progress" + + # Test ExecutorCompletedEvent + complete_event = ExecutorCompletedEvent(executor_id="exec_123", result={"data": "success"}) + events = await mapper.convert_event(complete_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.output_item.done" + assert events[0].item["type"] == "executor_action" + assert events[0].item["executor_id"] == "exec_123" + assert events[0].item["status"] == "completed" + assert events[0].item["result"] == {"data": "success"} + + # Test ExecutorFailedEvent + failed_event = ExecutorFailedEvent(executor_id="exec_123", error=Exception("Executor failed")) + events = await mapper.convert_event(failed_event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.output_item.done" + assert events[0].item["type"] == "executor_action" + assert events[0].item["executor_id"] == "exec_123" + assert events[0].item["status"] == "failed" + assert "Executor failed" in str(events[0].item["error"]["message"]) + + if __name__ == "__main__": # Simple test runner async def run_all_tests() -> None: diff --git a/python/packages/devui/tests/test_server.py b/python/packages/devui/tests/test_server.py index ab5c387eff..bf03a612bd 100644 --- a/python/packages/devui/tests/test_server.py +++ b/python/packages/devui/tests/test_server.py @@ -143,6 +143,104 @@ def test_select_primary_input_type_prefers_string_and_dict(): assert fallback is int +@pytest.mark.asyncio +async def test_credential_cleanup() -> None: + """Test that async credentials are properly closed during server cleanup.""" + from unittest.mock import AsyncMock, Mock + + from agent_framework import ChatAgent + + # Create mock credential with async close + mock_credential = AsyncMock() + mock_credential.close = AsyncMock() + + # Create mock chat client with credential + mock_client = Mock() + mock_client.async_credential = mock_credential + mock_client.model_id = "test-model" + + # Create agent with mock client + agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent") + + # Create DevUI server with agent + server = DevServer() + server._pending_entities = [agent] + await server._ensure_executor() + + # Run cleanup + await server._cleanup_entities() + + # Verify credential.close() was called + assert mock_credential.close.called, "Async credential close should have been called" + assert mock_credential.close.call_count == 1 + + +@pytest.mark.asyncio +async def test_credential_cleanup_error_handling() -> None: + """Test that credential cleanup errors are handled gracefully.""" + from unittest.mock import AsyncMock, Mock + + from agent_framework import ChatAgent + + # Create mock credential that raises error on close + mock_credential = AsyncMock() + mock_credential.close = AsyncMock(side_effect=Exception("Close failed")) + + # Create mock chat client with credential + mock_client = Mock() + mock_client.async_credential = mock_credential + mock_client.model_id = "test-model" + + # Create agent with mock client + agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent") + + # Create DevUI server with agent + server = DevServer() + server._pending_entities = [agent] + await server._ensure_executor() + + # Run cleanup - should not raise despite credential error + await server._cleanup_entities() + + # Verify close was attempted + assert mock_credential.close.called + + +@pytest.mark.asyncio +async def test_multiple_credential_attributes() -> None: + """Test that we check all common credential attribute names.""" + from unittest.mock import AsyncMock, Mock + + from agent_framework import ChatAgent + + # Create mock credentials + mock_cred1 = Mock() + mock_cred1.close = Mock() + mock_cred2 = AsyncMock() + mock_cred2.close = AsyncMock() + + # Create mock chat client with multiple credential attributes + mock_client = Mock() + mock_client.credential = mock_cred1 + mock_client.async_credential = mock_cred2 + mock_client.model_id = "test-model" + + # Create agent with mock client + agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent") + + # Create DevUI server with agent + server = DevServer() + server._pending_entities = [agent] + await server._ensure_executor() + + # Run cleanup + await server._cleanup_entities() + + # Verify both credentials were closed + assert mock_cred1.close.called, "Sync credential should be closed" + assert mock_cred2.close.called, "Async credential should be closed" + + if __name__ == "__main__": # Simple test runner async def run_tests():