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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,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"
}
}
]
}