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
1 change: 1 addition & 0 deletions contributing/samples/skip_synthesis_followup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import agent
56 changes: 56 additions & 0 deletions contributing/samples/skip_synthesis_followup/agent.py
Original file line number Diff line number Diff line change
@@ -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 explanation_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=explanation_instruction,
tools=[followup_agent_tool]
)

# Root agent is the explainer
root_agent = explainer_agent
38 changes: 38 additions & 0 deletions contributing/samples/skip_synthesis_followup/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
explanation_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"]`
"""
2 changes: 1 addition & 1 deletion src/google/adk/events/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/google/adk/events/event_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
4 changes: 4 additions & 0 deletions src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
7 changes: 6 additions & 1 deletion src/google/adk/tools/agent_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions src/google/adk/tools/base_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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]:
Expand Down
3 changes: 2 additions & 1 deletion src/google/adk/tools/function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/unittests/events/test_event.py
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions tests/unittests/flows/llm_flows/test_functions_skip_synthesis.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions tests/unittests/tools/test_function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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."""
Expand Down
Loading