Skip to content

Commit f35d129

Browse files
google-genai-botcopybara-github
authored andcommitted
feat(fix): Check all content parts for emptiness in _contains_empty_content
PiperOrigin-RevId: 845954226
1 parent 589f15c commit f35d129

File tree

2 files changed

+89
-9
lines changed

2 files changed

+89
-9
lines changed

src/google/adk/flows/llm_flows/contents.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,26 @@ def _rearrange_events_for_latest_function_response(
219219
return result_events
220220

221221

222+
def _is_part_invisible(p: types.Part) -> bool:
223+
"""A part is considered invisble if it's a thought, or has no visible content."""
224+
return getattr(p, 'thought', False) or not (
225+
p.text
226+
or p.inline_data
227+
or p.file_data
228+
or p.function_call
229+
or p.function_response
230+
)
231+
232+
222233
def _contains_empty_content(event: Event) -> bool:
223234
"""Check if an event should be skipped due to missing or empty content.
224235
225236
This can happen to the events that only changed session state.
226237
When both content and transcriptions are empty, the event will be considered
227238
as empty. The content is considered empty if none of its parts contain text,
228-
inline data, file data, function call, or function response.
239+
inline data, file data, function call, or function response. Parts with
240+
only thoughts are also considered empty.
241+
229242
230243
Args:
231244
event: The event to check.
@@ -240,14 +253,7 @@ def _contains_empty_content(event: Event) -> bool:
240253
not event.content
241254
or not event.content.role
242255
or not event.content.parts
243-
or all(
244-
not p.text
245-
and not p.inline_data
246-
and not p.file_data
247-
and not p.function_call
248-
and not p.function_response
249-
for p in [event.content.parts[0]]
250-
)
256+
or all(_is_part_invisible(p) for p in event.content.parts)
251257
) and (not event.output_transcription and not event.input_transcription)
252258

253259

tests/unittests/flows/llm_flows/test_contents.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from google.adk.events.event import Event
1717
from google.adk.events.event_actions import EventActions
1818
from google.adk.flows.llm_flows import contents
19+
from google.adk.flows.llm_flows.contents import request_processor
1920
from google.adk.flows.llm_flows.functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
2021
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
2122
from google.adk.models.llm_request import LlmRequest
@@ -433,6 +434,58 @@ async def test_rewind_events_are_filtered_out():
433434
]
434435

435436

437+
@pytest.mark.asyncio
438+
async def test_other_agent_empty_content():
439+
"""Test that other agent messages with only thoughts or empty content are filtered out."""
440+
agent = Agent(model="gemini-2.5-flash", name="current_agent")
441+
llm_request = LlmRequest(model="gemini-2.5-flash")
442+
invocation_context = await testing_utils.create_invocation_context(
443+
agent=agent
444+
)
445+
# Add events: user message, other agents with empty content, user message
446+
events = [
447+
Event(
448+
invocation_id="inv1",
449+
author="user",
450+
content=types.UserContent("Hello"),
451+
),
452+
# Other agent with only thoughts
453+
Event(
454+
invocation_id="inv2",
455+
author="other_agent1",
456+
content=types.ModelContent([
457+
types.Part(text="This is a private thought", thought=True),
458+
types.Part(text="Another private thought", thought=True),
459+
]),
460+
),
461+
# Other agent with empty text and thoughts
462+
Event(
463+
invocation_id="inv3",
464+
author="other_agent2",
465+
content=types.ModelContent([
466+
types.Part(text="", thought=False),
467+
types.Part(text="Secret thought", thought=True),
468+
]),
469+
),
470+
Event(
471+
invocation_id="inv4",
472+
author="user",
473+
content=types.UserContent("World"),
474+
),
475+
]
476+
invocation_context.session.events = events
477+
478+
# Process the request
479+
async for _ in request_processor.run_async(invocation_context, llm_request):
480+
pass
481+
482+
# Verify empty content events are completely filtered out
483+
assert llm_request.contents == [
484+
types.UserContent("Hello"),
485+
types.UserContent("World"),
486+
]
487+
488+
436489
@pytest.mark.asyncio
437490
async def test_events_with_empty_content_are_skipped():
438491
"""Test that events with empty content (state-only changes) are skipped."""
@@ -471,6 +524,14 @@ async def test_events_with_empty_content_are_skipped():
471524
author="user",
472525
content=types.Content(parts=[types.Part(text="")], role="model"),
473526
),
527+
# Event with content that has multiple empty text parts
528+
Event(
529+
invocation_id="inv6_2",
530+
author="user",
531+
content=types.Content(
532+
parts=[types.Part(text=""), types.Part(text="")], role="model"
533+
),
534+
),
474535
# Event with content that has only inline data part
475536
Event(
476537
invocation_id="inv7",
@@ -502,6 +563,15 @@ async def test_events_with_empty_content_are_skipped():
502563
role="user",
503564
),
504565
),
566+
# Event with mixed empty and non-empty text parts
567+
Event(
568+
invocation_id="inv9",
569+
author="user",
570+
content=types.Content(
571+
parts=[types.Part(text=""), types.Part(text="Mixed content")],
572+
role="user",
573+
),
574+
),
505575
]
506576
invocation_context.session.events = events
507577

@@ -534,4 +604,8 @@ async def test_events_with_empty_content_are_skipped():
534604
],
535605
role="user",
536606
),
607+
types.Content(
608+
parts=[types.Part(text=""), types.Part(text="Mixed content")],
609+
role="user",
610+
),
537611
]

0 commit comments

Comments
 (0)