Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/google/adk/models/interactions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,9 +1013,18 @@ async def generate_content_via_interactions(
# Log the streaming event
logger.debug(build_interactions_event_log(event))

# Extract interaction ID from event if available
if hasattr(event, 'id') and event.id:
current_interaction_id = event.id
# Extract interaction ID from event if available.
# SSE events carry the ID in different attributes depending on type:
# - InteractionStartEvent/CompleteEvent: event.interaction.id
# - InteractionStatusUpdate: event.interaction_id
if (
hasattr(event, 'interaction')
and hasattr(event.interaction, 'id')
and event.interaction.id
):
current_interaction_id = event.interaction.id
elif hasattr(event, 'interaction_id') and event.interaction_id:
current_interaction_id = event.interaction_id
llm_response = convert_interaction_event_to_llm_response(
event, aggregated_parts, current_interaction_id
)
Expand Down
99 changes: 99 additions & 0 deletions tests/unittests/models/test_interactions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -955,3 +955,102 @@ def test_unknown_event_type_returns_none(self):

assert result is None
assert not aggregated_parts


class TestSSEInteractionIdExtraction:
"""Tests for interaction_id extraction from SSE events.

SSE events carry the interaction ID in different attributes:
- InteractionStartEvent/CompleteEvent: event.interaction.id
- InteractionStatusUpdate: event.interaction_id
- ContentDelta/ContentStop: no interaction ID

The call_interactions_api function must extract the ID from these
locations so it can be propagated to LlmResponse objects and
ultimately stored in session events for chaining.
"""

def test_interaction_start_event_carries_id(self):
"""InteractionStartEvent has interaction.id — should be found."""
event = MagicMock()
event.event_type = 'interaction.start'
event.interaction = MagicMock()
event.interaction.id = 'int_start_abc'
# Should NOT have direct interaction_id
del event.interaction_id

# Verify the extraction logic matches what call_interactions_api does
current_id = None
if (
hasattr(event, 'interaction')
and hasattr(event.interaction, 'id')
and event.interaction.id
):
current_id = event.interaction.id
elif hasattr(event, 'interaction_id') and event.interaction_id:
current_id = event.interaction_id

assert current_id == 'int_start_abc'

def test_status_update_event_carries_interaction_id(self):
"""InteractionStatusUpdate has interaction_id — should be found."""
event = MagicMock(spec=['event_type', 'interaction_id', 'status'])
event.event_type = 'interaction.status_update'
event.interaction_id = 'int_status_xyz'
event.status = 'requires_action'

current_id = None
if (
hasattr(event, 'interaction')
and hasattr(event.interaction, 'id')
and event.interaction.id
):
current_id = event.interaction.id
elif hasattr(event, 'interaction_id') and event.interaction_id:
current_id = event.interaction_id

assert current_id == 'int_status_xyz'

def test_content_delta_has_no_interaction_id(self):
"""ContentDelta events don't carry interaction ID."""
event = MagicMock(spec=['event_type', 'delta', 'index', 'event_id'])
event.event_type = 'content.delta'

current_id = None
if (
hasattr(event, 'interaction')
and hasattr(event.interaction, 'id')
and event.interaction.id
):
current_id = event.interaction.id
elif hasattr(event, 'interaction_id') and event.interaction_id:
current_id = event.interaction_id

assert current_id is None

def test_interaction_id_propagated_to_status_update_response(self):
"""When interaction_id is extracted from earlier events, it should
be passed to convert_interaction_event_to_llm_response and appear
in the resulting LlmResponse for status_update events."""
event = MagicMock()
event.event_type = 'interaction.status_update'
event.status = 'requires_action'

# Function call was aggregated earlier
aggregated_parts = [
types.Part(
function_call=types.FunctionCall(
id='call_1', name='get_weather', args={'city': 'Tokyo'}
)
)
]

# The interaction_id should have been extracted from an earlier
# InteractionStartEvent and passed here
result = interactions_utils.convert_interaction_event_to_llm_response(
event, aggregated_parts, interaction_id='int_from_start'
)

assert result is not None
assert result.interaction_id == 'int_from_start'
assert result.turn_complete is True
Loading