mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
chore: Always skip executing partial function calls
Related: https://github.com/google/adk-python/issues/4159 Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 859184844
This commit is contained in:
committed by
Copybara-Service
parent
7b25b8fb1d
commit
d62f9c896c
@@ -38,8 +38,6 @@ from ...agents.readonly_context import ReadonlyContext
|
||||
from ...agents.run_config import StreamingMode
|
||||
from ...agents.transcription_entry import TranscriptionEntry
|
||||
from ...events.event import Event
|
||||
from ...features import FeatureName
|
||||
from ...features import is_feature_enabled
|
||||
from ...models.base_llm_connection import BaseLlmConnection
|
||||
from ...models.llm_request import LlmRequest
|
||||
from ...models.llm_response import LlmResponse
|
||||
@@ -551,14 +549,11 @@ class BaseLlmFlow(ABC):
|
||||
# Handles function calls.
|
||||
if model_response_event.get_function_calls():
|
||||
|
||||
if is_feature_enabled(FeatureName.PROGRESSIVE_SSE_STREAMING):
|
||||
# In progressive SSE streaming mode stage 1, we skip partial FC events
|
||||
# Only execute FCs in the final aggregated event (partial=False)
|
||||
if (
|
||||
invocation_context.run_config.streaming_mode == StreamingMode.SSE
|
||||
and model_response_event.partial
|
||||
):
|
||||
return
|
||||
# Skip partial function call events - they should not trigger execution
|
||||
# since partial events are not saved to session (see runners.py).
|
||||
# Only execute function calls in the non-partial events.
|
||||
if model_response_event.partial:
|
||||
return
|
||||
|
||||
async with Aclosing(
|
||||
self._postprocess_handle_function_calls_async(
|
||||
|
||||
@@ -631,3 +631,144 @@ def test_progressive_sse_handles_empty_function_call():
|
||||
args = fc_part.function_call.args
|
||||
assert args["num"] == 100
|
||||
assert args["s"] == "ADK"
|
||||
|
||||
|
||||
class PartialFunctionCallMockModel(BaseLlm):
|
||||
"""A mock model that yields partial function call events followed by final."""
|
||||
|
||||
model: str = "partial-fc-mock"
|
||||
tool_call_count: int = 0
|
||||
|
||||
@classmethod
|
||||
def supported_models(cls) -> list[str]:
|
||||
return ["partial-fc-mock"]
|
||||
|
||||
async def generate_content_async(
|
||||
self, llm_request: LlmRequest, stream: bool = False
|
||||
) -> AsyncGenerator[LlmResponse, None]:
|
||||
"""Yield partial FC events then final, simulating streaming behavior."""
|
||||
|
||||
# Check if this is a follow-up call (after function response)
|
||||
has_function_response = False
|
||||
for content in llm_request.contents:
|
||||
for part in content.parts or []:
|
||||
if part.function_response:
|
||||
has_function_response = True
|
||||
break
|
||||
|
||||
if has_function_response:
|
||||
# Final response after function execution
|
||||
yield LlmResponse(
|
||||
content=types.Content(
|
||||
role="model",
|
||||
parts=[types.Part.from_text(text="Function executed once.")],
|
||||
),
|
||||
partial=False,
|
||||
)
|
||||
return
|
||||
|
||||
# First call: yield partial FC events then final
|
||||
# Partial event 1
|
||||
yield LlmResponse(
|
||||
content=types.Content(
|
||||
role="model",
|
||||
parts=[
|
||||
types.Part.from_function_call(
|
||||
name="track_execution", args={"call_id": "partial_1"}
|
||||
)
|
||||
],
|
||||
),
|
||||
partial=True,
|
||||
)
|
||||
|
||||
# Partial event 2
|
||||
yield LlmResponse(
|
||||
content=types.Content(
|
||||
role="model",
|
||||
parts=[
|
||||
types.Part.from_function_call(
|
||||
name="track_execution", args={"call_id": "partial_2"}
|
||||
)
|
||||
],
|
||||
),
|
||||
partial=True,
|
||||
)
|
||||
|
||||
# Final aggregated event (only this should trigger execution)
|
||||
yield LlmResponse(
|
||||
content=types.Content(
|
||||
role="model",
|
||||
parts=[
|
||||
types.Part.from_function_call(
|
||||
name="track_execution", args={"call_id": "final"}
|
||||
)
|
||||
],
|
||||
),
|
||||
partial=False,
|
||||
finish_reason=types.FinishReason.STOP,
|
||||
)
|
||||
|
||||
|
||||
def test_partial_function_calls_not_executed_in_none_streaming_mode():
|
||||
"""Test that partial function call events are skipped regardless of mode."""
|
||||
execution_log = []
|
||||
|
||||
def track_execution(call_id: str) -> str:
|
||||
"""A tool that logs each execution to verify call count."""
|
||||
execution_log.append(call_id)
|
||||
return f"Executed: {call_id}"
|
||||
|
||||
mock_model = PartialFunctionCallMockModel()
|
||||
|
||||
agent = Agent(
|
||||
name="partial_fc_test_agent",
|
||||
model=mock_model,
|
||||
tools=[track_execution],
|
||||
)
|
||||
|
||||
# Use StreamingMode.NONE to verify partial FCs are still skipped
|
||||
run_config = RunConfig(streaming_mode=StreamingMode.NONE)
|
||||
|
||||
runner = InMemoryRunner(agent=agent)
|
||||
|
||||
session = runner.session_service.create_session_sync(
|
||||
app_name=runner.app_name, user_id="test_user"
|
||||
)
|
||||
|
||||
events = []
|
||||
for event in runner.run(
|
||||
user_id="test_user",
|
||||
session_id=session.id,
|
||||
new_message=types.Content(
|
||||
role="user",
|
||||
parts=[types.Part.from_text(text="Test partial FC handling")],
|
||||
),
|
||||
run_config=run_config,
|
||||
):
|
||||
events.append(event)
|
||||
|
||||
# Verify the tool was only executed once (from the final event)
|
||||
assert (
|
||||
len(execution_log) == 1
|
||||
), f"Expected 1 execution, got {len(execution_log)}: {execution_log}"
|
||||
assert (
|
||||
execution_log[0] == "final"
|
||||
), f"Expected 'final' execution, got: {execution_log[0]}"
|
||||
|
||||
# Verify partial events were yielded but not executed
|
||||
partial_events = [e for e in events if e.partial]
|
||||
assert (
|
||||
len(partial_events) == 2
|
||||
), f"Expected 2 partial events, got {len(partial_events)}"
|
||||
|
||||
# Verify there's a function response event (from the final FC execution)
|
||||
function_response_events = [
|
||||
e
|
||||
for e in events
|
||||
if e.content
|
||||
and e.content.parts
|
||||
and any(p.function_response for p in e.content.parts)
|
||||
]
|
||||
assert (
|
||||
len(function_response_events) == 1
|
||||
), f"Expected 1 function response event, got {len(function_response_events)}"
|
||||
|
||||
Reference in New Issue
Block a user