Skip to content

Commit 90b9193

Browse files
seanzhougooglecopybara-github
authored andcommitted
chore: Add sample agent for testing parallel functions execution
PiperOrigin-RevId: 790208057
1 parent 57cd41f commit 90b9193

File tree

3 files changed

+364
-0
lines changed

3 files changed

+364
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Parallel Function Test Agent
2+
3+
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.
4+
5+
## Features
6+
7+
- **Multiple async tool types**: All functions use proper async patterns for true parallelism
8+
- **Thread safety testing**: Tools modify shared state to verify thread-safe operations
9+
- **Performance demonstration**: Clear time differences between parallel and sequential execution
10+
- **GIL-aware design**: Uses `await asyncio.sleep()` instead of `time.sleep()` to avoid blocking
11+
12+
## Tools
13+
14+
1. **get_weather(city)** - Async function, 2-second delay
15+
2. **get_currency_rate(from_currency, to_currency)** - Async function, 1.5-second delay
16+
3. **calculate_distance(city1, city2)** - Async function, 1-second delay
17+
4. **get_population(cities)** - Async function, 0.5 seconds per city
18+
19+
**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.
20+
21+
## Testing Parallel Function Calling
22+
23+
### Basic Parallel Test
24+
```
25+
Get the weather for New York, London, and Tokyo
26+
```
27+
Expected: 3 parallel get_weather calls (~2 seconds total instead of ~6 seconds sequential)
28+
29+
### Mixed Function Types Test
30+
```
31+
Get the weather in Paris, the USD to EUR exchange rate, and the distance between New York and London
32+
```
33+
Expected: 3 parallel async calls with different functions (~2 seconds total)
34+
35+
### Complex Parallel Test
36+
```
37+
Compare New York and London by getting weather, population, and distance between them
38+
```
39+
Expected: Multiple parallel calls combining different data types
40+
41+
### Performance Comparison Test
42+
You can test the timing difference by asking for the same information in different ways:
43+
44+
**Sequential-style request:**
45+
```
46+
First get the weather in New York, then get the weather in London, then get the weather in Tokyo
47+
```
48+
*Expected time: ~6 seconds (2s + 2s + 2s)*
49+
50+
**Parallel-style request:**
51+
```
52+
Get the weather in New York, London, and Tokyo
53+
```
54+
*Expected time: ~2 seconds (max of parallel 2s delays)*
55+
56+
The parallel version should be **3x faster** due to concurrent execution.
57+
58+
## Thread Safety Testing
59+
60+
All tools modify the agent's state (`tool_context.state`) with request logs including timestamps. This helps verify that:
61+
- Multiple tools can safely modify state concurrently
62+
- No race conditions occur during parallel execution
63+
- State modifications are preserved correctly
64+
65+
## Running the Agent
66+
67+
```bash
68+
# Start the agent in interactive mode
69+
adk run contributing/samples/parallel_functions
70+
71+
# Or use the web interface
72+
adk web
73+
```
74+
75+
## Example Queries
76+
77+
- "Get weather for New York, London, Tokyo, and Paris" *(4 parallel calls, ~2s total)*
78+
- "What's the USD to EUR rate and GBP to USD rate?" *(2 parallel calls, ~1.5s total)*
79+
- "Compare New York and San Francisco: weather, population, and distance" *(3 parallel calls, ~2s total)*
80+
- "Get population data for Tokyo, London, Paris, and Sydney" *(1 call with 4 cities, ~2s total)*
81+
- "What's the weather in Paris and the distance from Paris to London?" *(2 parallel calls, ~2s total)*
82+
83+
## Common Issues and Solutions
84+
85+
### ❌ Problem: Functions still execute sequentially (6+ seconds for 3 weather calls)
86+
87+
**Root Cause**: Using blocking operations like `time.sleep()` in function implementations.
88+
89+
**Solution**: Always use async patterns:
90+
```python
91+
# ❌ Wrong - blocks the GIL, forces sequential execution
92+
def my_tool():
93+
time.sleep(2) # Blocks entire event loop
94+
95+
# ✅ Correct - allows true parallelism
96+
async def my_tool():
97+
await asyncio.sleep(2) # Non-blocking, parallel-friendly
98+
```
99+
100+
### ✅ Verification: Check execution timing
101+
- Parallel execution: ~2 seconds for 3 weather calls
102+
- Sequential execution: ~6 seconds for 3 weather calls
103+
- If you see 6+ seconds, your functions are blocking the GIL
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Sample agent for testing parallel function calling."""
16+
17+
import asyncio
18+
import time
19+
from typing import List
20+
21+
from google.adk import Agent
22+
from google.adk.tools.tool_context import ToolContext
23+
24+
25+
async def get_weather(city: str, tool_context: ToolContext) -> dict:
26+
"""Get the current weather for a city.
27+
28+
Args:
29+
city: The name of the city to get weather for.
30+
31+
Returns:
32+
A dictionary with weather information.
33+
"""
34+
# Simulate some async processing time (non-blocking)
35+
await asyncio.sleep(2)
36+
37+
# Mock weather data
38+
weather_data = {
39+
'New York': {'temp': 72, 'condition': 'sunny', 'humidity': 45},
40+
'London': {'temp': 60, 'condition': 'cloudy', 'humidity': 80},
41+
'Tokyo': {'temp': 68, 'condition': 'rainy', 'humidity': 90},
42+
'San Francisco': {'temp': 65, 'condition': 'foggy', 'humidity': 85},
43+
'Paris': {'temp': 58, 'condition': 'overcast', 'humidity': 70},
44+
'Sydney': {'temp': 75, 'condition': 'sunny', 'humidity': 60},
45+
}
46+
47+
result = weather_data.get(
48+
city,
49+
{
50+
'temp': 70,
51+
'condition': 'unknown',
52+
'humidity': 50,
53+
'note': (
54+
f'Weather data not available for {city}, showing default values'
55+
),
56+
},
57+
)
58+
59+
# Store in context for testing thread safety
60+
if 'weather_requests' not in tool_context.state:
61+
tool_context.state['weather_requests'] = []
62+
tool_context.state['weather_requests'].append(
63+
{'city': city, 'timestamp': time.time(), 'result': result}
64+
)
65+
66+
return {
67+
'city': city,
68+
'temperature': result['temp'],
69+
'condition': result['condition'],
70+
'humidity': result['humidity'],
71+
**({'note': result['note']} if 'note' in result else {}),
72+
}
73+
74+
75+
async def get_currency_rate(
76+
from_currency: str, to_currency: str, tool_context: ToolContext
77+
) -> dict:
78+
"""Get the exchange rate between two currencies.
79+
80+
Args:
81+
from_currency: The source currency code (e.g., 'USD').
82+
to_currency: The target currency code (e.g., 'EUR').
83+
84+
Returns:
85+
A dictionary with exchange rate information.
86+
"""
87+
# Simulate async processing time
88+
await asyncio.sleep(1.5)
89+
90+
# Mock exchange rates
91+
rates = {
92+
('USD', 'EUR'): 0.85,
93+
('USD', 'GBP'): 0.75,
94+
('USD', 'JPY'): 110.0,
95+
('EUR', 'USD'): 1.18,
96+
('EUR', 'GBP'): 0.88,
97+
('GBP', 'USD'): 1.33,
98+
('GBP', 'EUR'): 1.14,
99+
('JPY', 'USD'): 0.009,
100+
}
101+
102+
rate = rates.get((from_currency, to_currency), 1.0)
103+
104+
# Store in context for testing thread safety
105+
if 'currency_requests' not in tool_context.state:
106+
tool_context.state['currency_requests'] = []
107+
tool_context.state['currency_requests'].append({
108+
'from': from_currency,
109+
'to': to_currency,
110+
'rate': rate,
111+
'timestamp': time.time(),
112+
})
113+
114+
return {
115+
'from_currency': from_currency,
116+
'to_currency': to_currency,
117+
'exchange_rate': rate,
118+
'timestamp': time.time(),
119+
}
120+
121+
122+
async def calculate_distance(
123+
city1: str, city2: str, tool_context: ToolContext
124+
) -> dict:
125+
"""Calculate the distance between two cities.
126+
127+
Args:
128+
city1: The first city.
129+
city2: The second city.
130+
131+
Returns:
132+
A dictionary with distance information.
133+
"""
134+
# Simulate async processing time (non-blocking)
135+
await asyncio.sleep(1)
136+
137+
# Mock distances (in kilometers)
138+
city_coords = {
139+
'New York': (40.7128, -74.0060),
140+
'London': (51.5074, -0.1278),
141+
'Tokyo': (35.6762, 139.6503),
142+
'San Francisco': (37.7749, -122.4194),
143+
'Paris': (48.8566, 2.3522),
144+
'Sydney': (-33.8688, 151.2093),
145+
}
146+
147+
# Simple distance calculation (mock)
148+
if city1 in city_coords and city2 in city_coords:
149+
coord1 = city_coords[city1]
150+
coord2 = city_coords[city2]
151+
# Simplified distance calculation
152+
distance = int(
153+
((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2) ** 0.5
154+
* 111
155+
) # rough km conversion
156+
else:
157+
distance = 5000 # default distance
158+
159+
# Store in context for testing thread safety
160+
if 'distance_requests' not in tool_context.state:
161+
tool_context.state['distance_requests'] = []
162+
tool_context.state['distance_requests'].append({
163+
'city1': city1,
164+
'city2': city2,
165+
'distance': distance,
166+
'timestamp': time.time(),
167+
})
168+
169+
return {
170+
'city1': city1,
171+
'city2': city2,
172+
'distance_km': distance,
173+
'distance_miles': int(distance * 0.621371),
174+
}
175+
176+
177+
async def get_population(cities: List[str], tool_context: ToolContext) -> dict:
178+
"""Get population information for multiple cities.
179+
180+
Args:
181+
cities: A list of city names.
182+
183+
Returns:
184+
A dictionary with population data for each city.
185+
"""
186+
# Simulate async processing time proportional to number of cities (non-blocking)
187+
await asyncio.sleep(len(cities) * 0.5)
188+
189+
# Mock population data
190+
populations = {
191+
'New York': 8336817,
192+
'London': 9648110,
193+
'Tokyo': 13960000,
194+
'San Francisco': 873965,
195+
'Paris': 2161000,
196+
'Sydney': 5312163,
197+
}
198+
199+
results = {}
200+
for city in cities:
201+
results[city] = populations.get(city, 1000000) # default 1M if not found
202+
203+
# Store in context for testing thread safety
204+
if 'population_requests' not in tool_context.state:
205+
tool_context.state['population_requests'] = []
206+
tool_context.state['population_requests'].append(
207+
{'cities': cities, 'results': results, 'timestamp': time.time()}
208+
)
209+
210+
return {
211+
'populations': results,
212+
'total_population': sum(results.values()),
213+
'cities_count': len(cities),
214+
}
215+
216+
217+
root_agent = Agent(
218+
model='gemini-2.0-flash',
219+
name='parallel_function_test_agent',
220+
description=(
221+
'Agent for testing parallel function calling performance and thread'
222+
' safety.'
223+
),
224+
instruction="""
225+
You are a helpful assistant that can provide information about weather, currency rates,
226+
distances between cities, and population data. You have access to multiple tools and
227+
should use them efficiently.
228+
229+
When users ask for information about multiple cities or multiple types of data,
230+
you should call multiple functions in parallel to provide faster responses.
231+
232+
For example:
233+
- If asked about weather in multiple cities, call get_weather for each city in parallel
234+
- If asked about weather and currency rates, call both functions in parallel
235+
- If asked to compare cities, you might need weather, population, and distance data in parallel
236+
237+
Always aim to be efficient and call multiple functions simultaneously when possible.
238+
Be informative and provide clear, well-structured responses.
239+
""",
240+
tools=[
241+
get_weather,
242+
get_currency_rate,
243+
calculate_distance,
244+
get_population,
245+
],
246+
)

0 commit comments

Comments
 (0)