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
21 changes: 20 additions & 1 deletion src/google/adk/workflow/utils/_rehydration_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ def _extract_schema_from_event(event: Event, interrupt_id: str) -> Any | None:
return None


def _process_content_object(event: Event) -> Any:
"""Extracts output from event.content."""
if not event.content or not getattr(event.content, 'parts', None):
return None

text = ''.join(
p.text for p in event.content.parts if p.text and not p.thought
)
text = text.strip()

if not text:
return None

try:
return json.loads(text)
except (json.JSONDecodeError, ValueError):
return text


def _validate_resume_response(response_data: Any, schema: Any) -> Any:
"""Validates and coerces resume response data against a schema.

Expand Down Expand Up @@ -275,7 +294,7 @@ def get_owner_key(event_path_builder: _NodePathBuilder) -> str | None:
child.output = event.output
child.branch = event.branch
elif use_message_as_output:
child.output = event.content
child.output = _process_content_object(event)
if event.actions and event.actions.route is not None:
child.route = event.actions.route
if event.actions and event.actions.transfer_to_agent is not None:
Expand Down
45 changes: 44 additions & 1 deletion tests/unittests/workflow/utils/test_rehydration_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from google.adk.events.event import NodeInfo
from google.adk.events.request_input import RequestInput
from google.adk.workflow.utils._rehydration_utils import _ChildScanState
from google.adk.workflow.utils._rehydration_utils import _process_content_object
from google.adk.workflow.utils._rehydration_utils import _reconstruct_node_states
from google.adk.workflow.utils._rehydration_utils import _unwrap_response
from google.adk.workflow.utils._rehydration_utils import _validate_resume_response
Expand Down Expand Up @@ -103,6 +104,48 @@ def test_roundtrip_wrap_unwrap_dict(self):
assert _unwrap_response(_wrap_response(d)) == d


# --- _process_content_object ---


class TestProcessContentObject:

def test_extracts_plain_text(self):
content = types.Content(parts=[types.Part(text="hello world")])
event = Event(content=content, invocation_id="id")
assert _process_content_object(event) == "hello world"

def test_parses_json_text(self):
content = types.Content(parts=[types.Part(text='{"foo": "bar"}')])
event = Event(content=content, invocation_id="id")
assert _process_content_object(event) == {"foo": "bar"}

def test_joins_multiple_parts(self):
content = types.Content(
parts=[types.Part(text="hello "), types.Part(text="world")]
)
event = Event(content=content, invocation_id="id")
assert _process_content_object(event) == "hello world"

def test_filters_thought_parts(self):
content = types.Content(
parts=[
types.Part(text="thinking...", thought=True),
types.Part(text='{"answer": 42}'),
]
)
event = Event(content=content, invocation_id="id")
assert _process_content_object(event) == {"answer": 42}

def test_returns_none_for_no_content(self):
event = Event(invocation_id="id")
assert _process_content_object(event) is None

def test_returns_none_for_empty_text(self):
content = types.Content(parts=[types.Part(text=" ")])
event = Event(content=content, invocation_id="id")
assert _process_content_object(event) is None


# --- _validate_resume_response ---


Expand Down Expand Up @@ -192,7 +235,7 @@ def test_scan_message_as_output(self):
)

assert "node_a@1" in results
assert results["node_a@1"].output == content
assert results["node_a@1"].output == "hello"

def test_scan_descendant_interrupts(self):
event = Event(
Expand Down