Python: fix tool call content not showing up in workflow events (#1290)

* fix tool call content not showing up in workflow events

* fix lint

* touch up

* add another sample to show function bridge

* missing sample
This commit is contained in:
Eric Zhu
2025-10-08 09:39:08 -07:00
committed by GitHub
Unverified
parent 334d52f300
commit 1c5e607a1f
6 changed files with 542 additions and 29 deletions
@@ -104,9 +104,6 @@ class AgentExecutor(Executor):
self._cache,
thread=self._agent_thread,
):
if not update.text:
# Skip empty updates (no textual or structural content)
continue
updates.append(update)
await ctx.add_event(AgentRunUpdateEvent(self.id, update))
@@ -12,7 +12,7 @@ from typing import Any, Protocol, TypedDict, TypeVar, cast, runtime_checkable
from ._checkpoint import CheckpointStorage, WorkflowCheckpoint
from ._const import DEFAULT_MAX_ITERATIONS
from ._events import AgentRunUpdateEvent, WorkflowEvent
from ._events import WorkflowEvent
from ._shared_state import SharedState
logger = logging.getLogger(__name__)
@@ -487,28 +487,6 @@ class InProcRunnerContext:
Events are enqueued so runners can stream them in real time instead of
waiting for superstep boundaries.
"""
# Filter out empty AgentRunUpdateEvent updates to avoid emitting None/empty chunks
try:
if isinstance(event, AgentRunUpdateEvent):
update = getattr(event, "data", None)
# Skip if no update payload
if not update:
return
# Robust emptiness check: allow either top-level text or any text-bearing content
text_val = getattr(update, "text", None)
contents = getattr(update, "contents", None)
has_text_content = False
if contents:
for c in contents:
if getattr(c, "text", None):
has_text_content = True
break
if not (text_val or has_text_content):
return
except Exception as exc: # pragma: no cover - defensive logging path
# Best-effort filtering only; never block event delivery on filtering errors
logger.debug(f"Error while filtering event {event!r}: {exc}", exc_info=True)
await self._event_queue.put(event)
async def drain_events(self) -> list[WorkflowEvent]:
@@ -0,0 +1,122 @@
# Copyright (c) Microsoft. All rights reserved.
"""Tests for AgentExecutor handling of tool calls and results in streaming mode."""
from collections.abc import AsyncIterable
from typing import Any
from agent_framework import (
AgentExecutor,
AgentRunResponse,
AgentRunResponseUpdate,
AgentRunUpdateEvent,
AgentThread,
BaseAgent,
ChatMessage,
FunctionCallContent,
FunctionResultContent,
Role,
TextContent,
WorkflowBuilder,
)
class _ToolCallingAgent(BaseAgent):
"""Mock agent that simulates tool calls and results in streaming mode."""
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
async def run(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentRunResponse:
"""Non-streaming run - not used in this test."""
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="done")])
async def run_stream(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
"""Simulate streaming with tool calls and results."""
# First update: some text
yield AgentRunResponseUpdate(
contents=[TextContent(text="Let me search for that...")],
role=Role.ASSISTANT,
)
# Second update: tool call (no text!)
yield AgentRunResponseUpdate(
contents=[
FunctionCallContent(
call_id="call_123",
name="search",
arguments={"query": "weather"},
)
],
role=Role.ASSISTANT,
)
# Third update: tool result (no text!)
yield AgentRunResponseUpdate(
contents=[
FunctionResultContent(
call_id="call_123",
result={"temperature": 72, "condition": "sunny"},
)
],
role=Role.TOOL,
)
# Fourth update: final text response
yield AgentRunResponseUpdate(
contents=[TextContent(text="The weather is sunny, 72°F.")],
role=Role.ASSISTANT,
)
async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
"""Test that AgentExecutor emits updates containing FunctionCallContent and FunctionResultContent."""
# Arrange
agent = _ToolCallingAgent(id="tool_agent", name="ToolAgent")
agent_exec = AgentExecutor(agent, id="tool_exec")
workflow = WorkflowBuilder().set_start_executor(agent_exec).build()
# Act: run in streaming mode
events: list[AgentRunUpdateEvent] = []
async for event in workflow.run_stream("What's the weather?"):
if isinstance(event, AgentRunUpdateEvent):
events.append(event)
# Assert: we should receive 4 events (text, function call, function result, text)
assert len(events) == 4, f"Expected 4 events, got {len(events)}"
# First event: text update
assert events[0].data is not None
assert isinstance(events[0].data.contents[0], TextContent)
assert "Let me search" in events[0].data.contents[0].text
# Second event: function call
assert events[1].data is not None
assert isinstance(events[1].data.contents[0], FunctionCallContent)
func_call = events[1].data.contents[0]
assert func_call.call_id == "call_123"
assert func_call.name == "search"
# Third event: function result
assert events[2].data is not None
assert isinstance(events[2].data.contents[0], FunctionResultContent)
func_result = events[2].data.contents[0]
assert func_result.call_id == "call_123"
# Fourth event: final text
assert events[3].data is not None
assert isinstance(events[3].data.contents[0], TextContent)
assert "sunny" in events[3].data.contents[0].text