diff --git a/src/google/adk/workflow/utils/_rehydration_utils.py b/src/google/adk/workflow/utils/_rehydration_utils.py index 0e84e98e01..60a0ed0dc7 100644 --- a/src/google/adk/workflow/utils/_rehydration_utils.py +++ b/src/google/adk/workflow/utils/_rehydration_utils.py @@ -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. @@ -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: diff --git a/tests/unittests/workflow/utils/test_rehydration_utils.py b/tests/unittests/workflow/utils/test_rehydration_utils.py index 37405960db..0bb9f22c06 100644 --- a/tests/unittests/workflow/utils/test_rehydration_utils.py +++ b/tests/unittests/workflow/utils/test_rehydration_utils.py @@ -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 @@ -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 --- @@ -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(