You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-03-30 10:57:20 -07:00
fix: Update empty event check to include executable code and execution results
Close #3859 Close #3921 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 852375447
This commit is contained in:
committed by
Copybara-Service
parent
6f259f08b3
commit
688f48fffb
@@ -220,13 +220,15 @@ def _rearrange_events_for_latest_function_response(
|
||||
|
||||
|
||||
def _is_part_invisible(p: types.Part) -> bool:
|
||||
"""A part is considered invisble if it's a thought, or has no visible content."""
|
||||
"""Returns whether a part is invisible for LLM context."""
|
||||
return getattr(p, 'thought', False) or not (
|
||||
p.text
|
||||
or p.inline_data
|
||||
or p.file_data
|
||||
or p.function_call
|
||||
or p.function_response
|
||||
or p.executable_code
|
||||
or p.code_execution_result
|
||||
)
|
||||
|
||||
|
||||
@@ -236,9 +238,8 @@ def _contains_empty_content(event: Event) -> bool:
|
||||
This can happen to the events that only changed session state.
|
||||
When both content and transcriptions are empty, the event will be considered
|
||||
as empty. The content is considered empty if none of its parts contain text,
|
||||
inline data, file data, function call, or function response. Parts with
|
||||
only thoughts are also considered empty.
|
||||
|
||||
inline data, file data, function call, function response, executable code, or
|
||||
code execution result. Parts with only thoughts are also considered empty.
|
||||
|
||||
Args:
|
||||
event: The event to check.
|
||||
@@ -520,7 +521,7 @@ def _present_other_agent_message(event: Event) -> Optional[Event]:
|
||||
if part.thought:
|
||||
# Exclude thoughts from the context.
|
||||
continue
|
||||
elif part.text:
|
||||
elif part.text is not None and part.text.strip():
|
||||
content.parts.append(
|
||||
types.Part(text=f'[{event.author}] said: {part.text}')
|
||||
)
|
||||
@@ -543,11 +544,17 @@ def _present_other_agent_message(event: Event) -> Optional[Event]:
|
||||
)
|
||||
)
|
||||
)
|
||||
# Fallback to the original part for non-text and non-functionCall parts.
|
||||
else:
|
||||
elif (
|
||||
part.inline_data
|
||||
or part.file_data
|
||||
or part.executable_code
|
||||
or part.code_execution_result
|
||||
):
|
||||
content.parts.append(part)
|
||||
else:
|
||||
continue
|
||||
|
||||
# If no meaningful parts were added (only "For context:" remains), return None
|
||||
# Return None when only "For context:" remains.
|
||||
if len(content.parts) == 1:
|
||||
return None
|
||||
|
||||
|
||||
@@ -572,6 +572,38 @@ async def test_events_with_empty_content_are_skipped():
|
||||
role="user",
|
||||
),
|
||||
),
|
||||
# Event with content that has executable code part
|
||||
Event(
|
||||
invocation_id="inv10",
|
||||
author="test_agent",
|
||||
content=types.Content(
|
||||
parts=[
|
||||
types.Part(
|
||||
executable_code=types.ExecutableCode(
|
||||
code="print('hello')",
|
||||
language="PYTHON",
|
||||
)
|
||||
)
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
),
|
||||
# Event with content that has code execution result part
|
||||
Event(
|
||||
invocation_id="inv11",
|
||||
author="test_agent",
|
||||
content=types.Content(
|
||||
parts=[
|
||||
types.Part(
|
||||
code_execution_result=types.CodeExecutionResult(
|
||||
outcome="OUTCOME_OK",
|
||||
output="hello",
|
||||
)
|
||||
)
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
@@ -608,4 +640,153 @@ async def test_events_with_empty_content_are_skipped():
|
||||
parts=[types.Part(text=""), types.Part(text="Mixed content")],
|
||||
role="user",
|
||||
),
|
||||
types.Content(
|
||||
parts=[
|
||||
types.Part(
|
||||
executable_code=types.ExecutableCode(
|
||||
code="print('hello')",
|
||||
language="PYTHON",
|
||||
)
|
||||
)
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
types.Content(
|
||||
parts=[
|
||||
types.Part(
|
||||
code_execution_result=types.CodeExecutionResult(
|
||||
outcome="OUTCOME_OK",
|
||||
output="hello",
|
||||
)
|
||||
)
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_code_execution_result_events_are_not_skipped():
|
||||
"""Test that events with code execution result are not skipped.
|
||||
|
||||
This is a regression test for the endless loop bug where code executor
|
||||
outputs were not passed to the LLM because the events were incorrectly
|
||||
filtered as empty.
|
||||
"""
|
||||
agent = Agent(model="gemini-2.5-flash", name="test_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("Write code to calculate factorial"),
|
||||
),
|
||||
# Model generates code
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="test_agent",
|
||||
content=types.Content(
|
||||
parts=[
|
||||
types.Part(text="Here's the code:"),
|
||||
types.Part(
|
||||
executable_code=types.ExecutableCode(
|
||||
code=(
|
||||
"def factorial(n):\n return 1 if n <= 1 else n *"
|
||||
" factorial(n-1)\nprint(factorial(5))"
|
||||
),
|
||||
language="PYTHON",
|
||||
)
|
||||
),
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
),
|
||||
# Code execution result
|
||||
Event(
|
||||
invocation_id="inv3",
|
||||
author="test_agent",
|
||||
content=types.Content(
|
||||
parts=[
|
||||
types.Part(
|
||||
code_execution_result=types.CodeExecutionResult(
|
||||
outcome="OUTCOME_OK",
|
||||
output="120",
|
||||
)
|
||||
)
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
# Process the request
|
||||
async for _ in contents.request_processor.run_async(
|
||||
invocation_context, llm_request
|
||||
):
|
||||
pass
|
||||
|
||||
# Verify all three events are included, especially the code execution result
|
||||
assert len(llm_request.contents) == 3
|
||||
assert llm_request.contents[0] == types.UserContent(
|
||||
"Write code to calculate factorial"
|
||||
)
|
||||
# Second event has executable code
|
||||
assert llm_request.contents[1].parts[1].executable_code is not None
|
||||
# Third event has code execution result - this was the bug!
|
||||
assert llm_request.contents[2].parts[0].code_execution_result is not None
|
||||
assert llm_request.contents[2].parts[0].code_execution_result.output == "120"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_code_execution_result_not_in_first_part_is_not_skipped():
|
||||
"""Test that code execution results aren't skipped.
|
||||
|
||||
This covers results that appear in a non-first part.
|
||||
"""
|
||||
agent = Agent(model="gemini-2.5-flash", name="test_agent")
|
||||
llm_request = LlmRequest(model="gemini-2.5-flash")
|
||||
invocation_context = await testing_utils.create_invocation_context(
|
||||
agent=agent
|
||||
)
|
||||
|
||||
events = [
|
||||
Event(
|
||||
invocation_id="inv1",
|
||||
author="user",
|
||||
content=types.UserContent("Run some code."),
|
||||
),
|
||||
Event(
|
||||
invocation_id="inv2",
|
||||
author="test_agent",
|
||||
content=types.Content(
|
||||
parts=[
|
||||
types.Part(text=""),
|
||||
types.Part(
|
||||
code_execution_result=types.CodeExecutionResult(
|
||||
outcome="OUTCOME_OK",
|
||||
output="42",
|
||||
)
|
||||
),
|
||||
],
|
||||
role="model",
|
||||
),
|
||||
),
|
||||
]
|
||||
invocation_context.session.events = events
|
||||
|
||||
async for _ in contents.request_processor.run_async(
|
||||
invocation_context, llm_request
|
||||
):
|
||||
pass
|
||||
|
||||
assert len(llm_request.contents) == 2
|
||||
assert any(
|
||||
part.code_execution_result is not None
|
||||
and part.code_execution_result.output == "42"
|
||||
for part in llm_request.contents[1].parts
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user