Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 25 additions & 4 deletions python/packages/devui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,32 @@ agents/
└── .env # Optional: shared environment variables
```

### Importing from External Modules

If your agents import tools or utilities from sibling directories (e.g., `from tools.helpers import my_tool`), you must set `PYTHONPATH` to include the parent directory:

```bash
# Project structure:
# backend/
# ├── agents/
# │ └── my_agent/
# │ └── agent.py # contains: from tools.helpers import my_tool
# └── tools/
# └── helpers.py

# Run from project root with PYTHONPATH
cd backend
PYTHONPATH=. devui ./agents --port 8080
```

Without `PYTHONPATH`, Python cannot find modules in sibling directories and DevUI will report an import error.

## Viewing Telemetry (Otel Traces) in DevUI

Agent Framework emits OpenTelemetry (Otel) traces for various operations. You can view these traces in DevUI by enabling tracing when starting the server.
Agent Framework emits OpenTelemetry (Otel) traces for various operations. You can view these traces in DevUI by enabling instrumentation when starting the server.

```bash
devui ./agents --tracing framework
devui ./agents --instrumentation
```

## OpenAI-Compatible API
Expand Down Expand Up @@ -196,11 +216,12 @@ 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
--no-open Don't automatically open browser
--instrumentation Enable OpenTelemetry instrumentation
--reload Enable auto-reload
--mode developer|user (default: developer)
--auth Enable Bearer token authentication
--auth-token Custom authentication token
```

### UI Modes
Expand Down
24 changes: 7 additions & 17 deletions python/packages/devui/agent_framework_devui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def serve(
auto_open: bool = False,
cors_origins: list[str] | None = None,
ui_enabled: bool = True,
tracing_enabled: bool = False,
instrumentation_enabled: bool = False,
mode: str = "developer",
auth_enabled: bool = False,
auth_token: str | None = None,
Expand All @@ -109,7 +109,7 @@ def serve(
auto_open: Whether to automatically open browser
cors_origins: List of allowed CORS origins
ui_enabled: Whether to enable the UI
tracing_enabled: Whether to enable OpenTelemetry tracing
instrumentation_enabled: Whether to enable OpenTelemetry instrumentation
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)
Expand Down Expand Up @@ -172,22 +172,12 @@ def serve(
os.environ["AUTH_REQUIRED"] = "true"
os.environ["DEVUI_AUTH_TOKEN"] = auth_token

# Configure tracing environment variables if enabled
if tracing_enabled:
import os

# Only set if not already configured by user
if not os.environ.get("ENABLE_INSTRUMENTATION"):
os.environ["ENABLE_INSTRUMENTATION"] = "true"
logger.info("Set ENABLE_INSTRUMENTATION=true for tracing")

if not os.environ.get("ENABLE_SENSITIVE_DATA"):
os.environ["ENABLE_SENSITIVE_DATA"] = "true"
logger.info("Set ENABLE_SENSITIVE_DATA=true for tracing")
# Enable instrumentation if requested
if instrumentation_enabled:
from agent_framework.observability import enable_instrumentation

if not os.environ.get("OTLP_ENDPOINT"):
os.environ["OTLP_ENDPOINT"] = "http://localhost:4317"
logger.info("Set OTLP_ENDPOINT=http://localhost:4317 for tracing")
enable_instrumentation(enable_sensitive_data=True)
logger.info("Enabled Agent Framework instrumentation with sensitive data")

# Create server with direct parameters
server = DevServer(
Expand Down
6 changes: 3 additions & 3 deletions python/packages/devui/agent_framework_devui/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
devui ./agents # Scan specific directory
devui --port 8000 # Custom port
devui --headless # API only, no UI
devui --tracing # Enable OpenTelemetry tracing
devui --instrumentation # Enable OpenTelemetry instrumentation
""",
)

Expand All @@ -53,7 +53,7 @@ def create_cli_parser() -> argparse.ArgumentParser:

parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")

parser.add_argument("--tracing", action="store_true", help="Enable OpenTelemetry tracing for Agent Framework")
parser.add_argument("--instrumentation", action="store_true", help="Enable OpenTelemetry instrumentation")

parser.add_argument(
"--mode",
Expand Down Expand Up @@ -182,7 +182,7 @@ def main() -> None:
host=args.host,
auto_open=not args.no_open,
ui_enabled=ui_enabled,
tracing_enabled=args.tracing,
instrumentation_enabled=args.instrumentation,
mode=mode,
auth_enabled=args.auth,
auth_token=args.auth_token, # Pass through explicit token only
Expand Down
72 changes: 68 additions & 4 deletions python/packages/devui/agent_framework_devui/_conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,31 @@ async def list_conversations_by_metadata(self, metadata_filter: dict[str, str])
"""
pass

@abstractmethod
def add_trace(self, conversation_id: str, trace_event: dict[str, Any]) -> None:
"""Add a trace event to the conversation for context inspection.

Traces capture execution metadata like token usage, timing, and LLM context
that isn't stored in the AgentThread but is useful for debugging.

Args:
conversation_id: Conversation ID
trace_event: Trace event data (from ResponseTraceEvent.data)
"""
pass

@abstractmethod
def get_traces(self, conversation_id: str) -> list[dict[str, Any]]:
"""Get all trace events for a conversation.

Args:
conversation_id: Conversation ID

Returns:
List of trace event dicts, or empty list if not found
"""
pass


class InMemoryConversationStore(ConversationStore):
"""In-memory conversation storage wrapping AgentThread.
Expand Down Expand Up @@ -215,6 +240,7 @@ def create_conversation(
"metadata": metadata or {},
"created_at": created_at,
"items": [],
"traces": [], # Trace events for context inspection (token usage, timing, etc.)
}

# Initialize item index for this conversation
Expand Down Expand Up @@ -407,10 +433,20 @@ async def list_items(
elif content_type == "function_result":
# Function result - create separate ConversationItem
call_id = getattr(content, "call_id", None)
# Output is stored in additional_properties
output = ""
if hasattr(content, "additional_properties"):
output = content.additional_properties.get("output", "")
# Output is stored in the 'result' field of FunctionResultContent
result_value = getattr(content, "result", None)
# Convert result to string (it could be dict, list, or other types)
if result_value is None:
output = ""
elif isinstance(result_value, str):
output = result_value
else:
import json

try:
output = json.dumps(result_value)
except (TypeError, ValueError):
output = str(result_value)

if call_id:
function_results.append(
Expand Down Expand Up @@ -556,6 +592,34 @@ def get_thread(self, conversation_id: str) -> AgentThread | None:
conv_data = self._conversations.get(conversation_id)
return conv_data["thread"] if conv_data else None

def add_trace(self, conversation_id: str, trace_event: dict[str, Any]) -> None:
"""Add a trace event to the conversation for context inspection.

Traces capture execution metadata like token usage, timing, and LLM context
that isn't stored in the AgentThread but is useful for debugging.

Args:
conversation_id: Conversation ID
trace_event: Trace event data (from ResponseTraceEvent.data)
"""
conv_data = self._conversations.get(conversation_id)
if conv_data:
traces = conv_data.get("traces", [])
traces.append(trace_event)
conv_data["traces"] = traces

def get_traces(self, conversation_id: str) -> list[dict[str, Any]]:
"""Get all trace events for a conversation.

Args:
conversation_id: Conversation ID

Returns:
List of trace event dicts, or empty list if not found
"""
conv_data = self._conversations.get(conversation_id)
return conv_data.get("traces", []) if conv_data else []

async def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]:
"""Filter conversations by metadata (e.g., agent_id)."""
results = []
Expand Down
11 changes: 10 additions & 1 deletion python/packages/devui/agent_framework_devui/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,16 @@ def _load_module_from_pattern(self, pattern: str) -> tuple[Any | None, Exception
logger.debug(f"Successfully imported {pattern}")
return module, None

except ModuleNotFoundError:
except ModuleNotFoundError as e:
# Distinguish between "module pattern doesn't exist" vs "module has import errors"
# If the missing module is the pattern itself, it's just not found (try next pattern)
# If the missing module is something else (a dependency), capture the error
missing_module = getattr(e, "name", None)
if missing_module and missing_module != pattern and not pattern.endswith(f".{missing_module}"):
# The module exists but has an import error (missing dependency)
logger.warning(f"Error importing {pattern}: {e}")
return None, e
# The module pattern itself doesn't exist - this is expected, try next pattern
logger.debug(f"Import pattern {pattern} not found")
return None, None
except Exception as e:
Expand Down
Loading
Loading