From 816697f9df6dd79b4a2ffc0c65271cb57f5fb4cc Mon Sep 17 00:00:00 2001 From: Ishaan Date: Thu, 6 Aug 2026 09:16:46 +0000 Subject: [PATCH] Add to_content_blocks() to BasePlanner for standardized content block output Both `PlanReActPlanner` and `BuiltInPlanner` expose their output as a raw list of `google.genai.types.Part` objects, making it hard for callers to consume planning results in a provider-neutral way. Signed-off-by: Ishaan --- src/google/adk/planners/base_planner.py | 30 +++++++++ .../planners/test_plan_re_act_planner.py | 62 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/google/adk/planners/base_planner.py b/src/google/adk/planners/base_planner.py index 05ac2ca3bc9..5f0da0da546 100644 --- a/src/google/adk/planners/base_planner.py +++ b/src/google/adk/planners/base_planner.py @@ -16,6 +16,8 @@ import abc from abc import ABC +from typing import Any +from typing import Dict from typing import List from typing import Optional @@ -66,3 +68,31 @@ def process_planning_response( The processed response parts, or None if no processing is needed. """ pass + + def to_content_blocks( + self, + response_parts: List[types.Part], + ) -> List[Dict[str, Any]]: + """Converts planner response parts to standardized content blocks. + + Produces a representation compatible with the standard content block format + (similar to LangChain 1.0), where thought/reasoning parts are emitted as + ``{'type': 'reasoning', 'reasoning': '...'}`` blocks and regular text parts + as ``{'type': 'text', 'text': '...'}`` blocks. Parts that are neither text + nor thought are skipped. + + Args: + response_parts: The planner response parts to convert. + + Returns: + A list of standardized content block dicts. + """ + blocks: List[Dict[str, Any]] = [] + for part in response_parts: + if part.text is None: + continue + if part.thought: + blocks.append({'type': 'reasoning', 'reasoning': part.text}) + else: + blocks.append({'type': 'text', 'text': part.text}) + return blocks diff --git a/tests/unittests/planners/test_plan_re_act_planner.py b/tests/unittests/planners/test_plan_re_act_planner.py index ccafdf48a99..674444f417c 100644 --- a/tests/unittests/planners/test_plan_re_act_planner.py +++ b/tests/unittests/planners/test_plan_re_act_planner.py @@ -14,6 +14,7 @@ """Tests for PlanReActPlanner.process_planning_response.""" +from google.adk.planners.built_in_planner import BuiltInPlanner from google.adk.planners.plan_re_act_planner import PlanReActPlanner from google.genai import types @@ -56,3 +57,64 @@ def test_preserves_parallel_function_calls_after_leading_text(): ) assert _function_call_names(result) == ["get_weather", "get_time"] + + +# --------------------------------------------------------------------------- +# Tests for BasePlanner.to_content_blocks (exercised via PlanReActPlanner and +# BuiltInPlanner which are the two concrete subclasses). +# --------------------------------------------------------------------------- + + +def test_to_content_blocks_text_and_reasoning(): + """Thought parts map to 'reasoning' blocks; plain text maps to 'text' blocks.""" + planner = PlanReActPlanner() + response_parts = [ + types.Part(text="I should check the weather first.", thought=True), + types.Part(text="Here is your answer."), + ] + + blocks = planner.to_content_blocks(response_parts) + + assert blocks == [ + {"type": "reasoning", "reasoning": "I should check the weather first."}, + {"type": "text", "text": "Here is your answer."}, + ] + + +def test_to_content_blocks_empty_parts(): + """Empty input returns an empty list.""" + planner = PlanReActPlanner() + assert planner.to_content_blocks([]) == [] + + +def test_to_content_blocks_skips_non_text_parts(): + """Parts without text (e.g. function calls) are skipped.""" + planner = PlanReActPlanner() + response_parts = [ + types.Part(text="Some reasoning.", thought=True), + types.Part.from_function_call(name="get_weather", args={"city": "NY"}), + types.Part(text="Final answer."), + ] + + blocks = planner.to_content_blocks(response_parts) + + assert blocks == [ + {"type": "reasoning", "reasoning": "Some reasoning."}, + {"type": "text", "text": "Final answer."}, + ] + + +def test_to_content_blocks_built_in_planner(): + """BuiltInPlanner inherits to_content_blocks correctly.""" + planner = BuiltInPlanner(thinking_config=types.ThinkingConfig()) + response_parts = [ + types.Part(text="Thinking step.", thought=True), + types.Part(text="Response text."), + ] + + blocks = planner.to_content_blocks(response_parts) + + assert blocks == [ + {"type": "reasoning", "reasoning": "Thinking step."}, + {"type": "text", "text": "Response text."}, + ]