diff --git a/python/packages/devui/README.md b/python/packages/devui/README.md index 7fbe4ac6a7..bba1a2a730 100644 --- a/python/packages/devui/README.md +++ b/python/packages/devui/README.md @@ -1,6 +1,6 @@ -# DevUI - Agent Framework Debug Interface +# DevUI - A Sample App for Running Agents and Workflows -A lightweight, standalone sample app interface for running entities (agents/workflows) in the Microsoft Agent Framework supporting both **directory-based discovery** and **in-memory entity registration**. +A lightweight, standalone sample app interface for running entities (agents/workflows) in the Microsoft Agent Framework supporting **directory-based discovery**, **in-memory entity registration**, and **sample entity gallery**. > [!IMPORTANT] > DevUI is a **sample app** to help you get started with the Agent Framework. It is **not** intended for production use. For production, or for features beyond what is provided in this sample app, it is recommended that you build your own custom interface and API server using the Agent Framework SDK. @@ -12,11 +12,6 @@ A lightweight, standalone sample app interface for running entities (agents/work ```bash # Install pip install agent-framework-devui - -# Launch web UI + API server -devui ./agents --port 8080 -# → Web UI: http://localhost:8080 -# → API: http://localhost:8080/v1/* ``` You can also launch it programmatically @@ -42,6 +37,18 @@ serve(entities=[agent], auto_open=True) # → Opens browser to http://localhost:8080 ``` +In addition, if you have agents/workflows defined in a specific directory structure (see below), you can launch DevUI from the _cli_ to discover and run them. + +```bash + +# Launch web UI + API server +devui ./agents --port 8080 +# → Web UI: http://localhost:8080 +# → API: http://localhost:8080/v1/* +``` + +When DevUI starts with no discovered entities, it displays a **sample entity gallery** with curated examples from the Agent Framework repository to help you get started quickly. + ## 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`). @@ -61,6 +68,14 @@ agents/ └── .env # Optional: shared environment variables ``` +## Viewing Telemetry (Otel Traces) in DevUI + +Agent Framework emits OpenTelemetry (Otel) traces for various operations. You can view these traces in DevUI by enabling tracing when starting the server. + +```bash +devui ./agents --tracing framework +``` + ## OpenAI-Compatible API For convenience, you can interact with the agents/workflows using the standard OpenAI API format. Just specify the `entity_id` in the `extra_body` field. This can be an `agent_id` or `workflow_id`. @@ -75,27 +90,9 @@ curl -X POST http://localhost:8080/v1/responses \ "input": "Hello world", "extra_body": {"entity_id": "weather_agent"} } -EOF + ``` -Messages and events from agents/workflows are mapped to OpenAI response types in `agent_framework_devui/_mapper.py`. See the mapping table below: - -| Agent Framework Content | OpenAI Event | Type | -| --------------------------------- | ----------------------------------------- | -------- | -| `TextContent` | `ResponseTextDeltaEvent` | Official | -| `TextReasoningContent` | `ResponseReasoningTextDeltaEvent` | Official | -| `FunctionCallContent` | `ResponseFunctionCallArgumentsDeltaEvent` | Official | -| `FunctionResultContent` | `ResponseFunctionResultComplete` | Custom | -| `ErrorContent` | `ResponseErrorEvent` | Official | -| `UsageContent` | `ResponseUsageEventComplete` | Custom | -| `DataContent` | `ResponseTraceEventComplete` | Custom | -| `UriContent` | `ResponseTraceEventComplete` | Custom | -| `HostedFileContent` | `ResponseTraceEventComplete` | Custom | -| `HostedVectorStoreContent` | `ResponseTraceEventComplete` | Custom | -| `FunctionApprovalRequestContent` | Custom event | Custom | -| `FunctionApprovalResponseContent` | Custom event | Custom | -| `WorkflowEvent` | `ResponseWorkflowEventComplete` | Custom | - ## CLI Options ```bash @@ -114,6 +111,8 @@ Options: - `GET /v1/entities` - List discovered agents/workflows - `GET /v1/entities/{entity_id}/info` - Get detailed entity information +- `POST /v1/entities/add` - Add entity from URL (for gallery samples) +- `DELETE /v1/entities/{entity_id}` - Remove remote entity - `POST /v1/responses` - Execute agent/workflow (streaming or sync) - `GET /health` - Health check - `POST /v1/threads` - Create thread for agent (optional) diff --git a/python/packages/devui/agent_framework_devui/__init__.py b/python/packages/devui/agent_framework_devui/__init__.py index 2185f73882..c259b33ae8 100644 --- a/python/packages/devui/agent_framework_devui/__init__.py +++ b/python/packages/devui/agent_framework_devui/__init__.py @@ -9,7 +9,7 @@ from typing import Any from ._server import DevServer from .models import AgentFrameworkRequest, OpenAIError, OpenAIResponse, ResponseStreamEvent -from .models._discovery_models import DiscoveryResponse, EntityInfo +from .models._discovery_models import DiscoveryResponse, EntityInfo, EnvVarRequirement logger = logging.getLogger(__name__) @@ -27,6 +27,7 @@ def serve( auto_open: bool = False, cors_origins: list[str] | None = None, ui_enabled: bool = True, + tracing_enabled: bool = False, ) -> None: """Launch Agent Framework DevUI with simple API. @@ -38,6 +39,7 @@ def serve( auto_open: Whether to automatically open browser cors_origins: List of allowed CORS origins ui_enabled: Whether to enable the UI + tracing_enabled: Whether to enable OpenTelemetry tracing """ import re @@ -51,6 +53,23 @@ def serve( if not isinstance(port, int) or not (1 <= port <= 65535): raise ValueError(f"Invalid port: {port}. Must be integer between 1 and 65535") + # Configure tracing environment variables if enabled + if tracing_enabled: + import os + + # Only set if not already configured by user + if not os.environ.get("ENABLE_OTEL"): + os.environ["ENABLE_OTEL"] = "true" + logger.info("Set ENABLE_OTEL=true for tracing") + + if not os.environ.get("ENABLE_SENSITIVE_DATA"): + os.environ["ENABLE_SENSITIVE_DATA"] = "true" + logger.info("Set ENABLE_SENSITIVE_DATA=true for tracing") + + if not os.environ.get("OTLP_ENDPOINT"): + os.environ["OTLP_ENDPOINT"] = "http://localhost:4317" + logger.info("Set OTLP_ENDPOINT=http://localhost:4317 for tracing") + # Create server with direct parameters server = DevServer( entities_dir=entities_dir, port=port, host=host, cors_origins=cors_origins, ui_enabled=ui_enabled @@ -123,6 +142,7 @@ __all__ = [ "DevServer", "DiscoveryResponse", "EntityInfo", + "EnvVarRequirement", "OpenAIError", "OpenAIResponse", "ResponseStreamEvent", diff --git a/python/packages/devui/agent_framework_devui/_cli.py b/python/packages/devui/agent_framework_devui/_cli.py index 0e36731b1c..2a36d0aa98 100644 --- a/python/packages/devui/agent_framework_devui/_cli.py +++ b/python/packages/devui/agent_framework_devui/_cli.py @@ -28,6 +28,7 @@ Examples: devui ./agents # Scan specific directory devui --port 8000 # Custom port devui --headless # API only, no UI + devui --tracing # Enable OpenTelemetry tracing """, ) @@ -52,6 +53,8 @@ Examples: parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") + parser.add_argument("--tracing", action="store_true", help="Enable OpenTelemetry tracing for Agent Framework") + parser.add_argument("--version", action="version", version=f"Agent Framework DevUI {get_version()}") return parser @@ -119,7 +122,12 @@ def main() -> None: from . import serve serve( - entities_dir=entities_dir, port=args.port, host=args.host, auto_open=not args.no_open, ui_enabled=ui_enabled + entities_dir=entities_dir, + port=args.port, + host=args.host, + auto_open=not args.no_open, + ui_enabled=ui_enabled, + tracing_enabled=args.tracing, ) except KeyboardInterrupt: diff --git a/python/packages/devui/agent_framework_devui/_discovery.py b/python/packages/devui/agent_framework_devui/_discovery.py index e989048ae2..391204b99a 100644 --- a/python/packages/devui/agent_framework_devui/_discovery.py +++ b/python/packages/devui/agent_framework_devui/_discovery.py @@ -2,6 +2,9 @@ """Agent Framework entity discovery implementation.""" +from __future__ import annotations + +import hashlib import importlib import importlib.util import logging @@ -10,6 +13,7 @@ import uuid from pathlib import Path from typing import Any +import httpx from dotenv import load_dotenv from .models._discovery_models import EntityInfo @@ -29,6 +33,7 @@ class EntityDiscovery: self.entities_dir = entities_dir self._entities: dict[str, EntityInfo] = {} self._loaded_objects: dict[str, Any] = {} + self._remote_cache_dir = Path.home() / ".agent_framework_devui" / "remote_cache" async def discover_entities(self) -> list[EntityInfo]: """Scan for Agent Framework entities. @@ -88,12 +93,15 @@ class EntityDiscovery: self._loaded_objects[entity_id] = entity_object logger.debug(f"Registered entity: {entity_id} ({entity_info.type})") - async def create_entity_info_from_object(self, entity_object: Any, entity_type: str | None = None) -> EntityInfo: + async def create_entity_info_from_object( + self, entity_object: Any, entity_type: str | None = None, source: str = "in_memory" + ) -> EntityInfo: """Create EntityInfo from Agent Framework entity object. Args: entity_object: Agent Framework entity object entity_type: Optional entity type override + source: Source of entity (directory, in_memory, remote) Returns: EntityInfo with Agent Framework specific metadata @@ -121,7 +129,7 @@ class EntityDiscovery: description = getattr(entity_object, "description", "") # Generate entity ID using Agent Framework specific naming - entity_id = self._generate_entity_id(entity_object, entity_type) + entity_id = self._generate_entity_id(entity_object, entity_type, source) # Extract tools/executors using Agent Framework specific logic tools_list = await self._extract_tools_from_object(entity_object, entity_type) @@ -343,9 +351,9 @@ class EntityDiscovery: continue if self._is_valid_entity(obj, obj_type): - entity_id = f"{obj_type}_{base_id}" - await self._register_entity_from_object(entity_id, obj, obj_type, module_path) - entities_found.append(entity_id) + # Pass source as "directory" for directory-discovered entities + await self._register_entity_from_object(obj, obj_type, module_path, source="directory") + entities_found.append(obj_type) return entities_found @@ -405,35 +413,31 @@ class EntityDiscovery: # Check for workflow - must have run_stream method and executors return hasattr(obj, "run_stream") and (hasattr(obj, "executors") or hasattr(obj, "get_executors_list")) - async def _register_entity_from_object(self, entity_id: str, obj: Any, obj_type: str, module_path: str) -> None: + async def _register_entity_from_object( + self, obj: Any, obj_type: str, module_path: str, source: str = "directory" + ) -> None: """Register an entity from a live object. Args: - entity_id: Unique entity identifier obj: Entity object obj_type: Type of entity ("agent" or "workflow") module_path: Path to module for metadata + source: Source of entity (directory, in_memory, remote) """ try: + # Generate entity ID with source information + entity_id = self._generate_entity_id(obj, obj_type, source) + # Extract metadata from the live object with improved fallback naming name = getattr(obj, "name", None) if not name: - # For directory-based entities, prefer directory name over UUID - # entity_id format: "workflow_fanout_workflow" or "agent_weather_agent" - if entity_id and "_" in entity_id: - # Directory-based: use formatted directory name (remove type prefix) - directory_name = entity_id.split("_", 1)[1] if "_" in entity_id else entity_id - name = directory_name.replace("_", " ").title() + entity_id_raw = getattr(obj, "id", None) + if entity_id_raw: + # Truncate UUID to first 8 characters for readability + short_id = str(entity_id_raw)[:8] if len(str(entity_id_raw)) > 8 else str(entity_id_raw) + name = f"{obj_type.title()} {short_id}" else: - # In-memory: use ID with entity type prefix - entity_id_raw = getattr(obj, "id", None) - if entity_id_raw: - # Truncate UUID to first 8 characters for readability - short_id = str(entity_id_raw)[:8] if len(str(entity_id_raw)) > 8 else str(entity_id_raw) - name = f"{obj_type.title()} {short_id}" - else: - # Final fallback to class name - name = f"{obj_type.title()} {obj.__class__.__name__}" + name = f"{obj_type.title()} {obj.__class__.__name__}" description = getattr(obj, "description", None) tools = await self._extract_tools_from_object(obj, obj_type) @@ -452,7 +456,7 @@ class EntityDiscovery: metadata={ "module_path": module_path, "entity_type": obj_type, - "source": "module_import", + "source": source, "has_run_stream": hasattr(obj, "run_stream"), "class_name": obj.__class__.__name__ if hasattr(obj, "__class__") else str(type(obj)), }, @@ -462,7 +466,7 @@ class EntityDiscovery: self.register_entity(entity_id, entity_info, obj) except Exception as e: - logger.error(f"Error registering entity {entity_id}: {e}") + logger.error(f"Error registering entity from {source}: {e}") async def _extract_tools_from_object(self, obj: Any, obj_type: str) -> list[str]: """Extract tool/executor names from a live object. @@ -517,34 +521,204 @@ class EntityDiscovery: return tools - def _generate_entity_id(self, entity: Any, entity_type: str) -> str: - """Generate entity ID with priority: name -> id -> class_name -> uuid. + def _generate_entity_id(self, entity: Any, entity_type: str, source: str = "directory") -> str: + """Generate unique entity ID with UUID suffix for collision resistance. Args: entity: Entity object entity_type: Type of entity (agent, workflow, etc.) + source: Source of entity (directory, in_memory, remote) Returns: - Generated entity ID + Unique entity ID with format: {type}_{source}_{name}_{uuid8} """ import re - # Priority 1: entity.name + # Extract base name with priority: name -> id -> class_name if hasattr(entity, "name") and entity.name: - name = str(entity.name).lower().replace(" ", "-").replace("_", "-") - return f"{entity_type}_{name}" - - # Priority 2: entity.id - if hasattr(entity, "id") and entity.id: - entity_id = str(entity.id).lower().replace(" ", "-").replace("_", "-") - return f"{entity_type}_{entity_id}" - - # Priority 3: class name - if hasattr(entity, "__class__"): + base_name = str(entity.name).lower().replace(" ", "-").replace("_", "-") + elif hasattr(entity, "id") and entity.id: + base_name = str(entity.id).lower().replace(" ", "-").replace("_", "-") + elif hasattr(entity, "__class__"): class_name = entity.__class__.__name__ # Convert CamelCase to kebab-case - class_name = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", class_name).lower() - return f"{entity_type}_{class_name}" + base_name = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", class_name).lower() + else: + base_name = "entity" - # Priority 4: fallback to uuid - return f"{entity_type}_{uuid.uuid4().hex[:8]}" + # Generate short UUID (8 chars = 4 billion combinations) + short_uuid = uuid.uuid4().hex[:8] + + return f"{entity_type}_{source}_{base_name}_{short_uuid}" + + async def fetch_remote_entity( + self, url: str, metadata: dict[str, Any] | None = None + ) -> tuple[EntityInfo | None, str | None]: + """Fetch and register entity from URL. + + Args: + url: URL to Python file containing entity + metadata: Additional metadata (source, sampleId, etc.) + + Returns: + Tuple of (EntityInfo if successful, error_message if failed) + """ + try: + normalized_url = self._normalize_url(url) + logger.info(f"Normalized URL: {normalized_url}") + + content = await self._fetch_url_content(normalized_url) + if not content: + error_msg = "Failed to fetch content from URL. The file may not exist or is not accessible." + logger.warning(error_msg) + return None, error_msg + + if not self._validate_python_syntax(content): + error_msg = "Invalid Python syntax in the file. Please check the file contains valid Python code." + logger.warning(error_msg) + return None, error_msg + + entity_object = await self._load_entity_from_content(content, url) + if not entity_object: + error_msg = ( + "No valid agent or workflow found in the file. " + "Make sure the file contains an 'agent' or 'workflow' variable." + ) + logger.warning(error_msg) + return None, error_msg + + entity_info = await self.create_entity_info_from_object( + entity_object, + entity_type=None, # Auto-detect + source="remote", + ) + + entity_info.source = metadata.get("source", "remote_gallery") if metadata else "remote_gallery" + entity_info.original_url = url + if metadata: + entity_info.metadata.update(metadata) + + self.register_entity(entity_info.id, entity_info, entity_object) + + logger.info(f"Successfully added remote entity: {entity_info.id}") + return entity_info, None + + except Exception as e: + error_msg = f"Unexpected error: {e!s}" + logger.error(f"Error fetching remote entity from {url}: {e}", exc_info=True) + return None, error_msg + + def _normalize_url(self, url: str) -> str: + """Convert various Git hosting URLs to raw content URLs.""" + # GitHub: blob -> raw + if "github.com" in url and "/blob/" in url: + return url.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/") + + # GitLab: blob -> raw + if "gitlab.com" in url and "/-/blob/" in url: + return url.replace("/-/blob/", "/-/raw/") + + # Bitbucket: src -> raw + if "bitbucket.org" in url and "/src/" in url: + return url.replace("/src/", "/raw/") + + return url + + async def _fetch_url_content(self, url: str, max_size_mb: int = 10) -> str | None: + """Fetch content from URL with size and timeout limits.""" + try: + timeout = 30.0 # 30 second timeout + + async with httpx.AsyncClient(timeout=timeout) as client: + response = await client.get(url) + + if response.status_code != 200: + logger.warning(f"HTTP {response.status_code} for {url}") + return None + + # Check content length + content_length = response.headers.get("content-length") + if content_length and int(content_length) > max_size_mb * 1024 * 1024: + logger.warning(f"File too large: {content_length} bytes") + return None + + # Read with size limit + content = response.text + if len(content.encode("utf-8")) > max_size_mb * 1024 * 1024: + logger.warning("Content too large after reading") + return None + + return content + + except Exception as e: + logger.error(f"Error fetching {url}: {e}") + return None + + def _validate_python_syntax(self, content: str) -> bool: + """Validate that content is valid Python code.""" + try: + compile(content, "", "exec") + return True + except SyntaxError as e: + logger.warning(f"Python syntax error: {e}") + return False + + async def _load_entity_from_content(self, content: str, source_url: str) -> Any | None: + """Load entity object from Python content string using disk-based import. + + This method caches remote entities to disk and uses importlib for loading, + making it consistent with local entity discovery and avoiding exec() security warnings. + """ + try: + # Create cache directory if it doesn't exist + self._remote_cache_dir.mkdir(parents=True, exist_ok=True) + + # Generate a unique filename based on URL hash + url_hash = hashlib.sha256(source_url.encode()).hexdigest()[:16] + module_name = f"remote_entity_{url_hash}" + cached_file = self._remote_cache_dir / f"{module_name}.py" + + # Write content to cache file + cached_file.write_text(content, encoding="utf-8") + logger.debug(f"Cached remote entity to {cached_file}") + + # Load module from cached file using importlib (same as local scanning) + module = self._load_module_from_file(cached_file, module_name) + if not module: + logger.warning(f"Failed to load module from cached file: {cached_file}") + return None + + # Look for agent or workflow objects in the loaded module + for name in dir(module): + if name.startswith("_"): + continue + + obj = getattr(module, name) + + # Check for explicitly named entities first + if name in ["agent", "workflow"] and self._is_valid_entity(obj, name): + return obj + + # Also check if any object looks like an agent/workflow + if self._is_valid_agent(obj) or self._is_valid_workflow(obj): + return obj + + return None + + except Exception as e: + logger.error(f"Error loading entity from content: {e}") + return None + + def remove_remote_entity(self, entity_id: str) -> bool: + """Remove a remote entity by ID.""" + if entity_id in self._entities: + entity_info = self._entities[entity_id] + if entity_info.source in ["remote_gallery", "remote"]: + del self._entities[entity_id] + if entity_id in self._loaded_objects: + del self._loaded_objects[entity_id] + logger.info(f"Removed remote entity: {entity_id}") + return True + logger.warning(f"Cannot remove local entity: {entity_id}") + return False + return False diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index f28765cc2c..cf75f78a87 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -71,18 +71,17 @@ class AgentFrameworkExecutor: def _setup_agent_framework_tracing(self) -> None: """Set up Agent Framework's built-in tracing.""" - # Configure Agent Framework tracing only if OTLP endpoint is configured - otlp_endpoint = os.environ.get("OTLP_ENDPOINT") - if otlp_endpoint: + # Configure Agent Framework tracing only if ENABLE_OTEL is set + if os.environ.get("ENABLE_OTEL"): try: from agent_framework.observability import setup_observability - setup_observability(enable_sensitive_data=True, otlp_endpoint=otlp_endpoint) - logger.info(f"Enabled Agent Framework observability with endpoint: {otlp_endpoint}") + setup_observability(enable_sensitive_data=True) + logger.info("Enabled Agent Framework observability") except Exception as e: logger.warning(f"Failed to enable Agent Framework observability: {e}") else: - logger.debug("No OTLP endpoint configured, skipping observability setup") + logger.debug("ENABLE_OTEL not set, skipping observability setup") # Thread Management Methods def create_thread(self, agent_id: str) -> str: @@ -118,7 +117,6 @@ class AgentFrameworkExecutor: if thread_id not in self.thread_storage: return False - # Remove from agent mapping for _agent_id, thread_ids in self.agent_threads.items(): if thread_id in thread_ids: thread_ids.remove(thread_id) @@ -128,7 +126,7 @@ class AgentFrameworkExecutor: return True async def get_thread_messages(self, thread_id: str) -> list[dict[str, Any]]: - """Get messages from a thread's message store, filtering for UI display.""" + """Get messages from a thread's message store, preserving all content types for UI display.""" thread = self.get_thread(thread_id) if not thread or not thread.message_store: return [] @@ -142,21 +140,21 @@ class AgentFrameworkExecutor: # Extract role value (handle enum) role = af_msg.role.value if hasattr(af_msg.role, "value") else str(af_msg.role) - # Skip tool/function messages - only show user and assistant text + # Skip tool/function messages - only show user and assistant messages if role not in ["user", "assistant"]: continue - # Extract user-facing text content only - text_content = self._extract_display_text(af_msg.contents) + # Extract all user-facing content (text, images, files, etc.) + display_contents = self._extract_display_contents(af_msg.contents) - # Skip messages with no displayable text - if not text_content: + # Skip messages with no displayable content + if not display_contents: continue ui_message = { "id": af_msg.message_id or f"restored-{i}", "role": role, - "contents": [{"type": "text", "text": text_content}], + "contents": display_contents, "timestamp": __import__("datetime").datetime.now().isoformat(), "author_name": af_msg.author_name, "message_id": af_msg.message_id, @@ -174,14 +172,18 @@ class AgentFrameworkExecutor: logger.error(traceback.format_exc()) return [] - def _extract_display_text(self, contents: list[Any]) -> str: - """Extract user-facing text from message contents, filtering out internal mechanics.""" - text_parts = [] + def _extract_display_contents(self, contents: list[Any]) -> list[dict[str, Any]]: + """Extract all user-facing content (text, images, files, etc.) from message contents. + + Filters out internal mechanics like function calls/results while preserving + all content types that should be displayed in the UI. + """ + display_contents = [] for content in contents: content_type = getattr(content, "type", None) - # Only include text content for display + # Text content if content_type == "text": text = getattr(content, "text", "") @@ -194,15 +196,31 @@ class AgentFrameworkExecutor: if parsed.get("contents"): for sub_content in parsed["contents"]: if sub_content.get("type") == "text": - text_parts.append(sub_content.get("text", "")) + display_contents.append({"type": "text", "text": sub_content.get("text", "")}) except Exception: - text_parts.append(text) # Fallback to raw text + display_contents.append({"type": "text", "text": text}) else: - text_parts.append(text) + display_contents.append({"type": "text", "text": text}) + + # Data content (images, files, PDFs, etc.) + elif content_type == "data": + display_contents.append({ + "type": "data", + "uri": getattr(content, "uri", ""), + "media_type": getattr(content, "media_type", None), + }) + + # URI content (external links to images/files) + elif content_type == "uri": + display_contents.append({ + "type": "uri", + "uri": getattr(content, "uri", ""), + "media_type": getattr(content, "media_type", None), + }) # Skip function_call, function_result, and other internal content types - return " ".join(text_parts).strip() + return display_contents async def serialize_thread(self, thread_id: str) -> dict[str, Any] | None: """Serialize thread state for persistence.""" @@ -380,7 +398,6 @@ class AgentFrameworkExecutor: else: logger.warning(f"Thread {thread_id} not found, proceeding without thread") - # Debug logging - handle both string and ChatMessage if isinstance(user_message, str): logger.debug(f"Executing agent with text input: {user_message[:100]}...") else: @@ -389,19 +406,15 @@ class AgentFrameworkExecutor: # Use Agent Framework's native streaming with optional thread if thread: async for update in agent.run_stream(user_message, thread=thread): - # Yield any pending trace events first for trace_event in trace_collector.get_pending_events(): yield trace_event - # Then yield the execution update yield update else: async for update in agent.run_stream(user_message): - # Yield any pending trace events first for trace_event in trace_collector.get_pending_events(): yield trace_event - # Then yield the execution update yield update except Exception as e: @@ -546,6 +559,17 @@ class AgentFrameworkExecutor: media_type = "application/pdf" elif filename.lower().endswith((".png", ".jpg", ".jpeg", ".gif")): media_type = f"image/{filename.split('.')[-1].lower()}" + elif filename.lower().endswith(( + ".wav", + ".mp3", + ".m4a", + ".ogg", + ".flac", + ".aac", + )): + ext = filename.split(".")[-1].lower() + # Normalize extensions to match audio MIME types + media_type = "audio/mp4" if ext == "m4a" else f"audio/{ext}" # Use file_data or file_url if file_data: @@ -562,8 +586,17 @@ class AgentFrameworkExecutor: if not contents: contents.append(TextContent(text="")) - # Create ChatMessage with user role - return ChatMessage(role=Role.USER, contents=contents) + chat_message = ChatMessage(role=Role.USER, contents=contents) + + logger.info(f"Created ChatMessage with {len(contents)} contents:") + for idx, content in enumerate(contents): + content_type = content.__class__.__name__ + if hasattr(content, "media_type"): + logger.info(f" [{idx}] {content_type} - media_type: {content.media_type}") + else: + logger.info(f" [{idx}] {content_type}") + + return chat_message def _extract_user_message_fallback(self, input_data: Any) -> str: """Fallback method to extract user message as string. diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index 476bda2588..d35b6ed715 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -2,6 +2,7 @@ """FastAPI server implementation.""" +import inspect import json import logging from collections.abc import AsyncGenerator @@ -19,8 +20,6 @@ from ._mapper import MessageMapper from .models import AgentFrameworkRequest, OpenAIError from .models._discovery_models import DiscoveryResponse, EntityInfo -# Removed ExecutionEngine import - using direct executor approach - logger = logging.getLogger(__name__) @@ -72,7 +71,7 @@ class DevServer: discovery = self.executor.entity_discovery for entity in self._pending_entities: try: - entity_info = await discovery.create_entity_info_from_object(entity) + entity_info = await discovery.create_entity_info_from_object(entity, source="in-memory") discovery.register_entity(entity_info.id, entity_info, entity) logger.info(f"Registered in-memory entity: {entity_info.id}") except Exception as e: @@ -85,6 +84,33 @@ class DevServer: return self.executor + async def _cleanup_entities(self) -> None: + """Cleanup entity resources (close clients, credentials, etc.).""" + if not self.executor: + return + + logger.info("Cleaning up entity resources...") + entities = self.executor.entity_discovery.list_entities() + closed_count = 0 + + for entity_info in entities: + try: + entity_obj = self.executor.entity_discovery.get_entity_object(entity_info.id) + if entity_obj and hasattr(entity_obj, "chat_client"): + client = entity_obj.chat_client + if hasattr(client, "close") and callable(client.close): + if inspect.iscoroutinefunction(client.close): + await client.close() + else: + client.close() + closed_count += 1 + logger.debug(f"Closed client for entity: {entity_info.id}") + 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)") + def create_app(self) -> FastAPI: """Create the FastAPI application.""" @@ -97,6 +123,10 @@ class DevServer: # Shutdown logger.info("Shutting down Agent Framework Server") + # Cleanup entity resources (e.g., close credentials, clients) + if self.executor: + await self._cleanup_entities() + app = FastAPI( title="Agent Framework Server", description="OpenAI-compatible API server for Agent Framework and other AI frameworks", @@ -125,7 +155,8 @@ class DevServer: async def health_check() -> dict[str, Any]: """Health check endpoint.""" executor = await self._ensure_executor() - entities = await executor.discover_entities() + # Use list_entities() to avoid re-discovering and re-registering entities + entities = executor.entity_discovery.list_entities() return {"status": "healthy", "entities_count": len(entities), "framework": "agent_framework"} @@ -235,11 +266,75 @@ class DevServer: logger.error(f"Error getting entity info for {entity_id}: {e}") raise HTTPException(status_code=500, detail=f"Failed to get entity info: {e!s}") from e + @app.post("/v1/entities/add") + async def add_entity(request: dict[str, Any]) -> dict[str, Any]: + """Add entity from URL.""" + try: + url = request.get("url") + metadata = request.get("metadata", {}) + + if not url: + raise HTTPException(status_code=400, detail="URL is required") + + logger.info(f"Attempting to add entity from URL: {url}") + executor = await self._ensure_executor() + entity_info, error_msg = await executor.entity_discovery.fetch_remote_entity(url, metadata) + + if not entity_info: + # Sanitize error message - only return safe, user-friendly errors + logger.error(f"Failed to fetch or validate entity from {url}: {error_msg}") + safe_error = error_msg if error_msg else "Failed to fetch or validate entity" + raise HTTPException(status_code=400, detail=safe_error) + + logger.info(f"Successfully added entity: {entity_info.id}") + return {"success": True, "entity": entity_info.model_dump()} + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error adding entity: {e}", exc_info=True) + # Don't expose internal error details to client + raise HTTPException( + status_code=500, detail="An unexpected error occurred while adding the entity" + ) from e + + @app.delete("/v1/entities/{entity_id}") + async def remove_entity(entity_id: str) -> dict[str, Any]: + """Remove entity by ID.""" + try: + executor = await self._ensure_executor() + + # Cleanup entity resources before removal + try: + entity_obj = executor.entity_discovery.get_entity_object(entity_id) + if entity_obj and hasattr(entity_obj, "chat_client"): + client = entity_obj.chat_client + if hasattr(client, "close") and callable(client.close): + if inspect.iscoroutinefunction(client.close): + await client.close() + else: + client.close() + logger.info(f"Closed client for entity: {entity_id}") + except Exception as e: + logger.warning(f"Error closing entity {entity_id} during removal: {e}") + + # Remove entity from registry + success = executor.entity_discovery.remove_remote_entity(entity_id) + + if success: + return {"success": True} + raise HTTPException(status_code=404, detail="Entity not found or cannot be removed") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error removing entity {entity_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to remove entity: {e!s}") from e + @app.post("/v1/responses") async def create_response(request: AgentFrameworkRequest, raw_request: Request) -> Any: """OpenAI Responses API endpoint.""" try: - # Debug: log the incoming request raw_body = await raw_request.body() logger.info(f"Raw request body: {raw_body.decode()}") logger.info(f"Parsed request: model={request.model}, extra_body={request.extra_body}") @@ -385,15 +480,21 @@ class DevServer: try: # Direct call to executor - simple and clean async for event in executor.execute_streaming(request): - if hasattr(event, "to_json") and callable(getattr(event, "to_json", None)): - payload = event.to_json() # type: ignore[attr-defined] - elif hasattr(event, "model_dump_json"): + # IMPORTANT: Check model_dump_json FIRST because to_json() can have newlines (pretty-printing) + # which breaks SSE format. model_dump_json() returns single-line JSON. + if hasattr(event, "model_dump_json"): payload = event.model_dump_json() # type: ignore[attr-defined] + elif hasattr(event, "to_json") and callable(getattr(event, "to_json", None)): + payload = event.to_json() # type: ignore[attr-defined] + # Strip newlines from pretty-printed JSON for SSE compatibility + payload = payload.replace("\n", "").replace("\r", "") + elif isinstance(event, dict): + # Handle plain dict events (e.g., error events from executor) + payload = json.dumps(event) + elif hasattr(event, "to_dict") and callable(getattr(event, "to_dict", None)): + payload = json.dumps(event.to_dict()) # type: ignore[attr-defined] else: - if hasattr(event, "to_dict") and callable(getattr(event, "to_dict", None)): - payload = json.dumps(event.to_dict()) # type: ignore[attr-defined] - else: - payload = json.dumps(str(event)) + payload = json.dumps(str(event)) yield f"data: {payload}\n\n" # Send final done event @@ -402,7 +503,7 @@ class DevServer: except Exception as e: logger.error(f"Error in streaming execution: {e}") error_event = {"id": "error", "object": "error", "error": {"message": str(e), "type": "execution_error"}} - yield f"data: {error_event}\n\n" + yield f"data: {json.dumps(error_event)}\n\n" def _mount_ui(self, app: FastAPI) -> None: """Mount the UI as static files.""" diff --git a/python/packages/devui/agent_framework_devui/models/_discovery_models.py b/python/packages/devui/agent_framework_devui/models/_discovery_models.py index 307ec7e37f..6577611c77 100644 --- a/python/packages/devui/agent_framework_devui/models/_discovery_models.py +++ b/python/packages/devui/agent_framework_devui/models/_discovery_models.py @@ -2,11 +2,22 @@ """Discovery API models for entity information.""" +from __future__ import annotations + from typing import Any from pydantic import BaseModel, Field +class EnvVarRequirement(BaseModel): + """Environment variable requirement for an entity.""" + + name: str + description: str + required: bool = True + example: str | None = None + + class EntityInfo(BaseModel): """Entity information for discovery and detailed views.""" @@ -19,6 +30,13 @@ class EntityInfo(BaseModel): tools: list[str | dict[str, Any]] | None = None metadata: dict[str, Any] = Field(default_factory=dict) + # Source information + source: str = "directory" # "directory", "in_memory", "remote_gallery" + original_url: str | None = None + + # Environment variable requirements + required_env_vars: list[EnvVarRequirement] | None = None + # Workflow-specific fields (populated only for detailed info requests) executors: list[str] | None = None workflow_dump: dict[str, Any] | None = None 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 be88e81061..91aae0eb5f 100644 --- a/python/packages/devui/agent_framework_devui/models/_openai_custom.py +++ b/python/packages/devui/agent_framework_devui/models/_openai_custom.py @@ -6,6 +6,8 @@ These are custom event types that extend beyond the standard OpenAI Responses AP to support Agent Framework specific features like workflows, traces, and function results. """ +from __future__ import annotations + from typing import Any, Literal from pydantic import BaseModel, ConfigDict @@ -177,7 +179,7 @@ class OpenAIError(BaseModel): error: dict[str, Any] @classmethod - def create(cls, message: str, type: str = "invalid_request_error", code: str | None = None) -> "OpenAIError": + def create(cls, message: str, type: str = "invalid_request_error", code: str | None = None) -> OpenAIError: """Create a standard OpenAI error response.""" error_data = {"message": message, "type": type, "code": code} return cls(error=error_data) diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index-BESRiUNX.js b/python/packages/devui/agent_framework_devui/ui/assets/index-BESRiUNX.js deleted file mode 100644 index b2cdcf0dc4..0000000000 --- a/python/packages/devui/agent_framework_devui/ui/assets/index-BESRiUNX.js +++ /dev/null @@ -1,383 +0,0 @@ -function t_(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 c of l)if(c.type==="childList")for(const u of c.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&s(u)}).observe(document,{childList:!0,subtree:!0});function o(l){const c={};return l.integrity&&(c.integrity=l.integrity),l.referrerPolicy&&(c.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?c.credentials="include":l.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function s(l){if(l.ep)return;l.ep=!0;const c=o(l);fetch(l.href,c)}})();function hm(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var eh={exports:{}},ai={};/** - * @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 Gx;function n_(){if(Gx)return ai;Gx=1;var e=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function o(s,l,c){var u=null;if(c!==void 0&&(u=""+c),l.key!==void 0&&(u=""+l.key),"key"in l){c={};for(var f in l)f!=="key"&&(c[f]=l[f])}else c=l;return l=c.ref,{$$typeof:e,type:s,key:u,ref:l!==void 0?l:null,props:c}}return ai.Fragment=r,ai.jsx=o,ai.jsxs=o,ai}var qx;function r_(){return qx||(qx=1,eh.exports=n_()),eh.exports}var d=r_(),th={exports:{}},Me={};/** - * @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 Xx;function o_(){if(Xx)return Me;Xx=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"),c=Symbol.for("react.consumer"),u=Symbol.for("react.context"),f=Symbol.for("react.forward_ref"),m=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),v=Symbol.iterator;function y(M){return M===null||typeof M!="object"?null:(M=v&&M[v]||M["@@iterator"],typeof M=="function"?M:null)}var b={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},E=Object.assign,S={};function C(M,I,X){this.props=M,this.context=I,this.refs=S,this.updater=X||b}C.prototype.isReactComponent={},C.prototype.setState=function(M,I){if(typeof M!="object"&&typeof M!="function"&&M!=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,M,I,"setState")},C.prototype.forceUpdate=function(M){this.updater.enqueueForceUpdate(this,M,"forceUpdate")};function j(){}j.prototype=C.prototype;function R(M,I,X){this.props=M,this.context=I,this.refs=S,this.updater=X||b}var N=R.prototype=new j;N.constructor=R,E(N,C.prototype),N.isPureReactComponent=!0;var A=Array.isArray,D={H:null,A:null,T:null,S:null,V:null},z=Object.prototype.hasOwnProperty;function B(M,I,X,F,ee,ue){return X=ue.ref,{$$typeof:e,type:M,key:I,ref:X!==void 0?X:null,props:ue}}function U(M,I){return B(M.type,I,void 0,void 0,void 0,M.props)}function Y(M){return typeof M=="object"&&M!==null&&M.$$typeof===e}function oe(M){var I={"=":"=0",":":"=2"};return"$"+M.replace(/[=:]/g,function(X){return I[X]})}var J=/\/+/g;function q(M,I){return typeof M=="object"&&M!==null&&M.key!=null?oe(""+M.key):I.toString(36)}function re(){}function L(M){switch(M.status){case"fulfilled":return M.value;case"rejected":throw M.reason;default:switch(typeof M.status=="string"?M.then(re,re):(M.status="pending",M.then(function(I){M.status==="pending"&&(M.status="fulfilled",M.value=I)},function(I){M.status==="pending"&&(M.status="rejected",M.reason=I)})),M.status){case"fulfilled":return M.value;case"rejected":throw M.reason}}throw M}function $(M,I,X,F,ee){var ue=typeof M;(ue==="undefined"||ue==="boolean")&&(M=null);var se=!1;if(M===null)se=!0;else switch(ue){case"bigint":case"string":case"number":se=!0;break;case"object":switch(M.$$typeof){case e:case r:se=!0;break;case g:return se=M._init,$(se(M._payload),I,X,F,ee)}}if(se)return ee=ee(M),se=F===""?"."+q(M,0):F,A(ee)?(X="",se!=null&&(X=se.replace(J,"$&/")+"/"),$(ee,I,X,"",function(de){return de})):ee!=null&&(Y(ee)&&(ee=U(ee,X+(ee.key==null||M&&M.key===ee.key?"":(""+ee.key).replace(J,"$&/")+"/")+se)),I.push(ee)),1;se=0;var Q=F===""?".":F+":";if(A(M))for(var ae=0;ae>>1,M=T[P];if(0>>1;Pl(F,H))eel(ue,F)?(T[P]=ue,T[ee]=H,P=ee):(T[P]=F,T[X]=H,P=X);else if(eel(ue,H))T[P]=ue,T[ee]=H,P=ee;else break e}}return O}function l(T,O){var H=T.sortIndex-O.sortIndex;return H!==0?H:T.id-O.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var c=performance;e.unstable_now=function(){return c.now()}}else{var u=Date,f=u.now();e.unstable_now=function(){return u.now()-f}}var m=[],p=[],g=1,v=null,y=3,b=!1,E=!1,S=!1,C=!1,j=typeof setTimeout=="function"?setTimeout:null,R=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function A(T){for(var O=o(p);O!==null;){if(O.callback===null)s(p);else if(O.startTime<=T)s(p),O.sortIndex=O.expirationTime,r(m,O);else break;O=o(p)}}function D(T){if(S=!1,A(T),!E)if(o(m)!==null)E=!0,z||(z=!0,q());else{var O=o(p);O!==null&&$(D,O.startTime-T)}}var z=!1,B=-1,U=5,Y=-1;function oe(){return C?!0:!(e.unstable_now()-YT&&oe());){var P=v.callback;if(typeof P=="function"){v.callback=null,y=v.priorityLevel;var M=P(v.expirationTime<=T);if(T=e.unstable_now(),typeof M=="function"){v.callback=M,A(T),O=!0;break t}v===o(m)&&s(m),A(T)}else s(m);v=o(m)}if(v!==null)O=!0;else{var I=o(p);I!==null&&$(D,I.startTime-T),O=!1}}break e}finally{v=null,y=H,b=!1}O=void 0}}finally{O?q():z=!1}}}var q;if(typeof N=="function")q=function(){N(J)};else if(typeof MessageChannel<"u"){var re=new MessageChannel,L=re.port2;re.port1.onmessage=J,q=function(){L.postMessage(null)}}else q=function(){j(J,0)};function $(T,O){B=j(function(){T(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(T){T.callback=null},e.unstable_forceFrameRate=function(T){0>T||125P?(T.sortIndex=H,r(p,T),o(m)===null&&T===o(p)&&(S?(R(B),B=-1):S=!0,$(D,H-P))):(T.sortIndex=M,r(m,T),E||b||(E=!0,z||(z=!0,q()))),T},e.unstable_shouldYield=oe,e.unstable_wrapCallback=function(T){var O=y;return function(){var H=y;y=O;try{return T.apply(this,arguments)}finally{y=H}}}})(oh)),oh}var Kx;function s_(){return Kx||(Kx=1,rh.exports=a_()),rh.exports}var ah={exports:{}},Nt={};/** - * @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 Qx;function i_(){if(Qx)return Nt;Qx=1;var e=Ri();function r(m){var p="https://react.dev/errors/"+m;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(r){console.error(r)}}return e(),ah.exports=i_(),ah.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 Jx;function l_(){if(Jx)return si;Jx=1;var e=s_(),r=Ri(),o=Yv();function s(t){var n="https://react.dev/errors/"+t;if(1M||(t.current=P[M],P[M]=null,M--)}function F(t,n){M++,P[M]=t.current,t.current=n}var ee=I(null),ue=I(null),se=I(null),Q=I(null);function ae(t,n){switch(F(se,n),F(ue,t),F(ee,null),n.nodeType){case 9:case 11:t=(t=n.documentElement)&&(t=t.namespaceURI)?vx(t):0;break;default:if(t=n.tagName,n=n.namespaceURI)n=vx(n),t=bx(n,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}X(ee),F(ee,t)}function de(){X(ee),X(ue),X(se)}function he(t){t.memoizedState!==null&&F(Q,t);var n=ee.current,a=bx(n,t.type);n!==a&&(F(ue,t),F(ee,a))}function me(t){ue.current===t&&(X(ee),X(ue)),Q.current===t&&(X(Q),ei._currentValue=H)}var fe=Object.prototype.hasOwnProperty,ve=e.unstable_scheduleCallback,we=e.unstable_cancelCallback,Be=e.unstable_shouldYield,Je=e.unstable_requestPaint,Xe=e.unstable_now,hr=e.unstable_getCurrentPriorityLevel,Qr=e.unstable_ImmediatePriority,Wr=e.unstable_UserBlockingPriority,zn=e.unstable_NormalPriority,Jr=e.unstable_LowPriority,mr=e.unstable_IdlePriority,Ja=e.log,Ho=e.unstable_setDisableYieldValue,qt=null,lt=null;function rn(t){if(typeof Ja=="function"&&Ho(t),lt&&typeof lt.setStrictMode=="function")try{lt.setStrictMode(qt,t)}catch{}}var _t=Math.clz32?Math.clz32:Pu,es=Math.log,Uu=Math.LN2;function Pu(t){return t>>>=0,t===0?32:31-(es(t)/Uu|0)|0}var Bo=256,Io=4194304;function Ln(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 Uo(t,n,a){var i=t.pendingLanes;if(i===0)return 0;var h=0,x=t.suspendedLanes,_=t.pingedLanes;t=t.warmLanes;var k=i&134217727;return k!==0?(i=k&~x,i!==0?h=Ln(i):(_&=k,_!==0?h=Ln(_):a||(a=k&~t,a!==0&&(h=Ln(a))))):(k=i&~x,k!==0?h=Ln(k):_!==0?h=Ln(_):a||(a=i&~t,a!==0&&(h=Ln(a)))),h===0?0:n!==0&&n!==h&&(n&x)===0&&(x=h&-h,a=n&-n,x>=a||x===32&&(a&4194048)!==0)?n:h}function eo(t,n){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&n)===0}function Vu(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 qi(){var t=Bo;return Bo<<=1,(Bo&4194048)===0&&(Bo=256),t}function Xi(){var t=Io;return Io<<=1,(Io&62914560)===0&&(Io=4194304),t}function ts(t){for(var n=[],a=0;31>a;a++)n.push(t);return n}function to(t,n){t.pendingLanes|=n,n!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function $u(t,n,a,i,h,x){var _=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 k=t.entanglements,V=t.expirationTimes,W=t.hiddenUpdates;for(a=_&~a;0)":-1h||V[i]!==W[h]){var ie=` -`+V[i].replace(" at new "," at ");return t.displayName&&ie.includes("")&&(ie=ie.replace("",t.displayName)),ie}while(1<=i&&0<=h);break}}}finally{cs=!1,Error.prepareStackTrace=a}return(a=t?t.displayName||t.name:"")?Un(a):""}function Fu(t){switch(t.tag){case 26:case 27:case 5:return Un(t.type);case 16:return Un("Lazy");case 13:return Un("Suspense");case 19:return Un("SuspenseList");case 0:case 15:return us(t.type,!1);case 11:return us(t.type.render,!1);case 1:return us(t.type,!0);case 31:return Un("Activity");default:return""}}function tl(t){try{var n="";do n+=Fu(t),t=t.return;while(t);return n}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}function Mt(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function nl(t){var n=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function Ku(t){var n=nl(t)?"checked":"value",a=Object.getOwnPropertyDescriptor(t.constructor.prototype,n),i=""+t[n];if(!t.hasOwnProperty(n)&&typeof a<"u"&&typeof a.get=="function"&&typeof a.set=="function"){var h=a.get,x=a.set;return Object.defineProperty(t,n,{configurable:!0,get:function(){return h.call(this)},set:function(_){i=""+_,x.call(this,_)}}),Object.defineProperty(t,n,{enumerable:a.enumerable}),{getValue:function(){return i},setValue:function(_){i=""+_},stopTracking:function(){t._valueTracker=null,delete t[n]}}}}function $o(t){t._valueTracker||(t._valueTracker=Ku(t))}function ds(t){if(!t)return!1;var n=t._valueTracker;if(!n)return!0;var a=n.getValue(),i="";return t&&(i=nl(t)?t.checked?"true":"false":t.value),t=i,t!==a?(n.setValue(t),!0):!1}function Yo(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 Qu=/[\n"\\]/g;function At(t){return t.replace(Qu,function(n){return"\\"+n.charCodeAt(0).toString(16)+" "})}function ro(t,n,a,i,h,x,_,k){t.name="",_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"?t.type=_:t.removeAttribute("type"),n!=null?_==="number"?(n===0&&t.value===""||t.value!=n)&&(t.value=""+Mt(n)):t.value!==""+Mt(n)&&(t.value=""+Mt(n)):_!=="submit"&&_!=="reset"||t.removeAttribute("value"),n!=null?fs(t,_,Mt(n)):a!=null?fs(t,_,Mt(a)):i!=null&&t.removeAttribute("value"),h==null&&x!=null&&(t.defaultChecked=!!x),h!=null&&(t.checked=h&&typeof h!="function"&&typeof h!="symbol"),k!=null&&typeof k!="function"&&typeof k!="symbol"&&typeof k!="boolean"?t.name=""+Mt(k):t.removeAttribute("name")}function rl(t,n,a,i,h,x,_,k){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?""+Mt(a):"",n=n!=null?""+Mt(n):a,k||n===t.value||(t.value=n),t.defaultValue=n}i=i??h,i=typeof i!="function"&&typeof i!="symbol"&&!!i,t.checked=k?t.checked:!!i,t.defaultChecked=!!i,_!=null&&typeof _!="function"&&typeof _!="symbol"&&typeof _!="boolean"&&(t.name=_)}function fs(t,n,a){n==="number"&&Yo(t.ownerDocument)===t||t.defaultValue===""+a||(t.defaultValue=""+a)}function Pn(t,n,a,i){if(t=t.options,n){n={};for(var h=0;h"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),nd=!1;if(Vn)try{var ms={};Object.defineProperty(ms,"passive",{get:function(){nd=!0}}),window.addEventListener("test",ms,ms),window.removeEventListener("test",ms,ms)}catch{nd=!1}var vr=null,rd=null,al=null;function Ep(){if(al)return al;var t,n=rd,a=n.length,i,h="value"in vr?vr.value:vr.textContent,x=h.length;for(t=0;t=xs),Ap=" ",Tp=!1;function Rp(t,n){switch(t){case"keyup":return _E.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function kp(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Zo=!1;function jE(t,n){switch(t){case"compositionend":return kp(n);case"keypress":return n.which!==32?null:(Tp=!0,Ap);case"textInput":return t=n.data,t===Ap&&Tp?null:t;default:return null}}function ME(t,n){if(Zo)return t==="compositionend"||!ld&&Rp(t,n)?(t=Ep(),al=rd=vr=null,Zo=!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=i}e:{for(;a;){if(a.nextSibling){a=a.nextSibling;break e}a=a.parentNode}a=void 0}a=Up(a)}}function Vp(t,n){return t&&n?t===n?!0:t&&t.nodeType===3?!1:n&&n.nodeType===3?Vp(t,n.parentNode):"contains"in t?t.contains(n):t.compareDocumentPosition?!!(t.compareDocumentPosition(n)&16):!1:!1}function $p(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var n=Yo(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=Yo(t.document)}return n}function dd(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 LE=Vn&&"documentMode"in document&&11>=document.documentMode,Fo=null,fd=null,ws=null,hd=!1;function Yp(t,n,a){var i=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;hd||Fo==null||Fo!==Yo(i)||(i=Fo,"selectionStart"in i&&dd(i)?i={start:i.selectionStart,end:i.selectionEnd}:(i=(i.ownerDocument&&i.ownerDocument.defaultView||window).getSelection(),i={anchorNode:i.anchorNode,anchorOffset:i.anchorOffset,focusNode:i.focusNode,focusOffset:i.focusOffset}),ws&&bs(ws,i)||(ws=i,i=Fl(fd,"onSelect"),0>=_,h-=_,Yn=1<<32-_t(n)+h|a<x?x:8;var _=T.T,k={};T.T=k,Wd(t,!1,n,a);try{var V=h(),W=T.S;if(W!==null&&W(k,V),V!==null&&typeof V=="object"&&typeof V.then=="function"){var ie=GE(V,i);Ls(t,n,ie,Ut(t))}else Ls(t,n,i,Ut(t))}catch(ce){Ls(t,n,{then:function(){},status:"rejected",reason:ce},Ut())}finally{O.p=x,T.T=_}}function KE(){}function Kd(t,n,a,i){if(t.tag!==5)throw Error(s(476));var h=Gg(t).queue;Yg(t,h,n,H,a===null?KE:function(){return qg(t),a(i)})}function Gg(t){var n=t.memoizedState;if(n!==null)return n;n={memoizedState:H,baseState:H,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Zn,lastRenderedState:H},next:null};var a={};return n.next={memoizedState:a,baseState:a,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Zn,lastRenderedState:a},next:null},t.memoizedState=n,t=t.alternate,t!==null&&(t.memoizedState=n),n}function qg(t){var n=Gg(t).next.queue;Ls(t,n,{},Ut())}function Qd(){return Et(ei)}function Xg(){return st().memoizedState}function Zg(){return st().memoizedState}function QE(t){for(var n=t.return;n!==null;){switch(n.tag){case 24:case 3:var a=Ut();t=Sr(a);var i=Er(n,t,a);i!==null&&(Pt(i,n,a),Ts(i,n,a)),n={cache:jd()},t.payload=n;return}n=n.return}}function WE(t,n,a){var i=Ut();a={lane:i,revertLane:0,action:a,hasEagerState:!1,eagerState:null,next:null},Al(t)?Kg(n,a):(a=xd(t,n,a,i),a!==null&&(Pt(a,t,i),Qg(a,n,i)))}function Fg(t,n,a){var i=Ut();Ls(t,n,a,i)}function Ls(t,n,a,i){var h={lane:i,revertLane:0,action:a,hasEagerState:!1,eagerState:null,next:null};if(Al(t))Kg(n,h);else{var x=t.alternate;if(t.lanes===0&&(x===null||x.lanes===0)&&(x=n.lastRenderedReducer,x!==null))try{var _=n.lastRenderedState,k=x(_,a);if(h.hasEagerState=!0,h.eagerState=k,zt(k,_))return fl(t,n,h,0),Ze===null&&dl(),!1}catch{}finally{}if(a=xd(t,n,h,i),a!==null)return Pt(a,t,i),Qg(a,n,i),!0}return!1}function Wd(t,n,a,i){if(i={lane:2,revertLane:Rf(),action:i,hasEagerState:!1,eagerState:null,next:null},Al(t)){if(n)throw Error(s(479))}else n=xd(t,a,i,2),n!==null&&Pt(n,t,2)}function Al(t){var n=t.alternate;return t===Ae||n!==null&&n===Ae}function Kg(t,n){aa=El=!0;var a=t.pending;a===null?n.next=n:(n.next=a.next,a.next=n),t.pending=n}function Qg(t,n,a){if((a&4194048)!==0){var i=n.lanes;i&=t.pendingLanes,a|=i,n.lanes=a,ns(t,a)}}var Tl={readContext:Et,use:_l,useCallback:nt,useContext:nt,useEffect:nt,useImperativeHandle:nt,useLayoutEffect:nt,useInsertionEffect:nt,useMemo:nt,useReducer:nt,useRef:nt,useState:nt,useDebugValue:nt,useDeferredValue:nt,useTransition:nt,useSyncExternalStore:nt,useId:nt,useHostTransitionStatus:nt,useFormState:nt,useActionState:nt,useOptimistic:nt,useMemoCache:nt,useCacheRefresh:nt},Wg={readContext:Et,use:_l,useCallback:function(t,n){return Rt().memoizedState=[t,n===void 0?null:n],t},useContext:Et,useEffect:zg,useImperativeHandle:function(t,n,a){a=a!=null?a.concat([t]):null,Ml(4194308,4,Ig.bind(null,n,t),a)},useLayoutEffect:function(t,n){return Ml(4194308,4,t,n)},useInsertionEffect:function(t,n){Ml(4,2,t,n)},useMemo:function(t,n){var a=Rt();n=n===void 0?null:n;var i=t();if(go){rn(!0);try{t()}finally{rn(!1)}}return a.memoizedState=[i,n],i},useReducer:function(t,n,a){var i=Rt();if(a!==void 0){var h=a(n);if(go){rn(!0);try{a(n)}finally{rn(!1)}}}else h=n;return i.memoizedState=i.baseState=h,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:h},i.queue=t,t=t.dispatch=WE.bind(null,Ae,t),[i.memoizedState,t]},useRef:function(t){var n=Rt();return t={current:t},n.memoizedState=t},useState:function(t){t=qd(t);var n=t.queue,a=Fg.bind(null,Ae,n);return n.dispatch=a,[t.memoizedState,a]},useDebugValue:Zd,useDeferredValue:function(t,n){var a=Rt();return Fd(a,t,n)},useTransition:function(){var t=qd(!1);return t=Yg.bind(null,Ae,t.queue,!0,!1),Rt().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,n,a){var i=Ae,h=Rt();if(Ie){if(a===void 0)throw Error(s(407));a=a()}else{if(a=n(),Ze===null)throw Error(s(349));(Oe&124)!==0||vg(i,n,a)}h.memoizedState=a;var x={value:a,getSnapshot:n};return h.queue=x,zg(wg.bind(null,i,x,t),[t]),i.flags|=2048,ia(9,jl(),bg.bind(null,i,x,a,n),null),a},useId:function(){var t=Rt(),n=Ze.identifierPrefix;if(Ie){var a=Gn,i=Yn;a=(i&~(1<<32-_t(i)-1)).toString(32)+a,n="«"+n+"R"+a,a=Nl++,0Ne?(gt=be,be=null):gt=be.sibling;var Le=te(Z,be,K[Ne],le);if(Le===null){be===null&&(be=gt);break}t&&be&&Le.alternate===null&&n(Z,be),G=x(Le,G,Ne),Re===null?pe=Le:Re.sibling=Le,Re=Le,be=gt}if(Ne===K.length)return a(Z,be),Ie&&co(Z,Ne),pe;if(be===null){for(;NeNe?(gt=be,be=null):gt=be.sibling;var Ur=te(Z,be,Le.value,le);if(Ur===null){be===null&&(be=gt);break}t&&be&&Ur.alternate===null&&n(Z,be),G=x(Ur,G,Ne),Re===null?pe=Ur:Re.sibling=Ur,Re=Ur,be=gt}if(Le.done)return a(Z,be),Ie&&co(Z,Ne),pe;if(be===null){for(;!Le.done;Ne++,Le=K.next())Le=ce(Z,Le.value,le),Le!==null&&(G=x(Le,G,Ne),Re===null?pe=Le:Re.sibling=Le,Re=Le);return Ie&&co(Z,Ne),pe}for(be=i(be);!Le.done;Ne++,Le=K.next())Le=ne(be,Z,Ne,Le.value,le),Le!==null&&(t&&Le.alternate!==null&&be.delete(Le.key===null?Ne:Le.key),G=x(Le,G,Ne),Re===null?pe=Le:Re.sibling=Le,Re=Le);return t&&be.forEach(function(e_){return n(Z,e_)}),Ie&&co(Z,Ne),pe}function Ge(Z,G,K,le){if(typeof K=="object"&&K!==null&&K.type===E&&K.key===null&&(K=K.props.children),typeof K=="object"&&K!==null){switch(K.$$typeof){case y:e:{for(var pe=K.key;G!==null;){if(G.key===pe){if(pe=K.type,pe===E){if(G.tag===7){a(Z,G.sibling),le=h(G,K.props.children),le.return=Z,Z=le;break e}}else if(G.elementType===pe||typeof pe=="object"&&pe!==null&&pe.$$typeof===U&&e0(pe)===G.type){a(Z,G.sibling),le=h(G,K.props),Bs(le,K),le.return=Z,Z=le;break e}a(Z,G);break}else n(Z,G);G=G.sibling}K.type===E?(le=io(K.props.children,Z.mode,le,K.key),le.return=Z,Z=le):(le=ml(K.type,K.key,K.props,null,Z.mode,le),Bs(le,K),le.return=Z,Z=le)}return _(Z);case b:e:{for(pe=K.key;G!==null;){if(G.key===pe)if(G.tag===4&&G.stateNode.containerInfo===K.containerInfo&&G.stateNode.implementation===K.implementation){a(Z,G.sibling),le=h(G,K.children||[]),le.return=Z,Z=le;break e}else{a(Z,G);break}else n(Z,G);G=G.sibling}le=bd(K,Z.mode,le),le.return=Z,Z=le}return _(Z);case U:return pe=K._init,K=pe(K._payload),Ge(Z,G,K,le)}if($(K))return _e(Z,G,K,le);if(q(K)){if(pe=q(K),typeof pe!="function")throw Error(s(150));return K=pe.call(K),Ee(Z,G,K,le)}if(typeof K.then=="function")return Ge(Z,G,Rl(K),le);if(K.$$typeof===N)return Ge(Z,G,yl(Z,K),le);kl(Z,K)}return typeof K=="string"&&K!==""||typeof K=="number"||typeof K=="bigint"?(K=""+K,G!==null&&G.tag===6?(a(Z,G.sibling),le=h(G,K),le.return=Z,Z=le):(a(Z,G),le=vd(K,Z.mode,le),le.return=Z,Z=le),_(Z)):a(Z,G)}return function(Z,G,K,le){try{Hs=0;var pe=Ge(Z,G,K,le);return la=null,pe}catch(be){if(be===Ms||be===bl)throw be;var Re=Lt(29,be,null,Z.mode);return Re.lanes=le,Re.return=Z,Re}finally{}}}var ca=t0(!0),n0=t0(!1),Qt=I(null),vn=null;function _r(t){var n=t.alternate;F(ut,ut.current&1),F(Qt,t),vn===null&&(n===null||oa.current!==null||n.memoizedState!==null)&&(vn=t)}function r0(t){if(t.tag===22){if(F(ut,ut.current),F(Qt,t),vn===null){var n=t.alternate;n!==null&&n.memoizedState!==null&&(vn=t)}}else Cr()}function Cr(){F(ut,ut.current),F(Qt,Qt.current)}function Fn(t){X(Qt),vn===t&&(vn=null),X(ut)}var ut=I(0);function Dl(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==="$?"||$f(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 Jd(t,n,a,i){n=t.memoizedState,a=a(i,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 i=Ut(),h=Sr(i);h.payload=n,a!=null&&(h.callback=a),n=Er(t,h,i),n!==null&&(Pt(n,t,i),Ts(n,t,i))},enqueueReplaceState:function(t,n,a){t=t._reactInternals;var i=Ut(),h=Sr(i);h.tag=1,h.payload=n,a!=null&&(h.callback=a),n=Er(t,h,i),n!==null&&(Pt(n,t,i),Ts(n,t,i))},enqueueForceUpdate:function(t,n){t=t._reactInternals;var a=Ut(),i=Sr(a);i.tag=2,n!=null&&(i.callback=n),n=Er(t,i,a),n!==null&&(Pt(n,t,a),Ts(n,t,a))}};function o0(t,n,a,i,h,x,_){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(i,x,_):n.prototype&&n.prototype.isPureReactComponent?!bs(a,i)||!bs(h,x):!0}function a0(t,n,a,i){t=n.state,typeof n.componentWillReceiveProps=="function"&&n.componentWillReceiveProps(a,i),typeof n.UNSAFE_componentWillReceiveProps=="function"&&n.UNSAFE_componentWillReceiveProps(a,i),n.state!==t&&ef.enqueueReplaceState(n,n.state,null)}function xo(t,n){var a=n;if("ref"in n){a={};for(var i in n)i!=="ref"&&(a[i]=n[i])}if(t=t.defaultProps){a===n&&(a=g({},a));for(var h in t)a[h]===void 0&&(a[h]=t[h])}return a}var Ol=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 s0(t){Ol(t)}function i0(t){console.error(t)}function l0(t){Ol(t)}function zl(t,n){try{var a=t.onUncaughtError;a(n.value,{componentStack:n.stack})}catch(i){setTimeout(function(){throw i})}}function c0(t,n,a){try{var i=t.onCaughtError;i(a.value,{componentStack:a.stack,errorBoundary:n.tag===1?n.stateNode:null})}catch(h){setTimeout(function(){throw h})}}function tf(t,n,a){return a=Sr(a),a.tag=3,a.payload={element:null},a.callback=function(){zl(t,n)},a}function u0(t){return t=Sr(t),t.tag=3,t}function d0(t,n,a,i){var h=a.type.getDerivedStateFromError;if(typeof h=="function"){var x=i.value;t.payload=function(){return h(x)},t.callback=function(){c0(n,a,i)}}var _=a.stateNode;_!==null&&typeof _.componentDidCatch=="function"&&(t.callback=function(){c0(n,a,i),typeof h!="function"&&(kr===null?kr=new Set([this]):kr.add(this));var k=i.stack;this.componentDidCatch(i.value,{componentStack:k!==null?k:""})})}function eN(t,n,a,i,h){if(a.flags|=32768,i!==null&&typeof i=="object"&&typeof i.then=="function"){if(n=a.alternate,n!==null&&_s(n,a,h,!0),a=Qt.current,a!==null){switch(a.tag){case 13:return vn===null?Cf():a.alternate===null&&tt===0&&(tt=3),a.flags&=-257,a.flags|=65536,a.lanes=h,i===Td?a.flags|=16384:(n=a.updateQueue,n===null?a.updateQueue=new Set([i]):n.add(i),Mf(t,i,h)),!1;case 22:return a.flags|=65536,i===Td?a.flags|=16384:(n=a.updateQueue,n===null?(n={transitions:null,markerInstances:null,retryQueue:new Set([i])},a.updateQueue=n):(a=n.retryQueue,a===null?n.retryQueue=new Set([i]):a.add(i)),Mf(t,i,h)),!1}throw Error(s(435,a.tag))}return Mf(t,i,h),Cf(),!1}if(Ie)return n=Qt.current,n!==null?((n.flags&65536)===0&&(n.flags|=256),n.flags|=65536,n.lanes=h,i!==Ed&&(t=Error(s(422),{cause:i}),Ns(Xt(t,a)))):(i!==Ed&&(n=Error(s(423),{cause:i}),Ns(Xt(n,a))),t=t.current.alternate,t.flags|=65536,h&=-h,t.lanes|=h,i=Xt(i,a),h=tf(t.stateNode,i,h),Dd(t,h),tt!==4&&(tt=2)),!1;var x=Error(s(520),{cause:i});if(x=Xt(x,a),Gs===null?Gs=[x]:Gs.push(x),tt!==4&&(tt=2),n===null)return!0;i=Xt(i,a),a=n;do{switch(a.tag){case 3:return a.flags|=65536,t=h&-h,a.lanes|=t,t=tf(a.stateNode,i,t),Dd(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"&&(kr===null||!kr.has(x))))return a.flags|=65536,h&=-h,a.lanes|=h,h=u0(h),d0(h,t,a,i),Dd(a,h),!1}a=a.return}while(a!==null);return!1}var f0=Error(s(461)),mt=!1;function xt(t,n,a,i){n.child=t===null?n0(n,null,a,i):ca(n,t.child,a,i)}function h0(t,n,a,i,h){a=a.render;var x=n.ref;if("ref"in i){var _={};for(var k in i)k!=="ref"&&(_[k]=i[k])}else _=i;return mo(n),i=Bd(t,n,a,_,x,h),k=Id(),t!==null&&!mt?(Ud(t,n,h),Kn(t,n,h)):(Ie&&k&&wd(n),n.flags|=1,xt(t,n,i,h),n.child)}function m0(t,n,a,i,h){if(t===null){var x=a.type;return typeof x=="function"&&!yd(x)&&x.defaultProps===void 0&&a.compare===null?(n.tag=15,n.type=x,p0(t,n,x,i,h)):(t=ml(a.type,null,i,n,n.mode,h),t.ref=n.ref,t.return=n,n.child=t)}if(x=t.child,!uf(t,h)){var _=x.memoizedProps;if(a=a.compare,a=a!==null?a:bs,a(_,i)&&t.ref===n.ref)return Kn(t,n,h)}return n.flags|=1,t=$n(x,i),t.ref=n.ref,t.return=n,n.child=t}function p0(t,n,a,i,h){if(t!==null){var x=t.memoizedProps;if(bs(x,i)&&t.ref===n.ref)if(mt=!1,n.pendingProps=i=x,uf(t,h))(t.flags&131072)!==0&&(mt=!0);else return n.lanes=t.lanes,Kn(t,n,h)}return nf(t,n,a,i,h)}function g0(t,n,a){var i=n.pendingProps,h=i.children,x=t!==null?t.memoizedState:null;if(i.mode==="hidden"){if((n.flags&128)!==0){if(i=x!==null?x.baseLanes|a:a,t!==null){for(h=n.child=t.child,x=0;h!==null;)x=x|h.lanes|h.childLanes,h=h.sibling;n.childLanes=x&~i}else n.childLanes=0,n.child=null;return x0(t,n,i,a)}if((a&536870912)!==0)n.memoizedState={baseLanes:0,cachePool:null},t!==null&&vl(n,x!==null?x.cachePool:null),x!==null?pg(n,x):zd(),r0(n);else return n.lanes=n.childLanes=536870912,x0(t,n,x!==null?x.baseLanes|a:a,a)}else x!==null?(vl(n,x.cachePool),pg(n,x),Cr(),n.memoizedState=null):(t!==null&&vl(n,null),zd(),Cr());return xt(t,n,h,a),n.child}function x0(t,n,a,i){var h=Ad();return h=h===null?null:{parent:ct._currentValue,pool:h},n.memoizedState={baseLanes:a,cachePool:h},t!==null&&vl(n,null),zd(),r0(n),t!==null&&_s(t,n,i,!0),null}function Ll(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 nf(t,n,a,i,h){return mo(n),a=Bd(t,n,a,i,void 0,h),i=Id(),t!==null&&!mt?(Ud(t,n,h),Kn(t,n,h)):(Ie&&i&&wd(n),n.flags|=1,xt(t,n,a,h),n.child)}function y0(t,n,a,i,h,x){return mo(n),n.updateQueue=null,a=xg(n,i,a,h),gg(t),i=Id(),t!==null&&!mt?(Ud(t,n,x),Kn(t,n,x)):(Ie&&i&&wd(n),n.flags|=1,xt(t,n,a,x),n.child)}function v0(t,n,a,i,h){if(mo(n),n.stateNode===null){var x=Jo,_=a.contextType;typeof _=="object"&&_!==null&&(x=Et(_)),x=new a(i,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=i,x.state=n.memoizedState,x.refs={},Rd(n),_=a.contextType,x.context=typeof _=="object"&&_!==null?Et(_):Jo,x.state=n.memoizedState,_=a.getDerivedStateFromProps,typeof _=="function"&&(Jd(n,a,_,i),x.state=n.memoizedState),typeof a.getDerivedStateFromProps=="function"||typeof x.getSnapshotBeforeUpdate=="function"||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(_=x.state,typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount(),_!==x.state&&ef.enqueueReplaceState(x,x.state,null),ks(n,i,x,h),Rs(),x.state=n.memoizedState),typeof x.componentDidMount=="function"&&(n.flags|=4194308),i=!0}else if(t===null){x=n.stateNode;var k=n.memoizedProps,V=xo(a,k);x.props=V;var W=x.context,ie=a.contextType;_=Jo,typeof ie=="object"&&ie!==null&&(_=Et(ie));var ce=a.getDerivedStateFromProps;ie=typeof ce=="function"||typeof x.getSnapshotBeforeUpdate=="function",k=n.pendingProps!==k,ie||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(k||W!==_)&&a0(n,x,i,_),wr=!1;var te=n.memoizedState;x.state=te,ks(n,i,x,h),Rs(),W=n.memoizedState,k||te!==W||wr?(typeof ce=="function"&&(Jd(n,a,ce,i),W=n.memoizedState),(V=wr||o0(n,a,V,i,te,W,_))?(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=i,n.memoizedState=W),x.props=i,x.state=W,x.context=_,i=V):(typeof x.componentDidMount=="function"&&(n.flags|=4194308),i=!1)}else{x=n.stateNode,kd(t,n),_=n.memoizedProps,ie=xo(a,_),x.props=ie,ce=n.pendingProps,te=x.context,W=a.contextType,V=Jo,typeof W=="object"&&W!==null&&(V=Et(W)),k=a.getDerivedStateFromProps,(W=typeof k=="function"||typeof x.getSnapshotBeforeUpdate=="function")||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(_!==ce||te!==V)&&a0(n,x,i,V),wr=!1,te=n.memoizedState,x.state=te,ks(n,i,x,h),Rs();var ne=n.memoizedState;_!==ce||te!==ne||wr||t!==null&&t.dependencies!==null&&xl(t.dependencies)?(typeof k=="function"&&(Jd(n,a,k,i),ne=n.memoizedState),(ie=wr||o0(n,a,ie,i,te,ne,V)||t!==null&&t.dependencies!==null&&xl(t.dependencies))?(W||typeof x.UNSAFE_componentWillUpdate!="function"&&typeof x.componentWillUpdate!="function"||(typeof x.componentWillUpdate=="function"&&x.componentWillUpdate(i,ne,V),typeof x.UNSAFE_componentWillUpdate=="function"&&x.UNSAFE_componentWillUpdate(i,ne,V)),typeof x.componentDidUpdate=="function"&&(n.flags|=4),typeof x.getSnapshotBeforeUpdate=="function"&&(n.flags|=1024)):(typeof x.componentDidUpdate!="function"||_===t.memoizedProps&&te===t.memoizedState||(n.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||_===t.memoizedProps&&te===t.memoizedState||(n.flags|=1024),n.memoizedProps=i,n.memoizedState=ne),x.props=i,x.state=ne,x.context=V,i=ie):(typeof x.componentDidUpdate!="function"||_===t.memoizedProps&&te===t.memoizedState||(n.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||_===t.memoizedProps&&te===t.memoizedState||(n.flags|=1024),i=!1)}return x=i,Ll(t,n),i=(n.flags&128)!==0,x||i?(x=n.stateNode,a=i&&typeof a.getDerivedStateFromError!="function"?null:x.render(),n.flags|=1,t!==null&&i?(n.child=ca(n,t.child,null,h),n.child=ca(n,null,a,h)):xt(t,n,a,h),n.memoizedState=x.state,t=n.child):t=Kn(t,n,h),t}function b0(t,n,a,i){return Es(),n.flags|=256,xt(t,n,a,i),n.child}var rf={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function of(t){return{baseLanes:t,cachePool:ig()}}function af(t,n,a){return t=t!==null?t.childLanes&~a:0,n&&(t|=Wt),t}function w0(t,n,a){var i=n.pendingProps,h=!1,x=(n.flags&128)!==0,_;if((_=x)||(_=t!==null&&t.memoizedState===null?!1:(ut.current&2)!==0),_&&(h=!0,n.flags&=-129),_=(n.flags&32)!==0,n.flags&=-33,t===null){if(Ie){if(h?_r(n):Cr(),Ie){var k=et,V;if(V=k){e:{for(V=k,k=yn;V.nodeType!==8;){if(!k){k=null;break e}if(V=ln(V.nextSibling),V===null){k=null;break e}}k=V}k!==null?(n.memoizedState={dehydrated:k,treeContext:lo!==null?{id:Yn,overflow:Gn}:null,retryLane:536870912,hydrationErrors:null},V=Lt(18,null,null,0),V.stateNode=k,V.return=n,n.child=V,Ct=n,et=null,V=!0):V=!1}V||fo(n)}if(k=n.memoizedState,k!==null&&(k=k.dehydrated,k!==null))return $f(k)?n.lanes=32:n.lanes=536870912,null;Fn(n)}return k=i.children,i=i.fallback,h?(Cr(),h=n.mode,k=Hl({mode:"hidden",children:k},h),i=io(i,h,a,null),k.return=n,i.return=n,k.sibling=i,n.child=k,h=n.child,h.memoizedState=of(a),h.childLanes=af(t,_,a),n.memoizedState=rf,i):(_r(n),sf(n,k))}if(V=t.memoizedState,V!==null&&(k=V.dehydrated,k!==null)){if(x)n.flags&256?(_r(n),n.flags&=-257,n=lf(t,n,a)):n.memoizedState!==null?(Cr(),n.child=t.child,n.flags|=128,n=null):(Cr(),h=i.fallback,k=n.mode,i=Hl({mode:"visible",children:i.children},k),h=io(h,k,a,null),h.flags|=2,i.return=n,h.return=n,i.sibling=h,n.child=i,ca(n,t.child,null,a),i=n.child,i.memoizedState=of(a),i.childLanes=af(t,_,a),n.memoizedState=rf,n=h);else if(_r(n),$f(k)){if(_=k.nextSibling&&k.nextSibling.dataset,_)var W=_.dgst;_=W,i=Error(s(419)),i.stack="",i.digest=_,Ns({value:i,source:null,stack:null}),n=lf(t,n,a)}else if(mt||_s(t,n,a,!1),_=(a&t.childLanes)!==0,mt||_){if(_=Ze,_!==null&&(i=a&-a,i=(i&42)!==0?1:rs(i),i=(i&(_.suspendedLanes|a))!==0?0:i,i!==0&&i!==V.retryLane))throw V.retryLane=i,Wo(t,i),Pt(_,t,i),f0;k.data==="$?"||Cf(),n=lf(t,n,a)}else k.data==="$?"?(n.flags|=192,n.child=t.child,n=null):(t=V.treeContext,et=ln(k.nextSibling),Ct=n,Ie=!0,uo=null,yn=!1,t!==null&&(Ft[Kt++]=Yn,Ft[Kt++]=Gn,Ft[Kt++]=lo,Yn=t.id,Gn=t.overflow,lo=n),n=sf(n,i.children),n.flags|=4096);return n}return h?(Cr(),h=i.fallback,k=n.mode,V=t.child,W=V.sibling,i=$n(V,{mode:"hidden",children:i.children}),i.subtreeFlags=V.subtreeFlags&65011712,W!==null?h=$n(W,h):(h=io(h,k,a,null),h.flags|=2),h.return=n,i.return=n,i.sibling=h,n.child=i,i=h,h=n.child,k=t.child.memoizedState,k===null?k=of(a):(V=k.cachePool,V!==null?(W=ct._currentValue,V=V.parent!==W?{parent:W,pool:W}:V):V=ig(),k={baseLanes:k.baseLanes|a,cachePool:V}),h.memoizedState=k,h.childLanes=af(t,_,a),n.memoizedState=rf,i):(_r(n),a=t.child,t=a.sibling,a=$n(a,{mode:"visible",children:i.children}),a.return=n,a.sibling=null,t!==null&&(_=n.deletions,_===null?(n.deletions=[t],n.flags|=16):_.push(t)),n.child=a,n.memoizedState=null,a)}function sf(t,n){return n=Hl({mode:"visible",children:n},t.mode),n.return=t,t.child=n}function Hl(t,n){return t=Lt(22,t,null,n),t.lanes=0,t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},t}function lf(t,n,a){return ca(n,t.child,null,a),t=sf(n,n.pendingProps.children),t.flags|=2,n.memoizedState=null,t}function S0(t,n,a){t.lanes|=n;var i=t.alternate;i!==null&&(i.lanes|=n),_d(t.return,n,a)}function cf(t,n,a,i,h){var x=t.memoizedState;x===null?t.memoizedState={isBackwards:n,rendering:null,renderingStartTime:0,last:i,tail:a,tailMode:h}:(x.isBackwards=n,x.rendering=null,x.renderingStartTime=0,x.last=i,x.tail=a,x.tailMode=h)}function E0(t,n,a){var i=n.pendingProps,h=i.revealOrder,x=i.tail;if(xt(t,n,i.children,a),i=ut.current,(i&2)!==0)i=i&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&&S0(t,a,n);else if(t.tag===19)S0(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}i&=1}switch(F(ut,i),h){case"forwards":for(a=n.child,h=null;a!==null;)t=a.alternate,t!==null&&Dl(t)===null&&(h=a),a=a.sibling;a=h,a===null?(h=n.child,n.child=null):(h=a.sibling,a.sibling=null),cf(n,!1,h,a,x);break;case"backwards":for(a=null,h=n.child,n.child=null;h!==null;){if(t=h.alternate,t!==null&&Dl(t)===null){n.child=h;break}t=h.sibling,h.sibling=a,a=h,h=t}cf(n,!0,a,null,x);break;case"together":cf(n,!1,null,null,void 0);break;default:n.memoizedState=null}return n.child}function Kn(t,n,a){if(t!==null&&(n.dependencies=t.dependencies),Rr|=n.lanes,(a&n.childLanes)===0)if(t!==null){if(_s(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=$n(t,t.pendingProps),n.child=a,a.return=n;t.sibling!==null;)t=t.sibling,a=a.sibling=$n(t,t.pendingProps),a.return=n;a.sibling=null}return n.child}function uf(t,n){return(t.lanes&n)!==0?!0:(t=t.dependencies,!!(t!==null&&xl(t)))}function tN(t,n,a){switch(n.tag){case 3:ae(n,n.stateNode.containerInfo),br(n,ct,t.memoizedState.cache),Es();break;case 27:case 5:he(n);break;case 4:ae(n,n.stateNode.containerInfo);break;case 10:br(n,n.type,n.memoizedProps.value);break;case 13:var i=n.memoizedState;if(i!==null)return i.dehydrated!==null?(_r(n),n.flags|=128,null):(a&n.child.childLanes)!==0?w0(t,n,a):(_r(n),t=Kn(t,n,a),t!==null?t.sibling:null);_r(n);break;case 19:var h=(t.flags&128)!==0;if(i=(a&n.childLanes)!==0,i||(_s(t,n,a,!1),i=(a&n.childLanes)!==0),h){if(i)return E0(t,n,a);n.flags|=128}if(h=n.memoizedState,h!==null&&(h.rendering=null,h.tail=null,h.lastEffect=null),F(ut,ut.current),i)break;return null;case 22:case 23:return n.lanes=0,g0(t,n,a);case 24:br(n,ct,t.memoizedState.cache)}return Kn(t,n,a)}function N0(t,n,a){if(t!==null)if(t.memoizedProps!==n.pendingProps)mt=!0;else{if(!uf(t,a)&&(n.flags&128)===0)return mt=!1,tN(t,n,a);mt=(t.flags&131072)!==0}else mt=!1,Ie&&(n.flags&1048576)!==0&&eg(n,gl,n.index);switch(n.lanes=0,n.tag){case 16:e:{t=n.pendingProps;var i=n.elementType,h=i._init;if(i=h(i._payload),n.type=i,typeof i=="function")yd(i)?(t=xo(i,t),n.tag=1,n=v0(null,n,i,t,a)):(n.tag=0,n=nf(null,n,i,t,a));else{if(i!=null){if(h=i.$$typeof,h===A){n.tag=11,n=h0(null,n,i,t,a);break e}else if(h===B){n.tag=14,n=m0(null,n,i,t,a);break e}}throw n=L(i)||i,Error(s(306,n,""))}}return n;case 0:return nf(t,n,n.type,n.pendingProps,a);case 1:return i=n.type,h=xo(i,n.pendingProps),v0(t,n,i,h,a);case 3:e:{if(ae(n,n.stateNode.containerInfo),t===null)throw Error(s(387));i=n.pendingProps;var x=n.memoizedState;h=x.element,kd(t,n),ks(n,i,null,a);var _=n.memoizedState;if(i=_.cache,br(n,ct,i),i!==x.cache&&Cd(n,[ct],a,!0),Rs(),i=_.element,x.isDehydrated)if(x={element:i,isDehydrated:!1,cache:_.cache},n.updateQueue.baseState=x,n.memoizedState=x,n.flags&256){n=b0(t,n,i,a);break e}else if(i!==h){h=Xt(Error(s(424)),n),Ns(h),n=b0(t,n,i,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(et=ln(t.firstChild),Ct=n,Ie=!0,uo=null,yn=!0,a=n0(n,null,i,a),n.child=a;a;)a.flags=a.flags&-3|4096,a=a.sibling}else{if(Es(),i===h){n=Kn(t,n,a);break e}xt(t,n,i,a)}n=n.child}return n;case 26:return Ll(t,n),t===null?(a=Mx(n.type,null,n.pendingProps,null))?n.memoizedState=a:Ie||(a=n.type,t=n.pendingProps,i=Ql(se.current).createElement(a),i[ht]=n,i[St]=t,vt(i,a,t),ot(i),n.stateNode=i):n.memoizedState=Mx(n.type,t.memoizedProps,n.pendingProps,t.memoizedState),null;case 27:return he(n),t===null&&Ie&&(i=n.stateNode=_x(n.type,n.pendingProps,se.current),Ct=n,yn=!0,h=et,zr(n.type)?(Yf=h,et=ln(i.firstChild)):et=h),xt(t,n,n.pendingProps.children,a),Ll(t,n),t===null&&(n.flags|=4194304),n.child;case 5:return t===null&&Ie&&((h=i=et)&&(i=AN(i,n.type,n.pendingProps,yn),i!==null?(n.stateNode=i,Ct=n,et=ln(i.firstChild),yn=!1,h=!0):h=!1),h||fo(n)),he(n),h=n.type,x=n.pendingProps,_=t!==null?t.memoizedProps:null,i=x.children,Uf(h,x)?i=null:_!==null&&Uf(h,_)&&(n.flags|=32),n.memoizedState!==null&&(h=Bd(t,n,XE,null,null,a),ei._currentValue=h),Ll(t,n),xt(t,n,i,a),n.child;case 6:return t===null&&Ie&&((t=a=et)&&(a=TN(a,n.pendingProps,yn),a!==null?(n.stateNode=a,Ct=n,et=null,t=!0):t=!1),t||fo(n)),null;case 13:return w0(t,n,a);case 4:return ae(n,n.stateNode.containerInfo),i=n.pendingProps,t===null?n.child=ca(n,null,i,a):xt(t,n,i,a),n.child;case 11:return h0(t,n,n.type,n.pendingProps,a);case 7:return xt(t,n,n.pendingProps,a),n.child;case 8:return xt(t,n,n.pendingProps.children,a),n.child;case 12:return xt(t,n,n.pendingProps.children,a),n.child;case 10:return i=n.pendingProps,br(n,n.type,i.value),xt(t,n,i.children,a),n.child;case 9:return h=n.type._context,i=n.pendingProps.children,mo(n),h=Et(h),i=i(h),n.flags|=1,xt(t,n,i,a),n.child;case 14:return m0(t,n,n.type,n.pendingProps,a);case 15:return p0(t,n,n.type,n.pendingProps,a);case 19:return E0(t,n,a);case 31:return i=n.pendingProps,a=n.mode,i={mode:i.mode,children:i.children},t===null?(a=Hl(i,a),a.ref=n.ref,n.child=a,a.return=n,n=a):(a=$n(t.child,i),a.ref=n.ref,n.child=a,a.return=n,n=a),n;case 22:return g0(t,n,a);case 24:return mo(n),i=Et(ct),t===null?(h=Ad(),h===null&&(h=Ze,x=jd(),h.pooledCache=x,x.refCount++,x!==null&&(h.pooledCacheLanes|=a),h=x),n.memoizedState={parent:i,cache:h},Rd(n),br(n,ct,h)):((t.lanes&a)!==0&&(kd(t,n),ks(n,null,null,a),Rs()),h=t.memoizedState,x=n.memoizedState,h.parent!==i?(h={parent:i,cache:i},n.memoizedState=h,n.lanes===0&&(n.memoizedState=n.updateQueue.baseState=h),br(n,ct,i)):(i=x.cache,br(n,ct,i),i!==h.cache&&Cd(n,[ct],a,!0))),xt(t,n,n.pendingProps.children,a),n.child;case 29:throw n.pendingProps}throw Error(s(156,n.tag))}function Qn(t){t.flags|=4}function _0(t,n){if(n.type!=="stylesheet"||(n.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!Dx(n)){if(n=Qt.current,n!==null&&((Oe&4194048)===Oe?vn!==null:(Oe&62914560)!==Oe&&(Oe&536870912)===0||n!==vn))throw As=Td,lg;t.flags|=8192}}function Bl(t,n){n!==null&&(t.flags|=4),t.flags&16384&&(n=t.tag!==22?Xi():536870912,t.lanes|=n,ha|=n)}function Is(t,n){if(!Ie)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 i=null;a!==null;)a.alternate!==null&&(i=a),a=a.sibling;i===null?n||t.tail===null?t.tail=null:t.tail.sibling=null:i.sibling=null}}function Qe(t){var n=t.alternate!==null&&t.alternate.child===t.child,a=0,i=0;if(n)for(var h=t.child;h!==null;)a|=h.lanes|h.childLanes,i|=h.subtreeFlags&65011712,i|=h.flags&65011712,h.return=t,h=h.sibling;else for(h=t.child;h!==null;)a|=h.lanes|h.childLanes,i|=h.subtreeFlags,i|=h.flags,h.return=t,h=h.sibling;return t.subtreeFlags|=i,t.childLanes=a,n}function nN(t,n,a){var i=n.pendingProps;switch(Sd(n),n.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Qe(n),null;case 1:return Qe(n),null;case 3:return a=n.stateNode,i=null,t!==null&&(i=t.memoizedState.cache),n.memoizedState.cache!==i&&(n.flags|=2048),Xn(ct),de(),a.pendingContext&&(a.context=a.pendingContext,a.pendingContext=null),(t===null||t.child===null)&&(Ss(n)?Qn(n):t===null||t.memoizedState.isDehydrated&&(n.flags&256)===0||(n.flags|=1024,rg())),Qe(n),null;case 26:return a=n.memoizedState,t===null?(Qn(n),a!==null?(Qe(n),_0(n,a)):(Qe(n),n.flags&=-16777217)):a?a!==t.memoizedState?(Qn(n),Qe(n),_0(n,a)):(Qe(n),n.flags&=-16777217):(t.memoizedProps!==i&&Qn(n),Qe(n),n.flags&=-16777217),null;case 27:me(n),a=se.current;var h=n.type;if(t!==null&&n.stateNode!=null)t.memoizedProps!==i&&Qn(n);else{if(!i){if(n.stateNode===null)throw Error(s(166));return Qe(n),null}t=ee.current,Ss(n)?tg(n):(t=_x(h,i,a),n.stateNode=t,Qn(n))}return Qe(n),null;case 5:if(me(n),a=n.type,t!==null&&n.stateNode!=null)t.memoizedProps!==i&&Qn(n);else{if(!i){if(n.stateNode===null)throw Error(s(166));return Qe(n),null}if(t=ee.current,Ss(n))tg(n);else{switch(h=Ql(se.current),t){case 1:t=h.createElementNS("http://www.w3.org/2000/svg",a);break;case 2:t=h.createElementNS("http://www.w3.org/1998/Math/MathML",a);break;default:switch(a){case"svg":t=h.createElementNS("http://www.w3.org/2000/svg",a);break;case"math":t=h.createElementNS("http://www.w3.org/1998/Math/MathML",a);break;case"script":t=h.createElement("div"),t.innerHTML=" - + +
diff --git a/python/packages/devui/dev.md b/python/packages/devui/dev.md index 2ff2f430dc..f5881c2b79 100644 --- a/python/packages/devui/dev.md +++ b/python/packages/devui/dev.md @@ -1,6 +1,6 @@ # Testing DevUI - Quick Setup Guide -Hi everyone! Here are the step-by-step instructions to test the new DevUI feature: +Here are the step-by-step instructions to test the new DevUI feature: ## 1. Get the Code @@ -9,6 +9,8 @@ git pull git checkout victordibia/devui ``` +(or use the latest main branch if merged) + ## 2. Setup Environment Navigate to the Python directory and install dependencies: @@ -80,9 +82,29 @@ curl -X POST http://localhost:8080/v1/responses \ }' ``` +## API Mapping + +Messages and events from agents/workflows are mapped to OpenAI response types in `agent_framework_devui/_mapper.py`. See the mapping table below: + +| Agent Framework Content | OpenAI Event | Type | +| --------------------------------- | ----------------------------------------- | -------- | +| `TextContent` | `ResponseTextDeltaEvent` | Official | +| `TextReasoningContent` | `ResponseReasoningTextDeltaEvent` | Official | +| `FunctionCallContent` | `ResponseFunctionCallArgumentsDeltaEvent` | Official | +| `FunctionResultContent` | `ResponseFunctionResultComplete` | Custom | +| `ErrorContent` | `ResponseErrorEvent` | Official | +| `UsageContent` | `ResponseUsageEventComplete` | Custom | +| `DataContent` | `ResponseTraceEventComplete` | Custom | +| `UriContent` | `ResponseTraceEventComplete` | Custom | +| `HostedFileContent` | `ResponseTraceEventComplete` | Custom | +| `HostedVectorStoreContent` | `ResponseTraceEventComplete` | Custom | +| `FunctionApprovalRequestContent` | Custom event | Custom | +| `FunctionApprovalResponseContent` | Custom event | Custom | +| `WorkflowEvent` | `ResponseWorkflowEventComplete` | Custom | + ## Troubleshooting -- **Missing API key**: Make sure your `.env` file is in the `python/` directory with valid credentials +- **Missing API key**: Make sure your `.env` file is in the `python/` directory with valid credentials. Or set environment variables directly in your shell before running DevUI. - **Import errors**: Run `uv sync --dev` again to ensure all dependencies are installed - **Port conflicts**: DevUI uses ports 8080 and 8090 by default - close other services using these ports diff --git a/python/packages/devui/frontend/.gitignore b/python/packages/devui/frontend/.gitignore index 8b23663a88..95345bf2df 100644 --- a/python/packages/devui/frontend/.gitignore +++ b/python/packages/devui/frontend/.gitignore @@ -10,6 +10,7 @@ /build .env.* +claude.md # misc .DS_Store diff --git a/python/packages/devui/frontend/src/App.tsx b/python/packages/devui/frontend/src/App.tsx index c2e57918de..c191552006 100644 --- a/python/packages/devui/frontend/src/App.tsx +++ b/python/packages/devui/frontend/src/App.tsx @@ -7,12 +7,14 @@ import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { AppHeader } from "@/components/shared/app-header"; import { DebugPanel } from "@/components/shared/debug-panel"; -import { AboutModal } from "@/components/shared/about-modal"; +import { SettingsModal } from "@/components/shared/settings-modal"; +import { GalleryView } from "@/components/gallery"; import { AgentView } from "@/components/agent/agent-view"; import { WorkflowView } from "@/components/workflow/workflow-view"; import { LoadingState } from "@/components/ui/loading-state"; import { apiClient } from "@/services/api"; -import { ChevronLeft } from "lucide-react"; +import { ChevronLeft, ChevronDown, ServerOff } from "lucide-react"; +import type { SampleEntity } from "@/data/gallery"; import type { AgentInfo, WorkflowInfo, @@ -27,23 +29,23 @@ export default function App() { isLoading: true, }); - const [debugEvents, setDebugEvents] = useState( - [] - ); + const [debugEvents, setDebugEvents] = useState([]); const [debugPanelOpen, setDebugPanelOpen] = useState(true); const [debugPanelWidth, setDebugPanelWidth] = useState(() => { - // Initialize from localStorage or default to 320 const savedWidth = localStorage.getItem("debugPanelWidth"); return savedWidth ? parseInt(savedWidth, 10) : 320; }); const [isResizing, setIsResizing] = useState(false); const [showAboutModal, setShowAboutModal] = useState(false); + const [showGallery, setShowGallery] = useState(false); + const [addingEntityId, setAddingEntityId] = useState(null); + const [errorEntityId, setErrorEntityId] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); // Initialize app - load agents and workflows useEffect(() => { const loadData = async () => { try { - // Load agents and workflows in parallel const [agents, workflows] = await Promise.all([ apiClient.getAgents(), apiClient.getWorkflows(), @@ -135,6 +137,109 @@ export default function App() { } }, []); + // Handle adding sample entity + const handleAddSample = useCallback(async (sample: SampleEntity) => { + setAddingEntityId(sample.id); + setErrorEntityId(null); + setErrorMessage(null); + + try { + // Call backend to fetch and add entity + const newEntity = await apiClient.addEntity(sample.url, { + source: 'remote_gallery', + originalUrl: sample.url, + sampleId: sample.id + }); + + // Convert backend entity to frontend format + const convertedEntity = { + id: newEntity.id, + name: newEntity.name, + description: newEntity.description, + type: newEntity.type, + source: (newEntity.source as "directory" | "in_memory" | "remote_gallery") || 'remote_gallery', + has_env: false, + module_path: undefined + }; + + // Update app state + if (newEntity.type === 'agent') { + const agentEntity = { + ...convertedEntity, + tools: (newEntity.tools || []).map(tool => + typeof tool === 'string' ? tool : JSON.stringify(tool) + ) + } as AgentInfo; + + setAppState(prev => ({ + ...prev, + agents: [...prev.agents, agentEntity], + selectedAgent: agentEntity + })); + } else { + const workflowEntity = { + ...convertedEntity, + executors: (newEntity.tools || []).map(tool => + typeof tool === 'string' ? tool : JSON.stringify(tool) + ), + input_schema: { type: "string" }, + input_type_name: "Input", + start_executor_id: (newEntity.tools && newEntity.tools.length > 0) + ? (typeof newEntity.tools[0] === 'string' ? newEntity.tools[0] : JSON.stringify(newEntity.tools[0])) + : "unknown" + } as WorkflowInfo; + + setAppState(prev => ({ + ...prev, + workflows: [...prev.workflows, workflowEntity], + selectedAgent: workflowEntity + })); + } + + // Close gallery and clear debug events + setShowGallery(false); + setDebugEvents([]); + + } catch (error) { + const errMsg = error instanceof Error ? error.message : 'Failed to add sample entity'; + console.error('Failed to add sample entity:', errMsg); + setErrorEntityId(sample.id); + setErrorMessage(errMsg); + } finally { + setAddingEntityId(null); + } + }, []); + + const handleClearError = useCallback(() => { + setErrorEntityId(null); + setErrorMessage(null); + }, []); + + // Handle removing entity + const handleRemoveEntity = useCallback(async (entityId: string) => { + try { + await apiClient.removeEntity(entityId); + + // Update app state + setAppState(prev => ({ + ...prev, + agents: prev.agents.filter(a => a.id !== entityId), + workflows: prev.workflows.filter(w => w.id !== entityId), + selectedAgent: prev.selectedAgent?.id === entityId + ? undefined + : prev.selectedAgent + })); + + // Clear debug events if we removed the selected entity + if (appState.selectedAgent?.id === entityId) { + setDebugEvents([]); + } + + } catch (error) { + console.error('Failed to remove entity:', error); + } + }, [appState.selectedAgent?.id]); + // Show loading state while initializing if (appState.isLoading) { return ( @@ -167,54 +272,77 @@ export default function App() { workflows={[]} selectedItem={undefined} onSelect={() => {}} + onRemove={handleRemoveEntity} isLoading={false} + onSettingsClick={() => setShowAboutModal(true)} /> {/* Error Content */} -
-
-
- Failed to load entities +
+
+ {/* Icon */} +
+
+ +
-

{appState.error}

-
-
- ); - } - // Show empty state if no agents or workflows are available - if ( - !appState.isLoading && - appState.agents.length === 0 && - appState.workflows.length === 0 - ) { - return ( -
- {}} - isLoading={false} + {/* Settings Modal */} + - - {/* Empty State Content */} -
-
-
No entities configured
-

- No agents or workflows were found in your configuration. Please - check your setup and ensure entities are properly configured. -

- -
-
); } @@ -226,35 +354,63 @@ export default function App() { workflows={appState.workflows} selectedItem={appState.selectedAgent} onSelect={handleEntitySelect} + onRemove={handleRemoveEntity} + onBrowseGallery={() => setShowGallery(true)} isLoading={appState.isLoading} onSettingsClick={() => setShowAboutModal(true)} /> - {/* Main Content - Split Panel */} + {/* Main Content - Split Panel or Gallery */}
- {/* Left Panel - Main View */} -
- {appState.selectedAgent ? ( - appState.selectedAgent.type === "agent" ? ( - - ) : ( - - ) - ) : ( -
- Select an agent or workflow to get started. + {showGallery ? ( + // Show gallery full screen (w-full ensures it takes entire width) +
+ setShowGallery(false)} + hasExistingEntities={appState.agents.length > 0 || appState.workflows.length > 0} + /> +
+ ) : appState.agents.length === 0 && appState.workflows.length === 0 ? ( + // Empty state - show gallery inline (full width, no debug panel) + + ) : ( + <> + {/* Left Panel - Main View */} +
+ {appState.selectedAgent ? ( + appState.selectedAgent.type === "agent" ? ( + + ) : ( + + ) + ) : ( +
+ Select an agent or workflow to get started. +
+ )}
- )} -
- {/* Resize Handle */} - {debugPanelOpen && ( + {/* Resize Handle */} + {debugPanelOpen && (
)} - {/* Right Panel - Debug */} - {debugPanelOpen && ( -
- -
+ {/* Right Panel - Debug */} + {debugPanelOpen && ( +
+ +
+ )} + )}
- {/* About Modal */} - diff --git a/python/packages/devui/frontend/src/components/agent/agent-view.tsx b/python/packages/devui/frontend/src/components/agent/agent-view.tsx index 069599080f..00afc509bf 100644 --- a/python/packages/devui/frontend/src/components/agent/agent-view.tsx +++ b/python/packages/devui/frontend/src/components/agent/agent-view.tsx @@ -5,7 +5,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { ScrollArea } from "@/components/ui/scroll-area"; import { FileUpload } from "@/components/ui/file-upload"; import { @@ -21,7 +21,22 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Send, User, Bot, Plus, AlertCircle } from "lucide-react"; +import { + SendHorizontal, + User, + Bot, + Plus, + AlertCircle, + Paperclip, + FileText, + ChevronDown, + Package, + FolderOpen, + Database, + Globe, + CheckCircle, + XCircle, +} from "lucide-react"; import { apiClient } from "@/services/api"; import type { AgentInfo, @@ -36,7 +51,7 @@ interface ChatState { isStreaming: boolean; } -type DebugEventHandler = (event: ExtendedResponseStreamEvent | 'clear') => void; +type DebugEventHandler = (event: ExtendedResponseStreamEvent | "clear") => void; interface AgentViewProps { selectedAgent: AgentInfo; @@ -136,10 +151,15 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { const [loadingThreads, setLoadingThreads] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const [dragCounter, setDragCounter] = useState(0); + const [pasteNotification, setPasteNotification] = useState( + null + ); + const [detailsExpanded, setDetailsExpanded] = useState(false); const scrollAreaRef = useRef(null); const messagesEndRef = useRef(null); const accumulatedText = useRef(""); + const textareaRef = useRef(null); // Auto-scroll to bottom when new messages arrive useEffect(() => { @@ -163,7 +183,9 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // Load messages for the selected thread try { - const threadMessages = await apiClient.getThreadMessages(mostRecentThread.id); + const threadMessages = await apiClient.getThreadMessages( + mostRecentThread.id + ); setChatState({ messages: threadMessages, isStreaming: false, @@ -262,10 +284,137 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { } }; + // Paste handler + const handlePaste = async (e: React.ClipboardEvent) => { + const items = Array.from(e.clipboardData.items); + const files: File[] = []; + let hasProcessedText = false; + const TEXT_THRESHOLD = 8000; // Convert to file if text is larger than this + + for (const item of items) { + // Handle pasted images (screenshots) + if (item.type.startsWith("image/")) { + e.preventDefault(); + const blob = item.getAsFile(); + if (blob) { + const timestamp = Date.now(); + files.push( + new File([blob], `screenshot-${timestamp}.png`, { type: blob.type }) + ); + } + } + // Handle text - only process first text item (browsers often duplicate) + else if (item.type === "text/plain" && !hasProcessedText) { + hasProcessedText = true; + + // We need to check the text synchronously to decide whether to prevent default + // Unfortunately, getAsString is async, so we'll prevent default for all text + // and then decide whether to actually create a file or manually insert the text + e.preventDefault(); + + await new Promise((resolve) => { + item.getAsString((text) => { + // Check if text should be converted to file + const lineCount = (text.match(/\n/g) || []).length; + const shouldConvert = + text.length > TEXT_THRESHOLD || + lineCount > 50 || // Many lines suggests logs/data + /^\s*[{[][\s\S]*[}\]]\s*$/.test(text) || // JSON-like + /^<\?xml|^ { + textarea.selectionStart = textarea.selectionEnd = start + text.length; + textarea.focus(); + }, 0); + } + } + resolve(); + }); + }); + } + } + + // Process collected files + if (files.length > 0) { + await handleFilesSelected(files); + + // Show notification with appropriate icon + const message = + files.length === 1 + ? files[0].name.includes("screenshot") + ? "Screenshot added as attachment" + : "Large text converted to file" + : `${files.length} files added`; + + setPasteNotification(message); + setTimeout(() => setPasteNotification(null), 3000); + } + }; + + // Detect file extension from content + const detectFileExtension = (text: string): string => { + const trimmed = text.trim(); + const lines = trimmed.split('\n'); + + // JSON detection + if (/^{[\s\S]*}$|^\[[\s\S]*\]$/.test(trimmed)) return ".json"; + + // XML/HTML detection + if (/^<\?xml|^ 1) return ".tsv"; + + // CSV detection (more strict) - need multiple lines with consistent comma patterns + if (lines.length > 2) { + const commaLines = lines.filter(line => line.includes(',')); + const semicolonLines = lines.filter(line => line.includes(';')); + + // If >50% of lines have commas and it looks tabular + if (commaLines.length > lines.length * 0.5) { + const avgCommas = commaLines.reduce((sum, line) => sum + (line.match(/,/g) || []).length, 0) / commaLines.length; + if (avgCommas >= 2) return ".csv"; + } + + // If >50% of lines have semicolons and it looks tabular + if (semicolonLines.length > lines.length * 0.5) { + const avgSemicolons = semicolonLines.reduce((sum, line) => sum + (line.match(/;/g) || []).length, 0) / semicolonLines.length; + if (avgSemicolons >= 2) return ".csv"; + } + } + + return ".txt"; + }; + // Helper functions const getFileType = (file: File): AttachmentItem["type"] => { if (file.type.startsWith("image/")) return "image"; if (file.type === "application/pdf") return "pdf"; + if (file.type.startsWith("audio/")) return "audio"; return "other"; }; @@ -304,6 +453,9 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setCurrentThread(thread); + // Clear debug panel when switching threads + onDebugEvent("clear"); + try { // Load thread messages from backend const threadMessages = await apiClient.getThreadMessages(threadId); @@ -354,10 +506,21 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { } as import("@/types/agent-framework").DataContent); } else if (contentItem.type === "input_file") { const dataUri = `data:application/octet-stream;base64,${contentItem.file_data}`; + // Determine media type from filename + const filename = (contentItem as import("@/types/agent-framework").ResponseInputFileParam).filename || ""; + let mediaType = "application/octet-stream"; + + if (filename.endsWith(".pdf")) mediaType = "application/pdf"; + else if (filename.endsWith(".txt")) mediaType = "text/plain"; + else if (filename.endsWith(".json")) mediaType = "application/json"; + else if (filename.endsWith(".csv")) mediaType = "text/csv"; + else if (filename.endsWith(".html")) mediaType = "text/html"; + else if (filename.endsWith(".md")) mediaType = "text/markdown"; + attachmentContents.push({ type: "data", uri: dataUri, - media_type: "application/pdf", // Should be dynamic based on filename + media_type: mediaType, } as import("@/types/agent-framework").DataContent); } } @@ -427,7 +590,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { accumulatedText.current = ""; // Clear debug panel events for new agent run - onDebugEvent('clear'); + onDebugEvent("clear"); // Use OpenAI-compatible API streaming - direct event handling const streamGenerator = apiClient.streamAgentExecutionOpenAI( @@ -576,8 +739,24 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { type: "input_image", image_url: dataUri, } as import("@/types/agent-framework").ResponseInputImageParam); + } else if ( + attachment.file.type === "text/plain" && + (attachment.file.name.includes("pasted-text-") || + attachment.file.name.endsWith(".txt") || + attachment.file.name.endsWith(".csv") || + attachment.file.name.endsWith(".json") || + attachment.file.name.endsWith(".html") || + attachment.file.name.endsWith(".md") || + attachment.file.name.endsWith(".tsv")) + ) { + // Convert all text files (from pasted large text) back to input_text + const text = await attachment.file.text(); + content.push({ + text: text, + type: "input_text", + } as import("@/types/agent-framework").ResponseInputTextParam); } else { - // EXACT OpenAI ResponseInputFileParam (but we need to handle the required fields) + // EXACT OpenAI ResponseInputFileParam for other files const base64Data = dataUri.split(",")[1]; // Extract base64 part content.push({ type: "input_file", @@ -641,22 +820,36 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
{/* Header */}
-
-

-
- - Chat with {selectedAgent.name || selectedAgent.id} -
-

+
+
+

+
+ + Chat with {selectedAgent.name || selectedAgent.id} +
+

+ +
{/* Thread Controls */} -
+
+