From affdf2877ff4e0fc9e6c19cef3e661c925a61443 Mon Sep 17 00:00:00 2001 From: Harshal Patil Date: Sun, 7 Dec 2025 12:03:04 +0530 Subject: [PATCH 1/5] feat: Add `skip_synthesis` to tools and event actions to bypass LLM response generation, along with a new sample and tests. --- .../skip_synthesis_followup/__init__.py | 1 + .../samples/skip_synthesis_followup/agent.py | 56 +++++++++++++++++ .../skip_synthesis_followup/prompts.py | 38 ++++++++++++ src/google/adk/events/event.py | 2 +- src/google/adk/events/event_actions.py | 3 + src/google/adk/flows/llm_flows/functions.py | 4 ++ src/google/adk/tools/agent_tool.py | 7 ++- src/google/adk/tools/base_tool.py | 13 ++++ src/google/adk/tools/function_tool.py | 3 +- tests/unittests/events/test_event.py | 45 ++++++++++++++ .../test_functions_skip_synthesis.py | 62 +++++++++++++++++++ tests/unittests/tools/test_function_tool.py | 8 +++ 12 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 contributing/samples/skip_synthesis_followup/__init__.py create mode 100644 contributing/samples/skip_synthesis_followup/agent.py create mode 100644 contributing/samples/skip_synthesis_followup/prompts.py create mode 100644 tests/unittests/events/test_event.py create mode 100644 tests/unittests/flows/llm_flows/test_functions_skip_synthesis.py diff --git a/contributing/samples/skip_synthesis_followup/__init__.py b/contributing/samples/skip_synthesis_followup/__init__.py new file mode 100644 index 0000000000..63bd45e6d2 --- /dev/null +++ b/contributing/samples/skip_synthesis_followup/__init__.py @@ -0,0 +1 @@ +from . import agent \ No newline at end of file diff --git a/contributing/samples/skip_synthesis_followup/agent.py b/contributing/samples/skip_synthesis_followup/agent.py new file mode 100644 index 0000000000..80ff94ee3d --- /dev/null +++ b/contributing/samples/skip_synthesis_followup/agent.py @@ -0,0 +1,56 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.adk import Agent +from google.adk.tools import AgentTool +from google.adk.agents import LlmAgent +from .prompts import explaination_instruction, followup_instruction +from google.genai import types as genai_types +from pydantic import BaseModel, Field +from typing import List + +class FollowupsPayload(BaseModel): + questions: List[str] = Field( + description="List of 3 short follow-up questions as strings." + ) + +# Follow-up questions agent +followup_agent = LlmAgent( + model="gemini-2.5-flash-lite", + name="followup_agent", + description="Generates 3 follow-up questions to spark curiosity and deepen understanding after explaining concepts. Creates questions covering application, comparison, and exploration. DO NOT call when student is stuck on problems or during practice sessions.", + output_schema=FollowupsPayload, + include_contents="none", + instruction=followup_instruction, + generate_content_config=genai_types.GenerateContentConfig( + temperature=0.0, + ), +) + +# Convert agent to tool +followup_agent_tool = AgentTool( + agent=followup_agent, + skip_synthesis=True +) + +explainer_agent = Agent( + name="explainer_agent", + model="gemini-2.5-flash", + description="An agent that explains topics.", + instruction=explaination_instruction, + tools=[followup_agent_tool] +) + +# Root agent is the explainer +root_agent = explainer_agent \ No newline at end of file diff --git a/contributing/samples/skip_synthesis_followup/prompts.py b/contributing/samples/skip_synthesis_followup/prompts.py new file mode 100644 index 0000000000..2bc1338871 --- /dev/null +++ b/contributing/samples/skip_synthesis_followup/prompts.py @@ -0,0 +1,38 @@ +explaination_instruction = """You are a helpful AI assistant. + +For every user query: + +1. First understand the intent of the question. +2. Give a clear, step-by-step explanation that walks the user through: + - What the problem or question is + - The key concepts involved + - The reasoning or calculations needed + - The final answer or conclusion +3. Use simple, direct language. Avoid jargon unless the user is clearly an expert. +4. When there is more than one way to approach the problem, briefly mention the alternatives and explain which one you chose and why. +5. If something depends on an assumption, state the assumption explicitly. + +At the end of your explanation, generate helpful follow-up questions that the user might want to ask next. +""" + +followup_instruction = """ +Your task: Generate 3 follow-up questions a student should ask next to deepen their understanding of the concept just explained. + +**Context:** +The preceding explanation was conversational and grade-appropriate. Your questions must match that tone and build directly on the content provided. + +**Rules:** +1. **Exactly 3 questions** - no more, no less +2. **Max 8 words each** - concise and actionable +3. **Never reference** the input format or source ("the image", "what you typed") +4. **No generic questions** - avoid "Tell me more" or "Can you simplify it?" +5. **Mandatory structure:** + - **Q1 (Application):** Apply the concept to a new scenario or practical problem + - **Q2 (Comparison):** Compare to related ideas or explain its significance + - **Q3 (Exploration):** Out-of-the-box question about real-world impact, surprising uses, or limitations + +**Output Format:** +JSON array only. No other text. + +Schema: `["question1","question2","question3"]` +""" \ No newline at end of file diff --git a/src/google/adk/events/event.py b/src/google/adk/events/event.py index cca086430b..fee71a58ab 100644 --- a/src/google/adk/events/event.py +++ b/src/google/adk/events/event.py @@ -87,7 +87,7 @@ def is_final_response(self) -> bool: Note that when multiple agents participate in one invocation, there could be one event has `is_final_response()` as True for each participating agent. """ - if self.actions.skip_summarization or self.long_running_tool_ids: + if self.actions.skip_summarization or self.actions.skip_synthesis or self.long_running_tool_ids: return True return ( not self.get_function_calls() diff --git a/src/google/adk/events/event_actions.py b/src/google/adk/events/event_actions.py index d79a71a7b2..1a83bc9613 100644 --- a/src/google/adk/events/event_actions.py +++ b/src/google/adk/events/event_actions.py @@ -63,6 +63,9 @@ class EventActions(BaseModel): Only used for function_response event. """ + skip_synthesis: Optional[bool] = None + """If true, skip LLM synthesis after tool execution.""" + state_delta: dict[str, object] = Field(default_factory=dict) """Indicates that the event is updating the state with the given delta.""" diff --git a/src/google/adk/flows/llm_flows/functions.py b/src/google/adk/flows/llm_flows/functions.py index ffe1657be1..9141b11573 100644 --- a/src/google/adk/flows/llm_flows/functions.py +++ b/src/google/adk/flows/llm_flows/functions.py @@ -798,6 +798,10 @@ def __build_response_event( if not isinstance(function_result, dict): function_result = {'result': function_result} + # Check if tool has skip_synthesis flag set + if tool.skip_synthesis: + tool_context.actions.skip_synthesis = True + part_function_response = types.Part.from_function_response( name=tool.name, response=function_result ) diff --git a/src/google/adk/tools/agent_tool.py b/src/google/adk/tools/agent_tool.py index 46d8616619..ac100e65aa 100644 --- a/src/google/adk/tools/agent_tool.py +++ b/src/google/adk/tools/agent_tool.py @@ -55,14 +55,16 @@ def __init__( self, agent: BaseAgent, skip_summarization: bool = False, + skip_synthesis: bool = False, *, include_plugins: bool = True, ): self.agent = agent self.skip_summarization: bool = skip_summarization + self.skip_synthesis: bool = skip_synthesis self.include_plugins = include_plugins - super().__init__(name=agent.name, description=agent.description) + super().__init__(name=agent.name, description=agent.description, skip_synthesis=skip_synthesis) @model_validator(mode='before') @classmethod @@ -123,6 +125,9 @@ async def run_async( if self.skip_summarization: tool_context.actions.skip_summarization = True + if self.skip_synthesis: + tool_context.actions.skip_synthesis = True + if isinstance(self.agent, LlmAgent) and self.agent.input_schema: input_value = self.agent.input_schema.model_validate(args) content = types.Content( diff --git a/src/google/adk/tools/base_tool.py b/src/google/adk/tools/base_tool.py index c714fb11cb..50e7a6b3e4 100644 --- a/src/google/adk/tools/base_tool.py +++ b/src/google/adk/tools/base_tool.py @@ -56,6 +56,17 @@ class BaseTool(ABC): """Whether the tool is a long running operation, which typically returns a resource id first and finishes the operation later.""" + skip_synthesis: bool = False + """Whether to skip LLM synthesis after this tool executes. + + When True, the tool's response will be returned directly without calling + the LLM again to synthesize/format the results. This is useful for tools + that return data meant to be consumed programmatically or when the LLM + has already provided context before calling the tool. + + Default is False (LLM synthesis happens as normal). + """ + custom_metadata: Optional[dict[str, Any]] = None """The custom metadata of the BaseTool. @@ -71,11 +82,13 @@ def __init__( name, description, is_long_running: bool = False, + skip_synthesis: bool = False, custom_metadata: Optional[dict[str, Any]] = None, ): self.name = name self.description = description self.is_long_running = is_long_running + self.skip_synthesis = skip_synthesis self.custom_metadata = custom_metadata def _get_declaration(self) -> Optional[types.FunctionDeclaration]: diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index d957d1c16b..b09d240dc3 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -47,6 +47,7 @@ def __init__( func: Callable[..., Any], *, require_confirmation: Union[bool, Callable[..., bool]] = False, + skip_synthesis: bool = False, ): """Initializes the FunctionTool. Extracts metadata from a callable object. @@ -78,7 +79,7 @@ def __init__( # For callable objects, try to get docstring from __call__ method doc = inspect.cleandoc(func.__call__.__doc__) - super().__init__(name=name, description=doc) + super().__init__(name=name, description=doc, skip_synthesis=skip_synthesis) self.func = func self._ignore_params = ['tool_context', 'input_stream'] self._require_confirmation = require_confirmation diff --git a/tests/unittests/events/test_event.py b/tests/unittests/events/test_event.py new file mode 100644 index 0000000000..fa718e6b20 --- /dev/null +++ b/tests/unittests/events/test_event.py @@ -0,0 +1,45 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.adk.events.event import Event +from google.adk.events.event_actions import EventActions +from google.genai import types + +def test_is_final_response_with_skip_synthesis(): + """Test that is_final_response returns True when skip_synthesis is True.""" + event = Event( + author='agent', + content=types.Content(role='model', parts=[types.Part(text='response')]), + actions=EventActions(skip_synthesis=True), + ) + assert event.is_final_response() is True + +def test_is_final_response_without_skip_synthesis(): + """Test that is_final_response returns False/True correctly without skip_synthesis.""" + # Case 1: Normal text response -> True + event = Event( + author='agent', + content=types.Content(role='model', parts=[types.Part(text='response')]), + ) + assert event.is_final_response() is True + + # Case 2: Function call -> False + event_fc = Event( + author='agent', + content=types.Content( + role='model', + parts=[types.Part(function_call=types.FunctionCall(name='foo', args={}))] + ), + ) + assert event_fc.is_final_response() is False diff --git a/tests/unittests/flows/llm_flows/test_functions_skip_synthesis.py b/tests/unittests/flows/llm_flows/test_functions_skip_synthesis.py new file mode 100644 index 0000000000..2121951b99 --- /dev/null +++ b/tests/unittests/flows/llm_flows/test_functions_skip_synthesis.py @@ -0,0 +1,62 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from google.genai import types +from google.adk.events.event import Event +from google.adk.tools.function_tool import FunctionTool +from google.adk.agents.llm_agent import Agent +from google.adk.flows.llm_flows.functions import handle_function_calls_async +from ... import testing_utils + +@pytest.mark.asyncio +async def test_function_call_with_skip_synthesis(): + """Test that skip_synthesis is propagated to the response event.""" + + def simple_fn(**kwargs) -> dict: + return {'result': 'test'} + + # Create tool with skip_synthesis=True + tool = FunctionTool(simple_fn, skip_synthesis=True) + + model = testing_utils.MockModel.create(responses=[]) + agent = Agent( + name='test_agent', + model=model, + tools=[tool], + ) + invocation_context = await testing_utils.create_invocation_context( + agent=agent, user_content='' + ) + + function_call = types.FunctionCall(name=tool.name, args={}) + content = types.Content(parts=[types.Part(function_call=function_call)]) + event = Event( + invocation_id=invocation_context.invocation_id, + author=agent.name, + content=content, + ) + tools_dict = {tool.name: tool} + + # Execute the function call + result_event = await handle_function_calls_async( + invocation_context, + event, + tools_dict, + ) + + # Verify that the resulting event has SKIP_SYNTHESIS + assert result_event is not None + assert result_event.actions is not None + assert result_event.actions.skip_synthesis is True diff --git a/tests/unittests/tools/test_function_tool.py b/tests/unittests/tools/test_function_tool.py index 78610d330d..1fa8b987ca 100644 --- a/tests/unittests/tools/test_function_tool.py +++ b/tests/unittests/tools/test_function_tool.py @@ -109,6 +109,7 @@ def function_returning_empty_dict() -> dict[str, str]: return {} + def test_init(): """Test that the FunctionTool is initialized correctly.""" tool = FunctionTool(function_for_testing_with_no_args) @@ -117,6 +118,13 @@ def test_init(): assert tool.func == function_for_testing_with_no_args +def test_init_with_skip_synthesis(): + """Test that the FunctionTool is initialized correctly with skip_synthesis.""" + tool = FunctionTool(function_for_testing_with_no_args, skip_synthesis=True) + assert tool.skip_synthesis is True + + + @pytest.mark.asyncio async def test_function_returning_none(): """Test that the function returns with None actually returning None.""" From 03a61d6fe4fa0fd5be9fff939b2d2f7d694a2cbc Mon Sep 17 00:00:00 2001 From: "Harshal s. patil" <161000988+Harshal1000@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:58:11 +0530 Subject: [PATCH 2/5] Update contributing/samples/skip_synthesis_followup/prompts.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- contributing/samples/skip_synthesis_followup/prompts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing/samples/skip_synthesis_followup/prompts.py b/contributing/samples/skip_synthesis_followup/prompts.py index 2bc1338871..7bc439d28a 100644 --- a/contributing/samples/skip_synthesis_followup/prompts.py +++ b/contributing/samples/skip_synthesis_followup/prompts.py @@ -1,4 +1,4 @@ -explaination_instruction = """You are a helpful AI assistant. +explanation_instruction = """You are a helpful AI assistant. For every user query: From 2e9a0ac5b355a67ec62fd80b0b105ccc7919a5f3 Mon Sep 17 00:00:00 2001 From: "Harshal s. patil" <161000988+Harshal1000@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:58:19 +0530 Subject: [PATCH 3/5] Update contributing/samples/skip_synthesis_followup/agent.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- contributing/samples/skip_synthesis_followup/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing/samples/skip_synthesis_followup/agent.py b/contributing/samples/skip_synthesis_followup/agent.py index 80ff94ee3d..a37460fc4c 100644 --- a/contributing/samples/skip_synthesis_followup/agent.py +++ b/contributing/samples/skip_synthesis_followup/agent.py @@ -48,7 +48,7 @@ class FollowupsPayload(BaseModel): name="explainer_agent", model="gemini-2.5-flash", description="An agent that explains topics.", - instruction=explaination_instruction, + instruction=explanation_instruction, tools=[followup_agent_tool] ) From fe56780772a3a3a3ff6c6e6025e02851f2ff138b Mon Sep 17 00:00:00 2001 From: "Harshal s. patil" <161000988+Harshal1000@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:58:25 +0530 Subject: [PATCH 4/5] Update contributing/samples/skip_synthesis_followup/agent.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- contributing/samples/skip_synthesis_followup/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing/samples/skip_synthesis_followup/agent.py b/contributing/samples/skip_synthesis_followup/agent.py index a37460fc4c..70e7c2a75a 100644 --- a/contributing/samples/skip_synthesis_followup/agent.py +++ b/contributing/samples/skip_synthesis_followup/agent.py @@ -40,7 +40,7 @@ class FollowupsPayload(BaseModel): # Convert agent to tool followup_agent_tool = AgentTool( - agent=followup_agent, + agent=followup_agent, skip_synthesis=True ) From 8f8d6fcb0be44b9e1dc2159d6fe61ebe3f3ec930 Mon Sep 17 00:00:00 2001 From: "Harshal s. patil" <161000988+Harshal1000@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:58:35 +0530 Subject: [PATCH 5/5] Update contributing/samples/skip_synthesis_followup/agent.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- contributing/samples/skip_synthesis_followup/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing/samples/skip_synthesis_followup/agent.py b/contributing/samples/skip_synthesis_followup/agent.py index 70e7c2a75a..e825ecf517 100644 --- a/contributing/samples/skip_synthesis_followup/agent.py +++ b/contributing/samples/skip_synthesis_followup/agent.py @@ -15,7 +15,7 @@ from google.adk import Agent from google.adk.tools import AgentTool from google.adk.agents import LlmAgent -from .prompts import explaination_instruction, followup_instruction +from .prompts import explanation_instruction, followup_instruction from google.genai import types as genai_types from pydantic import BaseModel, Field from typing import List