Skip to content

Commit da24bb8

Browse files
authored
Python: DevUI Fix Serialization, Timestamp and Other Issues (microsoft#1584)
* refactor(devui): adopt standard OpenAI lifecycle events for agents and workflows - Replace custom workflow events with OpenAI Responses API standard lifecycle events - Add AgentStartedEvent, AgentCompletedEvent, AgentFailedEvent for clean separation - Implement ExecutorActionItem for workflow executor tracking - Convert informational events to trace events to reduce noise - Update README mapper table with comprehensive event mappings - Maintain full backward compatibility with legacy events * fix(devui): resolve timestamp overwriting and Content serialization errors - Fix tool call timestamps being overwritten on each render (microsoft#1483) - Add recursive Content serialization to handle ChatMessage and nested objects (microsoft#1548) - Implement proper MCP tool cleanup on server shutdown - Add timestamp field to function_result.complete events - Enhance credential and client resource cleanup Fixes microsoft#1483, microsoft#1548 Partial improvements for microsoft#1476
1 parent 2294373 commit da24bb8

21 files changed

Lines changed: 1860 additions & 683 deletions

File tree

python/packages/devui/README.md

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ devui ./agents --port 8080
4949

5050
When DevUI starts with no discovered entities, it displays a **sample entity gallery** with curated examples from the Agent Framework repository. You can download these samples, review them, and run them locally to get started quickly.
5151

52+
## Using MCP Tools
53+
54+
**Important:** Don't use `async with` context managers when creating agents with MCP tools for DevUI - connections will close before execution.
55+
56+
```python
57+
# ✅ Correct - DevUI handles cleanup automatically
58+
mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", chat_client=chat_client)
59+
agent = ChatAgent(tools=mcp_tool)
60+
serve(entities=[agent])
61+
```
62+
63+
MCP tools use lazy initialization and connect automatically on first use. DevUI attempts to clean up connections on shutdown
64+
5265
## Directory Structure
5366

5467
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`).
@@ -157,42 +170,62 @@ Options:
157170
158171
Given that DevUI offers an OpenAI Responses API, it internally maps messages and events from Agent Framework to OpenAI Responses API events (in `_mapper.py`). For transparency, this mapping is shown below:
159172
160-
| Agent Framework Content | OpenAI Event/Type | Status |
161-
| ------------------------------- | ---------------------------------------- | -------- |
162-
| `TextContent` | `response.output_text.delta` | Standard |
163-
| `TextReasoningContent` | `response.reasoning_text.delta` | Standard |
164-
| `FunctionCallContent` (initial) | `response.output_item.added` | Standard |
165-
| `FunctionCallContent` (args) | `response.function_call_arguments.delta` | Standard |
166-
| `FunctionResultContent` | `response.function_result.complete` | DevUI |
167-
| `FunctionApprovalRequestContent`| `response.function_approval.requested` | DevUI |
168-
| `FunctionApprovalResponseContent`| `response.function_approval.responded` | DevUI |
169-
| `ErrorContent` | `error` | Standard |
170-
| `UsageContent` | Final `Response.usage` field (not streamed) | Standard |
171-
| `WorkflowEvent` | `response.workflow_event.complete` | DevUI |
172-
| `DataContent` | `response.trace.complete` | DevUI |
173-
| `UriContent` | `response.trace.complete` | DevUI |
174-
| `HostedFileContent` | `response.trace.complete` | DevUI |
175-
| `HostedVectorStoreContent` | `response.trace.complete` | DevUI |
176-
177-
- **Standard** = OpenAI Responses API spec
178-
- **DevUI** = Custom extensions for Agent Framework features (workflows, traces, function approvals)
173+
| OpenAI Event/Type | Agent Framework Content | Status |
174+
| ------------------------------------------------------------ | --------------------------------- | -------- |
175+
| | **Lifecycle Events** | |
176+
| `response.created` + `response.in_progress` | `AgentStartedEvent` | OpenAI |
177+
| `response.completed` | `AgentCompletedEvent` | OpenAI |
178+
| `response.failed` | `AgentFailedEvent` | OpenAI |
179+
| `response.created` + `response.in_progress` | `WorkflowStartedEvent` | OpenAI |
180+
| `response.completed` | `WorkflowCompletedEvent` | OpenAI |
181+
| `response.failed` | `WorkflowFailedEvent` | OpenAI |
182+
| | **Content Types** | |
183+
| `response.content_part.added` + `response.output_text.delta` | `TextContent` | OpenAI |
184+
| `response.reasoning_text.delta` | `TextReasoningContent` | OpenAI |
185+
| `response.output_item.added` | `FunctionCallContent` (initial) | OpenAI |
186+
| `response.function_call_arguments.delta` | `FunctionCallContent` (args) | OpenAI |
187+
| `response.function_result.complete` | `FunctionResultContent` | DevUI |
188+
| `response.function_approval.requested` | `FunctionApprovalRequestContent` | DevUI |
189+
| `response.function_approval.responded` | `FunctionApprovalResponseContent` | DevUI |
190+
| `error` | `ErrorContent` | OpenAI |
191+
| Final `Response.usage` field (not streamed) | `UsageContent` | OpenAI |
192+
| | **Workflow Events** | |
193+
| `response.output_item.added` (ExecutorActionItem)* | `ExecutorInvokedEvent` | OpenAI |
194+
| `response.output_item.done` (ExecutorActionItem)* | `ExecutorCompletedEvent` | OpenAI |
195+
| `response.output_item.done` (ExecutorActionItem with error)* | `ExecutorFailedEvent` | OpenAI |
196+
| `response.workflow_event.complete` | `WorkflowEvent` (other) | DevUI |
197+
| `response.trace.complete` | `WorkflowStatusEvent` | DevUI |
198+
| `response.trace.complete` | `WorkflowWarningEvent` | DevUI |
199+
| | **Trace Content** | |
200+
| `response.trace.complete` | `DataContent` | DevUI |
201+
| `response.trace.complete` | `UriContent` | DevUI |
202+
| `response.trace.complete` | `HostedFileContent` | DevUI |
203+
| `response.trace.complete` | `HostedVectorStoreContent` | DevUI |
204+
205+
\*Uses standard OpenAI event structure but carries DevUI-specific `ExecutorActionItem` payload
206+
207+
- **OpenAI** = Standard OpenAI Responses API event types
208+
- **DevUI** = Custom event types specific to Agent Framework (e.g., workflows, traces, function approvals)
179209
180210
### OpenAI Responses API Compliance
181211
182212
DevUI follows the OpenAI Responses API specification for maximum compatibility:
183213
184-
**Standard OpenAI Types Used:**
214+
**OpenAI Standard Event Types Used:**
215+
185216
- `ResponseOutputItemAddedEvent` - Output item notifications (function calls and results)
217+
- `ResponseOutputItemDoneEvent` - Output item completion notifications
186218
- `Response.usage` - Token usage (in final response, not streamed)
187219
- All standard text, reasoning, and function call events
188220
189221
**Custom DevUI Extensions:**
222+
190223
- `response.function_approval.requested` - Function approval requests (for interactive approval workflows)
191224
- `response.function_approval.responded` - Function approval responses (user approval/rejection)
192225
- `response.workflow_event.complete` - Agent Framework workflow events
193226
- `response.trace.complete` - Execution traces and internal content (DataContent, UriContent, hosted files/stores)
194227
195-
These custom extensions are clearly namespaced and can be safely ignored by standard OpenAI clients.
228+
These custom extensions are clearly namespaced and can be safely ignored by standard OpenAI clients. Note that DevUI also uses standard OpenAI events with custom payloads (e.g., `ExecutorActionItem` within `response.output_item.added`).
196229
197230
### Entity Management
198231
@@ -224,12 +257,14 @@ These custom extensions are clearly namespaced and can be safely ignored by stan
224257
DevUI is designed as a **sample application for local development** and should not be exposed to untrusted networks or used in production environments.
225258
226259
**Security features:**
260+
227261
- Only loads entities from local directories or in-memory registration
228262
- No remote code execution capabilities
229263
- Binds to localhost (127.0.0.1) by default
230264
- All samples must be manually downloaded and reviewed before running
231265
232266
**Best practices:**
267+
233268
- Never expose DevUI to the internet
234269
- Review all agent/workflow code before running
235270
- Only load entities from trusted sources

python/packages/devui/agent_framework_devui/_discovery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def load_entity(self, entity_id: str) -> Any:
127127

128128
# Cache the loaded object
129129
self._loaded_objects[entity_id] = entity_obj
130-
logger.info(f"Successfully loaded entity: {entity_id} (type: {enriched_info.type})")
130+
logger.info(f"Successfully loaded entity: {entity_id} (type: {enriched_info.type})")
131131

132132
return entity_obj
133133

@@ -217,7 +217,7 @@ def invalidate_entity(self, entity_id: str) -> None:
217217
if entity_info and "lazy_loaded" in entity_info.metadata:
218218
entity_info.metadata["lazy_loaded"] = False
219219

220-
logger.info(f"♻️ Entity invalidated: {entity_id} (will reload on next access)")
220+
logger.info(f"Entity invalidated: {entity_id} (will reload on next access)")
221221

222222
def invalidate_all(self) -> None:
223223
"""Invalidate all cached entities.

python/packages/devui/agent_framework_devui/_executor.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ async def _execute_agent(
217217
Agent update events and trace events
218218
"""
219219
try:
220+
# Emit agent lifecycle start event
221+
from .models._openai_custom import AgentStartedEvent
222+
223+
yield AgentStartedEvent()
224+
220225
# Convert input to proper ChatMessage or string
221226
user_message = self._convert_input_to_chat_message(request.input)
222227

@@ -266,8 +271,19 @@ async def _execute_agent(
266271
else:
267272
raise ValueError("Agent must implement either run() or run_stream() method")
268273

274+
# Emit agent lifecycle completion event
275+
from .models._openai_custom import AgentCompletedEvent
276+
277+
yield AgentCompletedEvent()
278+
269279
except Exception as e:
270280
logger.error(f"Error in agent execution: {e}")
281+
# Emit agent lifecycle failure event
282+
from .models._openai_custom import AgentFailedEvent
283+
284+
yield AgentFailedEvent(error=e)
285+
286+
# Still yield the error for backward compatibility
271287
yield {"type": "error", "message": f"Agent execution error: {e!s}"}
272288

273289
async def _execute_workflow(
@@ -284,14 +300,9 @@ async def _execute_workflow(
284300
Workflow events and trace events
285301
"""
286302
try:
287-
# Get input data - prefer structured data from extra_body
288-
input_data: str | list[Any] | dict[str, Any]
289-
if request.extra_body and isinstance(request.extra_body, dict) and request.extra_body.get("input_data"):
290-
input_data = request.extra_body.get("input_data") # type: ignore
291-
logger.debug(f"Using structured input_data from extra_body: {type(input_data)}")
292-
else:
293-
input_data = request.input
294-
logger.debug(f"Using input field as fallback: {type(input_data)}")
303+
# Get input data directly from request.input field
304+
input_data = request.input
305+
logger.debug(f"Using input field: {type(input_data)}")
295306

296307
# Parse input based on workflow's expected input type
297308
parsed_input = await self._parse_workflow_input(workflow, input_data)

0 commit comments

Comments
 (0)