Skip to content

Commit 2e1189c

Browse files
authored
Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)
* Improve DevUI, add Context Inspector view as new tab under traces * fix mypy errors * fix: Handle stale MCP connections in DevUI executor MCP tools can become stale when HTTP streaming responses end - the underlying stdio streams close but `is_connected` remains True. This causes subsequent requests to fail with `ClosedResourceError`. Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before agent execution. This is a workaround for an upstream Agent Framework issue where connection state isn't properly tracked. Fixes MCP tools failing on second HTTP request in DevUI. fixes #1476 #1515 #2865 * fix #1572 report import dependency errors more clearly * Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode? * remove unused dead code * improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly * update ui build * update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes
1 parent db283cd commit 2e1189c

36 files changed

+7429
-1661
lines changed

python/packages/devui/README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,32 @@ agents/
102102
└── .env # Optional: shared environment variables
103103
```
104104

105+
### Importing from External Modules
106+
107+
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:
108+
109+
```bash
110+
# Project structure:
111+
# backend/
112+
# ├── agents/
113+
# │ └── my_agent/
114+
# │ └── agent.py # contains: from tools.helpers import my_tool
115+
# └── tools/
116+
# └── helpers.py
117+
118+
# Run from project root with PYTHONPATH
119+
cd backend
120+
PYTHONPATH=. devui ./agents --port 8080
121+
```
122+
123+
Without `PYTHONPATH`, Python cannot find modules in sibling directories and DevUI will report an import error.
124+
105125
## Viewing Telemetry (Otel Traces) in DevUI
106126

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

109129
```bash
110-
devui ./agents --tracing framework
130+
devui ./agents --instrumentation
111131
```
112132

113133
## OpenAI-Compatible API
@@ -196,11 +216,12 @@ Options:
196216
--port, -p Port (default: 8080)
197217
--host Host (default: 127.0.0.1)
198218
--headless API only, no UI
199-
--config YAML config file
200-
--tracing none|framework|workflow|all
219+
--no-open Don't automatically open browser
220+
--instrumentation Enable OpenTelemetry instrumentation
201221
--reload Enable auto-reload
202222
--mode developer|user (default: developer)
203223
--auth Enable Bearer token authentication
224+
--auth-token Custom authentication token
204225
```
205226
206227
### UI Modes

python/packages/devui/agent_framework_devui/__init__.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def serve(
9494
auto_open: bool = False,
9595
cors_origins: list[str] | None = None,
9696
ui_enabled: bool = True,
97-
tracing_enabled: bool = False,
97+
instrumentation_enabled: bool = False,
9898
mode: str = "developer",
9999
auth_enabled: bool = False,
100100
auth_token: str | None = None,
@@ -109,7 +109,7 @@ def serve(
109109
auto_open: Whether to automatically open browser
110110
cors_origins: List of allowed CORS origins
111111
ui_enabled: Whether to enable the UI
112-
tracing_enabled: Whether to enable OpenTelemetry tracing
112+
instrumentation_enabled: Whether to enable OpenTelemetry instrumentation
113113
mode: Server mode - 'developer' (full access, verbose errors) or 'user' (restricted APIs, generic errors)
114114
auth_enabled: Whether to enable Bearer token authentication
115115
auth_token: Custom authentication token (auto-generated if not provided with auth_enabled=True)
@@ -172,22 +172,12 @@ def serve(
172172
os.environ["AUTH_REQUIRED"] = "true"
173173
os.environ["DEVUI_AUTH_TOKEN"] = auth_token
174174

175-
# Configure tracing environment variables if enabled
176-
if tracing_enabled:
177-
import os
178-
179-
# Only set if not already configured by user
180-
if not os.environ.get("ENABLE_INSTRUMENTATION"):
181-
os.environ["ENABLE_INSTRUMENTATION"] = "true"
182-
logger.info("Set ENABLE_INSTRUMENTATION=true for tracing")
183-
184-
if not os.environ.get("ENABLE_SENSITIVE_DATA"):
185-
os.environ["ENABLE_SENSITIVE_DATA"] = "true"
186-
logger.info("Set ENABLE_SENSITIVE_DATA=true for tracing")
175+
# Enable instrumentation if requested
176+
if instrumentation_enabled:
177+
from agent_framework.observability import enable_instrumentation
187178

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

192182
# Create server with direct parameters
193183
server = DevServer(

python/packages/devui/agent_framework_devui/_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def create_cli_parser() -> argparse.ArgumentParser:
2828
devui ./agents # Scan specific directory
2929
devui --port 8000 # Custom port
3030
devui --headless # API only, no UI
31-
devui --tracing # Enable OpenTelemetry tracing
31+
devui --instrumentation # Enable OpenTelemetry instrumentation
3232
""",
3333
)
3434

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

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

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

5858
parser.add_argument(
5959
"--mode",
@@ -182,7 +182,7 @@ def main() -> None:
182182
host=args.host,
183183
auto_open=not args.no_open,
184184
ui_enabled=ui_enabled,
185-
tracing_enabled=args.tracing,
185+
instrumentation_enabled=args.instrumentation,
186186
mode=mode,
187187
auth_enabled=args.auth,
188188
auth_token=args.auth_token, # Pass through explicit token only

python/packages/devui/agent_framework_devui/_conversations.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,31 @@ async def list_conversations_by_metadata(self, metadata_filter: dict[str, str])
176176
"""
177177
pass
178178

179+
@abstractmethod
180+
def add_trace(self, conversation_id: str, trace_event: dict[str, Any]) -> None:
181+
"""Add a trace event to the conversation for context inspection.
182+
183+
Traces capture execution metadata like token usage, timing, and LLM context
184+
that isn't stored in the AgentThread but is useful for debugging.
185+
186+
Args:
187+
conversation_id: Conversation ID
188+
trace_event: Trace event data (from ResponseTraceEvent.data)
189+
"""
190+
pass
191+
192+
@abstractmethod
193+
def get_traces(self, conversation_id: str) -> list[dict[str, Any]]:
194+
"""Get all trace events for a conversation.
195+
196+
Args:
197+
conversation_id: Conversation ID
198+
199+
Returns:
200+
List of trace event dicts, or empty list if not found
201+
"""
202+
pass
203+
179204

180205
class InMemoryConversationStore(ConversationStore):
181206
"""In-memory conversation storage wrapping AgentThread.
@@ -215,6 +240,7 @@ def create_conversation(
215240
"metadata": metadata or {},
216241
"created_at": created_at,
217242
"items": [],
243+
"traces": [], # Trace events for context inspection (token usage, timing, etc.)
218244
}
219245

220246
# Initialize item index for this conversation
@@ -407,10 +433,20 @@ async def list_items(
407433
elif content_type == "function_result":
408434
# Function result - create separate ConversationItem
409435
call_id = getattr(content, "call_id", None)
410-
# Output is stored in additional_properties
411-
output = ""
412-
if hasattr(content, "additional_properties"):
413-
output = content.additional_properties.get("output", "")
436+
# Output is stored in the 'result' field of FunctionResultContent
437+
result_value = getattr(content, "result", None)
438+
# Convert result to string (it could be dict, list, or other types)
439+
if result_value is None:
440+
output = ""
441+
elif isinstance(result_value, str):
442+
output = result_value
443+
else:
444+
import json
445+
446+
try:
447+
output = json.dumps(result_value)
448+
except (TypeError, ValueError):
449+
output = str(result_value)
414450

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

595+
def add_trace(self, conversation_id: str, trace_event: dict[str, Any]) -> None:
596+
"""Add a trace event to the conversation for context inspection.
597+
598+
Traces capture execution metadata like token usage, timing, and LLM context
599+
that isn't stored in the AgentThread but is useful for debugging.
600+
601+
Args:
602+
conversation_id: Conversation ID
603+
trace_event: Trace event data (from ResponseTraceEvent.data)
604+
"""
605+
conv_data = self._conversations.get(conversation_id)
606+
if conv_data:
607+
traces = conv_data.get("traces", [])
608+
traces.append(trace_event)
609+
conv_data["traces"] = traces
610+
611+
def get_traces(self, conversation_id: str) -> list[dict[str, Any]]:
612+
"""Get all trace events for a conversation.
613+
614+
Args:
615+
conversation_id: Conversation ID
616+
617+
Returns:
618+
List of trace event dicts, or empty list if not found
619+
"""
620+
conv_data = self._conversations.get(conversation_id)
621+
return conv_data.get("traces", []) if conv_data else []
622+
559623
async def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]:
560624
"""Filter conversations by metadata (e.g., agent_id)."""
561625
results = []

python/packages/devui/agent_framework_devui/_discovery.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,16 @@ def _load_module_from_pattern(self, pattern: str) -> tuple[Any | None, Exception
666666
logger.debug(f"Successfully imported {pattern}")
667667
return module, None
668668

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

0 commit comments

Comments
 (0)