mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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
This commit is contained in:
committed by
GitHub
Unverified
parent
db283cd396
commit
2e1189ca65
@@ -8,8 +8,10 @@ import pytest
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
InMemoryCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -426,14 +428,11 @@ class TestIntegration:
|
||||
# Run workflow until it reaches IDLE_WITH_PENDING_REQUESTS (after checkpoint is created)
|
||||
saw_request_event = False
|
||||
async for event in test_workflow.run_stream(WorkflowTestData(value="test")):
|
||||
if hasattr(event, "__class__"):
|
||||
if event.__class__.__name__ == "RequestInfoEvent":
|
||||
saw_request_event = True
|
||||
# Wait for IDLE_WITH_PENDING_REQUESTS status (comes after checkpoint creation)
|
||||
is_status_event = event.__class__.__name__ == "WorkflowStatusEvent"
|
||||
has_pending_status = hasattr(event, "status") and "IDLE_WITH_PENDING_REQUESTS" in str(event.status)
|
||||
if is_status_event and has_pending_status:
|
||||
break
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
saw_request_event = True
|
||||
# Wait for IDLE_WITH_PENDING_REQUESTS status (comes after checkpoint creation)
|
||||
if isinstance(event, WorkflowStatusEvent) and "IDLE_WITH_PENDING_REQUESTS" in str(event.state):
|
||||
break
|
||||
|
||||
assert saw_request_event, "Test workflow should have emitted RequestInfoEvent"
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ async def test_list_items_converts_function_calls():
|
||||
{
|
||||
"type": "function_result",
|
||||
"call_id": "call_test123",
|
||||
"output": '{"temperature": 65, "condition": "sunny"}',
|
||||
"result": '{"temperature": 65, "condition": "sunny"}',
|
||||
}
|
||||
],
|
||||
),
|
||||
|
||||
@@ -441,73 +441,119 @@ async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MagenticAgentDeltaEvent Tests
|
||||
# Magentic Event Tests - Testing REAL AgentRunUpdateEvent with additional_properties
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_magentic_agent_delta_creates_message_container(
|
||||
async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test that MagenticAgentDeltaEvent creates message containers when no executor context."""
|
||||
from dataclasses import dataclass
|
||||
"""Test that AgentRunUpdateEvent with magentic_event_type='agent_delta' is handled correctly.
|
||||
|
||||
from agent_framework import WorkflowEvent
|
||||
This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class.
|
||||
Magentic uses AgentRunUpdateEvent with additional_properties containing magentic_event_type.
|
||||
"""
|
||||
from agent_framework._types import AgentRunResponseUpdate, Role, TextContent
|
||||
from agent_framework._workflows._events import AgentRunUpdateEvent
|
||||
|
||||
@dataclass
|
||||
class MagenticAgentDeltaEvent(WorkflowEvent):
|
||||
agent_id: str
|
||||
text: str | None = None
|
||||
# Create the REAL event format that Magentic emits
|
||||
update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Hello from agent")],
|
||||
role=Role.ASSISTANT,
|
||||
author_name="Writer",
|
||||
additional_properties={
|
||||
"magentic_event_type": "agent_delta",
|
||||
"agent_id": "writer_agent",
|
||||
},
|
||||
)
|
||||
event = AgentRunUpdateEvent(executor_id="magentic_executor", data=update)
|
||||
|
||||
# First delta should create message container
|
||||
first_delta = MagenticAgentDeltaEvent(agent_id="test_agent", text="Hello ")
|
||||
events = await mapper.convert_event(first_delta, test_request)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Should emit 3 events: message container, content part, and text delta
|
||||
assert len(events) == 3
|
||||
assert events[0].type == "response.output_item.added"
|
||||
assert events[0].item.type == "message"
|
||||
assert events[0].item.metadata["agent_id"] == "test_agent"
|
||||
message_id = events[0].item.id
|
||||
|
||||
# Second delta should NOT create new container
|
||||
second_delta = MagenticAgentDeltaEvent(agent_id="test_agent", text="world!")
|
||||
events = await mapper.convert_event(second_delta, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.output_text.delta"
|
||||
assert events[0].item_id == message_id
|
||||
# Should be treated as a regular AgentRunUpdateEvent with text content
|
||||
# The mapper should emit text delta events
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
assert len(text_events) >= 1
|
||||
assert text_events[0].delta == "Hello from agent"
|
||||
|
||||
|
||||
async def test_magentic_agent_delta_routes_to_executor_item(
|
||||
async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test that AgentRunUpdateEvent with magentic_event_type='orchestrator_message' is handled.
|
||||
|
||||
Magentic emits orchestrator planning/instruction messages using AgentRunUpdateEvent
|
||||
with additional_properties containing magentic_event_type='orchestrator_message'.
|
||||
"""
|
||||
from agent_framework._types import AgentRunResponseUpdate, Role, TextContent
|
||||
from agent_framework._workflows._events import AgentRunUpdateEvent
|
||||
|
||||
# Create orchestrator message event (REAL format from Magentic)
|
||||
update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Planning: First, the writer will create content...")],
|
||||
role=Role.ASSISTANT,
|
||||
author_name="Orchestrator",
|
||||
additional_properties={
|
||||
"magentic_event_type": "orchestrator_message",
|
||||
"orchestrator_message_kind": "task_ledger",
|
||||
"orchestrator_id": "magentic_orchestrator",
|
||||
},
|
||||
)
|
||||
event = AgentRunUpdateEvent(executor_id="magentic_orchestrator", data=update)
|
||||
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Currently, mapper treats this as regular AgentRunUpdateEvent (no special handling)
|
||||
# This test documents the current behavior
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
assert len(text_events) >= 1
|
||||
assert "Planning:" in text_events[0].delta
|
||||
|
||||
|
||||
async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test that MagenticAgentDeltaEvent routes to executor item when executor context is present."""
|
||||
from dataclasses import dataclass
|
||||
"""Verify Magentic uses the same AgentRunUpdateEvent class as other workflows.
|
||||
|
||||
from agent_framework import WorkflowEvent
|
||||
This test documents that Magentic does NOT define separate event classes like
|
||||
MagenticAgentDeltaEvent - it reuses AgentRunUpdateEvent with metadata in
|
||||
additional_properties. Any mapper code checking for 'MagenticAgentDeltaEvent'
|
||||
class names is dead code.
|
||||
"""
|
||||
from agent_framework._types import AgentRunResponseUpdate, Role, TextContent
|
||||
from agent_framework._workflows._events import AgentRunUpdateEvent
|
||||
|
||||
@dataclass
|
||||
class MagenticAgentDeltaEvent(WorkflowEvent):
|
||||
agent_id: str
|
||||
text: str | None = None
|
||||
# Create events the way different workflows do it
|
||||
# 1. Regular workflow (no additional_properties)
|
||||
regular_update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Regular workflow response")],
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
regular_event = AgentRunUpdateEvent(executor_id="regular_executor", data=regular_update)
|
||||
|
||||
# First, invoke an executor (sets current_executor_id in context)
|
||||
executor_event = create_executor_invoked_event(executor_id="agent_writer")
|
||||
executor_events = await mapper.convert_event(executor_event, test_request)
|
||||
# 2. Magentic workflow (with additional_properties)
|
||||
magentic_update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Magentic workflow response")],
|
||||
role=Role.ASSISTANT,
|
||||
additional_properties={"magentic_event_type": "agent_delta"},
|
||||
)
|
||||
magentic_event = AgentRunUpdateEvent(executor_id="magentic_executor", data=magentic_update)
|
||||
|
||||
assert len(executor_events) == 1
|
||||
assert executor_events[0].type == "response.output_item.added"
|
||||
executor_item_id = executor_events[0].item.id
|
||||
# Both should be the SAME class
|
||||
assert type(regular_event) is type(magentic_event)
|
||||
assert isinstance(regular_event, AgentRunUpdateEvent)
|
||||
assert isinstance(magentic_event, AgentRunUpdateEvent)
|
||||
|
||||
# Now send Magentic delta - should route to executor's item
|
||||
delta = MagenticAgentDeltaEvent(agent_id="writer", text="Hello world")
|
||||
delta_events = await mapper.convert_event(delta, test_request)
|
||||
# Both should be handled by the same isinstance check in mapper
|
||||
regular_events = await mapper.convert_event(regular_event, test_request)
|
||||
magentic_events = await mapper.convert_event(magentic_event, test_request)
|
||||
|
||||
# Should only emit 1 event: text delta routed to executor's item
|
||||
assert len(delta_events) == 1
|
||||
assert delta_events[0].type == "response.output_text.delta"
|
||||
assert delta_events[0].item_id == executor_item_id
|
||||
assert delta_events[0].delta == "Hello world"
|
||||
# Both produce text delta events
|
||||
regular_text = [e for e in regular_events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
magentic_text = [e for e in magentic_events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
|
||||
assert len(regular_text) >= 1
|
||||
assert len(magentic_text) >= 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user