mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
334d52f300
commit
1c5e607a1f
@@ -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
|
||||
Reference in New Issue
Block a user