Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cc70906
DevUI: Add OpenAI Responses API proxy support with enhanced UI features
victordibia Oct 27, 2025
bae374b
Merge remote-tracking branch 'origin/main' into devui_oai_responses
victordibia Oct 27, 2025
6b4d188
update ui, settings modal and workflow input form, add register clean…
victordibia Oct 29, 2025
c6434b0
add workflow HIL support, user mode, other fixes
victordibia Oct 30, 2025
a4cc42a
Merge branch 'main' into devui_oai_responses
markwallace-microsoft Oct 31, 2025
8cfbc1a
Merge branch 'main' into devui_oai_responses
markwallace-microsoft Oct 31, 2025
ea5a239
Merge branch 'main' into devui_oai_responses
victordibia Nov 3, 2025
c0efc00
Merge branch 'main' into devui_oai_responses
victordibia Nov 4, 2025
55389da
feat(devui): add human-in-the-loop (HIL) support with dynamic respons…
victordibia Nov 4, 2025
246af75
improve HIL support, improve workflow execution view
victordibia Nov 5, 2025
ee1f10e
Merge branch 'main' into devui_oai_responses
victordibia Nov 5, 2025
512967b
ui updates
victordibia Nov 5, 2025
53868a5
ui updates
victordibia Nov 5, 2025
037ab69
Merge branch 'devui_oai_responses' of github.com:victordibia/agent-fr…
victordibia Nov 5, 2025
695a99f
improve HIL for workflows, add auth and view modes
victordibia Nov 6, 2025
41b7465
update workflow
victordibia Nov 6, 2025
71e8699
Merge remote-tracking branch 'origin/main' into devui_oai_responses
victordibia Nov 7, 2025
146cbad
Merge branch 'main' into devui_oai_responses
victordibia Nov 7, 2025
b2cc3c6
Merge remote-tracking branch 'origin/main' into devui_oai_responses
victordibia Nov 7, 2025
b86c8fe
security improvements , ui fixes
victordibia Nov 7, 2025
39715db
Merge remote-tracking branch 'origin/main' into devui_oai_responses
victordibia Nov 7, 2025
130a669
Merge branch 'devui_oai_responses' of github.com:victordibia/agent-fr…
victordibia Nov 7, 2025
5cf185f
Merge branch 'main' into devui_oai_responses
victordibia Nov 7, 2025
bf5738c
Merge remote-tracking branch 'origin/main' into devui_oai_responses a…
victordibia Nov 7, 2025
a4bdd6b
fix mypy error
victordibia Nov 7, 2025
aecf480
update loading spinner in ui
victordibia Nov 7, 2025
44a4488
Merge branch 'main' into devui_oai_responses
victordibia Nov 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 78 additions & 7 deletions python/packages/devui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ serve(entities=[agent])

MCP tools use lazy initialization and connect automatically on first use. DevUI attempts to clean up connections on shutdown

## Resource Cleanup

Register cleanup hooks to properly close credentials and resources on shutdown:

```python
from azure.identity.aio import DefaultAzureCredential
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_devui import register_cleanup, serve

credential = DefaultAzureCredential()
client = AzureOpenAIChatClient()
agent = ChatAgent(name="MyAgent", chat_client=client)

# Register cleanup hook - credential will be closed on shutdown
register_cleanup(agent, credential.close)
serve(entities=[agent])
```

Works with multiple resources and file-based discovery. See tests for more examples.

## 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`).
Expand Down Expand Up @@ -150,6 +171,22 @@ response2 = client.responses.create(

**How it works:** DevUI automatically retrieves the conversation's message history from the stored thread and passes it to the agent. You don't need to manually manage message history - just provide the same `conversation` ID for follow-up requests.

### OpenAI Proxy Mode

DevUI provides an **OpenAI Proxy** feature for testing OpenAI models directly through the interface without creating custom agents. Enable via Settings → OpenAI Proxy tab.

**How it works:** The UI sends requests to the DevUI backend (with `X-Proxy-Backend: openai` header), which then proxies them to OpenAI's Responses API (and Conversations API for multi-turn chats). This proxy approach keeps your `OPENAI_API_KEY` secure on the server—never exposed in the browser or client-side code.

**Example:**

```bash
curl -X POST http://localhost:8080/v1/responses \
-H "X-Proxy-Backend: openai" \
-d '{"model": "gpt-4.1-mini", "input": "Hello"}'
```

**Note:** Requires `OPENAI_API_KEY` environment variable configured on the backend.

## CLI Options

```bash
Expand All @@ -162,6 +199,21 @@ Options:
--config YAML config file
--tracing none|framework|workflow|all
--reload Enable auto-reload
--mode developer|user (default: developer)
--auth Enable Bearer token authentication
```

### UI Modes

- **developer** (default): Full access - debug panel, entity details, hot reload, deployment
- **user**: Simplified UI with restricted APIs - only chat and conversation management

```bash
# Development
devui ./agents

# Production (user-facing)
devui ./agents --mode user --auth
```

## Key Endpoints
Expand All @@ -187,18 +239,23 @@ Given that DevUI offers an OpenAI Responses API, it internally maps messages and
| `response.function_result.complete` | `FunctionResultContent` | DevUI |
| `response.function_approval.requested` | `FunctionApprovalRequestContent` | DevUI |
| `response.function_approval.responded` | `FunctionApprovalResponseContent` | DevUI |
| `response.output_item.added` (ResponseOutputImage) | `DataContent` (images) | DevUI |
| `response.output_item.added` (ResponseOutputFile) | `DataContent` (files) | DevUI |
| `response.output_item.added` (ResponseOutputData) | `DataContent` (other) | DevUI |
| `response.output_item.added` (ResponseOutputImage/File) | `UriContent` (images/files) | DevUI |
| `error` | `ErrorContent` | OpenAI |
| Final `Response.usage` field (not streamed) | `UsageContent` | OpenAI |
| | **Workflow Events** | |
| `response.output_item.added` (ExecutorActionItem)* | `ExecutorInvokedEvent` | OpenAI |
| `response.output_item.done` (ExecutorActionItem)* | `ExecutorCompletedEvent` | OpenAI |
| `response.output_item.done` (ExecutorActionItem with error)* | `ExecutorFailedEvent` | OpenAI |
| `response.output_item.added` (ResponseOutputMessage) | `WorkflowOutputEvent` | OpenAI |
| `response.workflow_event.complete` | `WorkflowEvent` (other) | DevUI |
| `response.trace.complete` | `WorkflowStatusEvent` | DevUI |
| `response.trace.complete` | `WorkflowWarningEvent` | DevUI |
| | **Trace Content** | |
| `response.trace.complete` | `DataContent` | DevUI |
| `response.trace.complete` | `UriContent` | DevUI |
| `response.trace.complete` | `DataContent` (no data/errors) | DevUI |
| `response.trace.complete` | `UriContent` (unsupported MIME) | DevUI |
| `response.trace.complete` | `HostedFileContent` | DevUI |
| `response.trace.complete` | `HostedVectorStoreContent` | DevUI |

Expand All @@ -213,15 +270,19 @@ DevUI follows the OpenAI Responses API specification for maximum compatibility:

**OpenAI Standard Event Types Used:**

- `ResponseOutputItemAddedEvent` - Output item notifications (function calls and results)
- `ResponseOutputItemAddedEvent` - Output item notifications (function calls, images, files, data)
- `ResponseOutputItemDoneEvent` - Output item completion notifications
- `Response.usage` - Token usage (in final response, not streamed)
- All standard text, reasoning, and function call events

**Custom DevUI Extensions:**

- `response.output_item.added` with custom item types:
- `ResponseOutputImage` - Agent-generated images (inline display)
- `ResponseOutputFile` - Agent-generated files (inline display)
- `ResponseOutputData` - Agent-generated structured data (inline display)
- `response.function_approval.requested` - Function approval requests (for interactive approval workflows)
- `response.function_approval.responded` - Function approval responses (user approval/rejection)
- `response.function_result.complete` - Server-side function execution results
- `response.workflow_event.complete` - Agent Framework workflow events
- `response.trace.complete` - Execution traces and internal content (DataContent, UriContent, hosted files/stores)

Expand Down Expand Up @@ -254,18 +315,28 @@ These custom extensions are clearly namespaced and can be safely ignored by stan

## Security

DevUI is designed as a **sample application for local development** and should not be exposed to untrusted networks or used in production environments.
DevUI is designed as a **sample application for local development** and should not be exposed to untrusted networks without proper authentication.

**For production deployments:**

```bash
# User mode with authentication (recommended)
devui ./agents --mode user --auth --host 0.0.0.0
```

This restricts developer APIs (reload, deployment, entity details) and requires Bearer token authentication.

**Security features:**

- User mode restricts developer-facing APIs
- Optional Bearer token authentication via `--auth`
- Only loads entities from local directories or in-memory registration
- No remote code execution capabilities
- Binds to localhost (127.0.0.1) by default
- All samples must be manually downloaded and reviewed before running

**Best practices:**

- Never expose DevUI to the internet
- Use `--mode user --auth` for any deployment exposed to end users
- Review all agent/workflow code before running
- Only load entities from trusted sources
- Use `.env` files for sensitive credentials (never commit them)
Expand Down
128 changes: 127 additions & 1 deletion python/packages/devui/agent_framework_devui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,87 @@
import importlib.metadata
import logging
import webbrowser
from collections.abc import Callable
from typing import Any

from ._conversations import CheckpointConversationManager
from ._server import DevServer
from .models import AgentFrameworkRequest, OpenAIError, OpenAIResponse, ResponseStreamEvent
from .models._discovery_models import DiscoveryResponse, EntityInfo, EnvVarRequirement

logger = logging.getLogger(__name__)

# Module-level cleanup registry (before serve() is called)
_cleanup_registry: dict[int, list[Callable[[], Any]]] = {}

try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode


def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None:
"""Register cleanup hook(s) for an entity.

Cleanup hooks execute during DevUI server shutdown, before entity
clients are closed. Supports both synchronous and asynchronous callables.

Args:
entity: Agent, workflow, or other entity object
*hooks: One or more cleanup callables (sync or async)

Raises:
ValueError: If no hooks provided

Examples:
Single cleanup hook:
>>> from agent_framework.devui import serve, register_cleanup
>>> credential = DefaultAzureCredential()
>>> agent = ChatAgent(...)
>>> register_cleanup(agent, credential.close)
>>> serve(entities=[agent])

Multiple cleanup hooks:
>>> register_cleanup(agent, credential.close, session.close, db_pool.close)

Works with file-based discovery:
>>> # In agents/my_agent/agent.py
>>> from agent_framework.devui import register_cleanup
>>> credential = DefaultAzureCredential()
>>> agent = ChatAgent(...)
>>> register_cleanup(agent, credential.close)
>>> # Run: devui ./agents
"""
if not hooks:
raise ValueError("At least one cleanup hook required")

# Use id() to track entity identity (works across modules)
entity_id = id(entity)

if entity_id not in _cleanup_registry:
_cleanup_registry[entity_id] = []

_cleanup_registry[entity_id].extend(hooks)

logger.debug(
f"Registered {len(hooks)} cleanup hook(s) for {type(entity).__name__} "
f"(id: {entity_id}, total: {len(_cleanup_registry[entity_id])})"
)


def _get_registered_cleanup_hooks(entity: Any) -> list[Callable[[], Any]]:
"""Get cleanup hooks registered for an entity (internal use).

Args:
entity: Entity object to get hooks for

Returns:
List of cleanup hooks registered for the entity
"""
entity_id = id(entity)
return _cleanup_registry.get(entity_id, [])


def serve(
entities: list[Any] | None = None,
entities_dir: str | None = None,
Expand All @@ -28,6 +95,9 @@ def serve(
cors_origins: list[str] | None = None,
ui_enabled: bool = True,
tracing_enabled: bool = False,
mode: str = "developer",
auth_enabled: bool = False,
auth_token: str | None = None,
) -> None:
"""Launch Agent Framework DevUI with simple API.

Expand All @@ -40,6 +110,9 @@ def serve(
cors_origins: List of allowed CORS origins
ui_enabled: Whether to enable the UI
tracing_enabled: Whether to enable OpenTelemetry tracing
mode: Server mode - 'developer' (full access, verbose errors) or 'user' (restricted APIs, generic errors)
auth_enabled: Whether to enable Bearer token authentication
auth_token: Custom authentication token (auto-generated if not provided with auth_enabled=True)
"""
import re

Expand All @@ -53,6 +126,52 @@ def serve(
if not isinstance(port, int) or not (1 <= port <= 65535):
raise ValueError(f"Invalid port: {port}. Must be integer between 1 and 65535")

# Security check: Warn if network-exposed without authentication
if host not in ("127.0.0.1", "localhost") and not auth_enabled:
logger.warning("⚠️ WARNING: Exposing DevUI to network without authentication!")
logger.warning("⚠️ This is INSECURE - anyone on your network can access your agents")
logger.warning("💡 For network exposure, add --auth flag: devui --host 0.0.0.0 --auth")

# Handle authentication configuration
if auth_enabled:
import os
import secrets

# Check if token is in environment variable first
if not auth_token:
auth_token = os.environ.get("DEVUI_AUTH_TOKEN")

# Auto-generate token if STILL not provided
if not auth_token:
# Check if we're in a production-like environment
is_production = (
host not in ("127.0.0.1", "localhost") # Exposed to network
or os.environ.get("CI") == "true" # Running in CI
or os.environ.get("KUBERNETES_SERVICE_HOST") # Running in k8s
)

if is_production:
# REFUSE to start without explicit token
logger.error("❌ Authentication enabled but no token provided")
logger.error("❌ Auto-generated tokens are NOT secure for network-exposed deployments")
logger.error("💡 Set token: export DEVUI_AUTH_TOKEN=<your-secure-token>")
logger.error("💡 Or pass: serve(entities=[...], auth_token='your-token')")
raise ValueError("DEVUI_AUTH_TOKEN required when host is not localhost")

# Development mode: auto-generate and show
auth_token = secrets.token_urlsafe(32)
logger.info("🔒 Authentication enabled with auto-generated token")
logger.info("\n" + "=" * 70)
logger.info("🔑 DEV TOKEN (localhost only, shown once):")
logger.info(f" {auth_token}")
logger.info("=" * 70 + "\n")
else:
logger.info("🔒 Authentication enabled with provided token")

# Set environment variable for server to use
os.environ["AUTH_REQUIRED"] = "true"
os.environ["DEVUI_AUTH_TOKEN"] = auth_token

# Configure tracing environment variables if enabled
if tracing_enabled:
import os
Expand All @@ -72,7 +191,12 @@ def serve(

# Create server with direct parameters
server = DevServer(
entities_dir=entities_dir, port=port, host=host, cors_origins=cors_origins, ui_enabled=ui_enabled
entities_dir=entities_dir,
port=port,
host=host,
cors_origins=cors_origins,
ui_enabled=ui_enabled,
mode=mode,
)

# Register in-memory entities if provided
Expand Down Expand Up @@ -139,6 +263,7 @@ def main() -> None:
# Export main public API
__all__ = [
"AgentFrameworkRequest",
"CheckpointConversationManager",
"DevServer",
"DiscoveryResponse",
"EntityInfo",
Expand All @@ -147,5 +272,6 @@ def main() -> None:
"OpenAIResponse",
"ResponseStreamEvent",
"main",
"register_cleanup",
"serve",
]
Loading