chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,63 @@
# Sales Assistant Agent with Context Offloading
This agent acts as a sales assistant, capable of generating and retrieving large
sales reports for different regions (North America, EMEA, APAC).
## The Challenge: Large Context Windows
Storing large pieces of data, like full sales reports, directly in conversation
history consumes valuable LLM context window space. This limits how much
conversation history the model can see, potentially degrading response quality
in longer conversations and increasing token costs.
## The Solution: Context Offloading with Artifacts
This agent demonstrates how to use ADK's artifact feature to offload large data
from the main conversation context, while still making it available to the agent
on-demand. Large reports are generated by the `query_large_data` tool but are
immediately saved as artifacts instead of being returned in the function call
response. This keeps the turn events small, saving context space.
### How it Works
1. **Saving Artifacts**: When the user asks for a sales report (e.g., "Get EMEA
sales report"), the `query_large_data` tool is called. It generates a mock
report, saves it as an artifact (`EMEA_sales_report_q3_2025.txt`), and saves
a brief description in the artifact's metadata (e.g., `{'summary': 'Sales report for EMEA Q3 2025'}`). The tool returns only a confirmation message to
the agent, not the large report itself.
1. **Immediate Loading**: The `QueryLargeDataTool` then runs its
`process_llm_request` hook. It detects that `query_large_data` was just
called, loads the artifact that was just saved, and injects its content into
the *next* request to the LLM. This makes the report data available
immediately, allowing the agent to summarize it or answer questions in the
same turn, as seen in the logs. This artifact is only appended for that
round and not saved to session. For future rounds of conversation, it will
be removed from context.
1. **Loading on Demand**: The `CustomLoadArtifactsTool` enhances the default
`load_artifacts` behavior.
- It reads the `summary` metadata from all available artifacts and includes
these summaries in the instructions sent to the LLM (e.g., `You have access to artifacts: ["APAC_sales_report_q3_2025.txt: Sales report for APAC Q3 2025", ...]`). This lets the agent know *what* data is
available in artifacts, without having to load the full content.
- It instructs the agent to use data from the most recent turn if
available, but to call `load_artifacts` if it needs to access data from
an *older* turn that is no longer in the immediate context (e.g., if
comparing North America data after having discussed EMEA and APAC).
- When `load_artifacts` is called, this tool intercepts it and injects the
requested artifact content into the LLM request.
- Note that artifacts are never saved to session.
This pattern ensures that large data is only loaded into the LLM's context
window when it is immediately relevant—either just after being generated or when
explicitly requested later—thereby managing context size more effectively.
### How to Run
```bash
adk web
```
Then, ask the agent:
- "Hi, help me query the North America sales report"
- "help me query EMEA and APAC sales report"
- "Summarize sales report for North America?"
@@ -0,0 +1,15 @@
# Copyright 2026 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 . import agent
@@ -0,0 +1,249 @@
# Copyright 2026 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.
"""Sales Data Assistant Agent demonstrating context offloading with artifacts.
This agent simulates querying large sales reports. To avoid cluttering
the LLM context window with large amounts of data, queried reports are
saved as artifacts rather than returned directly in function responses.
Tools are used to inject artifact content into the LLM context only when
needed:
- QueryLargeDataTool injects content immediately after a report is generated.
- CustomLoadArtifactsTool injects content when load_artifacts is called, and
also provides artifact summaries to the LLM based on artifact metadata.
"""
import json
import logging
import random
from google.adk import Agent
from google.adk.apps import App
from google.adk.models.llm_request import LlmRequest
from google.adk.tools.function_tool import FunctionTool
from google.adk.tools.load_artifacts_tool import LoadArtifactsTool
from google.adk.tools.tool_context import ToolContext
from google.genai import types
from typing_extensions import override
logger = logging.getLogger('google_adk.' + __name__)
class CustomLoadArtifactsTool(LoadArtifactsTool):
"""A custom tool to load artifacts that also provides summaries.
This tool extends LoadArtifactsTool to read custom metadata from artifacts
and provide summaries to the LLM in the system instructions, allowing the
model to know what artifacts are available (e.g., "Sales report for APAC").
It also injects artifact content into the LLM request when load_artifacts
is called by the model.
"""
@override
async def _append_artifacts_to_llm_request(
self, *, tool_context: ToolContext, llm_request: LlmRequest
):
artifact_names = await tool_context.list_artifacts()
if not artifact_names:
return
summaries = {}
for name in artifact_names:
version_info = await tool_context.get_artifact_version(name)
if version_info and version_info.custom_metadata:
summaries[name] = version_info.custom_metadata.get('summary')
artifacts_with_summaries = [
f'{name}: {summaries.get(name)}'
if name in summaries and summaries.get(name)
else name
for name in artifact_names
]
# Tell the model about the available artifacts.
llm_request.append_instructions([
f"""You have access to artifacts: {json.dumps(artifacts_with_summaries)}.
If you need to answer a question that requires artifact content, first check if
the content was very recently added to the conversation (e.g., in the last
turn). If it is, use that content directly to answer. If the content is not
available in the recent conversation history, you MUST call `load_artifacts`
to retrieve it before answering.
"""
])
# Attach the content of the artifacts if the model requests them.
# This only adds the content to the model request, instead of the session.
if llm_request.contents and llm_request.contents[-1].parts:
function_response = llm_request.contents[-1].parts[0].function_response
if function_response and function_response.name == 'load_artifacts':
artifact_names = function_response.response['artifact_names']
if not artifact_names:
return
for artifact_name in artifact_names:
# Try session-scoped first (default behavior)
artifact = await tool_context.load_artifact(artifact_name)
# If not found and name doesn't already have user: prefix,
# try cross-session artifacts with user: prefix
if artifact is None and not artifact_name.startswith('user:'):
prefixed_name = f'user:{artifact_name}'
artifact = await tool_context.load_artifact(prefixed_name)
if artifact is None:
logger.warning('Artifact "%s" not found, skipping', artifact_name)
continue
llm_request.contents.append(
types.Content(
role='user',
parts=[
types.Part.from_text(
text=f'Artifact {artifact_name} is:'
),
artifact,
],
)
)
async def query_large_data(query: str, tool_context: ToolContext) -> dict:
"""Generates a mock sales report for a given region and saves it as an artifact.
This function simulates querying a large dataset. It generates a mock report
for North America, EMEA, or APAC, saves it as a text artifact, and includes
a data summary in the artifact's custom metadata.
Example queries: "Get sales data for North America", "EMEA sales report".
Args:
query: The user query, expected to contain a region name.
tool_context: The tool context for saving artifacts.
Returns:
A dictionary containing a confirmation message and the artifact name.
"""
region = 'Unknown'
if 'north america' in query.lower():
region = 'North America'
elif 'emea' in query.lower():
region = 'EMEA'
elif 'apac' in query.lower():
region = 'APAC'
else:
return {
'message': f"Sorry, I don't have data for query: {query}",
'artifact_name': None,
}
# simulate large data - Generate a mock sales report
report_content = f"""SALES REPORT: {region} Q3 2025
=========================================
Total Revenue: ${random.uniform(500, 2000):.2f}M
Units Sold: {random.randint(100000, 500000)}
Key Products: Gadget Pro, Widget Max, Thingy Plus
Highlights:
- Strong growth in Gadget Pro driven by new marketing campaign.
- Widget Max sales are stable.
- Thingy Plus saw a 15% increase in market share.
Regional Breakdown:
""" + ''.join([
f'Sub-region {i+1} performance metric: {random.random()*100:.2f}\n'
for i in range(500)
])
data_summary = f'Sales report for {region} Q3 2025'
artifact_name = f"{region.replace(' ', '_')}_sales_report_q3_2025.txt"
await tool_context.save_artifact(
artifact_name,
types.Part.from_text(text=report_content),
custom_metadata={'summary': data_summary},
)
return {
'message': (
f'Sales data for {region} for Q3 2025 is saved as artifact'
f" '{artifact_name}'."
),
'artifact_name': artifact_name,
}
class QueryLargeDataTool(FunctionTool):
"""A tool that queries large data and saves it as an artifact.
This tool wraps the query_large_data function. Its process_llm_request
method checks if query_large_data was just called. If so, it loads the
artifact that was just created and injects its content into the LLM
request, so the model can use the data immediately in the next turn.
"""
def __init__(self):
super().__init__(query_large_data)
@override
async def process_llm_request(
self,
*,
tool_context: ToolContext,
llm_request: LlmRequest,
) -> None:
await super().process_llm_request(
tool_context=tool_context, llm_request=llm_request
)
if llm_request.contents and llm_request.contents[-1].parts:
function_response = llm_request.contents[-1].parts[0].function_response
if function_response and function_response.name == 'query_large_data':
artifact_name = function_response.response.get('artifact_name')
if artifact_name:
artifact = await tool_context.load_artifact(artifact_name)
if artifact:
llm_request.contents.append(
types.Content(
role='user',
parts=[
types.Part.from_text(
text=f'Artifact {artifact_name} is:'
),
artifact,
],
)
)
root_agent = Agent(
name='context_offloading_with_artifact',
description='An assistant for querying large sales reports.',
instruction="""
You are a sales data assistant. You can query large sales reports by
region (North America, EMEA, APAC) using the query_large_data tool.
If you are asked to compare data between regions, make sure you have
queried the data for all required regions first, and then use the
load_artifacts tool if you need to access reports from previous turns.
""",
tools=[
QueryLargeDataTool(),
CustomLoadArtifactsTool(),
],
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting( # avoid false alarm about rolling dice.
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.OFF,
),
]
),
)
app = App(
name='context_offloading_with_artifact',
root_agent=root_agent,
)
+15
View File
@@ -0,0 +1,15 @@
# Copyright 2026 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 . import agent
+112
View File
@@ -0,0 +1,112 @@
# Copyright 2026 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 random
from google.adk.agents.llm_agent import Agent
from google.adk.planners.built_in_planner import BuiltInPlanner
from google.adk.planners.plan_re_act_planner import PlanReActPlanner
from google.adk.tools.tool_context import ToolContext
from google.genai import types
def roll_die(sides: int, tool_context: ToolContext) -> int:
"""Roll a die and return the rolled result.
Args:
sides: The integer number of sides the die has.
Returns:
An integer of the result of rolling the die.
"""
result = random.randint(1, sides)
if not 'rolls' in tool_context.state:
tool_context.state['rolls'] = []
tool_context.state['rolls'] = tool_context.state['rolls'] + [result]
return result
async def check_prime(nums: list[int]) -> str:
"""Check if a given list of numbers are prime.
Args:
nums: The list of numbers to check.
Returns:
A str indicating which number is prime.
"""
primes = set()
for number in nums:
number = int(number)
if number <= 1:
continue
is_prime = True
for i in range(2, int(number**0.5) + 1):
if number % i == 0:
is_prime = False
break
if is_prime:
primes.add(number)
return (
'No prime numbers found.'
if not primes
else f"{', '.join(str(num) for num in primes)} are prime numbers."
)
root_agent = Agent(
model='gemini-2.5-pro-preview-03-25',
# model='gemini-2.5-flash',
name='data_processing_agent',
description=(
'hello world agent that can roll a dice of 8 sides and check prime'
' numbers.'
),
instruction="""
You roll dice and answer questions about the outcome of the dice rolls.
You can roll dice of different sizes.
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
It is ok to discuss previous dice roles, and comment on the dice rolls.
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
You should never roll a die on your own.
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
You should not check prime numbers before calling the tool.
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
3. When you respond, you must include the roll_die result from step 1.
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
You should not rely on the previous history on prime results.
""",
tools=[
roll_die,
check_prime,
],
planner=BuiltInPlanner(
thinking_config=types.ThinkingConfig(
include_thoughts=True,
),
),
# planner=PlanReActPlanner(),
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting( # avoid false alarm about rolling dice.
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.OFF,
),
]
),
)
+72
View File
@@ -0,0 +1,72 @@
# Copyright 2026 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 asyncio
import time
import warnings
import agent
from dotenv import load_dotenv
from google.adk import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.cli.utils import logs
from google.adk.sessions.session import Session
from google.genai import types
load_dotenv(override=True)
warnings.filterwarnings('ignore', category=UserWarning)
logs.log_to_tmp_folder()
async def main():
app_name = 'my_app'
user_id_1 = 'user1'
session_service = InMemorySessionService()
artifact_service = InMemoryArtifactService()
runner = Runner(
app_name=app_name,
agent=agent.root_agent,
artifact_service=artifact_service,
session_service=session_service,
)
session_11 = await session_service.create_session(app_name, user_id_1)
async def run_prompt(session: Session, new_message: str):
content = types.Content(
role='user', parts=[types.Part.from_text(text=new_message)]
)
print('** User says:', content.model_dump(exclude_none=True))
async for event in runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
):
if event.content.parts and event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
start_time = time.time()
print('Start time:', start_time)
print('------------------------------------')
await run_prompt(session_11, 'Hi')
await run_prompt(session_11, 'Roll a die.')
await run_prompt(session_11, 'Roll a die again.')
await run_prompt(session_11, 'What numbers did I got?')
end_time = time.time()
print('------------------------------------')
print('End time:', end_time)
print('Total time:', end_time - start_time)
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,27 @@
# JSON Passing Agent
This sample demonstrates how to pass structured JSON data between agents. The example uses a pizza ordering scenario where one agent takes the order and passes it to another agent for confirmation.
## How to run
1. Run the agent:
```bash
adk run .
```
2. Talk to the agent:
```
I want to order a pizza
```
## Example conversation
```
[user]: I'd like a large pizza with pepperoni and mushrooms on a thin crust.
[order_intake_agent]: (tool call to get available sizes, crusts, toppings)
[order_intake_agent]: (returns a PizzaOrder JSON)
[order_confirmation_agent]: (tool call to calculate_price)
[order_confirmation_agent]: You ordered a large thin crust pizza with pepperoni and mushrooms. The total price is $15.00.
```
@@ -0,0 +1,15 @@
# Copyright 2026 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 . import agent
+120
View File
@@ -0,0 +1,120 @@
# Copyright 2026 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.agents import sequential_agent
from google.adk.tools import tool_context
from pydantic import BaseModel
SequentialAgent = sequential_agent.SequentialAgent
ToolContext = tool_context.ToolContext
# 1. Define the data structure for the pizza order.
class PizzaOrder(BaseModel):
"""A data class to hold the details of a pizza order."""
size: str
crust: str
toppings: list[str]
# 2. Define tools for the order intake agent.
def get_available_sizes() -> list[str]:
"""Returns the available pizza sizes."""
return ['small', 'medium', 'large']
def get_available_crusts() -> list[str]:
"""Returns the available pizza crusts."""
return ['thin', 'thick', 'stuffed']
def get_available_toppings() -> list[str]:
"""Returns the available pizza toppings."""
return ['pepperoni', 'mushrooms', 'onions', 'sausage', 'bacon', 'pineapple']
# 3. Define the order intake agent.
# This agent's job is to interact with the user to fill out a PizzaOrder object.
# It uses the output_schema to structure its response as a JSON object that
# conforms to the PizzaOrder model.
order_intake_agent = Agent(
name='order_intake_agent',
instruction=(
"You are a pizza order intake agent. Your goal is to get the user's"
' pizza order. Use the available tools to find out what sizes, crusts,'
' and toppings are available. Once you have all the information,'
' provide it in the requested format. Your output MUST be a JSON object'
' that conforms to the PizzaOrder schema and nothing else.'
),
output_key='pizza_order',
output_schema=PizzaOrder,
tools=[get_available_sizes, get_available_crusts, get_available_toppings],
)
# 4. Define a tool for the order confirmation agent.
def calculate_price(tool_context: ToolContext) -> str:
"""Calculates the price of a pizza order and returns a descriptive string."""
order_dict = tool_context.state.get('pizza_order')
if not order_dict:
return "I can't find an order to calculate the price for."
order = PizzaOrder.model_validate(order_dict)
price = 0.0
if order.size == 'small':
price += 8.0
elif order.size == 'medium':
price += 10.0
elif order.size == 'large':
price += 12.0
if order.crust == 'stuffed':
price += 2.0
price += len(order.toppings) * 1.5
return f'The total price for your order is ${price:.2f}.'
# 5. Define the order confirmation agent.
# This agent reads the PizzaOrder object from the session state (placed there by
# the order_intake_agent) and confirms the order with the user.
order_confirmation_agent = Agent(
name='order_confirmation_agent',
instruction=(
'Confirm the pizza order with the user. The order is in the state'
' variable `pizza_order`. First, use the `calculate_price` tool to get'
' the price. Then, summarize the order details from {pizza_order} and'
' include the price in your summary. For example: "You ordered a large'
' thin crust pizza with pepperoni and mushrooms. The total price is'
' $15.00."'
),
tools=[calculate_price],
)
# 6. Define the root agent as a sequential agent.
# This agent directs the conversation by running its sub-agents in order.
root_agent = SequentialAgent(
name='pizza_ordering_agent',
sub_agents=[
order_intake_agent,
order_confirmation_agent,
],
description=(
'This agent is used to order pizza. It will ask the user for their'
' pizza order and then confirm the order with the user.'
),
)
@@ -0,0 +1,68 @@
# Copyright 2026 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 asyncio
import time
import agent
from dotenv import load_dotenv
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
from google.adk.sessions.session import Session
from google.genai import types
load_dotenv(override=True)
logs.log_to_tmp_folder()
async def main():
"""Runs the pizza ordering agent."""
app_name = 'pizza_app'
user_id = 'user1'
runner = InMemoryRunner(
agent=agent.root_agent,
app_name=app_name,
)
session = await runner.session_service.create_session(
app_name=app_name, user_id=user_id
)
async def run_prompt(session: Session, new_message: str):
content = types.Content(
role='user', parts=[types.Part.from_text(text=new_message)]
)
print(f'** User says: {new_message}')
async for event in runner.run_async(
user_id=user_id,
session_id=session.id,
new_message=content,
):
if event.content and event.content.parts and event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
start_time = time.time()
print('Start time:', time.ctime(start_time))
print('------------------------------------')
await run_prompt(
session,
"I'd like a large pizza with pepperoni and mushrooms on a thin crust.",
)
print('------------------------------------')
end_time = time.time()
print('End time:', time.ctime(end_time))
print(f'Total time: {end_time - start_time:.2f} seconds')
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,112 @@
# Workflow Triage Sample
This sample demonstrates how to build a multi-agent workflow that intelligently triages incoming requests and delegates them to appropriate specialized agents.
## Overview
The workflow consists of three main components:
1. **Execution Manager Agent** (`agent.py`) - Analyzes user input and determines which execution agents are relevant
1. **Plan Execution Agent** - Sequential agent that coordinates execution and summarization
1. **Worker Execution Agents** (`execution_agent.py`) - Specialized agents that execute specific tasks in parallel
## Architecture
### Execution Manager Agent (`root_agent`)
- **Model**: gemini-2.5-flash
- **Name**: `execution_manager_agent`
- **Role**: Analyzes user requests and updates the execution plan
- **Tools**: `update_execution_plan` - Updates which execution agents should be activated
- **Sub-agents**: Delegates to `plan_execution_agent` for actual task execution
- **Clarification**: Asks for clarification if user intent is unclear before proceeding
### Plan Execution Agent
- **Type**: SequentialAgent
- **Name**: `plan_execution_agent`
- **Components**:
- `worker_parallel_agent` (ParallelAgent) - Runs relevant agents in parallel
- `execution_summary_agent` - Summarizes the execution results
### Worker Agents
The system includes two specialized execution agents that run in parallel:
- **Code Agent** (`code_agent`): Handles code generation tasks
- Uses `before_agent_callback_check_relevance` to skip if not relevant
- Output stored in `code_agent_output` state key
- **Math Agent** (`math_agent`): Performs mathematical calculations
- Uses `before_agent_callback_check_relevance` to skip if not relevant
- Output stored in `math_agent_output` state key
### Execution Summary Agent
- **Model**: gemini-2.5-flash
- **Name**: `execution_summary_agent`
- **Role**: Summarizes outputs from all activated agents
- **Dynamic Instructions**: Generated based on which agents were activated
- **Content Inclusion**: Set to "none" to focus on summarization
## Key Features
- **Dynamic Agent Selection**: Automatically determines which agents are needed based on user input
- **Parallel Execution**: Multiple relevant agents can work simultaneously via `ParallelAgent`
- **Relevance Filtering**: Agents skip execution if they're not relevant to the current state using callback mechanism
- **Stateful Workflow**: Maintains execution state through `ToolContext`
- **Execution Summarization**: Automatically summarizes results from all activated agents
- **Sequential Coordination**: Uses `SequentialAgent` to ensure proper execution flow
## Usage
The workflow follows this pattern:
1. User provides input to the root agent (`execution_manager_agent`)
1. Manager analyzes the request and identifies relevant agents (`code_agent`, `math_agent`)
1. If user intent is unclear, manager asks for clarification before proceeding
1. Manager updates the execution plan using `update_execution_plan`
1. Control transfers to `plan_execution_agent`
1. `worker_parallel_agent` (ParallelAgent) runs only relevant agents based on the updated plan
1. `execution_summary_agent` summarizes the results from all activated agents
### Example Queries
**Vague requests requiring clarification:**
```
> hi
> Help me do this.
```
The root agent (`execution_manager_agent`) will greet the user and ask for clarification about their specific task.
**Math-only requests:**
```
> What's 1+1?
```
Only the `math_agent` executes while `code_agent` is skipped.
**Multi-domain requests:**
```
> What's 1+11? Write a python function to verify it.
```
Both `code_agent` and `math_agent` execute in parallel, followed by summarization.
## Available Execution Agents
- `code_agent` - For code generation and programming tasks
- `math_agent` - For mathematical computations and analysis
## Implementation Details
- Uses Google ADK agents framework
- Implements callback-based relevance checking via `before_agent_callback_check_relevance`
- Maintains state through `ToolContext` and state keys
- Supports parallel agent execution with `ParallelAgent`
- Uses `SequentialAgent` for coordinated execution flow
- Dynamic instruction generation for summary agent based on activated agents
- Agent outputs stored in state with `{agent_name}_output` keys
+15
View File
@@ -0,0 +1,15 @@
# Copyright 2026 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 . import agent
+56
View File
@@ -0,0 +1,56 @@
# Copyright 2026 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.agents.llm_agent import Agent
from google.adk.tools.tool_context import ToolContext
from . import execution_agent
def update_execution_plan(
execution_agents: list[str], tool_context: ToolContext
) -> str:
"""Updates the execution plan for the agents to run."""
tool_context.state["execution_agents"] = execution_agents
return "execution_agents updated."
root_agent = Agent(
name="execution_manager_agent",
instruction="""\
You are the Execution Manager Agent, responsible for setting up execution plan and delegate to plan_execution_agent for the actual plan execution.
You ONLY have the following worker agents: `code_agent`, `math_agent`.
You should do the following:
1. Analyze the user input and decide any worker agents that are relevant;
2. If none of the worker agents are relevant, you should explain to user that no relevant agents are available and ask for something else;
3. Update the execution plan with the relevant worker agents using `update_execution_plan` tool.
4. Transfer control to the plan_execution_agent for the actual plan execution.
When calling the `update_execution_plan` tool, you should pass the list of worker agents that are relevant to user's input.
NOTE:
* If you are not clear about user's intent, you should ask for clarification first;
* Only after you're clear about user's intent, you can proceed to step #3.
""",
sub_agents=[
execution_agent.plan_execution_agent,
],
tools=[update_execution_plan],
)
@@ -0,0 +1,116 @@
# Copyright 2026 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 typing import Optional
from google.adk.agents import Agent
from google.adk.agents import ParallelAgent
from google.adk.agents.base_agent import BeforeAgentCallback
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.agents.sequential_agent import SequentialAgent
from google.genai import types
def before_agent_callback_check_relevance(
agent_name: str,
) -> BeforeAgentCallback:
"""Callback to check if the state is relevant before executing the agent."""
def callback(callback_context: CallbackContext) -> Optional[types.Content]:
"""Check if the state is relevant."""
if agent_name not in callback_context.state["execution_agents"]:
return types.Content(
parts=[
types.Part(
text=(
f"Skipping execution agent {agent_name} as it is not"
" relevant to the current state."
)
)
]
)
return callback
code_agent = Agent(
name="code_agent",
instruction="""\
You are the Code Agent, responsible for generating code.
NOTE: You should only generate code and ignore other askings from the user.
""",
before_agent_callback=before_agent_callback_check_relevance("code_agent"),
output_key="code_agent_output",
)
math_agent = Agent(
name="math_agent",
instruction="""\
You are the Math Agent, responsible for performing mathematical calculations.
NOTE: You should only perform mathematical calculations and ignore other askings from the user.
""",
before_agent_callback=before_agent_callback_check_relevance("math_agent"),
output_key="math_agent_output",
)
worker_parallel_agent = ParallelAgent(
name="worker_parallel_agent",
sub_agents=[
code_agent,
math_agent,
],
)
def instruction_provider_for_execution_summary_agent(
readonly_context: ReadonlyContext,
) -> str:
"""Provides the instruction for the execution agent."""
activated_agents = readonly_context.state["execution_agents"]
prompt = f"""\
You are the Execution Summary Agent, responsible for summarizing the execution of the plan in the current invocation.
In this invocation, the following agents were involved: {', '.join(activated_agents)}.
Below are their outputs:
"""
for agent_name in activated_agents:
output = readonly_context.state.get(f"{agent_name}_output", "")
prompt += f"\n\n{agent_name} output:\n{output}"
prompt += (
"\n\nPlease summarize the execution of the plan based on the above"
" outputs."
)
return prompt.strip()
execution_summary_agent = Agent(
name="execution_summary_agent",
instruction=instruction_provider_for_execution_summary_agent,
include_contents="none",
)
plan_execution_agent = SequentialAgent(
name="plan_execution_agent",
sub_agents=[
worker_parallel_agent,
execution_summary_agent,
],
)