From 1ef24d3e91dfddd999a5d48e241f4f3a42c42e41 Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Mon, 22 Sep 2025 16:30:08 -0700 Subject: [PATCH] Python: Add DevUI to AgentFramework (#781) * add initial backend service code for devui * add tests * add frontendcode * ui updates * update readme * ui updates and tweaks * update ui bundle * improve ui, add react flow base * add react flow ui, fix background * update ui, fix introspection bug * update readme * update ui build * add support for multimodal input - both backend and frontend * update ui build * refactor as main framework package * backend and tests refactor * ui build update * ui build update and refactor * update pyproject.toml, update uv.lock * update ui build * ui update to fit oai responses types * add backend updat and readme update * mypy and other fixes * add intial dev guide * update ui and fix workflow bug * update ui build, add thread support * type fixes * update workflow view * update uv.lock * fix workflow iport errors * lint and other fixes * mypy fixes * minor update * update ui build * refactor to use oai dependencies directly, update examples to samples, improve typing * readme update * update ui and ui build * fix workflow pyright error * update ui, fix issues with run workflow placement, miniamp menu, etc * make samples integrate serve --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Eric Zhu --- python/packages/devui/.gitignore | 19 + python/packages/devui/LICENSE | 21 + python/packages/devui/README.md | 140 + .../devui/agent_framework_devui/__init__.py | 131 + .../devui/agent_framework_devui/_cli.py | 135 + .../devui/agent_framework_devui/_discovery.py | 550 ++++ .../devui/agent_framework_devui/_executor.py | 745 +++++ .../devui/agent_framework_devui/_mapper.py | 527 ++++ .../devui/agent_framework_devui/_server.py | 397 +++ .../devui/agent_framework_devui/_session.py | 191 ++ .../devui/agent_framework_devui/_tracing.py | 168 ++ .../agent_framework_devui/models/__init__.py | 72 + .../models/_discovery_models.py | 33 + .../models/_openai_custom.py | 202 ++ .../ui/assets/index-BESRiUNX.js | 383 +++ .../ui/assets/index-BZfe_njJ.css | 1 + .../devui/agent_framework_devui/ui/index.html | 14 + .../devui/agent_framework_devui/ui/vite.svg | 1 + python/packages/devui/dev.md | 89 + python/packages/devui/docs/devuiscreen.png | Bin 0 -> 230754 bytes python/packages/devui/frontend/.gitignore | 22 + python/packages/devui/frontend/README.md | 69 + .../packages/devui/frontend/components.json | 21 + .../packages/devui/frontend/eslint.config.js | 23 + python/packages/devui/frontend/index.html | 13 + python/packages/devui/frontend/package.json | 46 + .../packages/devui/frontend/public/vite.svg | 1 + python/packages/devui/frontend/src/App.css | 42 + python/packages/devui/frontend/src/App.tsx | 312 +++ .../devui/frontend/src/assets/react.svg | 1 + .../src/components/agent/agent-view.tsx | 799 ++++++ .../message_renderer/ContentRenderer.tsx | 268 ++ .../message_renderer/MessageRenderer.tsx | 38 + .../message_renderer/StreamingRenderer.tsx | 114 + .../src/components/message_renderer/index.ts | 8 + .../src/components/message_renderer/types.ts | 48 + .../frontend/src/components/mode-toggle.tsx | 39 + .../src/components/shared/about-modal.tsx | 54 + .../src/components/shared/app-header.tsx | 48 + .../src/components/shared/debug-panel.tsx | 1390 ++++++++++ .../src/components/shared/entity-selector.tsx | 187 ++ .../src/components/theme-provider.tsx | 33 + .../src/components/ui/attachment-gallery.tsx | 115 + .../frontend/src/components/ui/badge.tsx | 36 + .../frontend/src/components/ui/button.tsx | 59 + .../devui/frontend/src/components/ui/card.tsx | 92 + .../frontend/src/components/ui/checkbox.tsx | 32 + .../frontend/src/components/ui/dialog.tsx | 84 + .../src/components/ui/dropdown-menu.tsx | 255 ++ .../src/components/ui/file-upload.tsx | 141 + .../frontend/src/components/ui/input.tsx | 21 + .../frontend/src/components/ui/label.tsx | 22 + .../src/components/ui/loading-spinner.tsx | 23 + .../src/components/ui/loading-state.tsx | 52 + .../src/components/ui/scroll-area.tsx | 46 + .../frontend/src/components/ui/select.tsx | 183 ++ .../devui/frontend/src/components/ui/tabs.tsx | 53 + .../frontend/src/components/ui/textarea.tsx | 18 + .../src/components/workflow/executor-node.tsx | 277 ++ .../src/components/workflow/workflow-flow.tsx | 463 ++++ .../workflow/workflow-input-form.tsx | 503 ++++ .../src/components/workflow/workflow-view.tsx | 839 ++++++ .../src/hooks/useWorkflowEventCorrelation.ts | 126 + python/packages/devui/frontend/src/index.css | 147 + python/packages/devui/frontend/src/main.tsx | 18 + .../devui/frontend/src/services/api.ts | 447 +++ .../frontend/src/types/agent-framework.ts | 312 +++ .../devui/frontend/src/types/index.ts | 144 + .../devui/frontend/src/types/openai.ts | 228 ++ .../devui/frontend/src/types/workflow.ts | 159 ++ .../devui/frontend/src/utils/simple-layout.ts | 139 + .../frontend/src/utils/workflow-utils.ts | 515 ++++ .../packages/devui/frontend/src/vite-env.d.ts | 9 + .../packages/devui/frontend/tsconfig.app.json | 29 + python/packages/devui/frontend/tsconfig.json | 13 + .../devui/frontend/tsconfig.node.json | 23 + python/packages/devui/frontend/vite.config.ts | 34 + python/packages/devui/frontend/yarn.lock | 2455 +++++++++++++++++ python/packages/devui/pyproject.toml | 101 + python/packages/devui/samples/__init__.py | 3 + .../devui/samples/fanout_workflow/__init__.py | 3 + .../devui/samples/fanout_workflow/workflow.py | 698 +++++ .../packages/devui/samples/in_memory_mode.py | 71 + .../devui/samples/spam_workflow/__init__.py | 7 + .../devui/samples/spam_workflow/workflow.py | 333 +++ .../devui/samples/weather_agent/__init__.py | 7 + .../devui/samples/weather_agent/agent.py | 69 + .../samples/weather_agent_azure/__init__.py | 7 + .../samples/weather_agent_azure/agent.py | 71 + .../packages/devui/tests/capture_messages.py | 284 ++ python/packages/devui/tests/test_discovery.py | 101 + python/packages/devui/tests/test_execution.py | 189 ++ python/packages/devui/tests/test_mapper.py | 192 ++ python/packages/devui/tests/test_server.py | 135 + .../main/agent_framework/devui/__init__.py | 35 + python/packages/main/pyproject.toml | 6 +- python/pyproject.toml | 2 + python/uv.lock | 228 +- 98 files changed, 18045 insertions(+), 4 deletions(-) create mode 100644 python/packages/devui/.gitignore create mode 100644 python/packages/devui/LICENSE create mode 100644 python/packages/devui/README.md create mode 100644 python/packages/devui/agent_framework_devui/__init__.py create mode 100644 python/packages/devui/agent_framework_devui/_cli.py create mode 100644 python/packages/devui/agent_framework_devui/_discovery.py create mode 100644 python/packages/devui/agent_framework_devui/_executor.py create mode 100644 python/packages/devui/agent_framework_devui/_mapper.py create mode 100644 python/packages/devui/agent_framework_devui/_server.py create mode 100644 python/packages/devui/agent_framework_devui/_session.py create mode 100644 python/packages/devui/agent_framework_devui/_tracing.py create mode 100644 python/packages/devui/agent_framework_devui/models/__init__.py create mode 100644 python/packages/devui/agent_framework_devui/models/_discovery_models.py create mode 100644 python/packages/devui/agent_framework_devui/models/_openai_custom.py create mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-BESRiUNX.js create mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-BZfe_njJ.css create mode 100644 python/packages/devui/agent_framework_devui/ui/index.html create mode 100644 python/packages/devui/agent_framework_devui/ui/vite.svg create mode 100644 python/packages/devui/dev.md create mode 100644 python/packages/devui/docs/devuiscreen.png create mode 100644 python/packages/devui/frontend/.gitignore create mode 100644 python/packages/devui/frontend/README.md create mode 100644 python/packages/devui/frontend/components.json create mode 100644 python/packages/devui/frontend/eslint.config.js create mode 100644 python/packages/devui/frontend/index.html create mode 100644 python/packages/devui/frontend/package.json create mode 100644 python/packages/devui/frontend/public/vite.svg create mode 100644 python/packages/devui/frontend/src/App.css create mode 100644 python/packages/devui/frontend/src/App.tsx create mode 100644 python/packages/devui/frontend/src/assets/react.svg create mode 100644 python/packages/devui/frontend/src/components/agent/agent-view.tsx create mode 100644 python/packages/devui/frontend/src/components/message_renderer/ContentRenderer.tsx create mode 100644 python/packages/devui/frontend/src/components/message_renderer/MessageRenderer.tsx create mode 100644 python/packages/devui/frontend/src/components/message_renderer/StreamingRenderer.tsx create mode 100644 python/packages/devui/frontend/src/components/message_renderer/index.ts create mode 100644 python/packages/devui/frontend/src/components/message_renderer/types.ts create mode 100644 python/packages/devui/frontend/src/components/mode-toggle.tsx create mode 100644 python/packages/devui/frontend/src/components/shared/about-modal.tsx create mode 100644 python/packages/devui/frontend/src/components/shared/app-header.tsx create mode 100644 python/packages/devui/frontend/src/components/shared/debug-panel.tsx create mode 100644 python/packages/devui/frontend/src/components/shared/entity-selector.tsx create mode 100644 python/packages/devui/frontend/src/components/theme-provider.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/attachment-gallery.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/badge.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/button.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/card.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/checkbox.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/dialog.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/dropdown-menu.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/file-upload.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/input.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/label.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/loading-spinner.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/loading-state.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/scroll-area.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/select.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/tabs.tsx create mode 100644 python/packages/devui/frontend/src/components/ui/textarea.tsx create mode 100644 python/packages/devui/frontend/src/components/workflow/executor-node.tsx create mode 100644 python/packages/devui/frontend/src/components/workflow/workflow-flow.tsx create mode 100644 python/packages/devui/frontend/src/components/workflow/workflow-input-form.tsx create mode 100644 python/packages/devui/frontend/src/components/workflow/workflow-view.tsx create mode 100644 python/packages/devui/frontend/src/hooks/useWorkflowEventCorrelation.ts create mode 100644 python/packages/devui/frontend/src/index.css create mode 100644 python/packages/devui/frontend/src/main.tsx create mode 100644 python/packages/devui/frontend/src/services/api.ts create mode 100644 python/packages/devui/frontend/src/types/agent-framework.ts create mode 100644 python/packages/devui/frontend/src/types/index.ts create mode 100644 python/packages/devui/frontend/src/types/openai.ts create mode 100644 python/packages/devui/frontend/src/types/workflow.ts create mode 100644 python/packages/devui/frontend/src/utils/simple-layout.ts create mode 100644 python/packages/devui/frontend/src/utils/workflow-utils.ts create mode 100644 python/packages/devui/frontend/src/vite-env.d.ts create mode 100644 python/packages/devui/frontend/tsconfig.app.json create mode 100644 python/packages/devui/frontend/tsconfig.json create mode 100644 python/packages/devui/frontend/tsconfig.node.json create mode 100644 python/packages/devui/frontend/vite.config.ts create mode 100644 python/packages/devui/frontend/yarn.lock create mode 100644 python/packages/devui/pyproject.toml create mode 100644 python/packages/devui/samples/__init__.py create mode 100644 python/packages/devui/samples/fanout_workflow/__init__.py create mode 100644 python/packages/devui/samples/fanout_workflow/workflow.py create mode 100644 python/packages/devui/samples/in_memory_mode.py create mode 100644 python/packages/devui/samples/spam_workflow/__init__.py create mode 100644 python/packages/devui/samples/spam_workflow/workflow.py create mode 100644 python/packages/devui/samples/weather_agent/__init__.py create mode 100644 python/packages/devui/samples/weather_agent/agent.py create mode 100644 python/packages/devui/samples/weather_agent_azure/__init__.py create mode 100644 python/packages/devui/samples/weather_agent_azure/agent.py create mode 100644 python/packages/devui/tests/capture_messages.py create mode 100644 python/packages/devui/tests/test_discovery.py create mode 100644 python/packages/devui/tests/test_execution.py create mode 100644 python/packages/devui/tests/test_mapper.py create mode 100644 python/packages/devui/tests/test_server.py create mode 100644 python/packages/main/agent_framework/devui/__init__.py diff --git a/python/packages/devui/.gitignore b/python/packages/devui/.gitignore new file mode 100644 index 0000000000..e545b92de0 --- /dev/null +++ b/python/packages/devui/.gitignore @@ -0,0 +1,19 @@ +# Test artifacts +tests/captured_messages/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class + +# Local development files +.env +*.log + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/python/packages/devui/LICENSE b/python/packages/devui/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/devui/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/devui/README.md b/python/packages/devui/README.md new file mode 100644 index 0000000000..7fbe4ac6a7 --- /dev/null +++ b/python/packages/devui/README.md @@ -0,0 +1,140 @@ +# DevUI - Agent Framework Debug Interface + +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**. + +> [!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. + +![DevUI Screenshot](./docs/devuiscreen.png) + +## Quick Start + +```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 + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient +from agent_framework.devui import serve + +def get_weather(location: str) -> str: + """Get weather for a location.""" + return f"Weather in {location}: 72°F and sunny" + +# Create your agent +agent = ChatAgent( + name="WeatherAgent", + chat_client=OpenAIChatClient(), + tools=[get_weather] +) + +# Launch debug UI - that's it! +serve(entities=[agent], auto_open=True) +# → Opens browser to http://localhost:8080 +``` + +## 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`). + +**Note**: `.env` files are optional but will be automatically loaded if present in the agent/workflow directory or parent entities directory. Use them to store API keys, configuration variables, and other environment-specific settings. + +``` +agents/ +├── weather_agent/ +│ ├── __init__.py # Must export: agent = ChatAgent(...) +│ ├── agent.py +│ └── .env # Optional: API keys, config vars +├── my_workflow/ +│ ├── __init__.py # Must export: workflow = WorkflowBuilder()... +│ ├── workflow.py +│ └── .env # Optional: environment variables +└── .env # Optional: shared environment variables +``` + +## 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`. + +```bash +# Standard OpenAI format +curl -X POST http://localhost:8080/v1/responses \ + -H "Content-Type: application/json" \ + -d @- << 'EOF' +{ + "model": "agent-framework", + "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 +devui [directory] [options] + +Options: + --port, -p Port (default: 8080) + --host Host (default: 127.0.0.1) + --headless API only, no UI + --config YAML config file + --tracing none|framework|workflow|all + --reload Enable auto-reload +``` + +## Key Endpoints + +- `GET /v1/entities` - List discovered agents/workflows +- `GET /v1/entities/{entity_id}/info` - Get detailed entity information +- `POST /v1/responses` - Execute agent/workflow (streaming or sync) +- `GET /health` - Health check +- `POST /v1/threads` - Create thread for agent (optional) +- `GET /v1/threads?agent_id={id}` - List threads for agent +- `GET /v1/threads/{thread_id}` - Get thread info +- `DELETE /v1/threads/{thread_id}` - Delete thread +- `GET /v1/threads/{thread_id}/messages` - Get thread messages + +## Implementation + +- **Discovery**: `agent_framework_devui/_discovery.py` +- **Execution**: `agent_framework_devui/_executor.py` +- **Message Mapping**: `agent_framework_devui/_mapper.py` +- **Session Management**: `agent_framework_devui/_session.py` +- **API Server**: `agent_framework_devui/_server.py` +- **CLI**: `agent_framework_devui/_cli.py` + +## Examples + +See `samples/` for working agent and workflow implementations. + +## License + +MIT diff --git a/python/packages/devui/agent_framework_devui/__init__.py b/python/packages/devui/agent_framework_devui/__init__.py new file mode 100644 index 0000000000..2185f73882 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/__init__.py @@ -0,0 +1,131 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Agent Framework DevUI - Debug interface with OpenAI compatible API server.""" + +import importlib.metadata +import logging +import webbrowser +from typing import Any + +from ._server import DevServer +from .models import AgentFrameworkRequest, OpenAIError, OpenAIResponse, ResponseStreamEvent +from .models._discovery_models import DiscoveryResponse, EntityInfo + +logger = logging.getLogger(__name__) + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + + +def serve( + entities: list[Any] | None = None, + entities_dir: str | None = None, + port: int = 8080, + host: str = "127.0.0.1", + auto_open: bool = False, + cors_origins: list[str] | None = None, + ui_enabled: bool = True, +) -> None: + """Launch Agent Framework DevUI with simple API. + + Args: + entities: List of entities for in-memory registration (IDs auto-generated) + entities_dir: Directory to scan for entities + port: Port to run server on + host: Host to bind server to + auto_open: Whether to automatically open browser + cors_origins: List of allowed CORS origins + ui_enabled: Whether to enable the UI + """ + import re + + import uvicorn + + # Validate host parameter early for security + if not re.match(r"^(localhost|127\.0\.0\.1|0\.0\.0\.0|[a-zA-Z0-9.-]+)$", host): + raise ValueError(f"Invalid host: {host}. Must be localhost, IP address, or valid hostname") + + # Validate port parameter + if not isinstance(port, int) or not (1 <= port <= 65535): + raise ValueError(f"Invalid port: {port}. Must be integer between 1 and 65535") + + # Create server with direct parameters + server = DevServer( + entities_dir=entities_dir, port=port, host=host, cors_origins=cors_origins, ui_enabled=ui_enabled + ) + + # Register in-memory entities if provided + if entities: + logger.info(f"Registering {len(entities)} in-memory entities") + # Store entities for later registration during server startup + server._pending_entities = entities + + app = server.get_app() + + if auto_open: + + def open_browser() -> None: + import http.client + import re + import time + + # Validate host and port for security + if not re.match(r"^(localhost|127\.0\.0\.1|0\.0\.0\.0|[a-zA-Z0-9.-]+)$", host): + logger.warning(f"Invalid host for auto-open: {host}") + return + + if not isinstance(port, int) or not (1 <= port <= 65535): + logger.warning(f"Invalid port for auto-open: {port}") + return + + # Wait for server to be ready by checking health endpoint + browser_url = f"http://{host}:{port}" + + for _ in range(30): # 15 second timeout (30 * 0.5s) + try: + # Use http.client for safe connection handling (standard library) + conn = http.client.HTTPConnection(host, port, timeout=1) + try: + conn.request("GET", "/health") + response = conn.getresponse() + if response.status == 200: + webbrowser.open(browser_url) + return + finally: + conn.close() + except (http.client.HTTPException, OSError, TimeoutError): + pass + time.sleep(0.5) + + # Fallback: open browser anyway after timeout + webbrowser.open(browser_url) + + import threading + + threading.Thread(target=open_browser, daemon=True).start() + + logger.info(f"Starting Agent Framework DevUI on {host}:{port}") + uvicorn.run(app, host=host, port=port, log_level="info") + + +def main() -> None: + """CLI entry point for devui command.""" + from ._cli import main as cli_main + + cli_main() + + +# Export main public API +__all__ = [ + "AgentFrameworkRequest", + "DevServer", + "DiscoveryResponse", + "EntityInfo", + "OpenAIError", + "OpenAIResponse", + "ResponseStreamEvent", + "main", + "serve", +] diff --git a/python/packages/devui/agent_framework_devui/_cli.py b/python/packages/devui/agent_framework_devui/_cli.py new file mode 100644 index 0000000000..0e36731b1c --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_cli.py @@ -0,0 +1,135 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Command line interface for Agent Framework DevUI.""" + +import argparse +import logging +import os +import sys + +logger = logging.getLogger(__name__) + + +def setup_logging(level: str = "INFO") -> None: + """Configure logging for the server.""" + log_format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" + logging.basicConfig(level=getattr(logging, level.upper()), format=log_format, datefmt="%Y-%m-%d %H:%M:%S") + + +def create_cli_parser() -> argparse.ArgumentParser: + """Create the command line argument parser.""" + parser = argparse.ArgumentParser( + prog="devui", + description="Launch Agent Framework DevUI - Debug interface with OpenAI compatible API", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + devui # Scan current directory + devui ./agents # Scan specific directory + devui --port 8000 # Custom port + devui --headless # API only, no UI + """, + ) + + parser.add_argument( + "directory", nargs="?", default=".", help="Directory to scan for entities (default: current directory)" + ) + + parser.add_argument("--port", "-p", type=int, default=8080, help="Port to run server on (default: 8080)") + + parser.add_argument("--host", default="127.0.0.1", help="Host to bind server to (default: 127.0.0.1)") + + parser.add_argument("--no-open", action="store_true", help="Don't automatically open browser") + + parser.add_argument("--headless", action="store_true", help="Run without UI (API only)") + + parser.add_argument( + "--log-level", + choices=["DEBUG", "INFO", "WARNING", "ERROR"], + default="INFO", + help="Logging level (default: INFO)", + ) + + parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") + + parser.add_argument("--version", action="version", version=f"Agent Framework DevUI {get_version()}") + + return parser + + +def get_version() -> str: + """Get the package version.""" + try: + from . import __version__ + + return __version__ + except ImportError: + return "unknown" + + +def validate_directory(directory: str) -> str: + """Validate and normalize the entities directory.""" + if not directory: + directory = "." + + abs_dir = os.path.abspath(directory) + + if not os.path.exists(abs_dir): + print(f"❌ Error: Directory '{directory}' does not exist", file=sys.stderr) # noqa: T201 + sys.exit(1) + + if not os.path.isdir(abs_dir): + print(f"❌ Error: '{directory}' is not a directory", file=sys.stderr) # noqa: T201 + sys.exit(1) + + return abs_dir + + +def print_startup_info(entities_dir: str, host: str, port: int, ui_enabled: bool, reload: bool) -> None: + """Print startup information.""" + print("🤖 Agent Framework DevUI") # noqa: T201 + print("=" * 50) # noqa: T201 + print(f"📁 Entities directory: {entities_dir}") # noqa: T201 + print(f"🌐 Server URL: http://{host}:{port}") # noqa: T201 + print(f"🎨 UI enabled: {'Yes' if ui_enabled else 'No'}") # noqa: T201 + print(f"🔄 Auto-reload: {'Yes' if reload else 'No'}") # noqa: T201 + print("=" * 50) # noqa: T201 + print("🔍 Scanning for entities...") # noqa: T201 + + +def main() -> None: + """Main CLI entry point.""" + parser = create_cli_parser() + args = parser.parse_args() + + # Setup logging + setup_logging(args.log_level) + + # Validate directory + entities_dir = validate_directory(args.directory) + + # Extract parameters directly from args + ui_enabled = not args.headless + + # Print startup info + print_startup_info(entities_dir, args.host, args.port, ui_enabled, args.reload) + + # Import and start server + try: + from . import serve + + serve( + entities_dir=entities_dir, port=args.port, host=args.host, auto_open=not args.no_open, ui_enabled=ui_enabled + ) + + except KeyboardInterrupt: + print("\n👋 Shutting down Agent Framework DevUI...") # noqa: T201 + sys.exit(0) + except Exception as e: + logger.exception("Failed to start server") + print(f"❌ Error: {e}", file=sys.stderr) # noqa: T201 + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/packages/devui/agent_framework_devui/_discovery.py b/python/packages/devui/agent_framework_devui/_discovery.py new file mode 100644 index 0000000000..d75d77a6c4 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_discovery.py @@ -0,0 +1,550 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Agent Framework entity discovery implementation.""" + +import importlib +import importlib.util +import logging +import sys +import uuid +from pathlib import Path +from typing import Any + +from dotenv import load_dotenv + +from .models._discovery_models import EntityInfo + +logger = logging.getLogger(__name__) + + +class EntityDiscovery: + """Discovery for Agent Framework entities - agents and workflows.""" + + def __init__(self, entities_dir: str | None = None): + """Initialize entity discovery. + + Args: + entities_dir: Directory to scan for entities (optional) + """ + self.entities_dir = entities_dir + self._entities: dict[str, EntityInfo] = {} + self._loaded_objects: dict[str, Any] = {} + + async def discover_entities(self) -> list[EntityInfo]: + """Scan for Agent Framework entities. + + Returns: + List of discovered entities + """ + if not self.entities_dir: + logger.info("No Agent Framework entities directory configured") + return [] + + entities_dir = Path(self.entities_dir).resolve() + await self._scan_entities_directory(entities_dir) + + logger.info(f"Discovered {len(self._entities)} Agent Framework entities") + return self.list_entities() + + def get_entity_info(self, entity_id: str) -> EntityInfo | None: + """Get entity metadata. + + Args: + entity_id: Entity identifier + + Returns: + Entity information or None if not found + """ + return self._entities.get(entity_id) + + def get_entity_object(self, entity_id: str) -> Any | None: + """Get the actual loaded entity object. + + Args: + entity_id: Entity identifier + + Returns: + Entity object or None if not found + """ + return self._loaded_objects.get(entity_id) + + def list_entities(self) -> list[EntityInfo]: + """List all discovered entities. + + Returns: + List of all entity information + """ + return list(self._entities.values()) + + def register_entity(self, entity_id: str, entity_info: EntityInfo, entity_object: Any) -> None: + """Register an entity with both metadata and object. + + Args: + entity_id: Unique entity identifier + entity_info: Entity metadata + entity_object: Actual entity object for execution + """ + self._entities[entity_id] = entity_info + 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: + """Create EntityInfo from Agent Framework entity object. + + Args: + entity_object: Agent Framework entity object + entity_type: Optional entity type override + + Returns: + EntityInfo with Agent Framework specific metadata + """ + # Determine entity type if not provided + if entity_type is None: + entity_type = "agent" + # Check if it's a workflow + if hasattr(entity_object, "get_executors_list") or hasattr(entity_object, "executors"): + entity_type = "workflow" + + # Extract metadata with improved fallback naming + name = getattr(entity_object, "name", None) + if not name: + # In-memory entities: use ID with entity type prefix since no directory name available + entity_id_raw = getattr(entity_object, "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"{entity_type.title()} {short_id}" + else: + # Fallback to class name with entity type + class_name = entity_object.__class__.__name__ + name = f"{entity_type.title()} {class_name}" + description = getattr(entity_object, "description", "") + + # Generate entity ID using Agent Framework specific naming + entity_id = self._generate_entity_id(entity_object, entity_type) + + # Extract tools/executors using Agent Framework specific logic + tools_list = await self._extract_tools_from_object(entity_object, entity_type) + + # Create EntityInfo with Agent Framework specifics + return EntityInfo( + id=entity_id, + name=name, + description=description, + type=entity_type, + framework="agent_framework", + tools=[str(tool) for tool in (tools_list or [])], + executors=tools_list if entity_type == "workflow" else [], + input_schema={"type": "string"}, # Default schema + start_executor_id=tools_list[0] if tools_list and entity_type == "workflow" else None, + metadata={ + "source": "agent_framework_object", + "class_name": entity_object.__class__.__name__ + if hasattr(entity_object, "__class__") + else str(type(entity_object)), + "has_run_stream": hasattr(entity_object, "run_stream"), + }, + ) + + async def _scan_entities_directory(self, entities_dir: Path) -> None: + """Scan the entities directory for Agent Framework entities. + + Args: + entities_dir: Directory to scan for entities + """ + if not entities_dir.exists(): + logger.warning(f"Entities directory not found: {entities_dir}") + return + + logger.info(f"Scanning {entities_dir} for Agent Framework entities...") + + # Add entities directory to Python path if not already there + entities_dir_str = str(entities_dir) + if entities_dir_str not in sys.path: + sys.path.insert(0, entities_dir_str) + + # Scan for directories and Python files + for item in entities_dir.iterdir(): + if item.name.startswith(".") or item.name == "__pycache__": + continue + + if item.is_dir(): + # Directory-based entity + await self._discover_entities_in_directory(item) + elif item.is_file() and item.suffix == ".py" and not item.name.startswith("_"): + # Single file entity + await self._discover_entities_in_file(item) + + async def _discover_entities_in_directory(self, dir_path: Path) -> None: + """Discover entities in a directory using module import. + + Args: + dir_path: Directory containing entity + """ + entity_id = dir_path.name + logger.debug(f"Scanning directory: {entity_id}") + + try: + # Load environment variables for this entity first + self._load_env_for_entity(dir_path) + + # Try different import patterns + import_patterns = [ + entity_id, # Direct module import + f"{entity_id}.agent", # agent.py submodule + f"{entity_id}.workflow", # workflow.py submodule + ] + + for pattern in import_patterns: + module = self._load_module_from_pattern(pattern) + if module: + entities_found = await self._find_entities_in_module(module, entity_id, str(dir_path)) + if entities_found: + logger.debug(f"Found {len(entities_found)} entities in {pattern}") + break + + except Exception as e: + logger.warning(f"Error scanning directory {entity_id}: {e}") + + async def _discover_entities_in_file(self, file_path: Path) -> None: + """Discover entities in a single Python file. + + Args: + file_path: Python file to scan + """ + try: + # Load environment variables for this entity's directory first + self._load_env_for_entity(file_path.parent) + + # Create module name from file path + base_name = file_path.stem + + # Load the module directly from file + module = self._load_module_from_file(file_path, base_name) + if module: + entities_found = await self._find_entities_in_module(module, base_name, str(file_path)) + if entities_found: + logger.debug(f"Found {len(entities_found)} entities in {file_path.name}") + + except Exception as e: + logger.warning(f"Error scanning file {file_path}: {e}") + + def _load_env_for_entity(self, entity_path: Path) -> bool: + """Load .env file for an entity. + + Args: + entity_path: Path to entity directory + + Returns: + True if .env was loaded successfully + """ + # Check for .env in the entity folder first + env_file = entity_path / ".env" + if self._load_env_file(env_file): + return True + + # Check one level up (the entities directory) for safety + if self.entities_dir: + entities_dir = Path(self.entities_dir).resolve() + entities_env = entities_dir / ".env" + if self._load_env_file(entities_env): + return True + + return False + + def _load_env_file(self, env_path: Path) -> bool: + """Load environment variables from .env file. + + Args: + env_path: Path to .env file + + Returns: + True if file was loaded successfully + """ + if env_path.exists(): + load_dotenv(env_path, override=True) + logger.debug(f"Loaded .env from {env_path}") + return True + return False + + def _load_module_from_pattern(self, pattern: str) -> Any | None: + """Load module using import pattern. + + Args: + pattern: Import pattern to try + + Returns: + Loaded module or None if failed + """ + try: + # Check if module exists first + spec = importlib.util.find_spec(pattern) + if spec is None: + return None + + module = importlib.import_module(pattern) + logger.debug(f"Successfully imported {pattern}") + return module + + except ModuleNotFoundError: + logger.debug(f"Import pattern {pattern} not found") + return None + except Exception as e: + logger.warning(f"Error importing {pattern}: {e}") + return None + + def _load_module_from_file(self, file_path: Path, module_name: str) -> Any | None: + """Load module directly from file path. + + Args: + file_path: Path to Python file + module_name: Name to assign to module + + Returns: + Loaded module or None if failed + """ + try: + spec = importlib.util.spec_from_file_location(module_name, file_path) + if spec is None or spec.loader is None: + return None + + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module # Add to sys.modules for proper imports + spec.loader.exec_module(module) + + logger.debug(f"Successfully loaded module from {file_path}") + return module + + except Exception as e: + logger.warning(f"Error loading module from {file_path}: {e}") + return None + + async def _find_entities_in_module(self, module: Any, base_id: str, module_path: str) -> list[str]: + """Find agent and workflow entities in a loaded module. + + Args: + module: Loaded Python module + base_id: Base identifier for entities + module_path: Path to module for metadata + + Returns: + List of entity IDs that were found and registered + """ + entities_found = [] + + # Look for explicit variable names first + candidates = [ + ("agent", getattr(module, "agent", None)), + ("workflow", getattr(module, "workflow", None)), + ] + + for obj_type, obj in candidates: + if obj is None: + 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) + + return entities_found + + def _is_valid_entity(self, obj: Any, expected_type: str) -> bool: + """Check if object is a valid agent or workflow using duck typing. + + Args: + obj: Object to validate + expected_type: Expected type ("agent" or "workflow") + + Returns: + True if object is valid for the expected type + """ + if expected_type == "agent": + return self._is_valid_agent(obj) + if expected_type == "workflow": + return self._is_valid_workflow(obj) + return False + + def _is_valid_agent(self, obj: Any) -> bool: + """Check if object is a valid Agent Framework agent. + + Args: + obj: Object to validate + + Returns: + True if object appears to be a valid agent + """ + try: + # Try to import AgentProtocol for proper type checking + try: + from agent_framework import AgentProtocol + + if isinstance(obj, AgentProtocol): + return True + except ImportError: + pass + + # Fallback to duck typing for agent protocol + if hasattr(obj, "run_stream") and hasattr(obj, "id") and hasattr(obj, "name"): + return True + + except (TypeError, AttributeError): + pass + + return False + + def _is_valid_workflow(self, obj: Any) -> bool: + """Check if object is a valid Agent Framework workflow. + + Args: + obj: Object to validate + + Returns: + True if object appears to be a valid workflow + """ + # 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: + """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 + """ + try: + # 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() + 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__}" + description = getattr(obj, "description", None) + tools = await self._extract_tools_from_object(obj, obj_type) + + # Create EntityInfo + tools_union: list[str | dict[str, Any]] | None = None + if tools: + tools_union = [tool for tool in tools] + + entity_info = EntityInfo( + id=entity_id, + type=obj_type, + name=name, + framework="agent_framework", + description=description, + tools=tools_union, + metadata={ + "module_path": module_path, + "entity_type": obj_type, + "source": "module_import", + "has_run_stream": hasattr(obj, "run_stream"), + "class_name": obj.__class__.__name__ if hasattr(obj, "__class__") else str(type(obj)), + }, + ) + + # Register the entity + self.register_entity(entity_id, entity_info, obj) + + except Exception as e: + logger.error(f"Error registering entity {entity_id}: {e}") + + async def _extract_tools_from_object(self, obj: Any, obj_type: str) -> list[str]: + """Extract tool/executor names from a live object. + + Args: + obj: Entity object + obj_type: Type of entity + + Returns: + List of tool/executor names + """ + tools = [] + + try: + if obj_type == "agent": + # For agents, check chat_options.tools first + chat_options = getattr(obj, "chat_options", None) + if chat_options and hasattr(chat_options, "tools"): + for tool in chat_options.tools: + if hasattr(tool, "__name__"): + tools.append(tool.__name__) + elif hasattr(tool, "name"): + tools.append(tool.name) + else: + tools.append(str(tool)) + else: + # Fallback to direct tools attribute + agent_tools = getattr(obj, "tools", None) + if agent_tools: + for tool in agent_tools: + if hasattr(tool, "__name__"): + tools.append(tool.__name__) + elif hasattr(tool, "name"): + tools.append(tool.name) + else: + tools.append(str(tool)) + + elif obj_type == "workflow": + # For workflows, extract executor names + if hasattr(obj, "get_executors_list"): + executor_objects = obj.get_executors_list() + tools = [getattr(ex, "id", str(ex)) for ex in executor_objects] + elif hasattr(obj, "executors"): + executors = obj.executors + if isinstance(executors, list): + tools = [getattr(ex, "id", str(ex)) for ex in executors] + elif isinstance(executors, dict): + tools = list(executors.keys()) + + except Exception as e: + logger.debug(f"Error extracting tools from {obj_type} {type(obj)}: {e}") + + return tools + + def _generate_entity_id(self, entity: Any, entity_type: str) -> str: + """Generate entity ID with priority: name -> id -> class_name -> uuid. + + Args: + entity: Entity object + entity_type: Type of entity (agent, workflow, etc.) + + Returns: + Generated entity ID + """ + import re + + # Priority 1: entity.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__"): + 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}" + + # Priority 4: fallback to uuid + return f"{entity_type}_{uuid.uuid4().hex[:8]}" diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py new file mode 100644 index 0000000000..9df2b32d68 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -0,0 +1,745 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Agent Framework executor implementation.""" + +import json +import logging +import os +import uuid +from collections.abc import AsyncGenerator +from typing import Any + +from agent_framework import AgentThread + +from ._discovery import EntityDiscovery +from ._mapper import MessageMapper +from ._tracing import capture_traces +from .models import AgentFrameworkRequest, OpenAIResponse +from .models._discovery_models import EntityInfo + +logger = logging.getLogger(__name__) + + +class EntityNotFoundError(Exception): + """Raised when an entity is not found.""" + + pass + + +class AgentFrameworkExecutor: + """Executor for Agent Framework entities - agents and workflows.""" + + def __init__(self, entity_discovery: EntityDiscovery, message_mapper: MessageMapper): + """Initialize Agent Framework executor. + + Args: + entity_discovery: Entity discovery instance + message_mapper: Message mapper instance + """ + self.entity_discovery = entity_discovery + self.message_mapper = message_mapper + self._setup_tracing_provider() + self._setup_agent_framework_tracing() + + # Minimal thread storage - no metadata needed + self.thread_storage: dict[str, AgentThread] = {} + self.agent_threads: dict[str, list[str]] = {} # agent_id -> thread_ids + + def _setup_tracing_provider(self) -> None: + """Set up our own TracerProvider so we can add processors.""" + try: + from opentelemetry import trace + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + + # Only set up if no provider exists yet + if not hasattr(trace, "_TRACER_PROVIDER") or trace._TRACER_PROVIDER is None: + resource = Resource.create({ + "service.name": "agent-framework-server", + "service.version": "1.0.0", + }) + provider = TracerProvider(resource=resource) + trace.set_tracer_provider(provider) + logger.info("Set up TracerProvider for server tracing") + else: + logger.debug("TracerProvider already exists") + + except ImportError: + logger.debug("OpenTelemetry not available") + except Exception as e: + logger.warning(f"Failed to setup TracerProvider: {e}") + + 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("AGENT_FRAMEWORK_OTLP_ENDPOINT") + if otlp_endpoint: + try: + from agent_framework.telemetry import setup_telemetry + + setup_telemetry(enable_otel=True, enable_sensitive_data=True, otlp_endpoint=otlp_endpoint) + logger.info(f"Enabled Agent Framework telemetry with endpoint: {otlp_endpoint}") + except Exception as e: + logger.warning(f"Failed to enable Agent Framework tracing: {e}") + else: + logger.debug("No OTLP endpoint configured, skipping telemetry setup") + + # Thread Management Methods + def create_thread(self, agent_id: str) -> str: + """Create new thread for agent.""" + thread_id = f"thread_{uuid.uuid4().hex[:8]}" + thread = AgentThread() + + self.thread_storage[thread_id] = thread + + if agent_id not in self.agent_threads: + self.agent_threads[agent_id] = [] + self.agent_threads[agent_id].append(thread_id) + + return thread_id + + def get_thread(self, thread_id: str) -> AgentThread | None: + """Get AgentThread by ID.""" + return self.thread_storage.get(thread_id) + + def list_threads_for_agent(self, agent_id: str) -> list[str]: + """List thread IDs for agent.""" + return self.agent_threads.get(agent_id, []) + + def get_agent_for_thread(self, thread_id: str) -> str | None: + """Find which agent owns this thread.""" + for agent_id, thread_ids in self.agent_threads.items(): + if thread_id in thread_ids: + return agent_id + return None + + def delete_thread(self, thread_id: str) -> bool: + """Delete thread.""" + 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) + break + + del self.thread_storage[thread_id] + 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.""" + thread = self.get_thread(thread_id) + if not thread or not thread.message_store: + return [] + + try: + # Get AgentFramework ChatMessage objects from thread + af_messages = await thread.message_store.list_messages() + + ui_messages = [] + for i, af_msg in enumerate(af_messages): + # 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 + if role not in ["user", "assistant"]: + continue + + # Extract user-facing text content only + text_content = self._extract_display_text(af_msg.contents) + + # Skip messages with no displayable text + if not text_content: + continue + + ui_message = { + "id": af_msg.message_id or f"restored-{i}", + "role": role, + "contents": [{"type": "text", "text": text_content}], + "timestamp": __import__("datetime").datetime.now().isoformat(), + "author_name": af_msg.author_name, + "message_id": af_msg.message_id, + } + + ui_messages.append(ui_message) + + logger.info(f"Restored {len(ui_messages)} display messages for thread {thread_id}") + return ui_messages + + except Exception as e: + logger.error(f"Error getting thread messages: {e}") + import traceback + + 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 = [] + + for content in contents: + content_type = getattr(content, "type", None) + + # Only include text content for display + if content_type == "text": + text = getattr(content, "text", "") + + # Handle double-encoded JSON from user messages + if text.startswith('{"role":'): + try: + import json + + parsed = json.loads(text) + if parsed.get("contents"): + for sub_content in parsed["contents"]: + if sub_content.get("type") == "text": + text_parts.append(sub_content.get("text", "")) + except Exception: + text_parts.append(text) # Fallback to raw text + else: + text_parts.append(text) + + # Skip function_call, function_result, and other internal content types + + return " ".join(text_parts).strip() + + async def serialize_thread(self, thread_id: str) -> dict[str, Any] | None: + """Serialize thread state for persistence.""" + thread = self.get_thread(thread_id) + if not thread: + return None + + try: + # Use AgentThread's built-in serialization + serialized_state = await thread.serialize() + + # Add our metadata + agent_id = self.get_agent_for_thread(thread_id) + serialized_state["metadata"] = {"agent_id": agent_id, "thread_id": thread_id} + + return serialized_state + + except Exception as e: + logger.error(f"Error serializing thread {thread_id}: {e}") + return None + + async def deserialize_thread(self, thread_id: str, agent_id: str, serialized_state: dict[str, Any]) -> bool: + """Deserialize thread state from persistence.""" + try: + # Create new thread + thread = AgentThread() + + # Use AgentThread's built-in deserialization + from agent_framework._threads import deserialize_thread_state + + await deserialize_thread_state(thread, serialized_state) + + # Store the restored thread + self.thread_storage[thread_id] = thread + + if agent_id not in self.agent_threads: + self.agent_threads[agent_id] = [] + self.agent_threads[agent_id].append(thread_id) + + return True + + except Exception as e: + logger.error(f"Error deserializing thread {thread_id}: {e}") + return False + + async def discover_entities(self) -> list[EntityInfo]: + """Discover all available entities. + + Returns: + List of discovered entities + """ + return await self.entity_discovery.discover_entities() + + def get_entity_info(self, entity_id: str) -> EntityInfo: + """Get entity information. + + Args: + entity_id: Entity identifier + + Returns: + Entity information + + Raises: + EntityNotFoundError: If entity is not found + """ + entity_info = self.entity_discovery.get_entity_info(entity_id) + if entity_info is None: + raise EntityNotFoundError(f"Entity '{entity_id}' not found") + return entity_info + + async def execute_streaming(self, request: AgentFrameworkRequest) -> AsyncGenerator[Any, None]: + """Execute request and stream results in OpenAI format. + + Args: + request: Request to execute + + Yields: + OpenAI response stream events + """ + try: + entity_id = request.get_entity_id() + if not entity_id: + logger.error("No entity_id specified in request") + return + + # Validate entity exists + if not self.entity_discovery.get_entity_info(entity_id): + logger.error(f"Entity '{entity_id}' not found") + return + + # Execute entity and convert events + async for raw_event in self.execute_entity(entity_id, request): + openai_events = await self.message_mapper.convert_event(raw_event, request) + for event in openai_events: + yield event + + except Exception as e: + logger.exception(f"Error in streaming execution: {e}") + # Could yield error event here + + async def execute_sync(self, request: AgentFrameworkRequest) -> OpenAIResponse: + """Execute request synchronously and return complete response. + + Args: + request: Request to execute + + Returns: + Final aggregated OpenAI response + """ + # Collect all streaming events + events = [event async for event in self.execute_streaming(request)] + + # Aggregate into final response + return await self.message_mapper.aggregate_to_response(events, request) + + async def execute_entity(self, entity_id: str, request: AgentFrameworkRequest) -> AsyncGenerator[Any, None]: + """Execute the entity and yield raw Agent Framework events plus trace events. + + Args: + entity_id: ID of entity to execute + request: Request to execute + + Yields: + Raw Agent Framework events and trace events + """ + try: + # Get entity info and object + entity_info = self.get_entity_info(entity_id) + entity_obj = self.entity_discovery.get_entity_object(entity_id) + + if not entity_obj: + raise EntityNotFoundError(f"Entity object for '{entity_id}' not found") + + logger.info(f"Executing {entity_info.type}: {entity_id}") + + # Extract session_id from request for trace context + session_id = getattr(request.extra_body, "session_id", None) if request.extra_body else None + + # Use simplified trace capture + with capture_traces(session_id=session_id, entity_id=entity_id) as trace_collector: + if entity_info.type == "agent": + async for event in self._execute_agent(entity_obj, request, trace_collector): + yield event + elif entity_info.type == "workflow": + async for event in self._execute_workflow(entity_obj, request, trace_collector): + yield event + else: + raise ValueError(f"Unsupported entity type: {entity_info.type}") + + # Yield any remaining trace events after execution completes + for trace_event in trace_collector.get_pending_events(): + yield trace_event + + except Exception as e: + logger.exception(f"Error executing entity {entity_id}: {e}") + # Yield error event + yield {"type": "error", "message": str(e), "entity_id": entity_id} + + async def _execute_agent( + self, agent: Any, request: AgentFrameworkRequest, trace_collector: Any + ) -> AsyncGenerator[Any, None]: + """Execute Agent Framework agent with trace collection and optional thread support. + + Args: + agent: Agent object to execute + request: Request to execute + trace_collector: Trace collector to get events from + + Yields: + Agent update events and trace events + """ + try: + # Convert input to proper ChatMessage or string + user_message = self._convert_input_to_chat_message(request.input) + + # Get thread if provided in extra_body + thread = None + if request.extra_body and hasattr(request.extra_body, "thread_id") and request.extra_body.thread_id: + thread_id = request.extra_body.thread_id + thread = self.get_thread(thread_id) + if thread: + logger.debug(f"Using existing thread: {thread_id}") + 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: + logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}") + + # 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: + logger.error(f"Error in agent execution: {e}") + yield {"type": "error", "message": f"Agent execution error: {e!s}"} + + async def _execute_workflow( + self, workflow: Any, request: AgentFrameworkRequest, trace_collector: Any + ) -> AsyncGenerator[Any, None]: + """Execute Agent Framework workflow with trace collection. + + Args: + workflow: Workflow object to execute + request: Request to execute + trace_collector: Trace collector to get events from + + Yields: + Workflow events and trace events + """ + try: + # Get input data - prefer structured data from extra_body + input_data: str | list[Any] | dict[str, Any] + if request.extra_body and hasattr(request.extra_body, "input_data") and request.extra_body.input_data: + input_data = request.extra_body.input_data + logger.debug(f"Using structured input_data from extra_body: {type(input_data)}") + else: + input_data = request.input + logger.debug(f"Using input field as fallback: {type(input_data)}") + + # Parse input based on workflow's expected input type + parsed_input = await self._parse_workflow_input(workflow, input_data) + + logger.debug(f"Executing workflow with parsed input type: {type(parsed_input)}") + + # Use Agent Framework workflow's native streaming + async for event in workflow.run_stream(parsed_input): + # Yield any pending trace events first + for trace_event in trace_collector.get_pending_events(): + yield trace_event + + # Then yield the workflow event + yield event + + except Exception as e: + logger.error(f"Error in workflow execution: {e}") + yield {"type": "error", "message": f"Workflow execution error: {e!s}"} + + def _convert_input_to_chat_message(self, input_data: Any) -> Any: + """Convert OpenAI Responses API input to Agent Framework ChatMessage or string. + + Args: + input_data: OpenAI ResponseInputParam (List[ResponseInputItemParam]) + + Returns: + ChatMessage for multimodal content, or string for simple text + """ + # Import Agent Framework types + try: + from agent_framework import ChatMessage, DataContent, Role, TextContent + except ImportError: + # Fallback to string extraction if Agent Framework not available + return self._extract_user_message_fallback(input_data) + + # Handle simple string input (backward compatibility) + if isinstance(input_data, str): + return input_data + + # Handle OpenAI ResponseInputParam (List[ResponseInputItemParam]) + if isinstance(input_data, list): + return self._convert_openai_input_to_chat_message(input_data, ChatMessage, TextContent, DataContent, Role) + + # Fallback for other formats + return self._extract_user_message_fallback(input_data) + + def _convert_openai_input_to_chat_message( + self, input_items: list[Any], ChatMessage: Any, TextContent: Any, DataContent: Any, Role: Any + ) -> Any: + """Convert OpenAI ResponseInputParam to Agent Framework ChatMessage. + + Args: + input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects) + ChatMessage: ChatMessage class for creating chat messages + TextContent: TextContent class for text content + DataContent: DataContent class for data/media content + Role: Role enum for message roles + + Returns: + ChatMessage with converted content + """ + contents = [] + + # Process each input item + for item in input_items: + # Handle dict format (from JSON) + if isinstance(item, dict): + item_type = item.get("type") + if item_type == "message": + # Extract content from OpenAI message + message_content = item.get("content", []) + + # Handle both string content and list content + if isinstance(message_content, str): + contents.append(TextContent(text=message_content)) + elif isinstance(message_content, list): + for content_item in message_content: + # Handle dict content items + if isinstance(content_item, dict): + content_type = content_item.get("type") + + if content_type == "input_text": + text = content_item.get("text", "") + contents.append(TextContent(text=text)) + + elif content_type == "input_image": + image_url = content_item.get("image_url", "") + if image_url: + # Extract media type from data URI if possible + # Parse media type from data URL, fallback to image/png + if image_url.startswith("data:"): + try: + # Extract media type from data:image/jpeg;base64,... format + media_type = image_url.split(";")[0].split(":")[1] + except (IndexError, AttributeError): + logger.warning( + f"Failed to parse media type from data URL: {image_url[:30]}..." + ) + media_type = "image/png" + else: + media_type = "image/png" + contents.append(DataContent(uri=image_url, media_type=media_type)) + + elif content_type == "input_file": + # Handle file input + file_data = content_item.get("file_data") + file_url = content_item.get("file_url") + filename = content_item.get("filename", "") + + # Determine media type from filename + media_type = "application/octet-stream" # default + if filename: + if filename.lower().endswith(".pdf"): + media_type = "application/pdf" + elif filename.lower().endswith((".png", ".jpg", ".jpeg", ".gif")): + media_type = f"image/{filename.split('.')[-1].lower()}" + + # Use file_data or file_url + if file_data: + # Assume file_data is base64, create data URI + data_uri = f"data:{media_type};base64,{file_data}" + contents.append(DataContent(uri=data_uri, media_type=media_type)) + elif file_url: + contents.append(DataContent(uri=file_url, media_type=media_type)) + + # Handle other OpenAI input item types as needed + # (tool calls, function results, etc.) + + # If no contents found, create a simple text message + if not contents: + contents.append(TextContent(text="")) + + # Create ChatMessage with user role + return ChatMessage(role=Role.USER, contents=contents) + + def _extract_user_message_fallback(self, input_data: Any) -> str: + """Fallback method to extract user message as string. + + Args: + input_data: Input data in various formats + + Returns: + Extracted user message string + """ + if isinstance(input_data, str): + return input_data + if isinstance(input_data, dict): + # Try common field names + for field in ["message", "text", "input", "content", "query"]: + if field in input_data: + return str(input_data[field]) + # Fallback to JSON string + return json.dumps(input_data) + return str(input_data) + + async def _parse_workflow_input(self, workflow: Any, raw_input: Any) -> Any: + """Parse input based on workflow's expected input type. + + Args: + workflow: Workflow object + raw_input: Raw input data + + Returns: + Parsed input appropriate for the workflow + """ + try: + # Handle structured input + if isinstance(raw_input, dict): + return self._parse_structured_workflow_input(workflow, raw_input) + return self._parse_raw_workflow_input(workflow, str(raw_input)) + + except Exception as e: + logger.warning(f"Error parsing workflow input: {e}") + return raw_input + + def _parse_structured_workflow_input(self, workflow: Any, input_data: dict[str, Any]) -> Any: + """Parse structured input data for workflow execution. + + Args: + workflow: Workflow object + input_data: Structured input data + + Returns: + Parsed input for workflow + """ + try: + # Get the start executor and its input type + start_executor = workflow.get_start_executor() + if not start_executor or not hasattr(start_executor, "_handlers"): + logger.debug("Cannot determine input type for workflow - using raw dict") + return input_data + + message_types = list(start_executor._handlers.keys()) + if not message_types: + logger.debug("No message types found for start executor - using raw dict") + return input_data + + # Get the first (primary) input type + input_type = message_types[0] + + # If input type is dict, return as-is + if input_type is dict: + return input_data + + # Handle primitive types + if input_type in (str, int, float, bool): + try: + if isinstance(input_data, input_type): + return input_data + if "input" in input_data: + return input_type(input_data["input"]) + if len(input_data) == 1: + value = next(iter(input_data.values())) + return input_type(value) + return input_data + except (ValueError, TypeError) as e: + logger.warning(f"Failed to convert input to {input_type}: {e}") + return input_data + + # If it's a Pydantic model, validate and create instance + if hasattr(input_type, "model_validate"): + try: + return input_type.model_validate(input_data) + except Exception as e: + logger.warning(f"Failed to validate input as {input_type}: {e}") + return input_data + + # If it's a dataclass or other type with annotations + elif hasattr(input_type, "__annotations__"): + try: + return input_type(**input_data) + except Exception as e: + logger.warning(f"Failed to create {input_type} from input data: {e}") + return input_data + + except Exception as e: + logger.warning(f"Error parsing structured workflow input: {e}") + + return input_data + + def _parse_raw_workflow_input(self, workflow: Any, raw_input: str) -> Any: + """Parse raw input string based on workflow's expected input type. + + Args: + workflow: Workflow object + raw_input: Raw input string + + Returns: + Parsed input for workflow + """ + try: + # Get the start executor and its input type + start_executor = workflow.get_start_executor() + if not start_executor or not hasattr(start_executor, "_handlers"): + logger.debug("Cannot determine input type for workflow - using raw string") + return raw_input + + message_types = list(start_executor._handlers.keys()) + if not message_types: + logger.debug("No message types found for start executor - using raw string") + return raw_input + + # Get the first (primary) input type + input_type = message_types[0] + + # If input type is str, return as-is + if input_type is str: + return raw_input + + # If it's a Pydantic model, try to parse JSON + if hasattr(input_type, "model_validate_json"): + try: + # First try to parse as JSON + if raw_input.strip().startswith("{"): + return input_type.model_validate_json(raw_input) + + # Try common field names + common_fields = ["message", "text", "input", "data", "content"] + for field in common_fields: + try: + return input_type(**{field: raw_input}) + except Exception as e: + logger.debug(f"Failed to parse input using field '{field}': {e}") + continue + + # Last resort: try default constructor + return input_type() + + except Exception as e: + logger.debug(f"Failed to parse input as {input_type}: {e}") + + # If it's a dataclass, try JSON parsing + elif hasattr(input_type, "__annotations__"): + try: + if raw_input.strip().startswith("{"): + parsed = json.loads(raw_input) + return input_type(**parsed) + except Exception as e: + logger.debug(f"Failed to parse input as {input_type}: {e}") + + except Exception as e: + logger.debug(f"Error determining workflow input type: {e}") + + # Fallback: return raw string + return raw_input diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py new file mode 100644 index 0000000000..30a9058677 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -0,0 +1,527 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Agent Framework message mapper implementation.""" + +import json +import logging +import uuid +from collections.abc import Sequence +from datetime import datetime +from typing import Any, Union + +from .models import ( + AgentFrameworkRequest, + InputTokensDetails, + OpenAIResponse, + OutputTokensDetails, + ResponseErrorEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionResultComplete, + ResponseOutputMessage, + ResponseOutputText, + ResponseReasoningTextDeltaEvent, + ResponseStreamEvent, + ResponseTextDeltaEvent, + ResponseTraceEventComplete, + ResponseUsage, + ResponseUsageEventComplete, + ResponseWorkflowEventComplete, +) + +logger = logging.getLogger(__name__) + +# Type alias for all possible event types +EventType = Union[ + ResponseStreamEvent, + ResponseWorkflowEventComplete, + ResponseFunctionResultComplete, + ResponseTraceEventComplete, + ResponseUsageEventComplete, +] + + +class MessageMapper: + """Maps Agent Framework messages/responses to OpenAI format.""" + + def __init__(self) -> None: + """Initialize Agent Framework message mapper.""" + self.sequence_counter = 0 + self._conversion_contexts: dict[int, dict[str, Any]] = {} + + # Register content type mappers for all 12 Agent Framework content types + self.content_mappers = { + "TextContent": self._map_text_content, + "TextReasoningContent": self._map_reasoning_content, + "FunctionCallContent": self._map_function_call_content, + "FunctionResultContent": self._map_function_result_content, + "ErrorContent": self._map_error_content, + "UsageContent": self._map_usage_content, + "DataContent": self._map_data_content, + "UriContent": self._map_uri_content, + "HostedFileContent": self._map_hosted_file_content, + "HostedVectorStoreContent": self._map_hosted_vector_store_content, + "FunctionApprovalRequestContent": self._map_approval_request_content, + "FunctionApprovalResponseContent": self._map_approval_response_content, + } + + async def convert_event(self, raw_event: Any, request: AgentFrameworkRequest) -> Sequence[Any]: + """Convert a single Agent Framework event to OpenAI events. + + Args: + raw_event: Agent Framework event (AgentRunResponseUpdate, WorkflowEvent, etc.) + request: Original request for context + + Returns: + List of OpenAI response stream events + """ + context = self._get_or_create_context(request) + + # Handle error events + if isinstance(raw_event, dict) and raw_event.get("type") == "error": + return [await self._create_error_event(raw_event.get("message", "Unknown error"), context)] + + # Handle ResponseTraceEvent objects from our trace collector + from .models import ResponseTraceEvent + + if isinstance(raw_event, ResponseTraceEvent): + return [ + ResponseTraceEventComplete( + type="response.trace.complete", + data=raw_event.data, + item_id=context["item_id"], + sequence_number=self._next_sequence(context), + ) + ] + + # Import Agent Framework types for proper isinstance checks + try: + from agent_framework import AgentRunResponseUpdate, WorkflowEvent + + # Handle agent updates (AgentRunResponseUpdate) + if isinstance(raw_event, AgentRunResponseUpdate): + return await self._convert_agent_update(raw_event, context) + + # Handle workflow events (any class that inherits from WorkflowEvent) + if isinstance(raw_event, WorkflowEvent): + return await self._convert_workflow_event(raw_event, context) + + except ImportError as e: + logger.warning(f"Could not import Agent Framework types: {e}") + # Fallback to attribute-based detection + if hasattr(raw_event, "contents"): + return await self._convert_agent_update(raw_event, context) + if hasattr(raw_event, "__class__") and "Event" in raw_event.__class__.__name__: + return await self._convert_workflow_event(raw_event, context) + + # Unknown event type + return [await self._create_unknown_event(raw_event, context)] + + async def aggregate_to_response(self, events: Sequence[Any], request: AgentFrameworkRequest) -> OpenAIResponse: + """Aggregate streaming events into final OpenAI response. + + Args: + events: List of OpenAI stream events + request: Original request for context + + Returns: + Final aggregated OpenAI response + """ + try: + # Extract text content from events + content_parts = [] + + for event in events: + # Extract delta text from ResponseTextDeltaEvent + if hasattr(event, "delta") and hasattr(event, "type") and event.type == "response.output_text.delta": + content_parts.append(event.delta) + + # Combine content + full_content = "".join(content_parts) + + # Create proper OpenAI Response + response_output_text = ResponseOutputText(type="output_text", text=full_content, annotations=[]) + + response_output_message = ResponseOutputMessage( + type="message", + role="assistant", + content=[response_output_text], + id=f"msg_{uuid.uuid4().hex[:8]}", + status="completed", + ) + + # Create usage object + input_token_count = len(str(request.input)) // 4 if request.input else 0 + output_token_count = len(full_content) // 4 + + usage = ResponseUsage( + input_tokens=input_token_count, + output_tokens=output_token_count, + total_tokens=input_token_count + output_token_count, + input_tokens_details=InputTokensDetails(cached_tokens=0), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ) + + return OpenAIResponse( + id=f"resp_{uuid.uuid4().hex[:12]}", + object="response", + created_at=datetime.now().timestamp(), + model=request.model, + output=[response_output_message], + usage=usage, + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) + + except Exception as e: + logger.exception(f"Error aggregating response: {e}") + return await self._create_error_response(str(e), request) + + def _get_or_create_context(self, request: AgentFrameworkRequest) -> dict[str, Any]: + """Get or create conversion context for this request. + + Args: + request: Request to get context for + + Returns: + Conversion context dictionary + """ + request_key = id(request) + if request_key not in self._conversion_contexts: + self._conversion_contexts[request_key] = { + "sequence_counter": 0, + "item_id": f"msg_{uuid.uuid4().hex[:8]}", + "content_index": 0, + "output_index": 0, + } + return self._conversion_contexts[request_key] + + def _next_sequence(self, context: dict[str, Any]) -> int: + """Get next sequence number for events. + + Args: + context: Conversion context + + Returns: + Next sequence number + """ + context["sequence_counter"] += 1 + return int(context["sequence_counter"]) + + async def _convert_agent_update(self, update: Any, context: dict[str, Any]) -> Sequence[Any]: + """Convert AgentRunResponseUpdate to OpenAI events using comprehensive content mapping. + + Args: + update: Agent run response update + context: Conversion context + + Returns: + List of OpenAI response stream events + """ + events: list[Any] = [] + + try: + # Handle different update types + if not hasattr(update, "contents") or not update.contents: + return events + + for content in update.contents: + content_type = content.__class__.__name__ + + if content_type in self.content_mappers: + mapped_events = await self.content_mappers[content_type](content, context) + if isinstance(mapped_events, list): + events.extend(mapped_events) + else: + events.append(mapped_events) + else: + # Graceful fallback for unknown content types + events.append(await self._create_unknown_content_event(content, context)) + + context["content_index"] += 1 + + except Exception as e: + logger.warning(f"Error converting agent update: {e}") + events.append(await self._create_error_event(str(e), context)) + + return events + + async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) -> Sequence[Any]: + """Convert workflow event to structured OpenAI events. + + Args: + event: Workflow event + context: Conversion context + + Returns: + List of OpenAI response stream events + """ + try: + # Create structured workflow event + workflow_event = ResponseWorkflowEventComplete( + type="response.workflow_event.complete", + data={ + "event_type": event.__class__.__name__, + "data": getattr(event, "data", None), + "executor_id": getattr(event, "executor_id", None), + "timestamp": datetime.now().isoformat(), + }, + executor_id=getattr(event, "executor_id", None), + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + return [workflow_event] + + except Exception as e: + logger.warning(f"Error converting workflow event: {e}") + return [await self._create_error_event(str(e), context)] + + # Content type mappers - implementing our comprehensive mapping plan + + async def _map_text_content(self, content: Any, context: dict[str, Any]) -> ResponseTextDeltaEvent: + """Map TextContent to ResponseTextDeltaEvent.""" + return self._create_text_delta_event(content.text, context) + + async def _map_reasoning_content(self, content: Any, context: dict[str, Any]) -> ResponseReasoningTextDeltaEvent: + """Map TextReasoningContent to ResponseReasoningTextDeltaEvent.""" + return ResponseReasoningTextDeltaEvent( + type="response.reasoning_text.delta", + delta=content.text, + item_id=context["item_id"], + output_index=context["output_index"], + content_index=context["content_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_function_call_content( + self, content: Any, context: dict[str, Any] + ) -> list[ResponseFunctionCallArgumentsDeltaEvent]: + """Map FunctionCallContent to ResponseFunctionCallArgumentsDeltaEvent(s).""" + events = [] + + # For streaming, need to chunk the arguments JSON + args_str = json.dumps(content.arguments) if hasattr(content, "arguments") and content.arguments else "{}" + + # Chunk the JSON string for streaming + for chunk in self._chunk_json_string(args_str): + events.append( + ResponseFunctionCallArgumentsDeltaEvent( + type="response.function_call_arguments.delta", + delta=chunk, + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + ) + + return events + + async def _map_function_result_content( + self, content: Any, context: dict[str, Any] + ) -> ResponseFunctionResultComplete: + """Map FunctionResultContent to structured event.""" + return ResponseFunctionResultComplete( + type="response.function_result.complete", + data={ + "call_id": getattr(content, "call_id", f"call_{uuid.uuid4().hex[:8]}"), + "result": getattr(content, "result", None), + "status": "completed" if not getattr(content, "exception", None) else "failed", + "exception": str(getattr(content, "exception", None)) if getattr(content, "exception", None) else None, + "timestamp": datetime.now().isoformat(), + }, + call_id=getattr(content, "call_id", f"call_{uuid.uuid4().hex[:8]}"), + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_error_content(self, content: Any, context: dict[str, Any]) -> ResponseErrorEvent: + """Map ErrorContent to ResponseErrorEvent.""" + return ResponseErrorEvent( + type="error", + message=getattr(content, "message", "Unknown error"), + code=getattr(content, "error_code", None), + param=None, + sequence_number=self._next_sequence(context), + ) + + async def _map_usage_content(self, content: Any, context: dict[str, Any]) -> ResponseUsageEventComplete: + """Map UsageContent to structured usage event.""" + # Store usage data in context for aggregation + if "usage_data" not in context: + context["usage_data"] = [] + context["usage_data"].append(content) + + return ResponseUsageEventComplete( + type="response.usage.complete", + data={ + "usage_data": getattr(content, "usage_data", {}), + "total_tokens": getattr(content, "total_tokens", 0), + "completion_tokens": getattr(content, "completion_tokens", 0), + "prompt_tokens": getattr(content, "prompt_tokens", 0), + "timestamp": datetime.now().isoformat(), + }, + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_data_content(self, content: Any, context: dict[str, Any]) -> ResponseTraceEventComplete: + """Map DataContent to structured trace event.""" + return ResponseTraceEventComplete( + type="response.trace.complete", + data={ + "content_type": "data", + "data": getattr(content, "data", None), + "mime_type": getattr(content, "mime_type", "application/octet-stream"), + "size_bytes": len(str(getattr(content, "data", ""))) if getattr(content, "data", None) else 0, + "timestamp": datetime.now().isoformat(), + }, + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_uri_content(self, content: Any, context: dict[str, Any]) -> ResponseTraceEventComplete: + """Map UriContent to structured trace event.""" + return ResponseTraceEventComplete( + type="response.trace.complete", + data={ + "content_type": "uri", + "uri": getattr(content, "uri", ""), + "mime_type": getattr(content, "mime_type", "text/plain"), + "timestamp": datetime.now().isoformat(), + }, + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_hosted_file_content(self, content: Any, context: dict[str, Any]) -> ResponseTraceEventComplete: + """Map HostedFileContent to structured trace event.""" + return ResponseTraceEventComplete( + type="response.trace.complete", + data={ + "content_type": "hosted_file", + "file_id": getattr(content, "file_id", "unknown"), + "timestamp": datetime.now().isoformat(), + }, + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_hosted_vector_store_content( + self, content: Any, context: dict[str, Any] + ) -> ResponseTraceEventComplete: + """Map HostedVectorStoreContent to structured trace event.""" + return ResponseTraceEventComplete( + type="response.trace.complete", + data={ + "content_type": "hosted_vector_store", + "vector_store_id": getattr(content, "vector_store_id", "unknown"), + "timestamp": datetime.now().isoformat(), + }, + item_id=context["item_id"], + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + + async def _map_approval_request_content(self, content: Any, context: dict[str, Any]) -> dict[str, Any]: + """Map FunctionApprovalRequestContent to custom event.""" + return { + "type": "response.function_approval.requested", + "request_id": getattr(content, "id", "unknown"), + "function_call": { + "id": getattr(content.function_call, "call_id", "") if hasattr(content, "function_call") else "", + "name": getattr(content.function_call, "name", "") if hasattr(content, "function_call") else "", + "arguments": getattr(content.function_call, "arguments", {}) + if hasattr(content, "function_call") + else {}, + }, + "item_id": context["item_id"], + "output_index": context["output_index"], + "sequence_number": self._next_sequence(context), + } + + async def _map_approval_response_content(self, content: Any, context: dict[str, Any]) -> dict[str, Any]: + """Map FunctionApprovalResponseContent to custom event.""" + return { + "type": "response.function_approval.responded", + "request_id": getattr(content, "request_id", "unknown"), + "approved": getattr(content, "approved", False), + "item_id": context["item_id"], + "output_index": context["output_index"], + "sequence_number": self._next_sequence(context), + } + + # Helper methods + + def _create_text_delta_event(self, text: str, context: dict[str, Any]) -> ResponseTextDeltaEvent: + """Create a ResponseTextDeltaEvent.""" + return ResponseTextDeltaEvent( + type="response.output_text.delta", + item_id=context["item_id"], + output_index=context["output_index"], + content_index=context["content_index"], + delta=text, + sequence_number=self._next_sequence(context), + logprobs=[], + ) + + async def _create_error_event(self, message: str, context: dict[str, Any]) -> ResponseErrorEvent: + """Create a ResponseErrorEvent.""" + return ResponseErrorEvent( + type="error", message=message, code=None, param=None, sequence_number=self._next_sequence(context) + ) + + async def _create_unknown_event(self, event_data: Any, context: dict[str, Any]) -> ResponseStreamEvent: + """Create event for unknown event types.""" + text = f"Unknown event: {event_data!s}\\n" + return self._create_text_delta_event(text, context) + + async def _create_unknown_content_event(self, content: Any, context: dict[str, Any]) -> ResponseStreamEvent: + """Create event for unknown content types.""" + content_type = content.__class__.__name__ + text = f"⚠️ Unknown content type: {content_type}\\n" + return self._create_text_delta_event(text, context) + + def _chunk_json_string(self, json_str: str, chunk_size: int = 50) -> list[str]: + """Chunk JSON string for streaming.""" + return [json_str[i : i + chunk_size] for i in range(0, len(json_str), chunk_size)] + + async def _create_error_response(self, error_message: str, request: AgentFrameworkRequest) -> OpenAIResponse: + """Create error response.""" + error_text = f"Error: {error_message}" + + response_output_text = ResponseOutputText(type="output_text", text=error_text, annotations=[]) + + response_output_message = ResponseOutputMessage( + type="message", + role="assistant", + content=[response_output_text], + id=f"msg_{uuid.uuid4().hex[:8]}", + status="completed", + ) + + usage = ResponseUsage( + input_tokens=0, + output_tokens=0, + total_tokens=0, + input_tokens_details=InputTokensDetails(cached_tokens=0), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ) + + return OpenAIResponse( + id=f"resp_{uuid.uuid4().hex[:12]}", + object="response", + created_at=datetime.now().timestamp(), + model=request.model, + output=[response_output_message], + usage=usage, + parallel_tool_calls=False, + tool_choice="none", + tools=[], + ) diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py new file mode 100644 index 0000000000..206877dcc4 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -0,0 +1,397 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""FastAPI server implementation.""" + +import logging +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from typing import Any + +from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles + +from ._discovery import EntityDiscovery +from ._executor import AgentFrameworkExecutor +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__) + + +class DevServer: + """Development Server - OpenAI compatible API server for debugging agents.""" + + def __init__( + self, + entities_dir: str | None = None, + port: int = 8080, + host: str = "127.0.0.1", + cors_origins: list[str] | None = None, + ui_enabled: bool = True, + ) -> None: + """Initialize the development server. + + Args: + entities_dir: Directory to scan for entities + port: Port to run server on + host: Host to bind server to + cors_origins: List of allowed CORS origins + ui_enabled: Whether to enable the UI + """ + self.entities_dir = entities_dir + self.port = port + self.host = host + self.cors_origins = cors_origins or ["*"] + self.ui_enabled = ui_enabled + self.executor: AgentFrameworkExecutor | None = None + self._app: FastAPI | None = None + self._pending_entities: list[Any] | None = None + + async def _ensure_executor(self) -> AgentFrameworkExecutor: + """Ensure executor is initialized.""" + if self.executor is None: + logger.info("Initializing Agent Framework executor...") + + # Create components directly + entity_discovery = EntityDiscovery(self.entities_dir) + message_mapper = MessageMapper() + self.executor = AgentFrameworkExecutor(entity_discovery, message_mapper) + + # Discover entities from directory + discovered_entities = await self.executor.discover_entities() + logger.info(f"Discovered {len(discovered_entities)} entities from directory") + + # Register any pending in-memory entities + if self._pending_entities: + discovery = self.executor.entity_discovery + for entity in self._pending_entities: + try: + entity_info = await discovery.create_entity_info_from_object(entity) + discovery.register_entity(entity_info.id, entity_info, entity) + logger.info(f"Registered in-memory entity: {entity_info.id}") + except Exception as e: + logger.error(f"Failed to register in-memory entity: {e}") + self._pending_entities = None # Clear after registration + + # Get the final entity count after all registration + all_entities = self.executor.entity_discovery.list_entities() + logger.info(f"Total entities available: {len(all_entities)}") + + return self.executor + + def create_app(self) -> FastAPI: + """Create the FastAPI application.""" + + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + # Startup + logger.info("Starting Agent Framework Server") + await self._ensure_executor() + yield + # Shutdown + logger.info("Shutting down Agent Framework Server") + + app = FastAPI( + title="Agent Framework Server", + description="OpenAI-compatible API server for Agent Framework and other AI frameworks", + version="1.0.0", + lifespan=lifespan, + ) + + # Add CORS middleware + app.add_middleware( + CORSMiddleware, + allow_origins=self.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + self._register_routes(app) + self._mount_ui(app) + + return app + + def _register_routes(self, app: FastAPI) -> None: + """Register API routes.""" + + @app.get("/health") + async def health_check() -> dict[str, Any]: + """Health check endpoint.""" + executor = await self._ensure_executor() + entities = await executor.discover_entities() + + return {"status": "healthy", "entities_count": len(entities), "framework": "agent_framework"} + + @app.get("/v1/entities", response_model=DiscoveryResponse) + async def discover_entities() -> DiscoveryResponse: + """List all registered entities.""" + try: + executor = await self._ensure_executor() + # Use list_entities() instead of discover_entities() to get already-registered entities + entities = executor.entity_discovery.list_entities() + return DiscoveryResponse(entities=entities) + except Exception as e: + logger.error(f"Error listing entities: {e}") + raise HTTPException(status_code=500, detail=f"Entity listing failed: {e!s}") from e + + @app.get("/v1/entities/{entity_id}/info", response_model=EntityInfo) + async def get_entity_info(entity_id: str) -> EntityInfo: + """Get detailed information about a specific entity.""" + try: + executor = await self._ensure_executor() + entity_info = executor.get_entity_info(entity_id) + + if not entity_info: + raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found") + + # For workflows, populate additional detailed information + if entity_info.type == "workflow": + entity_obj = executor.entity_discovery.get_entity_object(entity_id) + if entity_obj: + # Get workflow structure + workflow_dump = None + if hasattr(entity_obj, "model_dump"): + workflow_dump = entity_obj.model_dump() + elif hasattr(entity_obj, "__dict__"): + workflow_dump = {k: v for k, v in entity_obj.__dict__.items() if not k.startswith("_")} + + # Get input schema information + input_schema = {} + input_type_name = "Unknown" + start_executor_id = "" + + try: + start_executor = entity_obj.get_start_executor() + if start_executor and hasattr(start_executor, "_handlers"): + message_types = list(start_executor._handlers.keys()) + if message_types: + input_type = message_types[0] + input_type_name = getattr(input_type, "__name__", str(input_type)) + + # Basic schema generation for common types + if input_type is str: + input_schema = {"type": "string"} + elif input_type is dict: + input_schema = {"type": "object"} + elif hasattr(input_type, "model_json_schema"): + input_schema = input_type.model_json_schema() + + start_executor_id = getattr(start_executor, "executor_id", "") + except Exception as e: + logger.debug(f"Could not extract input info for workflow {entity_id}: {e}") + + # Get executor list + executor_list = [] + if hasattr(entity_obj, "executors") and entity_obj.executors: + executor_list = [getattr(ex, "executor_id", str(ex)) for ex in entity_obj.executors] + + # Create copy of entity info and populate workflow-specific fields + enhanced_info = entity_info.model_copy() + enhanced_info.workflow_dump = workflow_dump + enhanced_info.input_schema = input_schema + enhanced_info.input_type_name = input_type_name + enhanced_info.start_executor_id = start_executor_id + + # Update executors field if we found better data + if executor_list: + enhanced_info.executors = executor_list + return enhanced_info + + # For non-workflow entities, return as-is + return entity_info + + except HTTPException: + raise + except Exception as e: + 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/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}") + + # Get entity_id using the new method + entity_id = request.get_entity_id() + logger.info(f"Extracted entity_id: {entity_id}") + + if not entity_id: + error = OpenAIError.create(f"Missing entity_id. Request extra_body: {request.extra_body}") + return JSONResponse(status_code=400, content=error.model_dump()) + + # Get executor and validate entity exists + executor = await self._ensure_executor() + try: + entity_info = executor.get_entity_info(entity_id) + logger.info(f"Found entity: {entity_info.name} ({entity_info.type})") + except Exception: + error = OpenAIError.create(f"Entity not found: {entity_id}") + return JSONResponse(status_code=404, content=error.model_dump()) + + # Execute request + if request.stream: + return StreamingResponse( + self._stream_execution(executor, request), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Access-Control-Allow-Origin": "*", + }, + ) + return await executor.execute_sync(request) + + except Exception as e: + logger.error(f"Error executing request: {e}") + error = OpenAIError.create(f"Execution failed: {e!s}") + return JSONResponse(status_code=500, content=error.model_dump()) + + @app.post("/v1/threads") + async def create_thread(request_data: dict[str, Any]) -> dict[str, Any]: + """Create a new thread for an agent.""" + try: + agent_id = request_data.get("agent_id") + if not agent_id: + raise HTTPException(status_code=400, detail="agent_id is required") + + executor = await self._ensure_executor() + thread_id = executor.create_thread(agent_id) + + return { + "id": thread_id, + "object": "thread", + "created_at": int(__import__("time").time()), + "metadata": {"agent_id": agent_id}, + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Error creating thread: {e}") + raise HTTPException(status_code=500, detail=f"Failed to create thread: {e!s}") from e + + @app.get("/v1/threads") + async def list_threads(agent_id: str) -> dict[str, Any]: + """List threads for an agent.""" + try: + executor = await self._ensure_executor() + thread_ids = executor.list_threads_for_agent(agent_id) + + # Convert thread IDs to thread objects + threads = [] + for thread_id in thread_ids: + threads.append({"id": thread_id, "object": "thread", "agent_id": agent_id}) + + return {"object": "list", "data": threads} + except Exception as e: + logger.error(f"Error listing threads: {e}") + raise HTTPException(status_code=500, detail=f"Failed to list threads: {e!s}") from e + + @app.get("/v1/threads/{thread_id}") + async def get_thread(thread_id: str) -> dict[str, Any]: + """Get thread information.""" + try: + executor = await self._ensure_executor() + + # Check if thread exists + thread = executor.get_thread(thread_id) + if not thread: + raise HTTPException(status_code=404, detail="Thread not found") + + # Get the agent that owns this thread + agent_id = executor.get_agent_for_thread(thread_id) + + return {"id": thread_id, "object": "thread", "agent_id": agent_id} + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting thread {thread_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get thread: {e!s}") from e + + @app.delete("/v1/threads/{thread_id}") + async def delete_thread(thread_id: str) -> dict[str, Any]: + """Delete a thread.""" + try: + executor = await self._ensure_executor() + success = executor.delete_thread(thread_id) + + if not success: + raise HTTPException(status_code=404, detail="Thread not found") + + return {"id": thread_id, "object": "thread.deleted", "deleted": True} + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting thread {thread_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to delete thread: {e!s}") from e + + @app.get("/v1/threads/{thread_id}/messages") + async def get_thread_messages(thread_id: str) -> dict[str, Any]: + """Get messages from a thread.""" + try: + executor = await self._ensure_executor() + + # Check if thread exists + thread = executor.get_thread(thread_id) + if not thread: + raise HTTPException(status_code=404, detail="Thread not found") + + # Get messages from thread + messages = await executor.get_thread_messages(thread_id) + + return {"object": "list", "data": messages, "thread_id": thread_id} + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting messages for thread {thread_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get thread messages: {e!s}") from e + + async def _stream_execution( + self, executor: AgentFrameworkExecutor, request: AgentFrameworkRequest + ) -> AsyncGenerator[str, None]: + """Stream execution directly through executor.""" + try: + # Direct call to executor - simple and clean + async for event in executor.execute_streaming(request): + yield f"data: {event.model_dump_json()}\n\n" + + # Send final done event + yield "data: [DONE]\n\n" + + 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" + + def _mount_ui(self, app: FastAPI) -> None: + """Mount the UI as static files.""" + from pathlib import Path + + ui_dir = Path(__file__).parent / "ui" + if ui_dir.exists() and ui_dir.is_dir() and self.ui_enabled: + app.mount("/", StaticFiles(directory=str(ui_dir), html=True), name="ui") + + def register_entities(self, entities: list[Any]) -> None: + """Register entities to be discovered when server starts. + + Args: + entities: List of entity objects to register + """ + if self._pending_entities is None: + self._pending_entities = [] + self._pending_entities.extend(entities) + + def get_app(self) -> FastAPI: + """Get the FastAPI application instance.""" + if self._app is None: + self._app = self.create_app() + return self._app diff --git a/python/packages/devui/agent_framework_devui/_session.py b/python/packages/devui/agent_framework_devui/_session.py new file mode 100644 index 0000000000..587207a924 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_session.py @@ -0,0 +1,191 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Session management for agent execution tracking.""" + +import logging +import uuid +from datetime import datetime +from typing import Any + +logger = logging.getLogger(__name__) + +# Type aliases for better readability +SessionData = dict[str, Any] +RequestRecord = dict[str, Any] +SessionSummary = dict[str, Any] + + +class SessionManager: + """Manages execution sessions for tracking requests and context.""" + + def __init__(self) -> None: + """Initialize the session manager.""" + self.sessions: dict[str, SessionData] = {} + + def create_session(self, session_id: str | None = None) -> str: + """Create a new execution session. + + Args: + session_id: Optional session ID, if not provided a new one is generated + + Returns: + Session ID + """ + if not session_id: + session_id = str(uuid.uuid4()) + + self.sessions[session_id] = { + "id": session_id, + "created_at": datetime.now(), + "requests": [], + "context": {}, + "active": True, + } + + logger.debug(f"Created session: {session_id}") + return session_id + + def get_session(self, session_id: str) -> SessionData | None: + """Get session information. + + Args: + session_id: Session ID + + Returns: + Session data or None if not found + """ + return self.sessions.get(session_id) + + def close_session(self, session_id: str) -> None: + """Close and cleanup a session. + + Args: + session_id: Session ID to close + """ + if session_id in self.sessions: + self.sessions[session_id]["active"] = False + logger.debug(f"Closed session: {session_id}") + + def add_request_record( + self, session_id: str, entity_id: str, executor_name: str, request_input: Any, model: str + ) -> str: + """Add a request record to a session. + + Args: + session_id: Session ID + entity_id: ID of the entity being executed + executor_name: Name of the executor + request_input: Input for the request + model: Model name + + Returns: + Request ID + """ + session = self.get_session(session_id) + if not session: + return "" + + request_record: RequestRecord = { + "id": str(uuid.uuid4()), + "timestamp": datetime.now(), + "entity_id": entity_id, + "executor": executor_name, + "input": request_input, + "model": model, + "stream": True, + } + session["requests"].append(request_record) + return str(request_record["id"]) + + def update_request_record(self, session_id: str, request_id: str, updates: dict[str, Any]) -> None: + """Update a request record in a session. + + Args: + session_id: Session ID + request_id: Request ID to update + updates: Dictionary of updates to apply + """ + session = self.get_session(session_id) + if not session: + return + + for request in session["requests"]: + if request["id"] == request_id: + request.update(updates) + break + + def get_session_history(self, session_id: str) -> SessionSummary | None: + """Get session execution history. + + Args: + session_id: Session ID + + Returns: + Session history or None if not found + """ + session = self.get_session(session_id) + if not session: + return None + + return { + "session_id": session_id, + "created_at": session["created_at"].isoformat(), + "active": session["active"], + "request_count": len(session["requests"]), + "requests": [ + { + "id": req["id"], + "timestamp": req["timestamp"].isoformat(), + "entity_id": req["entity_id"], + "executor": req["executor"], + "model": req["model"], + "input_length": len(str(req["input"])) if req["input"] else 0, + "execution_time": req.get("execution_time"), + "status": req.get("status", "unknown"), + } + for req in session["requests"] + ], + } + + def get_active_sessions(self) -> list[SessionSummary]: + """Get list of active sessions. + + Returns: + List of active session summaries + """ + active_sessions = [] + + for session_id, session in self.sessions.items(): + if session["active"]: + active_sessions.append({ + "session_id": session_id, + "created_at": session["created_at"].isoformat(), + "request_count": len(session["requests"]), + "last_activity": ( + session["requests"][-1]["timestamp"].isoformat() + if session["requests"] + else session["created_at"].isoformat() + ), + }) + + return active_sessions + + def cleanup_old_sessions(self, max_age_hours: int = 24) -> None: + """Cleanup old sessions to prevent memory leaks. + + Args: + max_age_hours: Maximum age of sessions to keep in hours + """ + cutoff_time = datetime.now().timestamp() - (max_age_hours * 3600) + + sessions_to_remove = [] + for session_id, session in self.sessions.items(): + if session["created_at"].timestamp() < cutoff_time: + sessions_to_remove.append(session_id) + + for session_id in sessions_to_remove: + del self.sessions[session_id] + logger.debug(f"Cleaned up old session: {session_id}") + + if sessions_to_remove: + logger.info(f"Cleaned up {len(sessions_to_remove)} old sessions") diff --git a/python/packages/devui/agent_framework_devui/_tracing.py b/python/packages/devui/agent_framework_devui/_tracing.py new file mode 100644 index 0000000000..83abd2fcb7 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_tracing.py @@ -0,0 +1,168 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Simplified tracing integration for Agent Framework Server.""" + +import logging +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from datetime import datetime +from typing import Any + +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +from .models import ResponseTraceEvent + +logger = logging.getLogger(__name__) + + +class SimpleTraceCollector(SpanExporter): + """Simple trace collector that captures spans for direct yielding.""" + + def __init__(self, session_id: str | None = None, entity_id: str | None = None) -> None: + """Initialize trace collector. + + Args: + session_id: Session identifier for context + entity_id: Entity identifier for context + """ + self.session_id = session_id + self.entity_id = entity_id + self.collected_events: list[ResponseTraceEvent] = [] + + def export(self, spans: Sequence[Any]) -> SpanExportResult: + """Collect spans as trace events. + + Args: + spans: Sequence of OpenTelemetry spans + + Returns: + SpanExportResult indicating success + """ + logger.debug(f"SimpleTraceCollector received {len(spans)} spans") + + try: + for span in spans: + trace_event = self._convert_span_to_trace_event(span) + if trace_event: + self.collected_events.append(trace_event) + logger.debug(f"Collected trace event: {span.name}") + + return SpanExportResult.SUCCESS + + except Exception as e: + logger.error(f"Error collecting trace spans: {e}") + return SpanExportResult.FAILURE + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Force flush spans (no-op for simple collection).""" + return True + + def get_pending_events(self) -> list[ResponseTraceEvent]: + """Get and clear pending trace events. + + Returns: + List of collected trace events, clearing the internal list + """ + events = self.collected_events.copy() + self.collected_events.clear() + return events + + def _convert_span_to_trace_event(self, span: Any) -> ResponseTraceEvent | None: + """Convert OpenTelemetry span to ResponseTraceEvent. + + Args: + span: OpenTelemetry span + + Returns: + ResponseTraceEvent or None if conversion fails + """ + try: + start_time = span.start_time / 1_000_000_000 # Convert from nanoseconds + end_time = span.end_time / 1_000_000_000 if span.end_time else None + duration_ms = ((end_time - start_time) * 1000) if end_time else None + + # Build trace data + trace_data = { + "type": "trace_span", + "span_id": str(span.context.span_id), + "trace_id": str(span.context.trace_id), + "parent_span_id": str(span.parent.span_id) if span.parent else None, + "operation_name": span.name, + "start_time": start_time, + "end_time": end_time, + "duration_ms": duration_ms, + "attributes": dict(span.attributes) if span.attributes else {}, + "status": str(span.status.status_code) if hasattr(span, "status") else "OK", + "session_id": self.session_id, + "entity_id": self.entity_id, + } + + # Add events if available + if hasattr(span, "events") and span.events: + trace_data["events"] = [ + { + "name": event.name, + "timestamp": event.timestamp / 1_000_000_000, + "attributes": dict(event.attributes) if event.attributes else {}, + } + for event in span.events + ] + + # Add error information if span failed + if hasattr(span, "status") and span.status.status_code.name == "ERROR": + trace_data["error"] = span.status.description or "Unknown error" + + return ResponseTraceEvent(type="trace_event", data=trace_data, timestamp=datetime.now().isoformat()) + + except Exception as e: + logger.warning(f"Failed to convert span {getattr(span, 'name', 'unknown')}: {e}") + return None + + +@contextmanager +def capture_traces( + session_id: str | None = None, entity_id: str | None = None +) -> Generator[SimpleTraceCollector, None, None]: + """Context manager to capture traces during execution. + + Args: + session_id: Session identifier for context + entity_id: Entity identifier for context + + Yields: + SimpleTraceCollector instance to get trace events from + """ + collector = SimpleTraceCollector(session_id, entity_id) + + try: + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + + # Get current tracer provider and add our collector + provider = trace.get_tracer_provider() + processor = SimpleSpanProcessor(collector) + + # Check if this is a real TracerProvider (not the default NoOpTracerProvider) + if isinstance(provider, TracerProvider): + provider.add_span_processor(processor) + logger.debug(f"Added trace collector to TracerProvider for session: {session_id}, entity: {entity_id}") + + try: + yield collector + finally: + # Clean up - shutdown processor + try: + processor.shutdown() + except Exception as e: + logger.debug(f"Error shutting down processor: {e}") + else: + logger.warning(f"No real TracerProvider available, got: {type(provider)}") + yield collector + + except ImportError: + logger.debug("OpenTelemetry not available") + yield collector + except Exception as e: + logger.error(f"Error setting up trace capture: {e}") + yield collector diff --git a/python/packages/devui/agent_framework_devui/models/__init__.py b/python/packages/devui/agent_framework_devui/models/__init__.py new file mode 100644 index 0000000000..d4c2d0da24 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/models/__init__.py @@ -0,0 +1,72 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Agent Framework DevUI Models - OpenAI-compatible types and custom extensions.""" + +# Import discovery models +# Import all OpenAI types directly from the openai package +from openai.types.responses import ( + Response, + ResponseErrorEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseInputParam, + ResponseOutputMessage, + ResponseOutputText, + ResponseReasoningTextDeltaEvent, + ResponseStreamEvent, + ResponseTextDeltaEvent, + ResponseUsage, + ToolParam, +) +from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails +from openai.types.shared import Metadata, ResponsesModel + +from ._discovery_models import DiscoveryResponse, EntityInfo +from ._openai_custom import ( + AgentFrameworkRequest, + OpenAIError, + ResponseFunctionResultComplete, + ResponseFunctionResultDelta, + ResponseTraceEvent, + ResponseTraceEventComplete, + ResponseTraceEventDelta, + ResponseUsageEventComplete, + ResponseUsageEventDelta, + ResponseWorkflowEventComplete, + ResponseWorkflowEventDelta, +) + +# Type alias for compatibility +OpenAIResponse = Response + +# Export all types for easy importing +__all__ = [ + "AgentFrameworkRequest", + "DiscoveryResponse", + "EntityInfo", + "InputTokensDetails", + "Metadata", + "OpenAIError", + "OpenAIResponse", + "OutputTokensDetails", + "Response", + "ResponseErrorEvent", + "ResponseFunctionCallArgumentsDeltaEvent", + "ResponseFunctionResultComplete", + "ResponseFunctionResultDelta", + "ResponseInputParam", + "ResponseOutputMessage", + "ResponseOutputText", + "ResponseReasoningTextDeltaEvent", + "ResponseStreamEvent", + "ResponseTextDeltaEvent", + "ResponseTraceEvent", + "ResponseTraceEventComplete", + "ResponseTraceEventDelta", + "ResponseUsage", + "ResponseUsageEventComplete", + "ResponseUsageEventDelta", + "ResponseWorkflowEventComplete", + "ResponseWorkflowEventDelta", + "ResponsesModel", + "ToolParam", +] diff --git a/python/packages/devui/agent_framework_devui/models/_discovery_models.py b/python/packages/devui/agent_framework_devui/models/_discovery_models.py new file mode 100644 index 0000000000..307ec7e37f --- /dev/null +++ b/python/packages/devui/agent_framework_devui/models/_discovery_models.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Discovery API models for entity information.""" + +from typing import Any + +from pydantic import BaseModel, Field + + +class EntityInfo(BaseModel): + """Entity information for discovery and detailed views.""" + + # Always present (core entity data) + id: str + type: str # "agent", "workflow" + name: str + description: str | None = None + framework: str + tools: list[str | dict[str, Any]] | None = None + metadata: dict[str, Any] = Field(default_factory=dict) + + # Workflow-specific fields (populated only for detailed info requests) + executors: list[str] | None = None + workflow_dump: dict[str, Any] | None = None + input_schema: dict[str, Any] | None = None + input_type_name: str | None = None + start_executor_id: str | None = None + + +class DiscoveryResponse(BaseModel): + """Response model for entity discovery.""" + + entities: list[EntityInfo] = Field(default_factory=list) diff --git a/python/packages/devui/agent_framework_devui/models/_openai_custom.py b/python/packages/devui/agent_framework_devui/models/_openai_custom.py new file mode 100644 index 0000000000..bc2b006c64 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/models/_openai_custom.py @@ -0,0 +1,202 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Custom OpenAI-compatible event types for Agent Framework extensions. + +These are custom event types that extend beyond the standard OpenAI Responses API +to support Agent Framework specific features like workflows, traces, and function results. +""" + +from typing import Any, Literal + +from pydantic import BaseModel + +# Custom Agent Framework OpenAI event types for structured data + + +class ResponseWorkflowEventDelta(BaseModel): + """Structured workflow event with completion tracking.""" + + type: Literal["response.workflow_event.delta"] = "response.workflow_event.delta" + delta: dict[str, Any] + executor_id: str | None = None + is_complete: bool = False # Track if this is the final part + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseWorkflowEventComplete(BaseModel): + """Complete workflow event data.""" + + type: Literal["response.workflow_event.complete"] = "response.workflow_event.complete" + data: dict[str, Any] # Complete event data, not delta + executor_id: str | None = None + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseFunctionResultDelta(BaseModel): + """Structured function result with completion tracking.""" + + type: Literal["response.function_result.delta"] = "response.function_result.delta" + delta: dict[str, Any] + call_id: str + is_complete: bool = False + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseFunctionResultComplete(BaseModel): + """Complete function result data.""" + + type: Literal["response.function_result.complete"] = "response.function_result.complete" + data: dict[str, Any] # Complete function result data, not delta + call_id: str + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseTraceEventDelta(BaseModel): + """Structured trace event with completion tracking.""" + + type: Literal["response.trace.delta"] = "response.trace.delta" + delta: dict[str, Any] + span_id: str | None = None + is_complete: bool = False + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseTraceEventComplete(BaseModel): + """Complete trace event data.""" + + type: Literal["response.trace.complete"] = "response.trace.complete" + data: dict[str, Any] # Complete trace data, not delta + span_id: str | None = None + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseUsageEventDelta(BaseModel): + """Structured usage event with completion tracking.""" + + type: Literal["response.usage.delta"] = "response.usage.delta" + delta: dict[str, Any] + is_complete: bool = False + item_id: str + output_index: int = 0 + sequence_number: int + + +class ResponseUsageEventComplete(BaseModel): + """Complete usage event data.""" + + type: Literal["response.usage.complete"] = "response.usage.complete" + data: dict[str, Any] # Complete usage data, not delta + item_id: str + output_index: int = 0 + sequence_number: int + + +# Agent Framework extension fields +class AgentFrameworkExtraBody(BaseModel): + """Agent Framework specific routing fields for OpenAI requests.""" + + entity_id: str + thread_id: str | None = None + input_data: dict[str, Any] | None = None + + class Config: + extra = "allow" # Allow additional fields + + +# Agent Framework Request Model - Extending real OpenAI types +class AgentFrameworkRequest(BaseModel): + """OpenAI ResponseCreateParams with Agent Framework extensions. + + This properly extends the real OpenAI API request format while adding + our custom routing fields in extra_body. + """ + + # All OpenAI fields from ResponseCreateParams + model: str + input: str | list[Any] # ResponseInputParam + stream: bool | None = False + + # Common OpenAI optional fields + instructions: str | None = None + metadata: dict[str, Any] | None = None + temperature: float | None = None + max_output_tokens: int | None = None + tools: list[dict[str, Any]] | None = None + + # Agent Framework extension - strongly typed + extra_body: AgentFrameworkExtraBody | None = None + + class Config: + # Allow extra fields from OpenAI spec + extra = "allow" + + entity_id: str | None = None # Allow entity_id as top-level field + + def get_entity_id(self) -> str | None: + """Get entity_id from either top-level field or extra_body.""" + # Priority 1: Top-level entity_id field + if self.entity_id: + return self.entity_id + + # Priority 2: entity_id in extra_body + if self.extra_body and hasattr(self.extra_body, "entity_id"): + return self.extra_body.entity_id + + return None + + def to_openai_params(self) -> dict[str, Any]: + """Convert to dict for OpenAI client compatibility.""" + data = self.model_dump(exclude={"extra_body", "entity_id"}, exclude_none=True) + if self.extra_body: + # Don't merge extra_body into main params to keep them separate + data["extra_body"] = self.extra_body + return data + + +# Error handling +class ResponseTraceEvent(BaseModel): + """Trace event for execution tracing.""" + + type: Literal["trace_event"] = "trace_event" + data: dict[str, Any] + timestamp: str + + +class OpenAIError(BaseModel): + """OpenAI standard error response model.""" + + error: dict[str, Any] + + @classmethod + 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) + + +# Export all custom types +__all__ = [ + "AgentFrameworkRequest", + "OpenAIError", + "ResponseFunctionResultComplete", + "ResponseFunctionResultDelta", + "ResponseTraceEvent", + "ResponseTraceEventComplete", + "ResponseTraceEventDelta", + "ResponseUsageEventComplete", + "ResponseUsageEventDelta", + "ResponseWorkflowEventComplete", + "ResponseWorkflowEventDelta", +] 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 new file mode 100644 index 0000000000..b2cdcf0dc4 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/ui/assets/index-BESRiUNX.js @@ -0,0 +1,383 @@ +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/agent_framework_devui/ui/vite.svg b/python/packages/devui/agent_framework_devui/ui/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/ui/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python/packages/devui/dev.md b/python/packages/devui/dev.md new file mode 100644 index 0000000000..2ff2f430dc --- /dev/null +++ b/python/packages/devui/dev.md @@ -0,0 +1,89 @@ +# Testing DevUI - Quick Setup Guide + +Hi everyone! Here are the step-by-step instructions to test the new DevUI feature: + +## 1. Get the Code + +```bash +git pull +git checkout victordibia/devui +``` + +## 2. Setup Environment + +Navigate to the Python directory and install dependencies: + +```bash +cd python +uv sync --dev +source .venv/bin/activate +``` + +## 3. Configure Environment Variables + +Create a `.env` file in the `python/` directory with your API credentials: + +```bash +# Copy the example file +cp .env.example .env +``` + +Then edit `.env` and add your API keys: + +```bash +# For OpenAI (minimum required) +OPENAI_API_KEY="your-api-key-here" +OPENAI_CHAT_MODEL_ID="gpt-4o-mini" + +# Or for Azure OpenAI +AZURE_OPENAI_ENDPOINT="your-endpoint" +AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="your-deployment-name" +``` + +## 4. Test DevUI + +**Option A: In-Memory Mode (Recommended for quick testing)** + +```bash +cd packages/devui/samples +python in_memory_mode.py +``` + +This runs a simple example with predefined agents and opens your browser automatically at http://localhost:8090 + +**Option B: Directory-Based Discovery** + +```bash +cd packages/devui/samples +devui +``` + +This launches the UI with all example agents/workflows at http://localhost:8080 + +## 5. What You'll See + +- A web interface for testing agents interactively +- Multiple example agents (weather assistant, general assistant, etc.) +- OpenAI-compatible API endpoints for programmatic access + +## 6. API Testing (Optional) + +You can also test via API calls: + +```bash +curl -X POST http://localhost:8080/v1/responses \ + -H "Content-Type: application/json" \ + -d '{ + "model": "agent-framework", + "input": "What is the weather in Seattle?", + "extra_body": {"entity_id": "weather_agent"} + }' +``` + +## Troubleshooting + +- **Missing API key**: Make sure your `.env` file is in the `python/` directory with valid credentials +- **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 + +Let me know if you run into any issues! diff --git a/python/packages/devui/docs/devuiscreen.png b/python/packages/devui/docs/devuiscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..e3dc267610ddda194ab6a1c4161dc05f1b38b4b1 GIT binary patch literal 230754 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
    ^*kQcF?c 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

    Dy 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 a2g%io2K# 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 ayK<~fBI!OTh6UaM{wd0;K`W$9Km32oTdaKm literal 0 HcmV?d00001 diff --git a/python/packages/devui/frontend/.gitignore b/python/packages/devui/frontend/.gitignore new file mode 100644 index 0000000000..8b23663a88 --- /dev/null +++ b/python/packages/devui/frontend/.gitignore @@ -0,0 +1,22 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +.env.* + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/python/packages/devui/frontend/README.md b/python/packages/devui/frontend/README.md new file mode 100644 index 0000000000..7959ce4269 --- /dev/null +++ b/python/packages/devui/frontend/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/python/packages/devui/frontend/components.json b/python/packages/devui/frontend/components.json new file mode 100644 index 0000000000..73afbdbccb --- /dev/null +++ b/python/packages/devui/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/python/packages/devui/frontend/eslint.config.js b/python/packages/devui/frontend/eslint.config.js new file mode 100644 index 0000000000..d94e7deb72 --- /dev/null +++ b/python/packages/devui/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/python/packages/devui/frontend/index.html b/python/packages/devui/frontend/index.html new file mode 100644 index 0000000000..c326c28d0f --- /dev/null +++ b/python/packages/devui/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Agent Framework Dev UI + + +

    + + + diff --git a/python/packages/devui/frontend/package.json b/python/packages/devui/frontend/package.json new file mode 100644 index 0000000000..553a38e3ab --- /dev/null +++ b/python/packages/devui/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/vite": "^4.1.12", + "@xyflow/react": "^12.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.540.0", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.12" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/node": "^24.3.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "tw-animate-css": "^1.3.7", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2" + } +} diff --git a/python/packages/devui/frontend/public/vite.svg b/python/packages/devui/frontend/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/python/packages/devui/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python/packages/devui/frontend/src/App.css b/python/packages/devui/frontend/src/App.css new file mode 100644 index 0000000000..b9d355df2a --- /dev/null +++ b/python/packages/devui/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/python/packages/devui/frontend/src/App.tsx b/python/packages/devui/frontend/src/App.tsx new file mode 100644 index 0000000000..c2e57918de --- /dev/null +++ b/python/packages/devui/frontend/src/App.tsx @@ -0,0 +1,312 @@ +/** + * DevUI App - Minimal orchestrator for agent/workflow interactions + * Features: Entity selection, layout management, debug coordination + */ + +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 { 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 type { + AgentInfo, + WorkflowInfo, + AppState, + ExtendedResponseStreamEvent, +} from "@/types"; + +export default function App() { + const [appState, setAppState] = useState({ + agents: [], + workflows: [], + isLoading: true, + }); + + 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); + + // 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(), + ]); + + setAppState((prev) => ({ + ...prev, + agents, + workflows, + selectedAgent: + agents.length > 0 + ? agents[0] + : workflows.length > 0 + ? workflows[0] + : undefined, + isLoading: false, + })); + } catch (error) { + console.error("Failed to load agents/workflows:", error); + setAppState((prev) => ({ + ...prev, + error: error instanceof Error ? error.message : "Failed to load data", + isLoading: false, + })); + } + }; + + loadData(); + }, []); + + // Save debug panel width to localStorage + useEffect(() => { + localStorage.setItem("debugPanelWidth", debugPanelWidth.toString()); + }, [debugPanelWidth]); + + // Handle resize drag + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setIsResizing(true); + + const startX = e.clientX; + const startWidth = debugPanelWidth; + + const handleMouseMove = (e: MouseEvent) => { + const deltaX = startX - e.clientX; // Subtract because we're dragging from right + const newWidth = Math.max( + 200, + Math.min(window.innerWidth * 0.5, startWidth + deltaX) + ); + setDebugPanelWidth(newWidth); + }; + + const handleMouseUp = () => { + setIsResizing(false); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [debugPanelWidth] + ); + + // Handle double-click to collapse + const handleDoubleClick = useCallback(() => { + setDebugPanelOpen(false); + }, []); + + // Handle entity selection + const handleEntitySelect = useCallback((item: AgentInfo | WorkflowInfo) => { + setAppState((prev) => ({ + ...prev, + selectedAgent: item, + currentThread: undefined, + })); + + // Clear debug events when switching entities + setDebugEvents([]); + }, []); + + // Handle debug events from active view + const handleDebugEvent = useCallback((event: ExtendedResponseStreamEvent | 'clear') => { + if (event === 'clear') { + setDebugEvents([]); + } else { + setDebugEvents((prev) => [...prev, event]); + } + }, []); + + // Show loading state while initializing + if (appState.isLoading) { + return ( +
    + {/* Top Bar - Skeleton */} +
    +
    +
    +
    +
    +
    +
    + + {/* Loading Content */} + +
    + ); + } + + // Show error state if loading failed + if (appState.error) { + return ( +
    + {}} + isLoading={false} + /> + + {/* Error Content */} +
    +
    +
    + Failed to load entities +
    +

    {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} + /> + + {/* 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. +

    + +
    +
    +
    + ); + } + + return ( +
    + setShowAboutModal(true)} + /> + + {/* Main Content - Split Panel */} +
    + {/* Left Panel - Main View */} +
    + {appState.selectedAgent ? ( + appState.selectedAgent.type === "agent" ? ( + + ) : ( + + ) + ) : ( +
    + Select an agent or workflow to get started. +
    + )} +
    + + {/* Resize Handle */} + {debugPanelOpen && ( +
    +
    +
    +
    +
    + )} + + {/* Button to reopen when closed */} + {!debugPanelOpen && ( +
    + +
    + )} + + {/* Right Panel - Debug */} + {debugPanelOpen && ( +
    + +
    + )} +
    + + {/* About Modal */} + +
    + ); +} diff --git a/python/packages/devui/frontend/src/assets/react.svg b/python/packages/devui/frontend/src/assets/react.svg new file mode 100644 index 0000000000..6c87de9bb3 --- /dev/null +++ b/python/packages/devui/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/agent/agent-view.tsx b/python/packages/devui/frontend/src/components/agent/agent-view.tsx new file mode 100644 index 0000000000..069599080f --- /dev/null +++ b/python/packages/devui/frontend/src/components/agent/agent-view.tsx @@ -0,0 +1,799 @@ +/** + * AgentView - Complete agent interaction interface + * Features: Chat interface, message streaming, thread management + */ + +import { useState, useCallback, useRef, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { FileUpload } from "@/components/ui/file-upload"; +import { + AttachmentGallery, + type AttachmentItem, +} from "@/components/ui/attachment-gallery"; +import { MessageRenderer } from "@/components/message_renderer"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Send, User, Bot, Plus, AlertCircle } from "lucide-react"; +import { apiClient } from "@/services/api"; +import type { + AgentInfo, + ChatMessage, + RunAgentRequest, + ThreadInfo, + ExtendedResponseStreamEvent, +} from "@/types"; + +interface ChatState { + messages: ChatMessage[]; + isStreaming: boolean; +} + +type DebugEventHandler = (event: ExtendedResponseStreamEvent | 'clear') => void; + +interface AgentViewProps { + selectedAgent: AgentInfo; + onDebugEvent: DebugEventHandler; +} + +interface MessageBubbleProps { + message: ChatMessage; +} + +function MessageBubble({ message }: MessageBubbleProps) { + const isUser = message.role === "user"; + const isError = message.error; + const Icon = isUser ? User : isError ? AlertCircle : Bot; + + return ( +
    +
    + +
    + +
    +
    + {isError && ( +
    + + + Unable to process request + +
    + )} +
    + +
    +
    + +
    + {new Date(message.timestamp).toLocaleTimeString()} +
    +
    +
    + ); +} + +function TypingIndicator() { + return ( +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); +} + +export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { + const [chatState, setChatState] = useState({ + messages: [], + isStreaming: false, + }); + const [currentThread, setCurrentThread] = useState( + undefined + ); + const [availableThreads, setAvailableThreads] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [attachments, setAttachments] = useState([]); + const [loadingThreads, setLoadingThreads] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); + const [dragCounter, setDragCounter] = useState(0); + + const scrollAreaRef = useRef(null); + const messagesEndRef = useRef(null); + const accumulatedText = useRef(""); + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [chatState.messages, chatState.isStreaming]); + + // Load threads when agent changes + useEffect(() => { + const loadThreads = async () => { + if (!selectedAgent) return; + + setLoadingThreads(true); + try { + const threads = await apiClient.getThreads(selectedAgent.id); + setAvailableThreads(threads); + + // Auto-select the most recent thread if available + if (threads.length > 0) { + const mostRecentThread = threads[0]; // Assuming threads are sorted by creation date (newest first) + setCurrentThread(mostRecentThread); + + // Load messages for the selected thread + try { + const threadMessages = await apiClient.getThreadMessages(mostRecentThread.id); + setChatState({ + messages: threadMessages, + isStreaming: false, + }); + } catch (error) { + console.error("Failed to load thread messages:", error); + setChatState({ + messages: [], + isStreaming: false, + }); + } + } + } catch (error) { + console.error("Failed to load threads:", error); + setAvailableThreads([]); + } finally { + setLoadingThreads(false); + } + }; + + // Clear chat when agent changes + setChatState({ + messages: [], + isStreaming: false, + }); + setCurrentThread(undefined); + accumulatedText.current = ""; + + loadThreads(); + }, [selectedAgent]); + + // Handle file uploads + const handleFilesSelected = async (files: File[]) => { + const newAttachments: AttachmentItem[] = []; + + for (const file of files) { + const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const type = getFileType(file); + + let preview: string | undefined; + if (type === "image") { + preview = await readFileAsDataURL(file); + } + + newAttachments.push({ + id, + file, + preview, + type, + }); + } + + setAttachments((prev) => [...prev, ...newAttachments]); + }; + + const handleRemoveAttachment = (id: string) => { + setAttachments((prev) => prev.filter((att) => att.id !== id)); + }; + + // Drag and drop handlers + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragCounter((prev) => prev + 1); + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setIsDragOver(true); + } + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const newCounter = dragCounter - 1; + setDragCounter(newCounter); + if (newCounter === 0) { + setIsDragOver(false); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + setDragCounter(0); + + if (isSubmitting || chatState.isStreaming) return; + + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + await handleFilesSelected(files); + } + }; + + // Helper functions + const getFileType = (file: File): AttachmentItem["type"] => { + if (file.type.startsWith("image/")) return "image"; + if (file.type === "application/pdf") return "pdf"; + return "other"; + }; + + const readFileAsDataURL = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + + // Handle new thread creation + const handleNewThread = useCallback(async () => { + if (!selectedAgent) return; + + try { + const newThread = await apiClient.createThread(selectedAgent.id); + setCurrentThread(newThread); + setAvailableThreads((prev) => [newThread, ...prev]); + setChatState({ + messages: [], + isStreaming: false, + }); + accumulatedText.current = ""; + } catch (error) { + console.error("Failed to create thread:", error); + } + }, [selectedAgent]); + + // Handle thread selection + const handleThreadSelect = useCallback( + async (threadId: string) => { + const thread = availableThreads.find((t) => t.id === threadId); + if (!thread) return; + + setCurrentThread(thread); + + try { + // Load thread messages from backend + const threadMessages = await apiClient.getThreadMessages(threadId); + + setChatState({ + messages: threadMessages, + isStreaming: false, + }); + + console.log( + `Restored ${threadMessages.length} messages for thread ${threadId}` + ); + } catch (error) { + console.error("Failed to load thread messages:", error); + // Fallback to clearing messages + setChatState({ + messages: [], + isStreaming: false, + }); + } + + accumulatedText.current = ""; + }, + [availableThreads] + ); + + // Handle message sending + const handleSendMessage = useCallback( + async (request: RunAgentRequest) => { + if (!selectedAgent) return; + + // Extract text and attachments from OpenAI format for UI display + let displayText = ""; + const attachmentContents: import("@/types/agent-framework").Contents[] = + []; + + // Parse OpenAI ResponseInputParam to extract display content + for (const inputItem of request.input) { + if (inputItem.type === "message" && Array.isArray(inputItem.content)) { + for (const contentItem of inputItem.content) { + if (contentItem.type === "input_text") { + displayText += contentItem.text + " "; + } else if (contentItem.type === "input_image") { + attachmentContents.push({ + type: "data", + uri: contentItem.image_url || "", + media_type: "image/png", // Default, should extract from data URI + } as import("@/types/agent-framework").DataContent); + } else if (contentItem.type === "input_file") { + const dataUri = `data:application/octet-stream;base64,${contentItem.file_data}`; + attachmentContents.push({ + type: "data", + uri: dataUri, + media_type: "application/pdf", // Should be dynamic based on filename + } as import("@/types/agent-framework").DataContent); + } + } + } + } + + const userMessageContents: import("@/types/agent-framework").Contents[] = + [ + ...(displayText.trim() + ? [ + { + type: "text", + text: displayText.trim(), + } as import("@/types/agent-framework").TextContent, + ] + : []), + ...attachmentContents, + ]; + + // Add user message to UI state + const userMessage: ChatMessage = { + id: `user-${Date.now()}`, + role: "user", + contents: userMessageContents, + timestamp: new Date().toISOString(), + }; + + setChatState((prev) => ({ + ...prev, + messages: [...prev.messages, userMessage], + isStreaming: true, + })); + + // Create assistant message placeholder + const assistantMessage: ChatMessage = { + id: `assistant-${Date.now()}`, + role: "assistant", + contents: [], + timestamp: new Date().toISOString(), + streaming: true, + }; + + setChatState((prev) => ({ + ...prev, + messages: [...prev.messages, assistantMessage], + })); + + try { + // If no thread selected, create one automatically + let threadToUse = currentThread; + if (!threadToUse) { + try { + threadToUse = await apiClient.createThread(selectedAgent.id); + setCurrentThread(threadToUse); + setAvailableThreads((prev) => [threadToUse!, ...prev]); + } catch (error) { + console.error("Failed to create thread:", error); + } + } + + const apiRequest = { + input: request.input, + thread_id: threadToUse?.id, + }; + + // Clear text accumulator for new response + accumulatedText.current = ""; + + // Clear debug panel events for new agent run + onDebugEvent('clear'); + + // Use OpenAI-compatible API streaming - direct event handling + const streamGenerator = apiClient.streamAgentExecutionOpenAI( + selectedAgent.id, + apiRequest + ); + + for await (const openAIEvent of streamGenerator) { + // Pass all events to debug panel + onDebugEvent(openAIEvent); + + // Handle error events from the stream + if (openAIEvent.type === "error") { + const errorEvent = openAIEvent as ExtendedResponseStreamEvent & { + message?: string; + }; + const errorMessage = errorEvent.message || "An error occurred"; + + // Update assistant message with error and stop streaming + setChatState((prev) => ({ + ...prev, + isStreaming: false, + messages: prev.messages.map((msg) => + msg.id === assistantMessage.id + ? { + ...msg, + contents: [ + { + type: "text", + text: errorMessage, + }, + ], + streaming: false, + error: true, // Add error flag for styling + } + : msg + ), + })); + return; // Exit stream processing early on error + } + + // Handle text delta events for chat + if ( + openAIEvent.type === "response.output_text.delta" && + "delta" in openAIEvent && + openAIEvent.delta + ) { + accumulatedText.current += openAIEvent.delta; + + // Update assistant message with accumulated content + setChatState((prev) => ({ + ...prev, + messages: prev.messages.map((msg) => + msg.id === assistantMessage.id + ? { + ...msg, + contents: [ + { + type: "text", + text: accumulatedText.current, + }, + ], + } + : msg + ), + })); + } + + // Handle completion/error by detecting when streaming stops + // (Server will close the stream when done, so we'll exit the loop naturally) + } + + // Stream ended - mark as complete + setChatState((prev) => ({ + ...prev, + isStreaming: false, + messages: prev.messages.map((msg) => + msg.id === assistantMessage.id ? { ...msg, streaming: false } : msg + ), + })); + } catch (error) { + console.error("Streaming error:", error); + setChatState((prev) => ({ + ...prev, + isStreaming: false, + messages: prev.messages.map((msg) => + msg.id === assistantMessage.id + ? { + ...msg, + contents: [ + { + type: "text", + text: `Error: ${ + error instanceof Error + ? error.message + : "Failed to get response" + }`, + }, + ], + streaming: false, + } + : msg + ), + })); + } + }, + [selectedAgent, currentThread, onDebugEvent] + ); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if ( + (!inputValue.trim() && attachments.length === 0) || + isSubmitting || + !selectedAgent + ) + return; + + setIsSubmitting(true); + const messageText = inputValue.trim(); + setInputValue(""); + + try { + // Create OpenAI Responses API format + if (attachments.length > 0 || messageText) { + const content: import("@/types/agent-framework").ResponseInputContent[] = + []; + + // Add text content if present - EXACT OpenAI ResponseInputTextParam + if (messageText) { + content.push({ + text: messageText, + type: "input_text", + } as import("@/types/agent-framework").ResponseInputTextParam); + } + + // Add attachments using EXACT OpenAI types + for (const attachment of attachments) { + const dataUri = await readFileAsDataURL(attachment.file); + + if (attachment.file.type.startsWith("image/")) { + // EXACT OpenAI ResponseInputImageParam + content.push({ + detail: "auto", + type: "input_image", + image_url: dataUri, + } as import("@/types/agent-framework").ResponseInputImageParam); + } else { + // EXACT OpenAI ResponseInputFileParam (but we need to handle the required fields) + const base64Data = dataUri.split(",")[1]; // Extract base64 part + content.push({ + type: "input_file", + file_data: base64Data, + file_url: dataUri, // Use data URI as the URL + filename: attachment.file.name, + } as import("@/types/agent-framework").ResponseInputFileParam); + } + } + + const openaiInput: import("@/types/agent-framework").ResponseInputParam = + [ + { + type: "message", + role: "user", + content, + }, + ]; + + // Use pure OpenAI format + await handleSendMessage({ + input: openaiInput, + thread_id: currentThread?.id, + }); + } else { + // Simple text message using OpenAI format + const openaiInput: import("@/types/agent-framework").ResponseInputParam = + [ + { + type: "message", + role: "user", + content: [ + { + text: messageText, + type: "input_text", + } as import("@/types/agent-framework").ResponseInputTextParam, + ], + }, + ]; + + await handleSendMessage({ + input: openaiInput, + thread_id: currentThread?.id, + }); + } + + // Clear attachments after sending + setAttachments([]); + } finally { + setIsSubmitting(false); + } + }; + + const canSendMessage = + selectedAgent && + !isSubmitting && + !chatState.isStreaming && + (inputValue.trim() || attachments.length > 0); + + return ( +
    + {/* Header */} +
    +
    +

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

    + + {/* Thread Controls */} +
    + + + +
    +
    + + {selectedAgent.description && ( +

    + {selectedAgent.description} +

    + )} +
    + + {/* Messages */} + +
    + {chatState.messages.length === 0 ? ( +
    +
    + Start a conversation with{" "} + {selectedAgent.name || selectedAgent.id} +
    +
    + Type a message below to begin +
    +
    + ) : ( + chatState.messages.map((message) => ( + + )) + )} + + {chatState.isStreaming && !isSubmitting && } + +
    +
    + + + {/* Input */} +
    +
    + {/* Drag overlay */} + {isDragOver && ( +
    +
    +
    + Drop files here +
    +
    + Images, PDFs, and other files +
    +
    +
    + )} + + {/* Attachment gallery */} + {attachments.length > 0 && ( +
    + +
    + )} + + {/* Input form */} +
    + setInputValue(e.target.value)} + placeholder={`Message ${ + selectedAgent.name || selectedAgent.id + }...`} + disabled={isSubmitting || chatState.isStreaming} + className="flex-1" + /> + + + +
    +
    +
    + ); +} diff --git a/python/packages/devui/frontend/src/components/message_renderer/ContentRenderer.tsx b/python/packages/devui/frontend/src/components/message_renderer/ContentRenderer.tsx new file mode 100644 index 0000000000..d4df87701d --- /dev/null +++ b/python/packages/devui/frontend/src/components/message_renderer/ContentRenderer.tsx @@ -0,0 +1,268 @@ +/** + * ContentRenderer - Renders individual content items based on type + */ + +import { useState } from "react"; +import { Download, FileText, AlertCircle, Code } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import type { RenderProps } from "./types"; +import { + isTextContent, + isFunctionCallContent, + isFunctionResultContent, +} from "@/types/agent-framework"; + +function TextContentRenderer({ content, isStreaming, className }: RenderProps) { + if (!isTextContent(content)) return null; + + return ( +
    + {content.text} + {isStreaming && ( + + )} +
    + ); +} + +function DataContentRenderer({ content, className }: RenderProps) { + const [imageError, setImageError] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + if (content.type !== "data") return null; + + // Extract data URI and media type (updated for new field names) + const dataUri = typeof content.uri === "string" ? content.uri : ""; + const mediaTypeMatch = dataUri.match(/^data:([^;]+)/); + const mediaType = content.media_type || mediaTypeMatch?.[1] || "unknown"; + + const isImage = mediaType.startsWith("image/"); + const isPdf = mediaType === "application/pdf"; + + if (isImage && !imageError) { + return ( +
    + Uploaded image setIsExpanded(!isExpanded)} + onError={() => setImageError(true)} + /> +
    + {mediaType} • Click to {isExpanded ? "collapse" : "expand"} +
    +
    + ); + } + + // Fallback for non-images or failed images + return ( +
    +
    + {isPdf ? ( + + ) : ( + + )} + + {isPdf ? "PDF Document" : "File Attachment"} + + ({mediaType}) +
    + +
    + ); +} + +function FunctionCallRenderer({ content, className }: RenderProps) { + const [isExpanded, setIsExpanded] = useState(false); + + if (!isFunctionCallContent(content)) return null; + + let parsedArgs; + try { + parsedArgs = + typeof content.arguments === "string" + ? JSON.parse(content.arguments) + : content.arguments; + } catch { + parsedArgs = content.arguments; + } + + return ( +
    +
    setIsExpanded(!isExpanded)} + > + + + Function Call: {content.name} + + + {isExpanded ? "▼" : "▶"} + +
    + {isExpanded && ( +
    +
    Arguments:
    +
    +            {JSON.stringify(parsedArgs, null, 2)}
    +          
    +
    + )} +
    + ); +} + +function FunctionResultRenderer({ content, className }: RenderProps) { + const [isExpanded, setIsExpanded] = useState(false); + + if (!isFunctionResultContent(content)) return null; + + return ( +
    +
    setIsExpanded(!isExpanded)} + > + + + Function Result + + + {isExpanded ? "▼" : "▶"} + +
    + {isExpanded && ( +
    +
    +            {typeof content.result === "string"
    +              ? content.result
    +              : JSON.stringify(content.result, null, 2)}
    +          
    +
    + )} +
    + ); +} + +function ErrorContentRenderer({ content, className }: RenderProps) { + if (content.type !== "error") return null; + + return ( +
    +
    + + Error + {content.error_code && ( + ({content.error_code}) + )} +
    +
    {content.error}
    +
    + ); +} + +function UriContentRenderer({ content, className }: RenderProps) { + const [imageError, setImageError] = useState(false); + + if (content.type !== "uri") return null; + + const isImage = content.media_type?.startsWith("image/"); + + if (isImage && !imageError) { + return ( +
    + Referenced image setImageError(true)} + /> + +
    + ); + } + + return ( +
    + +
    + {content.uri} +
    +
    + ); +} + +export function ContentRenderer({ content, isStreaming, className }: RenderProps) { + switch (content.type) { + case "text": + return ( + + ); + case "data": + return ; + case "uri": + return ; + case "function_call": + return ( + + ); + case "function_result": + return ( + + ); + case "error": + return ; + default: + // Fallback for unsupported content types + return ( +
    +
    Unsupported content type: {content.type}
    +
    +            {JSON.stringify(content, null, 2)}
    +          
    +
    + ); + } +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/message_renderer/MessageRenderer.tsx b/python/packages/devui/frontend/src/components/message_renderer/MessageRenderer.tsx new file mode 100644 index 0000000000..21bef64730 --- /dev/null +++ b/python/packages/devui/frontend/src/components/message_renderer/MessageRenderer.tsx @@ -0,0 +1,38 @@ +/** + * MessageRenderer - Main orchestrator for rendering message contents + */ + +import { StreamingRenderer } from "./StreamingRenderer"; +import { ContentRenderer } from "./ContentRenderer"; +import type { MessageRendererProps } from "./types"; + +export function MessageRenderer({ + contents, + isStreaming = false, + className, +}: MessageRendererProps) { + // If not streaming, render each content item individually + if (!isStreaming) { + return ( +
    + {contents.map((content, index) => ( + 0 ? "mt-2" : ""} + /> + ))} +
    + ); + } + + // For streaming, use the streaming renderer for smart accumulation + return ( + + ); +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/message_renderer/StreamingRenderer.tsx b/python/packages/devui/frontend/src/components/message_renderer/StreamingRenderer.tsx new file mode 100644 index 0000000000..b08dceb6ef --- /dev/null +++ b/python/packages/devui/frontend/src/components/message_renderer/StreamingRenderer.tsx @@ -0,0 +1,114 @@ +/** + * StreamingRenderer - Handles accumulation and display of streaming content + */ + +import { useState, useEffect } from "react"; +import { ContentRenderer } from "./ContentRenderer"; +import type { Contents, MessageRenderState } from "./types"; +import { isTextContent } from "@/types/agent-framework"; + +interface StreamingRendererProps { + contents: Contents[]; + isStreaming?: boolean; + className?: string; +} + +export function StreamingRenderer({ + contents, + isStreaming = false, + className, +}: StreamingRendererProps) { + const [renderState, setRenderState] = useState({ + textAccumulator: "", + dataContentItems: [], + functionCalls: [], + errors: [], + isComplete: !isStreaming, + }); + + useEffect(() => { + // Process and accumulate content + let textAccumulator = ""; + const dataContentItems: Contents[] = []; + const functionCalls: Contents[] = []; + const errors: Contents[] = []; + + contents.forEach((content) => { + if (isTextContent(content)) { + textAccumulator += content.text; + } else if (content.type === "data") { + // Only show data content when streaming is complete or item is complete + if (!isStreaming) { + dataContentItems.push(content); + } + } else if (content.type === "function_call") { + functionCalls.push(content); + } else if (content.type === "error") { + errors.push(content); + } else { + // Other content types (uri, function_result, etc.) + dataContentItems.push(content); + } + }); + + setRenderState({ + textAccumulator, + dataContentItems, + functionCalls, + errors, + isComplete: !isStreaming, + }); + }, [contents, isStreaming]); + + const hasTextContent = renderState.textAccumulator.length > 0; + const hasOtherContent = + renderState.dataContentItems.length > 0 || + renderState.functionCalls.length > 0 || + renderState.errors.length > 0; + + return ( +
    + {/* Render accumulated text with streaming indicator */} + {hasTextContent && ( +
    + {renderState.textAccumulator} + {isStreaming && hasTextContent && ( + + )} +
    + )} + + {/* Render other content types when complete or non-data items immediately */} + {hasOtherContent && ( +
    + {renderState.errors.map((content, index) => ( + + ))} + + {renderState.functionCalls.map((content, index) => ( + + ))} + + {renderState.dataContentItems.map((content, index) => ( + + ))} +
    + )} + + {/* Show loading indicator when streaming and no text content yet */} + {isStreaming && !hasTextContent && !hasOtherContent && ( +
    +
    +
    +
    +
    +
    +
    + )} +
    + ); +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/message_renderer/index.ts b/python/packages/devui/frontend/src/components/message_renderer/index.ts new file mode 100644 index 0000000000..f4f8c0fd89 --- /dev/null +++ b/python/packages/devui/frontend/src/components/message_renderer/index.ts @@ -0,0 +1,8 @@ +/** + * Message Renderer - Exports + */ + +export { MessageRenderer } from "./MessageRenderer"; +export { ContentRenderer } from "./ContentRenderer"; +export { StreamingRenderer } from "./StreamingRenderer"; +export type { MessageRendererProps, RenderProps, MessageRenderState } from "./types"; \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/message_renderer/types.ts b/python/packages/devui/frontend/src/components/message_renderer/types.ts new file mode 100644 index 0000000000..ea00abb4d9 --- /dev/null +++ b/python/packages/devui/frontend/src/components/message_renderer/types.ts @@ -0,0 +1,48 @@ +/** + * Types for message rendering components + */ + +// Re-export and extend types from agent-framework +import type { + Contents, + TextContent, + DataContent, + UriContent, + FunctionCallContent, + FunctionResultContent, + ErrorContent, + AgentRunResponseUpdate, +} from "@/types/agent-framework"; + +export type { + Contents, + TextContent, + DataContent, + UriContent, + FunctionCallContent, + FunctionResultContent, + ErrorContent, + AgentRunResponseUpdate, +}; + +// UI-specific types for message rendering +export interface MessageRenderState { + // Track accumulated content during streaming + textAccumulator: string; + dataContentItems: Contents[]; + functionCalls: Contents[]; + errors: Contents[]; + isComplete: boolean; +} + +export interface RenderProps { + content: Contents; + isStreaming?: boolean; + className?: string; +} + +export interface MessageRendererProps { + contents: Contents[]; + isStreaming?: boolean; + className?: string; +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/mode-toggle.tsx b/python/packages/devui/frontend/src/components/mode-toggle.tsx new file mode 100644 index 0000000000..b4259e484a --- /dev/null +++ b/python/packages/devui/frontend/src/components/mode-toggle.tsx @@ -0,0 +1,39 @@ +"use client" + +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/shared/about-modal.tsx b/python/packages/devui/frontend/src/components/shared/about-modal.tsx new file mode 100644 index 0000000000..9573065dfd --- /dev/null +++ b/python/packages/devui/frontend/src/components/shared/about-modal.tsx @@ -0,0 +1,54 @@ +/** + * About DevUI Modal - Shows information about the DevUI sample app + */ + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogClose, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { ExternalLink } from "lucide-react"; + +interface AboutModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function AboutModal({ open, onOpenChange }: AboutModalProps) { + return ( + + + + About DevUI + onOpenChange(false)} /> + + +
    +

    + DevUI is a sample app for getting started with Agent Framework. +

    + +
    + +
    +
    +
    +
    + ); +} diff --git a/python/packages/devui/frontend/src/components/shared/app-header.tsx b/python/packages/devui/frontend/src/components/shared/app-header.tsx new file mode 100644 index 0000000000..9c2af3751d --- /dev/null +++ b/python/packages/devui/frontend/src/components/shared/app-header.tsx @@ -0,0 +1,48 @@ +/** + * AppHeader - Global application header + * Features: Entity selection, global settings, theme toggle + */ + +import { Button } from "@/components/ui/button"; +import { EntitySelector } from "@/components/shared/entity-selector"; +import { ModeToggle } from "@/components/mode-toggle"; +import { Settings } from "lucide-react"; +import type { AgentInfo, WorkflowInfo } from "@/types"; + +interface AppHeaderProps { + agents: AgentInfo[]; + workflows: WorkflowInfo[]; + selectedItem?: AgentInfo | WorkflowInfo; + onSelect: (item: AgentInfo | WorkflowInfo) => void; + isLoading?: boolean; + onSettingsClick?: () => void; +} + +export function AppHeader({ + agents, + workflows, + selectedItem, + onSelect, + isLoading = false, + onSettingsClick, +}: AppHeaderProps) { + return ( +
    +
    Dev UI
    + + +
    + + +
    +
    + ); +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/shared/debug-panel.tsx b/python/packages/devui/frontend/src/components/shared/debug-panel.tsx new file mode 100644 index 0000000000..4aa8d5e0ae --- /dev/null +++ b/python/packages/devui/frontend/src/components/shared/debug-panel.tsx @@ -0,0 +1,1390 @@ +/** + * DebugPanel - Tabbed interface for OpenAI events, traces, and tool information + * Features: Real-time event streaming, trace visualization, tool call details + */ + +import { useRef, useState } from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { + Activity, + Search, + Wrench, + CheckCircle2, + XCircle, + AlertCircle, + Zap, + MessageSquare, + ChevronRight, + ChevronDown, + Info, +} from "lucide-react"; +import type { ExtendedResponseStreamEvent } from "@/types"; + +// Type definitions for event data structures +interface EventDataBase { + call_id?: string; + executor_id?: string; + timestamp?: string; + [key: string]: unknown; +} + +interface FunctionResultData extends EventDataBase { + result?: unknown; + status?: "completed" | "failed"; + exception?: string; +} + +interface FunctionCallData extends EventDataBase { + name?: string; + arguments?: string | object; + function?: unknown; + tool_calls?: unknown[]; +} + +interface WorkflowEventData extends EventDataBase { + event_type?: string; + data?: Record; +} + +interface TraceEventData extends EventDataBase { + operation_name?: string; + duration_ms?: number; + status?: string; + attributes?: Record; + span_id?: string; + trace_id?: string; + parent_span_id?: string | null; + start_time?: number; + end_time?: number; + entity_id?: string; + session_id?: string | null; +} + +interface DebugPanelProps { + events: ExtendedResponseStreamEvent[]; + isStreaming?: boolean; +} + +// Helper function to accumulate OpenAI events into meaningful units +function processEventsForDisplay( + events: ExtendedResponseStreamEvent[] +): ExtendedResponseStreamEvent[] { + const processedEvents: ExtendedResponseStreamEvent[] = []; + const functionCalls = new Map< + string, + { + name?: string; + arguments: string; + callId: string; + timestamp: string; + } + >(); + const callIdToName = new Map(); // Track call_id -> function name mappings + let accumulatedText = ""; + const lastFunctionCallId: string | null = null; // Track the most recent function call + + for (const event of events) { + // Always show completion, error, workflow events, and function results + if ( + event.type === "response.done" || + event.type === "error" || + event.type === "response.workflow_event.complete" || + event.type === "response.trace_event.complete" || + event.type === "response.trace.complete" || + event.type === "response.function_result.complete" + ) { + // Flush any accumulated text before showing these events + if (accumulatedText.trim()) { + processedEvents.push({ + type: "response.output_text.delta", + delta: accumulatedText.trim(), + } as ExtendedResponseStreamEvent); + accumulatedText = ""; + } + + // Extract function names from trace events + if ( + (event.type === "response.trace_event.complete" || + event.type === "response.trace.complete") && + "data" in event + ) { + const traceData = event.data as TraceEventData; + if ( + traceData.attributes && + traceData.attributes["gen_ai.output.messages"] && + typeof traceData.attributes["gen_ai.output.messages"] === "string" + ) { + try { + const messages = JSON.parse( + traceData.attributes["gen_ai.output.messages"] as string + ); + for (const msg of messages) { + if (msg.parts) { + for (const part of msg.parts) { + if (part.type === "tool_call" && part.name && part.id) { + // Store the call_id -> function name mapping + callIdToName.set(part.id, part.name); + } + } + } + } + } catch { + // Ignore parsing errors + } + } + } + + // For function results, ensure we have the corresponding function call + if ( + event.type === "response.function_result.complete" && + "data" in event + ) { + const resultData = event.data as FunctionResultData; + const callId = resultData.call_id; + + // Only create function call event if we have actual argument data + if (callId && functionCalls.has(callId)) { + const call = functionCalls.get(callId)!; + const functionName = + callIdToName.get(callId) || call.name || "unknown"; + + processedEvents.push({ + type: "response.function_call.complete", + data: { + name: functionName, + arguments: call.arguments, + call_id: call.callId, + }, + } as ExtendedResponseStreamEvent); + functionCalls.delete(callId); + } + } + + processedEvents.push(event); + continue; + } + + // Handle function call start events + if (event.type === "response.function_call.delta" && "data" in event) { + const callData = event.data as FunctionCallData; + const callId = callData.call_id || `call_${Date.now()}`; + + // Initialize or update the function call + if (!functionCalls.has(callId)) { + functionCalls.set(callId, { + name: callData.name || undefined, + arguments: "", + callId, + timestamp: new Date().toISOString(), + }); + } + + // Update name if provided + if (callData.name && callData.name.trim()) { + functionCalls.get(callId)!.name = callData.name.trim(); + } + continue; + } + + // Handle function call complete events that come directly (not generated by us) + if (event.type === "response.function_call.complete" && "data" in event) { + // This is already a complete function call event, just pass it through + processedEvents.push(event); + continue; + } + + // Handle function call arguments accumulation - ACTUAL BACKEND FORMAT + if (event.type === "response.function_call_arguments.delta") { + let deltaData: string = ""; + let callId: string; + + // Extract delta from actual backend format + if ("delta" in event && typeof event.delta === "string") { + deltaData = event.delta; + } + + // Use a simple tracking approach since backend doesn't provide call_id in argument deltas + if ( + "data" in event && + event.data && + (event.data as EventDataBase).call_id + ) { + callId = String((event.data as EventDataBase).call_id); + } else { + callId = lastFunctionCallId || `call_${Date.now()}`; + } + + if (deltaData && callId) { + // Ensure we have a function call entry + if (!functionCalls.has(callId)) { + functionCalls.set(callId, { + name: "unknown", // Backend doesn't provide function name in these events + arguments: "", + callId, + timestamp: new Date().toISOString(), + }); + } + + // Accumulate the delta + const call = functionCalls.get(callId)!; + + // Skip the initial "{}" delta that backend sends + if (deltaData === "{}" && call.arguments === "") { + continue; + } + + // Accumulate and clean up the delta + let cleanedDelta = deltaData; + try { + // Remove extra quotes and escaping that backend adds + cleanedDelta = deltaData.replace(/^"|"$/g, "").replace(/\\"/g, '"'); + } catch { + cleanedDelta = deltaData; + } + call.arguments += cleanedDelta; + } + continue; + } + + // Handle text delta events + if (event.type === "response.output_text.delta" && "delta" in event) { + accumulatedText += event.delta || ""; + + // Only emit if we have substantial content AND hit a natural paragraph break + // This makes the text accumulation much more aggressive + if ( + accumulatedText.length > 100 && + (accumulatedText.includes("\n\n") || + accumulatedText.trim().match(/[.!?]\s*$/)) + ) { + processedEvents.push({ + type: "response.output_text.delta", + delta: accumulatedText.trim(), + } as ExtendedResponseStreamEvent); + accumulatedText = ""; + } + continue; + } + + // Handle usage events (skip them as they're noise) + if (event.type === "response.usage.complete") { + continue; + } + + // Handle other event types - pass through + processedEvents.push(event); + } + + // Finalize any remaining function calls that didn't get results + for (const [, call] of functionCalls) { + if (call.arguments.trim() && call.arguments.trim().length > 2) { + const functionName = + callIdToName.get(call.callId) || call.name || "unknown"; + processedEvents.push({ + type: "response.function_call.complete", + data: { + name: functionName, + arguments: call.arguments, + call_id: call.callId, + }, + } as ExtendedResponseStreamEvent); + } + } + + // Finalize any remaining text + if (accumulatedText.trim()) { + processedEvents.push({ + type: "response.output_text.delta", + delta: accumulatedText.trim(), + } as ExtendedResponseStreamEvent); + } + + return processedEvents; +} + +interface EventItemProps { + event: ExtendedResponseStreamEvent; +} + +function getEventSummary(event: ExtendedResponseStreamEvent): string { + switch (event.type) { + case "response.output_text.delta": + if ("delta" in event) { + const text = event.delta || ""; + return text.length > 60 ? `${text.slice(0, 60)}...` : text; + } + return "Text output"; + + case "response.function_call.complete": + if ("data" in event && event.data) { + const data = event.data as FunctionCallData; + + // Try to extract function name from various possible locations + let functionName = data.name || "unknown"; + + // Use the function name as provided, no complex inference needed + if (!functionName || functionName === "unknown") { + functionName = "function_call"; + } + + const argsStr = data.arguments + ? typeof data.arguments === "string" + ? data.arguments.slice(0, 30) + : JSON.stringify(data.arguments).slice(0, 30) + : ""; + return `Calling ${functionName}(${argsStr}${ + argsStr.length >= 30 ? "..." : "" + })`; + } + return "Function call"; + + case "response.function_call_arguments.delta": + if ("delta" in event && event.delta) { + return `Function arg delta: ${event.delta.slice(0, 30)}${ + event.delta.length > 30 ? "..." : "" + }`; + } + return "Function arguments..."; + + case "response.function_result.complete": + if ("data" in event && event.data) { + const data = event.data as FunctionResultData; + const resultStr = data.result + ? typeof data.result === "string" + ? data.result + : JSON.stringify(data.result) + : "no result"; + const truncated = resultStr.slice(0, 40); + return `Tool result: ${truncated}${ + truncated.length >= 40 ? "..." : "" + }`; + } + return "Function result"; + + case "response.workflow_event.complete": + if ("data" in event && event.data) { + const data = event.data as WorkflowEventData; + return `Executor: ${data.executor_id || "unknown"}`; + } + return "Workflow event"; + + case "response.trace_event.complete": + case "response.trace.complete": + if ("data" in event && event.data) { + const data = event.data as TraceEventData; + return `Trace: ${data.operation_name || "unknown"}`; + } + return "Trace event"; + + case "response.done": + return "Response complete"; + + case "error": + // Extract actual error message from error events + if ("message" in event && typeof event.message === "string") { + return event.message; + } + return "Error occurred"; + + default: + return `${event.type}`; + } +} + +function getEventIcon(type: string) { + switch (type) { + case "response.output_text.delta": + return MessageSquare; + case "response.function_call.complete": + case "response.function_call.delta": + case "response.function_call_arguments.delta": + return Wrench; + case "response.function_result.complete": + return CheckCircle2; + case "response.workflow_event.complete": + return Activity; + case "response.trace_event.complete": + case "response.trace.complete": + return Search; + case "response.done": + return CheckCircle2; + case "error": + return XCircle; + default: + return AlertCircle; + } +} + +function getEventColor(type: string) { + switch (type) { + case "response.output_text.delta": + return "text-gray-600 dark:text-gray-400"; + case "response.function_call.complete": + case "response.function_call.delta": + case "response.function_call_arguments.delta": + return "text-blue-600 dark:text-blue-400"; + case "response.function_result.complete": + return "text-green-600 dark:text-green-400"; + case "response.workflow_event.complete": + return "text-purple-600 dark:text-purple-400"; + case "response.trace_event.complete": + case "response.trace.complete": + return "text-orange-600 dark:text-orange-400"; + case "response.done": + return "text-green-600 dark:text-green-400"; + case "error": + return "text-red-600 dark:text-red-400"; + default: + return "text-gray-600 dark:text-gray-400"; + } +} + +function EventItem({ event }: EventItemProps) { + const [isExpanded, setIsExpanded] = useState(false); + const Icon = getEventIcon(event.type); + const colorClass = getEventColor(event.type); + const timestamp = new Date().toLocaleTimeString(); + const summary = getEventSummary(event); + + // Determine if this event has expandable content + const hasExpandableContent = + (event.type === "response.function_call.complete" && + "data" in event && + event.data) || + (event.type === "response.function_result.complete" && + "data" in event && + event.data) || + (event.type === "response.workflow_event.complete" && + "data" in event && + event.data) || + (event.type === "response.trace_event.complete" && + "data" in event && + event.data) || + (event.type === "response.trace.complete" && + "data" in event && + event.data) || + (event.type === "response.output_text.delta" && + "delta" in event && + event.delta && + event.delta.length > 100) || + // Make error events expandable to show full error details + event.type === "error"; + + return ( +
    +
    + + {timestamp} + + {event.type.replace("response.", "")} + +
    + +
    +
    hasExpandableContent && setIsExpanded(!isExpanded)} + > + {hasExpandableContent && ( +
    + {isExpanded ? ( + + ) : ( + + )} +
    + )} +
    + {hasExpandableContent && summary.length > 80 + ? `${summary.slice(0, 80)}...` + : summary} +
    +
    + + {/* Expandable content */} + {isExpanded && hasExpandableContent && ( +
    + +
    + )} +
    +
    + ); +} + +function EventExpandedContent({ + event, +}: { + event: ExtendedResponseStreamEvent; +}) { + // Handle error events with detailed information + if (event.type === "error") { + const errorEvent = event as ExtendedResponseStreamEvent & { + message?: string; + code?: string; + param?: string; + }; + return ( +
    +
    + + Error Details +
    +
    + {errorEvent.message && ( +
    + + Message: + +
    +
    +                  {errorEvent.message}
    +                
    +
    +
    + )} + {errorEvent.code && ( +
    + Code: + {errorEvent.code} +
    + )} + {errorEvent.param && ( +
    + + Parameter: + + {errorEvent.param} +
    + )} +
    + + Raw Event: + +
    +
    +                {JSON.stringify(event, null, 2)}
    +              
    +
    +
    +
    +
    + ); + } + + switch (event.type) { + case "response.function_call.complete": + if ("data" in event && event.data) { + const data = event.data as FunctionCallData; + return ( +
    +
    + + Function Call +
    +
    +
    + + Function: + + + {data.name || "unknown"} + +
    + {data.call_id && ( +
    + + Call ID: + + {data.call_id} +
    + )} + {data.arguments && ( +
    + + Arguments: + +
    +
    +                      {typeof data.arguments === "string"
    +                        ? data.arguments
    +                        : JSON.stringify(data.arguments, null, 1)}
    +                    
    +
    +
    + )} +
    +
    + ); + } + break; + + case "response.function_result.complete": + if ("data" in event && event.data) { + const data = event.data as FunctionResultData; + return ( +
    +
    + + Function Result +
    +
    + {data.call_id && ( +
    + + Call ID: + + {data.call_id} +
    + )} +
    + + Status: + + + {data.status || "unknown"} + +
    + {data.result !== undefined && ( +
    + + Result: + +
    +
    +                      {typeof data.result === "string"
    +                        ? data.result
    +                        : JSON.stringify(data.result, null, 1)}
    +                    
    +
    +
    + )} + {data.exception !== null && data.exception !== undefined && ( +
    + Error: +
    +
    +                      {data.exception}
    +                    
    +
    +
    + )} +
    +
    + ); + } + break; + + case "response.workflow_event.complete": + if ("data" in event && event.data) { + const data = event.data as WorkflowEventData; + return ( +
    +
    + + Workflow Event +
    +
    +
    + + Event Type: + + + {data.event_type || "unknown"} + +
    + {data.executor_id && ( +
    + + Executor: + + {data.executor_id} +
    + )} + {data.timestamp && ( +
    + + Timestamp: + + + {data.timestamp} + +
    + )} + {data.data && ( +
    + + Data: + +
    +
    +                      {typeof data.data === "string"
    +                        ? data.data
    +                        : JSON.stringify(data.data, null, 1)}
    +                    
    +
    +
    + )} +
    +
    + ); + } + break; + + case "response.trace_event.complete": + case "response.trace.complete": + if ("data" in event && event.data) { + const data = event.data as TraceEventData; + return ( +
    +
    + + Trace Event +
    +
    +
    + + Operation: + + + {data.operation_name || "unknown"} + +
    + {data.span_id && ( +
    + + Span ID: + + {data.span_id} +
    + )} + {data.trace_id && ( +
    + + Trace ID: + + + {data.trace_id} + +
    + )} + {data.duration_ms && ( +
    + + Duration: + + + {Number(data.duration_ms).toFixed(2)}ms + +
    + )} + {data.status && ( +
    + + Status: + + + {data.status || "unknown"} + +
    + )} + {data.entity_id && ( +
    + + Entity: + + + {data.entity_id} + +
    + )} + {data.attributes && Object.keys(data.attributes).length > 0 && ( +
    + + Attributes: + +
    +
    +                      {(() => {
    +                        try {
    +                          // Try to pretty-print JSON, and unescape string values that contain JSON
    +                          const attrs = { ...data.attributes };
    +                          Object.keys(attrs).forEach((key) => {
    +                            if (
    +                              typeof attrs[key] === "string" &&
    +                              attrs[key].startsWith("[")
    +                            ) {
    +                              try {
    +                                attrs[key] = JSON.parse(attrs[key]);
    +                              } catch {
    +                                // Keep original if parsing fails
    +                              }
    +                            }
    +                          });
    +                          return JSON.stringify(attrs, null, 2);
    +                        } catch {
    +                          return JSON.stringify(data.attributes, null, 2);
    +                        }
    +                      })()}
    +                    
    +
    +
    + )} +
    +
    + ); + } + break; + + case "response.output_text.delta": + if ("delta" in event && event.delta) { + return ( +
    +
    + + Text Output +
    +
    +
    +                {event.delta}
    +              
    +
    +
    + ); + } + break; + + default: + return ( +
    +
    +            {JSON.stringify(event, null, 2)}
    +          
    +
    + ); + } + + return null; +} + +function EventsTab({ + events, + isStreaming, +}: { + events: ExtendedResponseStreamEvent[]; + isStreaming?: boolean; +}) { + const scrollRef = useRef(null); + + // Process events to accumulate tool calls and reduce noise + const processedEvents = processEventsForDisplay(events); + + // Reverse events so latest appears at top + const reversedEvents = [...processedEvents].reverse(); + + return ( +
    +
    +
    + + Events + + {processedEvents.length} + {events.length > processedEvents.length + ? ` (${events.length} raw)` + : ""} + +
    + {isStreaming && ( +
    +
    + Streaming +
    + )} +
    + + +
    + {processedEvents.length === 0 ? ( +
    + {events.length === 0 + ? "No events yet. Start a conversation to see real-time events." + : "Processing events... Accumulated events will appear here."} +
    + ) : ( +
    + {reversedEvents.map((event, index) => ( + + ))} +
    + )} +
    +
    +
    + ); +} + +function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) { + // ONLY show actual trace events - handle both event type formats + const traceEvents = events.filter( + (e) => + e.type === "response.trace_event.complete" || + e.type === "response.trace.complete" + ); + + // Reverse to show latest traces at the top + const reversedTraceEvents = [...traceEvents].reverse(); + + return ( +
    +
    + + Traces + {traceEvents.length} +
    + + +
    + {traceEvents.length === 0 ? ( +
    + No trace data available. +
    + {events && events.length > 0 && ( +
    + {" "} + + You may have to set the environment variable{" "} + + ENABLE_OTEL=true + {" "} + to enable tracing. +
    + )} +
    + ) : ( +
    + {reversedTraceEvents.map((event, index) => ( + + ))} +
    + )} +
    +
    +
    + ); +} + +function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) { + const [isExpanded, setIsExpanded] = useState(false); + + if ( + (event.type !== "response.trace_event.complete" && + event.type !== "response.trace.complete") || + !("data" in event) + ) { + return ( +
    + Error: Expected trace event but got {event.type} +
    + ); + } + + const data = event.data as TraceEventData; + + // Use actual trace timestamp if available, fallback to current time + let timestamp = new Date().toLocaleTimeString(); + if (data.end_time) { + timestamp = new Date(data.end_time * 1000).toLocaleTimeString(); + } else if (data.start_time) { + timestamp = new Date(data.start_time * 1000).toLocaleTimeString(); + } else if (data.timestamp) { + timestamp = new Date(data.timestamp).toLocaleTimeString(); + } + + const operationName = data.operation_name || "Unknown Operation"; + const duration = data.duration_ms + ? `${Number(data.duration_ms).toFixed(1)}ms` + : ""; + const entityId = data.entity_id || ""; + + return ( +
    +
    + + {timestamp} + + trace + + {duration && ( + + {duration} + + )} +
    + +
    +
    setIsExpanded(!isExpanded)} + > +
    + {isExpanded ? ( + + ) : ( + + )} +
    +
    + {operationName} + {entityId && ({entityId})} +
    +
    + + {/* Expandable content */} + {isExpanded && ( +
    +
    +
    + + Trace Details +
    +
    +
    + + Operation: + + + {operationName} + +
    + {data.span_id && ( +
    + + Span ID: + + + {data.span_id} + +
    + )} + {data.trace_id && ( +
    + + Trace ID: + + + {data.trace_id} + +
    + )} + {data.parent_span_id && ( +
    + + Parent Span: + + + {data.parent_span_id} + +
    + )} + {data.duration_ms && ( +
    + + Duration: + + + {Number(data.duration_ms).toFixed(2)}ms + +
    + )} + {data.status && ( +
    + + Status: + + + {data.status || "unknown"} + +
    + )} + {data.entity_id && ( +
    + + Entity: + + + {data.entity_id} + +
    + )} + {data.attributes && Object.keys(data.attributes).length > 0 && ( +
    + + Attributes: + +
    +
    +                        {(() => {
    +                          try {
    +                            // Try to pretty-print JSON, and unescape string values that contain JSON
    +                            const attrs = { ...data.attributes };
    +                            Object.keys(attrs).forEach((key) => {
    +                              if (
    +                                typeof attrs[key] === "string" &&
    +                                attrs[key].startsWith("[")
    +                              ) {
    +                                try {
    +                                  attrs[key] = JSON.parse(attrs[key]);
    +                                } catch {
    +                                  // Keep original if parsing fails
    +                                }
    +                              }
    +                            });
    +                            return JSON.stringify(attrs, null, 2);
    +                          } catch {
    +                            return JSON.stringify(data.attributes, null, 2);
    +                          }
    +                        })()}
    +                      
    +
    +
    + )} +
    +
    +
    + )} +
    +
    + ); +} + +function ToolsTab({ events }: { events: ExtendedResponseStreamEvent[] }) { + // Process events first to get clean tool calls + const processedEvents = processEventsForDisplay(events); + + // Create call->result pairs in chronological order + const toolEvents: ExtendedResponseStreamEvent[] = []; + const functionCalls = processedEvents.filter( + (event) => event.type === "response.function_call.complete" + ); + const functionResults = events.filter( + (event) => event.type === "response.function_result.complete" + ); + + // Create a map of call_id to results for easy lookup + const resultsByCallId = new Map(); + functionResults.forEach((result) => { + if ( + "data" in result && + result.data && + (result.data as EventDataBase).call_id + ) { + resultsByCallId.set( + String((result.data as EventDataBase).call_id), + result + ); + } + }); + + // Add call->result pairs in chronological order + functionCalls.forEach((call) => { + toolEvents.push(call); + + // Find matching result and add it immediately after the call + if ("data" in call && call.data && (call.data as EventDataBase).call_id) { + const callId = String((call.data as EventDataBase).call_id); + const matchingResult = resultsByCallId.get(callId); + if (matchingResult) { + toolEvents.push(matchingResult); + resultsByCallId.delete(callId); // Remove so we don't add it again + } + } + }); + + // Add any orphaned results that didn't match calls + resultsByCallId.forEach((result) => { + toolEvents.push(result); + }); + + // Reverse to show latest tools at the top + const reversedToolEvents = [...toolEvents].reverse(); + + return ( +
    +
    + + Tools + {toolEvents.length} +
    + + +
    + {toolEvents.length === 0 ? ( +
    + No tool executions yet. Tool calls will appear here during + conversations. +
    + ) : ( +
    + {reversedToolEvents.map((event, index) => ( + + ))} +
    + )} +
    +
    +
    + ); +} + +function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) { + if (!("data" in event)) { + return null; + } + + const data = event.data as EventDataBase; + const timestamp = new Date().toLocaleTimeString(); + + // Check if this is a function call event + const isFunctionCall = event.type === "response.function_call.complete"; + const isFunctionResult = event.type === "response.function_result.complete"; + + if (!isFunctionCall && !isFunctionResult) { + return null; + } + + return ( +
    +
    +
    + + + {isFunctionCall ? "Tool Call" : "Tool Result"} + + {isFunctionCall && data.name !== undefined && ( + + ({String(data.name)}) + + )} +
    + + {timestamp} + +
    + + {/* Function Calls */} + {isFunctionCall && ( +
    +
    + + + CALL + + + {String(data.name || "unknown")} + +
    + + {data.arguments !== undefined && ( +
    + + Arguments: + +
    +                {typeof data.arguments === "string"
    +                  ? data.arguments
    +                  : JSON.stringify(data.arguments, null, 1)}
    +              
    +
    + )} +
    + )} + + {/* Function Results */} + {isFunctionResult && ( +
    +
    + + + RESULT + +
    + +
    + Result: +
    +              {typeof data.result === "string"
    +                ? data.result
    +                : JSON.stringify(data.result, null, 1)}
    +            
    +
    + + {data.exception !== null && data.exception !== undefined && ( +
    + + Error: {String(data.exception)} + +
    + )} +
    + )} +
    + ); +} + +export function DebugPanel({ events, isStreaming = false }: DebugPanelProps) { + return ( +
    + +
    + + + Events + + + Traces + + + Tools + + +
    + + + + + + + + + + + + +
    +
    + ); +} diff --git a/python/packages/devui/frontend/src/components/shared/entity-selector.tsx b/python/packages/devui/frontend/src/components/shared/entity-selector.tsx new file mode 100644 index 0000000000..72d717a215 --- /dev/null +++ b/python/packages/devui/frontend/src/components/shared/entity-selector.tsx @@ -0,0 +1,187 @@ +/** + * EntitySelector - High-quality dropdown for selecting agents/workflows + * Features: Type indicators, tool counts, keyboard navigation, search + */ + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { ChevronDown, Bot, Workflow, FolderOpen, Database } from "lucide-react"; +import type { AgentInfo, WorkflowInfo } from "@/types"; + +interface EntitySelectorProps { + agents: AgentInfo[]; + workflows: WorkflowInfo[]; + selectedItem?: AgentInfo | WorkflowInfo; + onSelect: (item: AgentInfo | WorkflowInfo) => void; + isLoading?: boolean; +} + +const getTypeIcon = (type: "agent" | "workflow") => { + return type === "workflow" ? Workflow : Bot; +}; + +const getSourceIcon = (source: "directory" | "in_memory") => { + return source === "directory" ? FolderOpen : Database; +}; + +export function EntitySelector({ + agents, + workflows, + selectedItem, + onSelect, + isLoading = false, +}: EntitySelectorProps) { + const [open, setOpen] = useState(false); + + const allItems = [...agents, ...workflows].sort( + (a, b) => a.name?.localeCompare(b.name || a.id) || a.id.localeCompare(b.id) + ); + + const handleSelect = (item: AgentInfo | WorkflowInfo) => { + onSelect(item); + setOpen(false); + }; + + const TypeIcon = selectedItem ? getTypeIcon(selectedItem.type) : Bot; + const displayName = selectedItem?.name || selectedItem?.id || "Select Entity"; + const itemCount = + selectedItem?.type === "workflow" + ? (selectedItem as WorkflowInfo).executors?.length || 0 + : (selectedItem as AgentInfo)?.tools?.length || 0; + const itemLabel = selectedItem?.type === "workflow" ? "executors" : "tools"; + + return ( + + + + + + + {agents.length > 0 && ( + <> + + + Agents ({agents.length}) + + {agents.map((agent) => { + const SourceIcon = getSourceIcon(agent.source); + return ( + handleSelect(agent)} + className="cursor-pointer" + > +
    +
    + +
    +
    + {agent.name || agent.id} +
    + {agent.description && ( +
    + {agent.description} +
    + )} +
    +
    +
    + + + {agent.tools.length} + +
    +
    +
    + ); + })} + + )} + + {workflows.length > 0 && ( + <> + {agents.length > 0 && } + + + Workflows ({workflows.length}) + + {workflows.map((workflow) => { + const SourceIcon = getSourceIcon(workflow.source); + return ( + handleSelect(workflow)} + className="cursor-pointer" + > +
    +
    + +
    +
    + {workflow.name || workflow.id} +
    + {workflow.description && ( +
    + {workflow.description} +
    + )} +
    +
    +
    + + + {workflow.executors.length} + +
    +
    +
    + ); + })} + + )} + + {allItems.length === 0 && ( + +
    + {isLoading ? "Loading entities..." : "No entities found"} +
    +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/theme-provider.tsx b/python/packages/devui/frontend/src/components/theme-provider.tsx new file mode 100644 index 0000000000..4712a22957 --- /dev/null +++ b/python/packages/devui/frontend/src/components/theme-provider.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +interface ThemeProviderProps { + children: React.ReactNode + attribute?: "class" | "data-theme" | "data-mode" + defaultTheme?: string + enableSystem?: boolean + disableTransitionOnChange?: boolean +} + +export function ThemeProvider({ + children, + attribute = "class", + defaultTheme = "dark", + enableSystem = true, + disableTransitionOnChange = true, + ...props +}: ThemeProviderProps) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/attachment-gallery.tsx b/python/packages/devui/frontend/src/components/ui/attachment-gallery.tsx new file mode 100644 index 0000000000..82e02bde27 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/attachment-gallery.tsx @@ -0,0 +1,115 @@ +/** + * AttachmentGallery - Shows uploaded files with thumbnails and remove options + */ + +import { useState } from "react"; +import { FileText, Image, Trash2 } from "lucide-react"; + +export interface AttachmentItem { + id: string; + file: File; + preview?: string; // Data URL for preview + type: "image" | "pdf" | "other"; +} + +interface AttachmentGalleryProps { + attachments: AttachmentItem[]; + onRemoveAttachment: (id: string) => void; + className?: string; +} + +export function AttachmentGallery({ + attachments, + onRemoveAttachment, + className = "", +}: AttachmentGalleryProps) { + if (attachments.length === 0) return null; + + return ( +
    + {attachments.map((attachment) => ( + onRemoveAttachment(attachment.id)} + /> + ))} +
    + ); +} + +interface AttachmentPreviewProps { + attachment: AttachmentItem; + onRemove: () => void; +} + +function AttachmentPreview({ attachment, onRemove }: AttachmentPreviewProps) { + const [isHovered, setIsHovered] = useState(false); + + const renderPreview = () => { + switch (attachment.type) { + case "image": + return attachment.preview ? ( + {attachment.file.name} + ) : ( +
    + +
    + ); + + case "pdf": + return ( +
    + + PDF +
    + ); + + default: + return ( +
    + + FILE +
    + ); + } + }; + + return ( +
    setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + title={attachment.file.name} + > + {renderPreview()} + + {/* Dark overlay with centered delete icon on hover */} +
    +
    + +
    +
    + + {/* File name tooltip */} +
    + {attachment.file.name} +
    +
    + ); +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/badge.tsx b/python/packages/devui/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000000..124fa26b45 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ); +} + +export { Badge }; diff --git a/python/packages/devui/frontend/src/components/ui/button.tsx b/python/packages/devui/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000000..a2df8dce67 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/python/packages/devui/frontend/src/components/ui/card.tsx b/python/packages/devui/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000000..027bfcd4d8 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/python/packages/devui/frontend/src/components/ui/checkbox.tsx b/python/packages/devui/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000000..fa0e4b59fa --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/python/packages/devui/frontend/src/components/ui/dialog.tsx b/python/packages/devui/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..a2fb2866fb --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { X } from "lucide-react"; +import { Button } from "./button"; + +interface DialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + children: React.ReactNode; +} + +interface DialogContentProps { + children: React.ReactNode; + className?: string; +} + +interface DialogHeaderProps { + children: React.ReactNode; +} + +interface DialogTitleProps { + children: React.ReactNode; +} + +interface DialogFooterProps { + children: React.ReactNode; +} + +export function Dialog({ open, onOpenChange, children }: DialogProps) { + if (!open) return null; + + return ( +
    onOpenChange(false)} + > + {/* Backdrop */} +
    + + {/* Modal content */} +
    e.stopPropagation()}>{children}
    +
    + ); +} + +export function DialogContent({ + children, + className = "", +}: DialogContentProps) { + return ( +
    + {children} +
    + ); +} + +export function DialogHeader({ children }: DialogHeaderProps) { + return ( +
    + {children} +
    + ); +} + +export function DialogTitle({ children }: DialogTitleProps) { + return

    {children}

    ; +} + +export function DialogClose({ onClose }: { onClose: () => void }) { + return ( + + ); +} + +export function DialogFooter({ children }: DialogFooterProps) { + return ( +
    + {children} +
    + ); +} diff --git a/python/packages/devui/frontend/src/components/ui/dropdown-menu.tsx b/python/packages/devui/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000000..0d6741b4dc --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/python/packages/devui/frontend/src/components/ui/file-upload.tsx b/python/packages/devui/frontend/src/components/ui/file-upload.tsx new file mode 100644 index 0000000000..4f2d9af71a --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/file-upload.tsx @@ -0,0 +1,141 @@ +/** + * FileUpload - Upload button with drag & drop support + */ + +import { useRef } from "react"; +import { Upload } from "lucide-react"; +import { Button } from "./button"; + +interface FileUploadProps { + onFilesSelected: (files: File[]) => void; + accept?: string; + multiple?: boolean; + maxSize?: number; // in bytes + disabled?: boolean; + className?: string; +} + +export function FileUpload({ + onFilesSelected, + accept = "image/*,.pdf", + multiple = true, + maxSize = 50 * 1024 * 1024, // 50MB default for local dev tool + disabled = false, + className = "", +}: FileUploadProps) { + const fileInputRef = useRef(null); + + const handleFileSelect = (files: FileList | null) => { + if (!files || files.length === 0) return; + + const validFiles: File[] = []; + const errors: string[] = []; + + Array.from(files).forEach((file) => { + // Size validation + if (file.size > maxSize) { + errors.push(`${file.name} is too large (max ${formatFileSize(maxSize)})`); + return; + } + + // Type validation (basic) + if (accept && !isFileAccepted(file, accept)) { + errors.push(`${file.name} is not an accepted file type`); + return; + } + + validFiles.push(file); + }); + + if (errors.length > 0) { + console.warn("File upload errors:", errors); + // In a production app, you might want to show these errors to the user + } + + if (validFiles.length > 0) { + onFilesSelected(validFiles); + } + }; + + const handleButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleFileInputChange = (e: React.ChangeEvent) => { + handleFileSelect(e.target.files); + // Reset input to allow selecting the same file again + e.target.value = ""; + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (disabled) return; + + const files = e.dataTransfer.files; + handleFileSelect(files); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + return ( +
    + + + +
    + ); +} + +// Helper functions +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} + +function isFileAccepted(file: File, accept: string): boolean { + const acceptPatterns = accept.split(",").map((pattern) => pattern.trim()); + + return acceptPatterns.some((pattern) => { + if (pattern.startsWith(".")) { + // File extension check + return file.name.toLowerCase().endsWith(pattern.toLowerCase()); + } else if (pattern.includes("/*")) { + // MIME type wildcard check (e.g., "image/*") + const [mainType] = pattern.split("/"); + return file.type.startsWith(mainType + "/"); + } else { + // Exact MIME type check + return file.type === pattern; + } + }); +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/input.tsx b/python/packages/devui/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000000..03295ca6ac --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/python/packages/devui/frontend/src/components/ui/label.tsx b/python/packages/devui/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000000..ef7133a756 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/python/packages/devui/frontend/src/components/ui/loading-spinner.tsx b/python/packages/devui/frontend/src/components/ui/loading-spinner.tsx new file mode 100644 index 0000000000..00b2e65f02 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/loading-spinner.tsx @@ -0,0 +1,23 @@ +import { Loader2 } from "lucide-react" +import { cn } from "@/lib/utils" + +interface LoadingSpinnerProps { + size?: "sm" | "md" | "lg" + className?: string +} + +export function LoadingSpinner({ size = "md", className }: LoadingSpinnerProps) { + return ( + + ) +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/loading-state.tsx b/python/packages/devui/frontend/src/components/ui/loading-state.tsx new file mode 100644 index 0000000000..ac4303a981 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/loading-state.tsx @@ -0,0 +1,52 @@ +import { LoadingSpinner } from "./loading-spinner" +import { cn } from "@/lib/utils" + +interface LoadingStateProps { + message?: string + description?: string + size?: "sm" | "md" | "lg" + className?: string + fullPage?: boolean +} + +export function LoadingState({ + message = "Loading...", + description, + size = "md", + className, + fullPage = false +}: LoadingStateProps) { + const content = ( +
    + +
    +

    + {message} +

    + {description && ( +

    + {description} +

    + )} +
    +
    + ) + + if (fullPage) { + return ( +
    + {content} +
    + ) + } + + return content +} \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/scroll-area.tsx b/python/packages/devui/frontend/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000000..62515484b8 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/select.tsx b/python/packages/devui/frontend/src/components/ui/select.tsx new file mode 100644 index 0000000000..51f466eccb --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/select.tsx @@ -0,0 +1,183 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/python/packages/devui/frontend/src/components/ui/tabs.tsx b/python/packages/devui/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000000..125b4bb119 --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } \ No newline at end of file diff --git a/python/packages/devui/frontend/src/components/ui/textarea.tsx b/python/packages/devui/frontend/src/components/ui/textarea.tsx new file mode 100644 index 0000000000..7f21b5e78a --- /dev/null +++ b/python/packages/devui/frontend/src/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( +