chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -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,74 @@
|
||||
# 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 os
|
||||
import random
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.agent_tool import AgentTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.adk.tools.vertex_ai_search_tool import VertexAiSearchTool
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
VERTEXAI_DATASTORE_ID = os.getenv("VERTEXAI_DATASTORE_ID")
|
||||
if not VERTEXAI_DATASTORE_ID:
|
||||
raise ValueError("VERTEXAI_DATASTORE_ID environment variable not set")
|
||||
|
||||
|
||||
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 "rolls" not in tool_context.state:
|
||||
tool_context.state["rolls"] = []
|
||||
|
||||
tool_context.state["rolls"] = tool_context.state["rolls"] + [result]
|
||||
return result
|
||||
|
||||
|
||||
vertex_ai_search_agent = Agent(
|
||||
model="gemini-3-flash-preview",
|
||||
name="vertex_ai_search_agent",
|
||||
description="An agent for performing Vertex AI search.",
|
||||
tools=[
|
||||
VertexAiSearchTool(
|
||||
data_store_id=VERTEXAI_DATASTORE_ID,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-3.1-pro-preview",
|
||||
name="hello_world_agent",
|
||||
description="A hello world agent with multiple tools.",
|
||||
instruction="""
|
||||
You are a helpful assistant which can help user to roll dice and search for information.
|
||||
- Use `roll_die` tool to roll dice.
|
||||
- Use `vertex_ai_search_agent` to search for Google Agent Development Kit (ADK) information in the datastore.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
AgentTool(
|
||||
agent=vertex_ai_search_agent, propagate_grounding_metadata=True
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
This agent is to demonstrate that the built-in google search tool and the
|
||||
VertexAiSearchTool can be used together with other tools, even though the model
|
||||
has the limitation that built-in tool cannot be used by other tools.
|
||||
|
||||
It is achieved by the workarounds added in https://github.com/google/adk-python/blob/4485379a049a5c84583a43c85d444ea1f1ba6f12/src/google/adk/agents/llm_agent.py#L124-L149.
|
||||
|
||||
To run this agent, set the environment variable `VERTEXAI_DATASTORE_ID`
|
||||
(e.g.
|
||||
`projects/{project}/locations/{location}/collections/{collection}/dataStores/{dataStore}`)
|
||||
and use `adk web`.
|
||||
|
||||
You can follow
|
||||
https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-es
|
||||
to set up the datastore.
|
||||
@@ -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,65 @@
|
||||
# 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 os
|
||||
import random
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.google_search_tool import GoogleSearchTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.adk.tools.vertex_ai_search_tool import VertexAiSearchTool
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
VERTEXAI_DATASTORE_ID = os.getenv("VERTEXAI_DATASTORE_ID")
|
||||
if not VERTEXAI_DATASTORE_ID:
|
||||
raise ValueError("VERTEXAI_DATASTORE_ID environment variable not set")
|
||||
|
||||
|
||||
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 "rolls" not in tool_context.state:
|
||||
tool_context.state["rolls"] = []
|
||||
|
||||
tool_context.state["rolls"] = tool_context.state["rolls"] + [result]
|
||||
return result
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
name="hello_world_agent",
|
||||
description="A hello world agent with multiple tools.",
|
||||
instruction="""
|
||||
You are a helpful assistant which can help user to roll dice and search for information.
|
||||
- Use `roll_die` tool to roll dice.
|
||||
- Use `VertexAISearchTool` to search for Google Agent Development Kit (ADK) information in the datastore.
|
||||
- Use `google_search` to search for general information.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
VertexAiSearchTool(
|
||||
data_store_id=VERTEXAI_DATASTORE_ID, bypass_multi_tools_limit=True
|
||||
),
|
||||
GoogleSearchTool(bypass_multi_tools_limit=True),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
# ADK Agent Function Tools Sample
|
||||
|
||||
## Overview
|
||||
|
||||
This sample demonstrates how to create an agent equipped with built-in Python function tools using the **ADK** framework.
|
||||
|
||||
It defines an `Agent` wrapped around two utility functions: `generate_random_number` and `is_even`. The LLM can automatically invoke these underlying Python functions based on user prompts. This sample shows how simple it is to turn raw python methods into actionable capabilities for your agents.
|
||||
|
||||
## Sample Inputs
|
||||
|
||||
- `Give me a random number.`
|
||||
|
||||
- `Give me a random number up to 50, and tell me if it's even.`
|
||||
|
||||
- `Give me a random number and is 44 even?`
|
||||
|
||||
*This will cause parallel tools being called in a single step*
|
||||
|
||||
## Graph
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Agent[Agent: function_tools] --> Tool1[Tool: generate_random_number]
|
||||
Agent --> Tool2[Tool: is_even]
|
||||
```
|
||||
|
||||
## How To
|
||||
|
||||
1. Define standard Python functions with type hints and precise docstrings:
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
def generate_random_number(max_value: int = 100) -> int:
|
||||
"""Generates a random integer between 0 and max_value (inclusive). ..."""
|
||||
return random.randint(0, max_value)
|
||||
|
||||
def is_even(number: int) -> bool:
|
||||
"""Checks if a given number is even. ..."""
|
||||
return number % 2 == 0
|
||||
```
|
||||
|
||||
1. Register the functions directly to the agent's `tools` list during instantiation:
|
||||
|
||||
```python
|
||||
from google.adk.agents import Agent
|
||||
|
||||
root_agent = Agent(
|
||||
name="function_tools",
|
||||
tools=[generate_random_number, is_even],
|
||||
)
|
||||
```
|
||||
@@ -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,58 @@
|
||||
# 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 __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
from google.adk.agents import Agent
|
||||
|
||||
_counter = 0
|
||||
|
||||
|
||||
def generate_random_number(max_value: int = 100) -> int:
|
||||
"""Generates a random integer between 0 and max_value (inclusive).
|
||||
|
||||
Args:
|
||||
max_value: The upper limit for the random number.
|
||||
|
||||
Returns:
|
||||
A random integer between 0 and max_value.
|
||||
"""
|
||||
# Return a growing value in tests to ensure determinism while allowing
|
||||
# multiple calls.
|
||||
if "PYTEST_CURRENT_TEST" in os.environ:
|
||||
global _counter
|
||||
_counter += 1
|
||||
return _counter
|
||||
return random.randint(0, max_value)
|
||||
|
||||
|
||||
def is_even(number: int) -> bool:
|
||||
"""Checks if a given number is even.
|
||||
|
||||
Args:
|
||||
number: The number to check.
|
||||
|
||||
Returns:
|
||||
True if the number is even, False otherwise.
|
||||
"""
|
||||
return number % 2 == 0
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name="function_tools",
|
||||
tools=[generate_random_number, is_even],
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "a random number"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {},
|
||||
"id": "fc-1",
|
||||
"name": "generate_random_number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "generate_random_number",
|
||||
"response": {
|
||||
"result": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Here is a random number: 1\n"
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "a random number and check even"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {},
|
||||
"id": "fc-1",
|
||||
"name": "generate_random_number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "generate_random_number",
|
||||
"response": {
|
||||
"result": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"number": 1
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "is_even"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "is_even",
|
||||
"response": {
|
||||
"result": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "The random number is 1. It is not an even number."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "random number and is 5 even?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {},
|
||||
"id": "fc-1",
|
||||
"name": "generate_random_number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"number": 5
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "is_even"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "generate_random_number",
|
||||
"response": {
|
||||
"result": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "is_even",
|
||||
"response": {
|
||||
"result": false
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "function_tools",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "A random number is 1, and no, 5 is not an even number."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "function_tools@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,65 @@
|
||||
# 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.genai import types
|
||||
|
||||
|
||||
def concat_number_and_string(num: int, s: str) -> str:
|
||||
"""Concatenate a number and a string.
|
||||
|
||||
Args:
|
||||
num: The number to concatenate.
|
||||
s: The string to concatenate.
|
||||
|
||||
Returns:
|
||||
The concatenated string.
|
||||
"""
|
||||
return str(num) + ': ' + s
|
||||
|
||||
|
||||
def write_document(document: str) -> dict[str, str]:
|
||||
"""Write a document."""
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-3-pro-preview',
|
||||
name='hello_world_stream_fc_args',
|
||||
description='Demo agent showcasing streaming function call arguments.',
|
||||
instruction="""
|
||||
You are a helpful assistant.
|
||||
You can use the `concat_number_and_string` tool to concatenate a number and a string.
|
||||
You should always call the concat_number_and_string tool to concatenate a number and a string.
|
||||
You should never concatenate on your own.
|
||||
|
||||
You can use the `write_document` tool to write a document.
|
||||
You should always call the write_document tool to write a document.
|
||||
You should never write a document on your own.
|
||||
""",
|
||||
tools=[
|
||||
concat_number_and_string,
|
||||
write_document,
|
||||
],
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(
|
||||
disable=True,
|
||||
),
|
||||
tool_config=types.ToolConfig(
|
||||
function_calling_config=types.FunctionCallingConfig(
|
||||
stream_function_call_arguments=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
# Long Running Functions
|
||||
|
||||
## Overview
|
||||
|
||||
This sample demonstrates how to use `LongRunningFunctionTool` in ADK to handle operations that take a significant amount of time to complete.
|
||||
|
||||
When a tool is marked as long-running, the framework understands that the function may return a pending status and that the final result will be provided asynchronously. This is useful for tasks like starting a background job, requesting human approval, or any operation where the result is not immediately available.
|
||||
|
||||
## Sample Inputs
|
||||
|
||||
- `Export my data to CSV`
|
||||
|
||||
- `Start a JSON data export`
|
||||
|
||||
- `Export my data to both CSV and JSON simultaneously`
|
||||
|
||||
## Graph
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Agent
|
||||
participant Tool
|
||||
|
||||
User->>Agent: "Export my data to CSV"
|
||||
Agent->>Tool: export_data(export_type="csv")
|
||||
Tool-->>Agent: {"status": "pending", ...}
|
||||
Agent-->>User: "Started csv export. Ticket ID: export-12345"
|
||||
```
|
||||
|
||||
## How To
|
||||
|
||||
To create a long-running function tool:
|
||||
|
||||
1. Define your Python function. It can return a status indicating it is in-progress (e.g., `{"status": "in-progress"}`).
|
||||
1. Wrap the function using `LongRunningFunctionTool(func=your_function)`.
|
||||
1. Pass the wrapped tool to the `Agent`.
|
||||
|
||||
After the initial in-progress response, you can send additional function responses (e.g., containing progress updates like `{"status": "in-progress", "progress": "50%"}`) to the agent. This will trigger another model turn, allowing the agent to report the current progress back to the user.
|
||||
|
||||
In the ADK Web UI, you can send an additional response by hovering over the function response button and selecting 'Send another response' from the menu.
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.long_running_tool import LongRunningFunctionTool
|
||||
def export_data(export_type: str) -> dict[str, str]:
|
||||
# Start async task...
|
||||
return {
|
||||
"status": "in-progress",
|
||||
"progress": "0%",
|
||||
"message": f"Exporting {export_type} data. This may take some time.",
|
||||
}
|
||||
|
||||
|
||||
agent = Agent(
|
||||
name="my_agent",
|
||||
model="gemini-2.5-flash",
|
||||
tools=[LongRunningFunctionTool(func=export_data)],
|
||||
)
|
||||
```
|
||||
@@ -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,44 @@
|
||||
# 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.tools.long_running_tool import LongRunningFunctionTool
|
||||
|
||||
|
||||
def export_data(export_type: str) -> dict[str, str]:
|
||||
"""Exports user data.
|
||||
|
||||
Args:
|
||||
export_type: The type of data to export (e.g., 'csv', 'json').
|
||||
|
||||
Returns:
|
||||
A dict with the status.
|
||||
"""
|
||||
# In a real application, this would kick off a background job.
|
||||
# Here we just return a status.
|
||||
return {
|
||||
"status": "in-progress",
|
||||
"progress": "0%",
|
||||
"message": f"Exporting {export_type} data. This may take some time.",
|
||||
}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name="long_running_functions",
|
||||
instruction="""
|
||||
You are an assistant that can export user data.
|
||||
When the user asks to export data, call the `export_data` tool.
|
||||
""",
|
||||
tools=[LongRunningFunctionTool(func=export_data)],
|
||||
)
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "export to csv"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"export_type": "csv"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "export_data"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-1"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"message": "Exporting csv data. This may take some time.",
|
||||
"progress": "0%",
|
||||
"status": "in-progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I'm exporting your data to CSV. This may take some time. I will let you know when it's done."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"progress": "50%",
|
||||
"status": "in-progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Your data is 50% exported. The export is still in progress."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "export to csv and json"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"export_type": "csv"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "export_data"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"export_type": "json"
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "export_data"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [
|
||||
"fc-1",
|
||||
"fc-2"
|
||||
],
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"message": "Exporting csv data. This may take some time.",
|
||||
"progress": "0%",
|
||||
"status": "in-progress"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"message": "Exporting json data. This may take some time.",
|
||||
"progress": "0%",
|
||||
"status": "in-progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I've started exporting your data to both CSV and JSON formats. This may take some time. I will notify you when it's complete."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"progress": "50%",
|
||||
"status": "in-progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I've started exporting your data to both CSV and JSON formats. This may take some time to complete."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-6",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"status": "completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-7",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I've started exporting your data to CSV and JSON. The JSON export is complete, and the CSV export is 50% complete. I'll let you know when the CSV export is finished."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-8",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "export_data",
|
||||
"response": {
|
||||
"status": "completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-9",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "long_running_functions",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have exported the data to both CSV and JSON formats."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-10",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "long_running_functions@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Output Schema with Tools Sample Agent
|
||||
|
||||
This sample demonstrates how to use structured output (`output_schema`)
|
||||
alongside other tools in an ADK agent. Previously, this combination was not
|
||||
allowed, but now it's supported through a special processor that handles the
|
||||
interaction.
|
||||
|
||||
## How it Works
|
||||
|
||||
The agent combines:
|
||||
|
||||
- **Tools**: `search_wikipedia` and `get_current_year` for gathering
|
||||
information
|
||||
- **Structured Output**: `PersonInfo` schema to ensure consistent response
|
||||
format
|
||||
|
||||
When both `output_schema` and `tools` are specified:
|
||||
|
||||
1. ADK automatically adds a special `set_model_response` tool
|
||||
1. The model can use the regular tools for information gathering
|
||||
1. For the final response, the model uses `set_model_response` with structured
|
||||
data
|
||||
1. ADK extracts and validates the structured response
|
||||
|
||||
## Expected Response Format
|
||||
|
||||
The agent will return information in this structured format for user query
|
||||
|
||||
> Tell me about Albert Einstein.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Albert Einstein",
|
||||
"age": 76,
|
||||
"occupation": "Theoretical Physicist",
|
||||
"location": "Princeton, New Jersey, USA",
|
||||
"biography": "German-born theoretical physicist who developed the theory of relativity..."
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
1. **Tool Usage**: Agent can search Wikipedia and get current year
|
||||
1. **Structured Output**: Response follows strict PersonInfo schema
|
||||
1. **Validation**: ADK validates the response matches the schema
|
||||
1. **Flexibility**: Works with any combination of tools and output schemas
|
||||
@@ -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,117 @@
|
||||
# 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.
|
||||
|
||||
"""Sample agent demonstrating output_schema with tools feature.
|
||||
|
||||
This agent shows how to use structured output (output_schema) alongside
|
||||
other tools. Previously, this combination was not allowed, but now it's
|
||||
supported through a workaround that uses a special set_model_response tool.
|
||||
"""
|
||||
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.tools.google_search_tool import google_search
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
import requests
|
||||
|
||||
|
||||
class PersonInfo(BaseModel):
|
||||
"""Structured information about a person."""
|
||||
|
||||
name: str = Field(description="The person's full name")
|
||||
age: int = Field(description="The person's age in years")
|
||||
occupation: str = Field(description="The person's job or profession")
|
||||
location: str = Field(description="The city and country where they live")
|
||||
biography: str = Field(description="A brief biography of the person")
|
||||
|
||||
|
||||
def search_wikipedia(query: str) -> str:
|
||||
"""Search Wikipedia for information about a topic.
|
||||
|
||||
Args:
|
||||
query: The search query to look up on Wikipedia
|
||||
|
||||
Returns:
|
||||
Summary of the Wikipedia article if found, or error message if not found
|
||||
"""
|
||||
try:
|
||||
# Use Wikipedia API to search for the article
|
||||
search_url = (
|
||||
"https://en.wikipedia.org/api/rest_v1/page/summary/"
|
||||
+ query.replace(" ", "_")
|
||||
)
|
||||
response = requests.get(search_url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return (
|
||||
f"Title: {data.get('title', 'N/A')}\n\nSummary:"
|
||||
f" {data.get('extract', 'No summary available')}"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"Wikipedia article not found for '{query}'. Status code:"
|
||||
f" {response.status_code}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return f"Error searching Wikipedia: {str(e)}"
|
||||
|
||||
|
||||
def get_current_year() -> str:
|
||||
"""Get the current year.
|
||||
|
||||
Returns:
|
||||
The current year as a string
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
return str(datetime.now().year)
|
||||
|
||||
|
||||
# Create the knowledge agent that uses google_search tool.
|
||||
knowledge_agent = LlmAgent(
|
||||
name="knowledge_agent",
|
||||
instruction="""
|
||||
You are a helpful assistant that gathers information about famous people.
|
||||
Use google_search tool to find information about them.
|
||||
Provide the output into a structured response using the PersonInfo format.
|
||||
""",
|
||||
description="""
|
||||
A knowledge agent that gathers information about famous people.
|
||||
""",
|
||||
tools=[google_search],
|
||||
output_schema=PersonInfo,
|
||||
)
|
||||
|
||||
# Create the agent with both output_schema and tools
|
||||
root_agent = LlmAgent(
|
||||
name="person_info_agent",
|
||||
model="gemini-2.5-pro",
|
||||
instruction="""
|
||||
You are a helpful assistant that gathers information about famous people.
|
||||
|
||||
When asked about a person, you should:
|
||||
1. Use the knowledge_agent to find information about politicians
|
||||
2. Use the search_wikipedia tool to find information about other people
|
||||
3. Use the get_current_year tool if you need to calculate ages
|
||||
4. Compile the information into a structured response using the PersonInfo format
|
||||
""".strip(),
|
||||
output_schema=PersonInfo,
|
||||
tools=[
|
||||
search_wikipedia,
|
||||
get_current_year,
|
||||
],
|
||||
sub_agents=[knowledge_agent],
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
# Parallel Function Test Agent
|
||||
|
||||
This agent demonstrates parallel function calling functionality in ADK. It includes multiple tools with different processing times to showcase how parallel execution improves performance compared to sequential execution.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple async tool types**: All functions use proper async patterns for true parallelism
|
||||
- **Thread safety testing**: Tools modify shared state to verify thread-safe operations
|
||||
- **Performance demonstration**: Clear time differences between parallel and sequential execution
|
||||
- **GIL-aware design**: Uses `await asyncio.sleep()` instead of `time.sleep()` to avoid blocking
|
||||
|
||||
## Tools
|
||||
|
||||
1. **get_weather(city)** - Async function, 2-second delay
|
||||
1. **get_currency_rate(from_currency, to_currency)** - Async function, 1.5-second delay
|
||||
1. **calculate_distance(city1, city2)** - Async function, 1-second delay
|
||||
1. **get_population(cities)** - Async function, 0.5 seconds per city
|
||||
|
||||
**Important**: All functions use `await asyncio.sleep()` instead of `time.sleep()` to ensure true parallel execution. Using `time.sleep()` would block Python's GIL and force sequential execution despite asyncio parallelism.
|
||||
|
||||
## Testing Parallel Function Calling
|
||||
|
||||
### Basic Parallel Test
|
||||
|
||||
```
|
||||
Get the weather for New York, London, and Tokyo
|
||||
```
|
||||
|
||||
Expected: 3 parallel get_weather calls (~2 seconds total instead of ~6 seconds sequential)
|
||||
|
||||
### Mixed Function Types Test
|
||||
|
||||
```
|
||||
Get the weather in Paris, the USD to EUR exchange rate, and the distance between New York and London
|
||||
```
|
||||
|
||||
Expected: 3 parallel async calls with different functions (~2 seconds total)
|
||||
|
||||
### Complex Parallel Test
|
||||
|
||||
```
|
||||
Compare New York and London by getting weather, population, and distance between them
|
||||
```
|
||||
|
||||
Expected: Multiple parallel calls combining different data types
|
||||
|
||||
### Performance Comparison Test
|
||||
|
||||
You can test the timing difference by asking for the same information in different ways:
|
||||
|
||||
**Sequential-style request:**
|
||||
|
||||
```
|
||||
First get the weather in New York, then get the weather in London, then get the weather in Tokyo
|
||||
```
|
||||
|
||||
*Expected time: ~6 seconds (2s + 2s + 2s)*
|
||||
|
||||
**Parallel-style request:**
|
||||
|
||||
```
|
||||
Get the weather in New York, London, and Tokyo
|
||||
```
|
||||
|
||||
*Expected time: ~2 seconds (max of parallel 2s delays)*
|
||||
|
||||
The parallel version should be **3x faster** due to concurrent execution.
|
||||
|
||||
## Thread Safety Testing
|
||||
|
||||
All tools modify the agent's state (`tool_context.state`) with request logs including timestamps. This helps verify that:
|
||||
|
||||
- Multiple tools can safely modify state concurrently
|
||||
- No race conditions occur during parallel execution
|
||||
- State modifications are preserved correctly
|
||||
|
||||
## Running the Agent
|
||||
|
||||
```bash
|
||||
# Start the agent in interactive mode
|
||||
adk run contributing/samples/parallel_functions
|
||||
|
||||
# Or use the web interface
|
||||
adk web
|
||||
```
|
||||
|
||||
## Example Queries
|
||||
|
||||
- "Get weather for New York, London, Tokyo, and Paris" *(4 parallel calls, ~2s total)*
|
||||
- "What's the USD to EUR rate and GBP to USD rate?" *(2 parallel calls, ~1.5s total)*
|
||||
- "Compare New York and San Francisco: weather, population, and distance" *(3 parallel calls, ~2s total)*
|
||||
- "Get population data for Tokyo, London, Paris, and Sydney" *(1 call with 4 cities, ~2s total)*
|
||||
- "What's the weather in Paris and the distance from Paris to London?" *(2 parallel calls, ~2s total)*
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### ❌ Problem: Functions still execute sequentially (6+ seconds for 3 weather calls)
|
||||
|
||||
**Root Cause**: Using blocking operations like `time.sleep()` in function implementations.
|
||||
|
||||
**Solution**: Always use async patterns:
|
||||
|
||||
```python
|
||||
# ❌ Wrong - blocks the GIL, forces sequential execution
|
||||
def my_tool():
|
||||
time.sleep(2) # Blocks entire event loop
|
||||
|
||||
# ✅ Correct - allows true parallelism
|
||||
async def my_tool():
|
||||
await asyncio.sleep(2) # Non-blocking, parallel-friendly
|
||||
```
|
||||
|
||||
### ✅ Verification: Check execution timing
|
||||
|
||||
- Parallel execution: ~2 seconds for 3 weather calls
|
||||
- Sequential execution: ~6 seconds for 3 weather calls
|
||||
- If you see 6+ seconds, your functions are blocking the GIL
|
||||
@@ -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,242 @@
|
||||
# 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.
|
||||
|
||||
"""Sample agent for testing parallel function calling."""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
|
||||
async def get_weather(city: str, tool_context: ToolContext) -> dict:
|
||||
"""Get the current weather for a city.
|
||||
|
||||
Args:
|
||||
city: The name of the city to get weather for.
|
||||
|
||||
Returns:
|
||||
A dictionary with weather information.
|
||||
"""
|
||||
# Simulate some async processing time (non-blocking)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Mock weather data
|
||||
weather_data = {
|
||||
'New York': {'temp': 72, 'condition': 'sunny', 'humidity': 45},
|
||||
'London': {'temp': 60, 'condition': 'cloudy', 'humidity': 80},
|
||||
'Tokyo': {'temp': 68, 'condition': 'rainy', 'humidity': 90},
|
||||
'San Francisco': {'temp': 65, 'condition': 'foggy', 'humidity': 85},
|
||||
'Paris': {'temp': 58, 'condition': 'overcast', 'humidity': 70},
|
||||
'Sydney': {'temp': 75, 'condition': 'sunny', 'humidity': 60},
|
||||
}
|
||||
|
||||
result = weather_data.get(
|
||||
city,
|
||||
{
|
||||
'temp': 70,
|
||||
'condition': 'unknown',
|
||||
'humidity': 50,
|
||||
'note': (
|
||||
f'Weather data not available for {city}, showing default values'
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# Store in context for testing thread safety
|
||||
if 'weather_requests' not in tool_context.state:
|
||||
tool_context.state['weather_requests'] = []
|
||||
tool_context.state['weather_requests'].append(
|
||||
{'city': city, 'result': result}
|
||||
)
|
||||
|
||||
return {
|
||||
'city': city,
|
||||
'temperature': result['temp'],
|
||||
'condition': result['condition'],
|
||||
'humidity': result['humidity'],
|
||||
**({'note': result['note']} if 'note' in result else {}),
|
||||
}
|
||||
|
||||
|
||||
async def get_currency_rate(
|
||||
from_currency: str, to_currency: str, tool_context: ToolContext
|
||||
) -> dict:
|
||||
"""Get the exchange rate between two currencies.
|
||||
|
||||
Args:
|
||||
from_currency: The source currency code (e.g., 'USD').
|
||||
to_currency: The target currency code (e.g., 'EUR').
|
||||
|
||||
Returns:
|
||||
A dictionary with exchange rate information.
|
||||
"""
|
||||
# Simulate async processing time
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# Mock exchange rates
|
||||
rates = {
|
||||
('USD', 'EUR'): 0.85,
|
||||
('USD', 'GBP'): 0.75,
|
||||
('USD', 'JPY'): 110.0,
|
||||
('EUR', 'USD'): 1.18,
|
||||
('EUR', 'GBP'): 0.88,
|
||||
('GBP', 'USD'): 1.33,
|
||||
('GBP', 'EUR'): 1.14,
|
||||
('JPY', 'USD'): 0.009,
|
||||
}
|
||||
|
||||
rate = rates.get((from_currency, to_currency), 1.0)
|
||||
|
||||
# Store in context for testing thread safety
|
||||
if 'currency_requests' not in tool_context.state:
|
||||
tool_context.state['currency_requests'] = []
|
||||
tool_context.state['currency_requests'].append({
|
||||
'from': from_currency,
|
||||
'to': to_currency,
|
||||
'rate': rate,
|
||||
})
|
||||
|
||||
return {
|
||||
'from_currency': from_currency,
|
||||
'to_currency': to_currency,
|
||||
'exchange_rate': rate,
|
||||
}
|
||||
|
||||
|
||||
async def calculate_distance(
|
||||
city1: str, city2: str, tool_context: ToolContext
|
||||
) -> dict:
|
||||
"""Calculate the distance between two cities.
|
||||
|
||||
Args:
|
||||
city1: The first city.
|
||||
city2: The second city.
|
||||
|
||||
Returns:
|
||||
A dictionary with distance information.
|
||||
"""
|
||||
# Simulate async processing time (non-blocking)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Mock distances (in kilometers)
|
||||
city_coords = {
|
||||
'New York': (40.7128, -74.0060),
|
||||
'London': (51.5074, -0.1278),
|
||||
'Tokyo': (35.6762, 139.6503),
|
||||
'San Francisco': (37.7749, -122.4194),
|
||||
'Paris': (48.8566, 2.3522),
|
||||
'Sydney': (-33.8688, 151.2093),
|
||||
}
|
||||
|
||||
# Simple distance calculation (mock)
|
||||
if city1 in city_coords and city2 in city_coords:
|
||||
coord1 = city_coords[city1]
|
||||
coord2 = city_coords[city2]
|
||||
# Simplified distance calculation
|
||||
distance = int(
|
||||
((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2) ** 0.5
|
||||
* 111
|
||||
) # rough km conversion
|
||||
else:
|
||||
distance = 5000 # default distance
|
||||
|
||||
# Store in context for testing thread safety
|
||||
if 'distance_requests' not in tool_context.state:
|
||||
tool_context.state['distance_requests'] = []
|
||||
tool_context.state['distance_requests'].append({
|
||||
'city1': city1,
|
||||
'city2': city2,
|
||||
'distance': distance,
|
||||
})
|
||||
|
||||
return {
|
||||
'city1': city1,
|
||||
'city2': city2,
|
||||
'distance_km': distance,
|
||||
'distance_miles': int(distance * 0.621371),
|
||||
}
|
||||
|
||||
|
||||
async def get_population(cities: List[str], tool_context: ToolContext) -> dict:
|
||||
"""Get population information for multiple cities.
|
||||
|
||||
Args:
|
||||
cities: A list of city names.
|
||||
|
||||
Returns:
|
||||
A dictionary with population data for each city.
|
||||
"""
|
||||
# Simulate async processing time proportional to number of cities (non-blocking)
|
||||
await asyncio.sleep(len(cities) * 0.5)
|
||||
|
||||
# Mock population data
|
||||
populations = {
|
||||
'New York': 8336817,
|
||||
'London': 9648110,
|
||||
'Tokyo': 13960000,
|
||||
'San Francisco': 873965,
|
||||
'Paris': 2161000,
|
||||
'Sydney': 5312163,
|
||||
}
|
||||
|
||||
results = {}
|
||||
for city in cities:
|
||||
results[city] = populations.get(city, 1000000) # default 1M if not found
|
||||
|
||||
# Store in context for testing thread safety
|
||||
if 'population_requests' not in tool_context.state:
|
||||
tool_context.state['population_requests'] = []
|
||||
tool_context.state['population_requests'].append(
|
||||
{'cities': cities, 'results': results}
|
||||
)
|
||||
|
||||
return {
|
||||
'populations': results,
|
||||
'total_population': sum(results.values()),
|
||||
'cities_count': len(cities),
|
||||
}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name='parallel_function_test_agent',
|
||||
description=(
|
||||
'Agent for testing parallel function calling performance and thread'
|
||||
' safety.'
|
||||
),
|
||||
instruction="""
|
||||
You are a helpful assistant that can provide information about weather, currency rates,
|
||||
distances between cities, and population data. You have access to multiple tools and
|
||||
should use them efficiently.
|
||||
|
||||
When users ask for information about multiple cities or multiple types of data,
|
||||
you should call multiple functions in parallel to provide faster responses.
|
||||
|
||||
For example:
|
||||
- If asked about weather in multiple cities, call get_weather for each city in parallel
|
||||
- If asked about weather and currency rates, call both functions in parallel
|
||||
- If asked to compare cities, you might need weather, population, and distance data in parallel
|
||||
|
||||
Always aim to be efficient and call multiple functions simultaneously when possible.
|
||||
Be informative and provide clear, well-structured responses.
|
||||
""",
|
||||
tools=[
|
||||
get_weather,
|
||||
get_currency_rate,
|
||||
calculate_distance,
|
||||
get_population,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Tell me about Paris and Sydney: weather, distance between them, and their populations."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "Paris"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "get_weather"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "Sydney"
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "get_weather"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city1": "Paris",
|
||||
"city2": "Sydney"
|
||||
},
|
||||
"id": "fc-3",
|
||||
"name": "calculate_distance"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"cities": [
|
||||
"Paris",
|
||||
"Sydney"
|
||||
]
|
||||
},
|
||||
"id": "fc-4",
|
||||
"name": "get_population"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"stateDelta": {
|
||||
"distance_requests": [
|
||||
{
|
||||
"city1": "Paris",
|
||||
"city2": "Sydney",
|
||||
"distance": 18903
|
||||
}
|
||||
],
|
||||
"population_requests": [
|
||||
{
|
||||
"cities": [
|
||||
"Paris",
|
||||
"Sydney"
|
||||
],
|
||||
"results": {
|
||||
"Paris": 2161000,
|
||||
"Sydney": 5312163
|
||||
}
|
||||
}
|
||||
],
|
||||
"weather_requests": [
|
||||
{
|
||||
"city": "Paris",
|
||||
"result": {
|
||||
"condition": "overcast",
|
||||
"humidity": 70,
|
||||
"temp": 58
|
||||
}
|
||||
},
|
||||
{
|
||||
"city": "Sydney",
|
||||
"result": {
|
||||
"condition": "sunny",
|
||||
"humidity": 60,
|
||||
"temp": 75
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"city": "Paris",
|
||||
"condition": "overcast",
|
||||
"humidity": 70,
|
||||
"temperature": 58
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"city": "Sydney",
|
||||
"condition": "sunny",
|
||||
"humidity": 60,
|
||||
"temperature": 75
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-3",
|
||||
"name": "calculate_distance",
|
||||
"response": {
|
||||
"city1": "Paris",
|
||||
"city2": "Sydney",
|
||||
"distance_km": 18903,
|
||||
"distance_miles": 11745
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-4",
|
||||
"name": "get_population",
|
||||
"response": {
|
||||
"cities_count": 2,
|
||||
"populations": {
|
||||
"Paris": 2161000,
|
||||
"Sydney": 5312163
|
||||
},
|
||||
"total_population": 7473163
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Here's the information about Paris and Sydney:\n\n**Paris:**\n* **Weather:** The current weather in Paris is overcast with a temperature of 58\u00b0F and 70% humidity.\n* **Population:** The population of Paris is 2,161,000.\n\n**Sydney:**\n* **Weather:** The current weather in Sydney is sunny with a temperature of 75\u00b0F and 60% humidity.\n* **Population:** The population of Sydney is 5,312,163.\n\n**Distance between Paris and Sydney:**\nThe distance between Paris and Sydney is 18,903 km (11,745 miles)."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "What is the weather in Tokyo and the exchange rate from USD to EUR?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "Tokyo"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "get_weather"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"from_currency": "USD",
|
||||
"to_currency": "EUR"
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "get_currency_rate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"stateDelta": {
|
||||
"currency_requests": [
|
||||
{
|
||||
"from": "USD",
|
||||
"rate": 0.85,
|
||||
"to": "EUR"
|
||||
}
|
||||
],
|
||||
"weather_requests": [
|
||||
{
|
||||
"city": "Tokyo",
|
||||
"result": {
|
||||
"condition": "rainy",
|
||||
"humidity": 90,
|
||||
"temp": 68
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"city": "Tokyo",
|
||||
"condition": "rainy",
|
||||
"humidity": 90,
|
||||
"temperature": 68
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "get_currency_rate",
|
||||
"response": {
|
||||
"exchange_rate": 0.85,
|
||||
"from_currency": "USD",
|
||||
"to_currency": "EUR"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "The weather in Tokyo is rainy with a temperature of 68 degrees Fahrenheit and 90% humidity. The exchange rate from USD to EUR is 0.85."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "What is the weather in New York and London?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "New York"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "get_weather"
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "London"
|
||||
},
|
||||
"id": "fc-2",
|
||||
"name": "get_weather"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"actions": {
|
||||
"stateDelta": {
|
||||
"weather_requests": [
|
||||
{
|
||||
"city": "New York",
|
||||
"result": {
|
||||
"condition": "sunny",
|
||||
"humidity": 45,
|
||||
"temp": 72
|
||||
}
|
||||
},
|
||||
{
|
||||
"city": "London",
|
||||
"result": {
|
||||
"condition": "cloudy",
|
||||
"humidity": 80,
|
||||
"temp": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"city": "New York",
|
||||
"condition": "sunny",
|
||||
"humidity": 45,
|
||||
"temperature": 72
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-2",
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"city": "London",
|
||||
"condition": "cloudy",
|
||||
"humidity": 80,
|
||||
"temperature": 60
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "parallel_function_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "The weather in New York is sunny with a temperature of 72 degrees Fahrenheit and 45% humidity. In London, it is cloudy with a temperature of 60 degrees Fahrenheit and 80% humidity."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "parallel_function_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
# Pydantic Argument Sample Agent
|
||||
|
||||
This sample demonstrates the automatic Pydantic model conversion feature in ADK FunctionTool.
|
||||
|
||||
## What This Demonstrates
|
||||
|
||||
This sample shows two key features of the Pydantic argument conversion:
|
||||
|
||||
### 1. Optional Type Handling
|
||||
|
||||
The `create_full_user_account` function demonstrates `Optional[PydanticModel]` conversion:
|
||||
|
||||
Before the fix, Optional parameters required manual conversion:
|
||||
|
||||
```python
|
||||
def create_full_user_account(
|
||||
profile: UserProfile,
|
||||
preferences: Optional[UserPreferences] = None
|
||||
) -> dict:
|
||||
# Manual conversion needed:
|
||||
if not isinstance(profile, UserProfile):
|
||||
profile = UserProfile.model_validate(profile)
|
||||
|
||||
if preferences is not None and not isinstance(preferences, UserPreferences):
|
||||
preferences = UserPreferences.model_validate(preferences)
|
||||
|
||||
# Your function logic here...
|
||||
```
|
||||
|
||||
**After the fix**, Union/Optional Pydantic models are handled automatically:
|
||||
|
||||
```python
|
||||
def create_full_user_account(
|
||||
profile: UserProfile,
|
||||
preferences: Optional[UserPreferences] = None
|
||||
) -> dict:
|
||||
# Both profile and preferences are guaranteed to be proper instances!
|
||||
# profile: UserProfile instance (converted from JSON)
|
||||
# preferences: UserPreferences instance OR None (converted from JSON or kept as None)
|
||||
return {"profile": profile.name, "theme": preferences.theme if preferences else "default"}
|
||||
```
|
||||
|
||||
### 2. Union Type Handling
|
||||
|
||||
The `create_entity_profile` function demonstrates `Union[PydanticModel1, PydanticModel2]` conversion:
|
||||
|
||||
**Before the fix**, Union types required complex manual type checking:
|
||||
|
||||
```python
|
||||
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
|
||||
# Manual conversion needed:
|
||||
if isinstance(entity, dict):
|
||||
# Try to determine which model to use and convert manually
|
||||
if 'company_name' in entity:
|
||||
entity = CompanyProfile.model_validate(entity)
|
||||
elif 'name' in entity:
|
||||
entity = UserProfile.model_validate(entity)
|
||||
else:
|
||||
raise ValueError("Cannot determine entity type")
|
||||
# Your function logic here...
|
||||
```
|
||||
|
||||
**After the fix**, Union Pydantic models are handled automatically:
|
||||
|
||||
```python
|
||||
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
|
||||
# entity is guaranteed to be either UserProfile or CompanyProfile instance!
|
||||
# The LLM sends appropriate JSON structure, and it gets converted
|
||||
# to the correct Pydantic model based on JSON schema matching
|
||||
if isinstance(entity, UserProfile):
|
||||
return {"type": "user", "name": entity.name}
|
||||
else: # CompanyProfile
|
||||
return {"type": "company", "name": entity.company_name}
|
||||
```
|
||||
|
||||
## How to Run
|
||||
|
||||
1. **Set up API credentials** (choose one):
|
||||
|
||||
**Option A: Google AI API**
|
||||
|
||||
```bash
|
||||
export GOOGLE_GENAI_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
**Option B: Vertex AI (requires Google Cloud project)**
|
||||
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
export GOOGLE_CLOUD_LOCATION="us-central1"
|
||||
```
|
||||
|
||||
1. **Run the sample**:
|
||||
|
||||
```bash
|
||||
cd contributing/samples
|
||||
python -m pydantic_argument.main
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
The agent will be prompted to create user profiles and accounts, demonstrating automatic Pydantic model conversion.
|
||||
|
||||
### Test Scenarios:
|
||||
|
||||
1. **Full Account with Preferences (Optional Type)**:
|
||||
|
||||
- **Input**: "Create an account for Alice, 25 years old, with dark theme and Spanish language preferences"
|
||||
- **Tool Called**: `create_full_user_account(profile=UserProfile(...), preferences=UserPreferences(...))`
|
||||
- **Conversion**: Two JSON dicts → `UserProfile` + `UserPreferences` instances
|
||||
|
||||
1. **Account with Different Preferences (Optional Type)**:
|
||||
|
||||
- **Input**: "Create a user account for Bob, age 30, with light theme, French language, and notifications disabled"
|
||||
- **Tool Called**: `create_full_user_account(profile=UserProfile(...), preferences=UserPreferences(...))`
|
||||
- **Conversion**: Two JSON dicts → `UserProfile` + `UserPreferences` instances
|
||||
|
||||
1. **Account with Default Preferences (Optional Type)**:
|
||||
|
||||
- **Input**: "Make an account for Charlie, 28 years old, but use default preferences"
|
||||
- **Tool Called**: `create_full_user_account(profile=UserProfile(...), preferences=None)`
|
||||
- **Conversion**: JSON dict → `UserProfile`, None → None (Optional handling)
|
||||
|
||||
1. **Company Profile Creation (Union Type)**:
|
||||
|
||||
- **Input**: "Create a profile for Tech Corp company, software industry, with 150 employees"
|
||||
- **Tool Called**: `create_entity_profile(entity=CompanyProfile(...))`
|
||||
- **Conversion**: JSON dict → `CompanyProfile` instance (Union type resolution)
|
||||
|
||||
1. **User Profile Creation (Union Type)**:
|
||||
|
||||
- **Input**: "Create an entity profile for Diana, 32 years old"
|
||||
- **Tool Called**: `create_entity_profile(entity=UserProfile(...))`
|
||||
- **Conversion**: JSON dict → `UserProfile` instance (Union type resolution)
|
||||
@@ -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,182 @@
|
||||
# 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.
|
||||
|
||||
"""Simple agent demonstrating Pydantic model arguments in tools."""
|
||||
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
import pydantic
|
||||
|
||||
|
||||
class UserProfile(pydantic.BaseModel):
|
||||
"""A user's profile information."""
|
||||
|
||||
name: str
|
||||
age: int
|
||||
email: Optional[str] = None
|
||||
|
||||
|
||||
class UserPreferences(pydantic.BaseModel):
|
||||
"""A user's preferences."""
|
||||
|
||||
theme: str = "light"
|
||||
language: str = "English"
|
||||
notifications_enabled: bool = True
|
||||
|
||||
|
||||
class CompanyProfile(pydantic.BaseModel):
|
||||
"""A company's profile information."""
|
||||
|
||||
company_name: str
|
||||
industry: str
|
||||
employee_count: int
|
||||
website: Optional[str] = None
|
||||
|
||||
|
||||
def create_full_user_account(
|
||||
profile: UserProfile, preferences: Optional[UserPreferences] = None
|
||||
) -> dict:
|
||||
"""Create a complete user account with profile and optional preferences.
|
||||
|
||||
This function demonstrates Union/Optional Pydantic model handling.
|
||||
The preferences parameter is Optional[UserPreferences], which is
|
||||
internally Union[UserPreferences, None].
|
||||
|
||||
Before the fix, we would need:
|
||||
if preferences is not None and not isinstance(preferences, UserPreferences):
|
||||
preferences = UserPreferences.model_validate(preferences)
|
||||
|
||||
Now the FunctionTool automatically handles this conversion!
|
||||
|
||||
Args:
|
||||
profile: The user's profile information (required)
|
||||
preferences: Optional user preferences (Union[UserPreferences, None])
|
||||
|
||||
Returns:
|
||||
A dictionary containing the complete user account.
|
||||
"""
|
||||
# Use default preferences if not provided
|
||||
if preferences is None:
|
||||
preferences = UserPreferences()
|
||||
|
||||
# Both profile and preferences are guaranteed to be proper Pydantic instances!
|
||||
return {
|
||||
"status": "account_created",
|
||||
"message": f"Full account created for {profile.name}!",
|
||||
"profile": {
|
||||
"name": profile.name,
|
||||
"age": profile.age,
|
||||
"email": profile.email or "Not provided",
|
||||
"profile_type": type(profile).__name__,
|
||||
},
|
||||
"preferences": {
|
||||
"theme": preferences.theme,
|
||||
"language": preferences.language,
|
||||
"notifications_enabled": preferences.notifications_enabled,
|
||||
"preferences_type": type(preferences).__name__,
|
||||
},
|
||||
"conversion_demo": {
|
||||
"profile_converted": "JSON dict → UserProfile instance",
|
||||
"preferences_converted": (
|
||||
"JSON dict → UserPreferences instance"
|
||||
if preferences
|
||||
else "None → default UserPreferences"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
|
||||
"""Create a profile for either a user or a company.
|
||||
|
||||
This function demonstrates Union type handling with multiple Pydantic models.
|
||||
The entity parameter accepts Union[UserProfile, CompanyProfile].
|
||||
|
||||
Before the fix, we would need complex type checking:
|
||||
if isinstance(entity, dict):
|
||||
# Try to determine which model to use and convert manually
|
||||
if 'company_name' in entity:
|
||||
entity = CompanyProfile.model_validate(entity)
|
||||
elif 'name' in entity:
|
||||
entity = UserProfile.model_validate(entity)
|
||||
else:
|
||||
raise ValueError("Cannot determine entity type")
|
||||
|
||||
Now the FunctionTool automatically handles Union type conversion!
|
||||
The LLM will send the appropriate JSON structure, and it gets converted
|
||||
to the correct Pydantic model based on the JSON schema matching.
|
||||
|
||||
Args:
|
||||
entity: Either a UserProfile or CompanyProfile (Union type)
|
||||
|
||||
Returns:
|
||||
A dictionary containing the entity profile information.
|
||||
"""
|
||||
if isinstance(entity, UserProfile):
|
||||
return {
|
||||
"status": "user_profile_created",
|
||||
"entity_type": "user",
|
||||
"message": f"User profile created for {entity.name}!",
|
||||
"profile": {
|
||||
"name": entity.name,
|
||||
"age": entity.age,
|
||||
"email": entity.email or "Not provided",
|
||||
"model_type": type(entity).__name__,
|
||||
},
|
||||
}
|
||||
elif isinstance(entity, CompanyProfile):
|
||||
return {
|
||||
"status": "company_profile_created",
|
||||
"entity_type": "company",
|
||||
"message": f"Company profile created for {entity.company_name}!",
|
||||
"profile": {
|
||||
"company_name": entity.company_name,
|
||||
"industry": entity.industry,
|
||||
"employee_count": entity.employee_count,
|
||||
"website": entity.website or "Not provided",
|
||||
"model_type": type(entity).__name__,
|
||||
},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected entity type: {type(entity)}",
|
||||
}
|
||||
|
||||
|
||||
# Create the agent with all Pydantic tools
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
name="profile_agent",
|
||||
description=(
|
||||
"Helpful assistant that helps creating accounts and profiles for users"
|
||||
" and companies"
|
||||
),
|
||||
instruction="""
|
||||
You are a helpful assistant that can create accounts and profiles for users and companies.
|
||||
|
||||
When someone asks you to create a user account, use `create_full_user_account`.
|
||||
When someone asks you to create a profile and it's unclear whether they mean a user or company, use `create_entity_profile`.
|
||||
When someone specifically mentions a company, use `create_entity_profile`.
|
||||
|
||||
Use the tools with the structured data provided by the user.
|
||||
""",
|
||||
tools=[
|
||||
FunctionTool(create_full_user_account),
|
||||
FunctionTool(create_entity_profile),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simple test script for Pydantic argument agent."""
|
||||
|
||||
# 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 logging
|
||||
|
||||
from google.adk.agents.run_config import RunConfig
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.genai import types
|
||||
from pydantic_argument import agent
|
||||
|
||||
APP_NAME = "pydantic_test_app"
|
||||
USER_ID = "test_user"
|
||||
|
||||
logs.setup_adk_logger(level=logging.INFO)
|
||||
|
||||
|
||||
async def call_agent_async(runner, user_id, session_id, prompt):
|
||||
"""Helper function to call the agent and return response."""
|
||||
content = types.Content(
|
||||
role="user", parts=[types.Part.from_text(text=prompt)]
|
||||
)
|
||||
|
||||
final_response_text = ""
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=content,
|
||||
run_config=RunConfig(save_input_blobs_as_artifacts=False),
|
||||
):
|
||||
if hasattr(event, "content") and event.content:
|
||||
final_response_text += event.content
|
||||
|
||||
return final_response_text
|
||||
|
||||
|
||||
async def main():
|
||||
print("🚀 Testing Pydantic Argument Feature")
|
||||
print("=" * 50)
|
||||
|
||||
runner = InMemoryRunner(
|
||||
agent=agent.root_agent,
|
||||
app_name=APP_NAME,
|
||||
)
|
||||
|
||||
# Create a session
|
||||
session = await runner.session_service.create_session(
|
||||
app_name=APP_NAME, user_id=USER_ID
|
||||
)
|
||||
|
||||
test_prompts = [
|
||||
# Test Optional[Pydantic] type handling (UserProfile + Optional[UserPreferences])
|
||||
(
|
||||
"Create an account for Alice, 25 years old, email: alice@example.com,"
|
||||
" with dark theme and Spanish language preferences"
|
||||
),
|
||||
(
|
||||
"Create a user account for Bob, age 30, no email, "
|
||||
"with light theme, French language, and notifications disabled"
|
||||
),
|
||||
(
|
||||
"Make an account for Charlie, 28 years old, email: charlie@test.com, "
|
||||
"but use default preferences"
|
||||
),
|
||||
# Test Union type handling (Union[UserProfile, CompanyProfile])
|
||||
(
|
||||
"Create a profile for Tech Corp company, software industry, "
|
||||
"with 150 employees and website techcorp.com"
|
||||
),
|
||||
(
|
||||
"Create an entity profile for Diana, 32 years old, "
|
||||
"email diana@example.com"
|
||||
),
|
||||
]
|
||||
|
||||
for i, prompt in enumerate(test_prompts, 1):
|
||||
print(f"\n📝 Test {i}: {prompt}")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
response = await call_agent_async(runner, USER_ID, session.id, prompt)
|
||||
print(f"✅ Response: {response}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✨ Testing complete!")
|
||||
print("🔧 Features demonstrated:")
|
||||
print(" • JSON dict → Pydantic model conversion (UserProfile)")
|
||||
print(" • Optional type handling (Optional[UserPreferences])")
|
||||
print(" • Union type handling (Union[UserProfile, CompanyProfile])")
|
||||
print(" • Automatic model validation and conversion")
|
||||
print(" • No manual isinstance() checks needed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Create a profile for company Acme Corp in tech industry with 50 employees."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"entity": {
|
||||
"company_name": "Acme Corp",
|
||||
"employee_count": 50,
|
||||
"industry": "tech"
|
||||
}
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "create_entity_profile"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "create_entity_profile",
|
||||
"response": {
|
||||
"entity_type": "company",
|
||||
"message": "Company profile created for Acme Corp!",
|
||||
"profile": {
|
||||
"company_name": "Acme Corp",
|
||||
"employee_count": 50,
|
||||
"industry": "tech",
|
||||
"model_type": "CompanyProfile",
|
||||
"website": "Not provided"
|
||||
},
|
||||
"status": "company_profile_created"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have created a profile for Acme Corp in the tech industry with 50 employees."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Create a user account for Alice, age 30, email alice@example.com."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"profile": {
|
||||
"age": 30,
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice"
|
||||
}
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "create_full_user_account"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "create_full_user_account",
|
||||
"response": {
|
||||
"conversion_demo": {
|
||||
"preferences_converted": "JSON dict \u2192 UserPreferences instance",
|
||||
"profile_converted": "JSON dict \u2192 UserProfile instance"
|
||||
},
|
||||
"message": "Full account created for Alice!",
|
||||
"preferences": {
|
||||
"language": "English",
|
||||
"notifications_enabled": true,
|
||||
"preferences_type": "UserPreferences",
|
||||
"theme": "light"
|
||||
},
|
||||
"profile": {
|
||||
"age": 30,
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice",
|
||||
"profile_type": "UserProfile"
|
||||
},
|
||||
"status": "account_created"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have created a user account for Alice, age 30, with the email alice@example.com."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Create a user account for Bob, age 25, with dark theme and French language."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"preferences": {
|
||||
"language": "French",
|
||||
"theme": "dark"
|
||||
},
|
||||
"profile": {
|
||||
"age": 25,
|
||||
"name": "Bob"
|
||||
}
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "create_full_user_account"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "create_full_user_account",
|
||||
"response": {
|
||||
"conversion_demo": {
|
||||
"preferences_converted": "JSON dict \u2192 UserPreferences instance",
|
||||
"profile_converted": "JSON dict \u2192 UserProfile instance"
|
||||
},
|
||||
"message": "Full account created for Bob!",
|
||||
"preferences": {
|
||||
"language": "French",
|
||||
"notifications_enabled": true,
|
||||
"preferences_type": "UserPreferences",
|
||||
"theme": "dark"
|
||||
},
|
||||
"profile": {
|
||||
"age": 25,
|
||||
"email": "Not provided",
|
||||
"name": "Bob",
|
||||
"profile_type": "UserProfile"
|
||||
},
|
||||
"status": "account_created"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "profile_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "OK. I have created a user account for Bob, age 25, with a dark theme and French language preference."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "profile_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: research_assistant_agent
|
||||
model: gemini-2.5-flash
|
||||
description: 'research assistant agent that can perform web search and summarize the results.'
|
||||
instruction: |
|
||||
You can perform web search and summarize the results.
|
||||
You should always use the web_search_agent to get the latest information.
|
||||
You should always use the summarizer_agent to summarize the results.
|
||||
tools:
|
||||
- name: AgentTool
|
||||
args:
|
||||
agent:
|
||||
config_path: ./web_search_agent.yaml
|
||||
skip_summarization: False
|
||||
- name: AgentTool
|
||||
args:
|
||||
agent:
|
||||
config_path: ./summarizer_agent.yaml
|
||||
skip_summarization: False
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: summarizer_agent
|
||||
model: gemini-2.5-flash
|
||||
description: 'summarizer agent that can summarize text.'
|
||||
instruction: "Given a text, summarize it."
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: web_search_agent
|
||||
model: gemini-2.5-flash
|
||||
description: 'an agent whose job it is to perform web search and return the results.'
|
||||
instruction: You are an agent whose job is to perform web search and return the results.
|
||||
tools:
|
||||
- name: google_search
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: search_agent
|
||||
model: gemini-2.5-flash
|
||||
description: 'an agent whose job it is to perform Google search queries and answer questions about the results.'
|
||||
instruction: You are an agent whose job is to perform Google search queries and answer questions about the results.
|
||||
tools:
|
||||
- name: google_search
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/adk-python/refs/heads/main/src/google/adk/agents/config_schemas/AgentConfig.json
|
||||
name: hello_world_agent
|
||||
model: gemini-2.5-flash
|
||||
description: 'hello world agent that can roll a dice 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:
|
||||
- name: tool_functions_config.tools.roll_die
|
||||
- name: tool_functions_config.tools.check_prime
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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.tools.tool_context import ToolContext
|
||||
|
||||
|
||||
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."
|
||||
)
|
||||
Reference in New Issue
Block a user