mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
chore: Add sample agent for testing parallel functions execution
PiperOrigin-RevId: 790208057
This commit is contained in:
committed by
Copybara-Service
parent
57cd41f424
commit
90b9193a20
@@ -0,0 +1,103 @@
|
|||||||
|
# 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
|
||||||
|
2. **get_currency_rate(from_currency, to_currency)** - Async function, 1.5-second delay
|
||||||
|
3. **calculate_distance(city1, city2)** - Async function, 1-second delay
|
||||||
|
4. **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 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from . import agent
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# Copyright 2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""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, 'timestamp': time.time(), '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,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'from_currency': from_currency,
|
||||||
|
'to_currency': to_currency,
|
||||||
|
'exchange_rate': rate,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
'timestamp': time.time(),
|
||||||
|
})
|
||||||
|
|
||||||
|
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, 'timestamp': time.time()}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'populations': results,
|
||||||
|
'total_population': sum(results.values()),
|
||||||
|
'cities_count': len(cities),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
root_agent = Agent(
|
||||||
|
model='gemini-2.0-flash',
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user