You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20c30d5819 | |||
| 83e5df7862 | |||
| 25b2806301 | |||
| f6a022cda3 | |||
| e0a8355219 | |||
| d9ce2e691c | |||
| dc193f7969 | |||
| 6277dae749 | |||
| ef837015f3 | |||
| 54cc849de7 | |||
| e73d71d324 | |||
| e528749a1c | |||
| 1686cc57c2 | |||
| 53803522b6 | |||
| e3c2bf3062 | |||
| 2fff882fb0 | |||
| a3b31ca950 | |||
| 8dc0c949af | |||
| 71fbc9275b | |||
| 423542a43f | |||
| 37dae9b631 | |||
| 0e28d64712 | |||
| 6da6c2a44c | |||
| 8ef2177658 | |||
| 97318bcd19 | |||
| 283303032a | |||
| e369c283b3 | |||
| 74589a1db7 | |||
| e41dbccf7f | |||
| d620bcb384 | |||
| 90b9193a20 | |||
| 57cd41f424 | |||
| 7556ebc76a | |||
| 2bb20411f4 | |||
| 86a44873e9 | |||
| faadef167e | |||
| bead607364 | |||
| 041f04e89c | |||
| 16a15c8709 | |||
| 9656ccc407 | |||
| db975dfe2a |
@@ -0,0 +1,44 @@
|
||||
name: ADK Answering Agent for Discussions
|
||||
|
||||
on:
|
||||
discussion:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
agent-answer-questions:
|
||||
if: github.event.discussion.category.name == 'Q&A'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
id: auth
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
credentials_json: '${{ secrets.ADK_GCP_SA_KEY }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install google-adk
|
||||
|
||||
- name: Run Answering Script
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }}
|
||||
VERTEXAI_DATASTORE_ID: ${{ secrets.VERTEXAI_DATASTORE_ID }}
|
||||
GOOGLE_GENAI_USE_VERTEXAI: 1
|
||||
OWNER: 'google'
|
||||
REPO: 'adk-python'
|
||||
INTERACTIVE: 0
|
||||
DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
|
||||
PYTHONPATH: contributing/samples
|
||||
run: python -m adk_answering_agent.main
|
||||
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## [1.10.0](https://github.com/google/adk-python/compare/v1.9.0...v1.10.0) (2025-08-07)
|
||||
|
||||
### Features
|
||||
|
||||
* [Live] Implement Live Session Resumption ([71fbc92](https://github.com/google/adk-python/commit/71fbc9275b3d74700ec410cb4155ba0cb18580b7))
|
||||
* [Tool] Support parallel execution of parallel function calls ([57cd41f](https://github.com/google/adk-python/commit/57cd41f424b469fb834bb8f2777b5f7be9aa6cdf))
|
||||
* [Models] Allow max tokens to be customizable in Claude ([7556ebc](https://github.com/google/adk-python/commit/7556ebc76abd3c776922c2803aed831661cf7f82))
|
||||
* [Tool] Create enterprise_web_search_tool as a tool instance ([0e28d64](https://github.com/google/adk-python/commit/0e28d64712e481cfd3b964be0166f529657024f6))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix shared default plugin manager and cost manager instances among multiple invocations ([423542a](https://github.com/google/adk-python/commit/423542a43fb8316195e9f79d97f87593751bebd3))
|
||||
* Correct the type annotation in anthropic_llm implementation ([97318bc](https://github.com/google/adk-python/commit/97318bcd199acdacadfe8664da3fbfc3c806cdd2))
|
||||
* Fix adk deploy cloud_run cli, which was broken in v1.9.0 ([e41dbcc](https://github.com/google/adk-python/commit/e41dbccf7f610e249108f9321f60f71fe2cc10f4))
|
||||
* Remove thoughts from contents in llm requests from history contents ([d620bcb](https://github.com/google/adk-python/commit/d620bcb384d3068228ea2059fb70274e68e69682))
|
||||
* Annotate response type as None for transfer_to_agent tool ([86a4487](https://github.com/google/adk-python/commit/86a44873e9b2dfc7e62fa31a9ac3be57c0bbff7b))
|
||||
* Fix incompatible a2a sdk changes ([faadef1](https://github.com/google/adk-python/commit/faadef167ee8e4dd1faf4da5685a577c3155556e))
|
||||
* Fix adk cli options and method parameters mismatching ([8ef2177](https://github.com/google/adk-python/commit/8ef2177658fbfc74b1a74b0c3ea8150bae866796))
|
||||
|
||||
### Improvements
|
||||
|
||||
* Add Github workflow config for the ADK Answering agent ([8dc0c94](https://github.com/google/adk-python/commit/8dc0c949afb9024738ff7ac1b2c19282175c3200))
|
||||
* Import AGENT_CARD_WELL_KNOWN_PATH from adk instead of from a2a directly ([37dae9b](https://github.com/google/adk-python/commit/37dae9b631db5060770b66fce0e25cf0ffb56948))
|
||||
* Make `LlmRequest.LiveConnectConfig` field default to a factory ([74589a1](https://github.com/google/adk-python/commit/74589a1db7df65e319d1ad2f0676ee0cf5d6ec1d))
|
||||
* Update the prompt to make the ADK Answering Agent more objective ([2833030](https://github.com/google/adk-python/commit/283303032a174d51b8d72f14df83c794d66cb605))
|
||||
* Add sample agent for testing parallel functions execution ([90b9193](https://github.com/google/adk-python/commit/90b9193a20499b8dd7f57d119cda4c534fcfda10))
|
||||
* Hide the ask_data_insights tool until the API is publicly available ([bead607](https://github.com/google/adk-python/commit/bead607364be7ac8109357c9d3076d9b345e9e8a))
|
||||
* Change `LlmRequest.config`'s default value to be `types.GenerateContentConfig()` ([041f04e](https://github.com/google/adk-python/commit/041f04e89cee30532facccce4900d10f1b8c69ce))
|
||||
* Prevent triggering of _load_from_yaml_config in AgentLoader ([db975df](https://github.com/google/adk-python/commit/db975dfe2a09a6d056d02bc03c1247ac10f6da7d))
|
||||
|
||||
### Documentation
|
||||
|
||||
* Fix typos ([16a15c8](https://github.com/google/adk-python/commit/16a15c8709b47c9bebe7cffe888e8e7e48ec605a))
|
||||
|
||||
|
||||
## [1.9.0](https://github.com/google/adk-python/compare/v1.8.0...v1.9.0) (2025-07-31)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
|
||||
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
|
||||
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
|
||||
|
||||
root_agent = RemoteA2aAgent(
|
||||
|
||||
@@ -137,6 +137,12 @@ def add_comment_to_discussion(
|
||||
}
|
||||
}
|
||||
"""
|
||||
comment_body = (
|
||||
"**Response from ADK Answering Agent (experimental, answer may be"
|
||||
" inaccurate)**\n\n"
|
||||
+ comment_body
|
||||
)
|
||||
|
||||
variables = {"discussionId": discussion_id, "body": comment_body}
|
||||
try:
|
||||
response = run_graphql_query(query, variables)
|
||||
@@ -247,10 +253,8 @@ root_agent = Agent(
|
||||
* {APPROVAL_INSTRUCTION}
|
||||
* Your response should be based on the information you found in the document store. Do not invent
|
||||
information that is not in the document store. Do not invent citations which are not in the document store.
|
||||
* **Be Objective**: your answer should be based on the facts you found in the document store, do not be misled by user's assumptions or user's understanding of ADK.
|
||||
* If you can't find the answer or information in the document store, **do not** respond.
|
||||
* Include a bolded note (e.g. "Response from ADK Answering Agent") in your comment
|
||||
to indicate this comment was added by an ADK Answering Agent.
|
||||
* Have an empty line between the note and the rest of your response.
|
||||
* Inlclude a short summary of your response in the comment as a TLDR, e.g. "**TLDR**: <your summary>".
|
||||
* Have a divider line between the TLDR and your detail response.
|
||||
* Do not respond to any other discussion except the one specified by the user.
|
||||
|
||||
@@ -25,16 +25,6 @@ distributed via the `google.adk.tools.bigquery` module. These tools include:
|
||||
|
||||
Runs a SQL query in BigQuery.
|
||||
|
||||
1. `ask_data_insights`
|
||||
|
||||
Natural language-in, natural language-out tool that answers questions
|
||||
about structured data in BigQuery. Provides a one-stop solution for generating
|
||||
insights from data.
|
||||
|
||||
**Note**: This tool requires additional setup in your project. Please refer to
|
||||
the official [Conversational Analytics API documentation](https://cloud.google.com/gemini/docs/conversational-analytics-api/overview)
|
||||
for instructions.
|
||||
|
||||
## How to use
|
||||
|
||||
Set up environment variables in your `.env` file for using
|
||||
|
||||
@@ -100,8 +100,8 @@ def get_current_weather(location: str):
|
||||
|
||||
root_agent = Agent(
|
||||
# find supported models here: https://google.github.io/adk-docs/get-started/streaming/quickstart-streaming/
|
||||
# model='gemini-live-2.5-flash-preview-native-audio', # for Vertex project
|
||||
model="gemini-live-2.5-flash-preview", # for AI studio key
|
||||
model="gemini-2.0-flash-live-preview-04-09", # for Vertex project
|
||||
# model="gemini-live-2.5-flash-preview", # for AI studio key
|
||||
name="root_agent",
|
||||
instruction="""
|
||||
You are a helpful assistant that can check time, roll dice and check if numbers are prime.
|
||||
|
||||
@@ -121,7 +121,9 @@ def stop_streaming(function_name: str):
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-live-2.5-flash-preview",
|
||||
# find supported models here: https://google.github.io/adk-docs/get-started/streaming/quickstart-streaming/
|
||||
model="gemini-2.0-flash-live-preview-04-09", # for Vertex project
|
||||
# model="gemini-live-2.5-flash-preview", # for AI studio key
|
||||
name="video_streaming_agent",
|
||||
instruction="""
|
||||
You are a monitoring agent. You can do video monitoring and stock price monitoring
|
||||
|
||||
@@ -217,8 +217,9 @@ import asyncio
|
||||
|
||||
# Create the agent with tool callbacks
|
||||
root_agent = Agent(
|
||||
# find supported models here: https://google.github.io/adk-docs/get-started/streaming/quickstart-streaming/
|
||||
model="gemini-2.0-flash-live-preview-04-09", # for Vertex project
|
||||
# model="gemini-2.0-flash-live-001", # for AI studio key
|
||||
# model="gemini-live-2.5-flash-preview", # for AI studio key
|
||||
name="tool_callbacks_agent",
|
||||
description=(
|
||||
"Live streaming agent that demonstrates tool callbacks functionality. "
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
+3
-3
@@ -14984,16 +14984,16 @@ While you have considerable flexibility in defining your function, remember that
|
||||
|
||||
Designed for tasks that require a significant amount of processing time without blocking the agent's execution. This tool is a subclass of `FunctionTool`.
|
||||
|
||||
When using a `LongRunningFunctionTool`, your function can initiate the long-running operation and optionally return an **initial result**** (e.g. the long-running operation id). Once a long running function tool is invoked the agent runner will pause the agent run and let the agent client to decide whether to continue or wait until the long-running operation finishes. The agent client can query the progress of the long-running operation and send back an intermediate or final response. The agent can then continue with other tasks. An example is the human-in-the-loop scenario where the agent needs human approval before proceeding with a task.
|
||||
When using a `LongRunningFunctionTool`, your function can initiate the long-running operation and optionally return an **initial result** (e.g. the long-running operation id). Once a long running function tool is invoked the agent runner will pause the agent run and let the agent client to decide whether to continue or wait until the long-running operation finishes. The agent client can query the progress of the long-running operation and send back an intermediate or final response. The agent can then continue with other tasks. An example is the human-in-the-loop scenario where the agent needs human approval before proceeding with a task.
|
||||
|
||||
### How it Works
|
||||
|
||||
In Python, you wrap a function with `LongRunningFunctionTool`. In Java, you pass a Method name to `LongRunningFunctionTool.create()`.
|
||||
In Python, you wrap a function with `LongRunningFunctionTool`. In Java, you pass a Method name to `LongRunningFunctionTool.create()`.
|
||||
|
||||
|
||||
1. **Initiation:** When the LLM calls the tool, your function starts the long-running operation.
|
||||
|
||||
2. **Initial Updates:** Your function should optionally return an initial result (e.g. the long-running operaiton id). The ADK framework takes the result and sends it back to the LLM packaged within a `FunctionResponse`. This allows the LLM to inform the user (e.g., status, percentage complete, messages). And then the agent run is ended / paused.
|
||||
2. **Initial Updates:** Your function should optionally return an initial result (e.g. the long-running operation id). The ADK framework takes the result and sends it back to the LLM packaged within a `FunctionResponse`. This allows the LLM to inform the user (e.g., status, percentage complete, messages). And then the agent run is ended / paused.
|
||||
|
||||
3. **Continue or Wait:** After each agent run is completed. Agent client can query the progress of the long-running operation and decide whether to continue the agent run with an intermediate response (to update the progress) or wait until a final response is retrieved. Agent client should send the intermediate or final response back to the agent for the next run.
|
||||
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ dev = [
|
||||
|
||||
a2a = [
|
||||
# go/keep-sorted start
|
||||
"a2a-sdk>=0.2.16,<0.3.0;python_version>='3.10'",
|
||||
"a2a-sdk>=0.3.0,<0.4.0;python_version>='3.10'",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
|
||||
@@ -136,11 +136,11 @@ def build_a2a_request_log(req: SendMessageRequest) -> str:
|
||||
config_log = "None"
|
||||
if req.params.configuration:
|
||||
config_data = {
|
||||
"acceptedOutputModes": req.params.configuration.acceptedOutputModes,
|
||||
"accepted_output_modes": req.params.configuration.accepted_output_modes,
|
||||
"blocking": req.params.configuration.blocking,
|
||||
"historyLength": req.params.configuration.historyLength,
|
||||
"pushNotificationConfig": bool(
|
||||
req.params.configuration.pushNotificationConfig
|
||||
"history_length": req.params.configuration.history_length,
|
||||
"push_notification_config": bool(
|
||||
req.params.configuration.push_notification_config
|
||||
),
|
||||
}
|
||||
config_log = json.dumps(config_data, indent=2)
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import Any
|
||||
from typing import AsyncGenerator
|
||||
from typing import Awaitable
|
||||
from typing import Callable
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Mapping
|
||||
@@ -75,6 +76,22 @@ class BaseAgent(BaseModel):
|
||||
)
|
||||
"""The pydantic model config."""
|
||||
|
||||
config_type: ClassVar[type[BaseAgentConfig]] = BaseAgentConfig
|
||||
"""The config type for this agent.
|
||||
|
||||
Sub-classes should override this to specify their own config type.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
class MyAgentConfig(BaseAgentConfig):
|
||||
my_field: str = ''
|
||||
|
||||
class MyAgent(BaseAgent):
|
||||
config_type: ClassVar[type[BaseAgentConfig]] = MyAgentConfig
|
||||
```
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""The agent's name.
|
||||
|
||||
@@ -487,8 +504,8 @@ class BaseAgent(BaseModel):
|
||||
sub_agent.parent_agent = self
|
||||
return self
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
@working_in_progress('BaseAgent.from_config is not ready for use.')
|
||||
def from_config(
|
||||
cls: Type[SelfAgent],
|
||||
config: BaseAgentConfig,
|
||||
@@ -496,11 +513,8 @@ class BaseAgent(BaseModel):
|
||||
) -> SelfAgent:
|
||||
"""Creates an agent from a config.
|
||||
|
||||
This method converts fields in a config to the corresponding
|
||||
fields in an agent.
|
||||
|
||||
Child classes should re-implement this method to support loading from their
|
||||
custom config types.
|
||||
If sub-classes uses a custom agent config, override `_from_config_kwargs`
|
||||
method to return an updated kwargs for agent construstor.
|
||||
|
||||
Args:
|
||||
config: The config to create the agent from.
|
||||
@@ -510,6 +524,40 @@ class BaseAgent(BaseModel):
|
||||
Returns:
|
||||
The created agent.
|
||||
"""
|
||||
kwargs = cls.__create_kwargs(config, config_abs_path)
|
||||
kwargs = cls._parse_config(config, config_abs_path, kwargs)
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def _parse_config(
|
||||
cls: Type[SelfAgent],
|
||||
config: BaseAgentConfig,
|
||||
config_abs_path: str,
|
||||
kwargs: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Parses the config and returns updated kwargs to construct the agent.
|
||||
|
||||
Sub-classes should override this method to use a custome agent config class.
|
||||
|
||||
Args:
|
||||
config: The config to parse.
|
||||
config_abs_path: The absolute path to the config file that contains the
|
||||
agent config.
|
||||
kwargs: The keyword arguments used for agent constructor.
|
||||
|
||||
Returns:
|
||||
The updated keyword arguments used for agent constructor.
|
||||
"""
|
||||
return kwargs
|
||||
|
||||
@classmethod
|
||||
def __create_kwargs(
|
||||
cls,
|
||||
config: BaseAgentConfig,
|
||||
config_abs_path: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Creates kwargs for the fields of BaseAgent."""
|
||||
|
||||
from .config_agent_utils import resolve_agent_reference
|
||||
from .config_agent_utils import resolve_callbacks
|
||||
|
||||
@@ -532,4 +580,4 @@ class BaseAgent(BaseModel):
|
||||
kwargs['after_agent_callback'] = resolve_callbacks(
|
||||
config.after_agent_callbacks
|
||||
)
|
||||
return cls(**kwargs)
|
||||
return kwargs
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import List
|
||||
@@ -24,16 +25,9 @@ import yaml
|
||||
from ..utils.feature_decorator import working_in_progress
|
||||
from .agent_config import AgentConfig
|
||||
from .base_agent import BaseAgent
|
||||
from .base_agent_config import BaseAgentConfig
|
||||
from .common_configs import AgentRefConfig
|
||||
from .common_configs import CodeConfig
|
||||
from .llm_agent import LlmAgent
|
||||
from .llm_agent_config import LlmAgentConfig
|
||||
from .loop_agent import LoopAgent
|
||||
from .loop_agent_config import LoopAgentConfig
|
||||
from .parallel_agent import ParallelAgent
|
||||
from .parallel_agent import ParallelAgentConfig
|
||||
from .sequential_agent import SequentialAgent
|
||||
from .sequential_agent import SequentialAgentConfig
|
||||
|
||||
|
||||
@working_in_progress("from_config is not ready for use.")
|
||||
@@ -53,17 +47,36 @@ def from_config(config_path: str) -> BaseAgent:
|
||||
"""
|
||||
abs_path = os.path.abspath(config_path)
|
||||
config = _load_config_from_path(abs_path)
|
||||
agent_config = config.root
|
||||
|
||||
if isinstance(config.root, LlmAgentConfig):
|
||||
return LlmAgent.from_config(config.root, abs_path)
|
||||
elif isinstance(config.root, LoopAgentConfig):
|
||||
return LoopAgent.from_config(config.root, abs_path)
|
||||
elif isinstance(config.root, ParallelAgentConfig):
|
||||
return ParallelAgent.from_config(config.root, abs_path)
|
||||
elif isinstance(config.root, SequentialAgentConfig):
|
||||
return SequentialAgent.from_config(config.root, abs_path)
|
||||
# pylint: disable=unidiomatic-typecheck Needs exact class matching.
|
||||
if type(agent_config) is BaseAgentConfig:
|
||||
# Resolve the concrete agent config for user-defined agent classes.
|
||||
agent_class = _resolve_agent_class(agent_config.agent_class)
|
||||
agent_config = agent_class.config_type.model_validate(
|
||||
agent_config.model_dump()
|
||||
)
|
||||
return agent_class.from_config(agent_config, abs_path)
|
||||
else:
|
||||
raise ValueError("Unsupported config type")
|
||||
# For built-in agent classes, no need to re-validate.
|
||||
agent_class = _resolve_agent_class(agent_config.agent_class)
|
||||
return agent_class.from_config(agent_config, abs_path)
|
||||
|
||||
|
||||
def _resolve_agent_class(agent_class: str) -> type[BaseAgent]:
|
||||
"""Resolve the agent class from its fully qualified name."""
|
||||
agent_class_name = agent_class or "LlmAgent"
|
||||
if "." not in agent_class_name:
|
||||
agent_class_name = f"google.adk.agents.{agent_class_name}"
|
||||
|
||||
agent_class = resolve_fully_qualified_name(agent_class_name)
|
||||
if inspect.isclass(agent_class) and issubclass(agent_class, BaseAgent):
|
||||
return agent_class
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid agent class `{agent_class_name}`. It must be a subclass of"
|
||||
" BaseAgent."
|
||||
)
|
||||
|
||||
|
||||
@working_in_progress("_load_config_from_path is not ready for use.")
|
||||
@@ -90,6 +103,16 @@ def _load_config_from_path(config_path: str) -> AgentConfig:
|
||||
return AgentConfig.model_validate(config_data)
|
||||
|
||||
|
||||
@working_in_progress("resolve_fully_qualified_name is not ready for use.")
|
||||
def resolve_fully_qualified_name(name: str) -> Any:
|
||||
try:
|
||||
module_path, obj_name = name.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, obj_name)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid fully qualified name: {name}") from e
|
||||
|
||||
|
||||
@working_in_progress("resolve_agent_reference is not ready for use.")
|
||||
def resolve_agent_reference(
|
||||
ref_config: AgentRefConfig, referencing_agent_config_abs_path: str
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,8 @@ import uuid
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from pydantic import PrivateAttr
|
||||
|
||||
from ..artifacts.base_artifact_service import BaseArtifactService
|
||||
from ..auth.credential_service.base_credential_service import BaseCredentialService
|
||||
@@ -151,13 +153,18 @@ class InvocationContext(BaseModel):
|
||||
transcription_cache: Optional[list[TranscriptionEntry]] = None
|
||||
"""Caches necessary data, audio or contents, that are needed by transcription."""
|
||||
|
||||
live_session_resumption_handle: Optional[str] = None
|
||||
"""The handle for live session resumption."""
|
||||
|
||||
run_config: Optional[RunConfig] = None
|
||||
"""Configurations for live agents under this invocation."""
|
||||
|
||||
plugin_manager: PluginManager = PluginManager()
|
||||
plugin_manager: PluginManager = Field(default_factory=PluginManager)
|
||||
"""The manager for keeping track of plugins in this invocation."""
|
||||
|
||||
_invocation_cost_manager: _InvocationCostManager = _InvocationCostManager()
|
||||
_invocation_cost_manager: _InvocationCostManager = PrivateAttr(
|
||||
default_factory=_InvocationCostManager
|
||||
)
|
||||
"""A container to keep track of different kinds of costs incurred as a part
|
||||
of this invocation.
|
||||
"""
|
||||
|
||||
@@ -17,11 +17,12 @@ from __future__ import annotations
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import AsyncGenerator
|
||||
from typing import Awaitable
|
||||
from typing import Callable
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
@@ -37,8 +38,6 @@ from typing_extensions import TypeAlias
|
||||
|
||||
from ..code_executors.base_code_executor import BaseCodeExecutor
|
||||
from ..events.event import Event
|
||||
from ..examples.base_example_provider import BaseExampleProvider
|
||||
from ..examples.example import Example
|
||||
from ..flows.llm_flows.auto_flow import AutoFlow
|
||||
from ..flows.llm_flows.base_llm_flow import BaseLlmFlow
|
||||
from ..flows.llm_flows.single_flow import SingleFlow
|
||||
@@ -47,16 +46,15 @@ from ..models.llm_request import LlmRequest
|
||||
from ..models.llm_response import LlmResponse
|
||||
from ..models.registry import LLMRegistry
|
||||
from ..planners.base_planner import BasePlanner
|
||||
from ..tools.agent_tool import AgentTool
|
||||
from ..tools.base_tool import BaseTool
|
||||
from ..tools.base_tool import ToolConfig
|
||||
from ..tools.base_toolset import BaseToolset
|
||||
from ..tools.function_tool import FunctionTool
|
||||
from ..tools.tool_configs import ToolConfig
|
||||
from ..tools.tool_context import ToolContext
|
||||
from ..utils.feature_decorator import working_in_progress
|
||||
from .base_agent import BaseAgent
|
||||
from .base_agent_config import BaseAgentConfig
|
||||
from .callback_context import CallbackContext
|
||||
from .common_configs import CodeConfig
|
||||
from .invocation_context import InvocationContext
|
||||
from .llm_agent_config import LlmAgentConfig
|
||||
from .readonly_context import ReadonlyContext
|
||||
@@ -108,7 +106,6 @@ InstructionProvider: TypeAlias = Callable[
|
||||
]
|
||||
|
||||
ToolUnion: TypeAlias = Union[Callable, BaseTool, BaseToolset]
|
||||
ExamplesUnion = Union[list[Example], BaseExampleProvider]
|
||||
|
||||
|
||||
async def _convert_tool_union_to_tools(
|
||||
@@ -131,6 +128,9 @@ class LlmAgent(BaseAgent):
|
||||
When not set, the agent will inherit the model from its ancestor.
|
||||
"""
|
||||
|
||||
config_type: ClassVar[type[BaseAgentConfig]] = LlmAgentConfig
|
||||
"""The config type for this agent."""
|
||||
|
||||
instruction: Union[str, InstructionProvider] = ''
|
||||
"""Instructions for the LLM model, guiding the agent's behavior."""
|
||||
|
||||
@@ -584,51 +584,55 @@ class LlmAgent(BaseAgent):
|
||||
|
||||
return resolved_tools
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
@working_in_progress('LlmAgent.from_config is not ready for use.')
|
||||
def from_config(
|
||||
@classmethod
|
||||
def _parse_config(
|
||||
cls: Type[LlmAgent],
|
||||
config: LlmAgentConfig,
|
||||
config_abs_path: str,
|
||||
) -> LlmAgent:
|
||||
kwargs: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
from .config_agent_utils import resolve_callbacks
|
||||
from .config_agent_utils import resolve_code_reference
|
||||
|
||||
agent = super().from_config(config, config_abs_path)
|
||||
if config.model:
|
||||
agent.model = config.model
|
||||
kwargs['model'] = config.model
|
||||
if config.instruction:
|
||||
agent.instruction = config.instruction
|
||||
kwargs['instruction'] = config.instruction
|
||||
if config.disallow_transfer_to_parent:
|
||||
agent.disallow_transfer_to_parent = config.disallow_transfer_to_parent
|
||||
kwargs['disallow_transfer_to_parent'] = config.disallow_transfer_to_parent
|
||||
if config.disallow_transfer_to_peers:
|
||||
agent.disallow_transfer_to_peers = config.disallow_transfer_to_peers
|
||||
kwargs['disallow_transfer_to_peers'] = config.disallow_transfer_to_peers
|
||||
if config.include_contents != 'default':
|
||||
agent.include_contents = config.include_contents
|
||||
kwargs['include_contents'] = config.include_contents
|
||||
if config.input_schema:
|
||||
agent.input_schema = resolve_code_reference(config.input_schema)
|
||||
kwargs['input_schema'] = resolve_code_reference(config.input_schema)
|
||||
if config.output_schema:
|
||||
agent.output_schema = resolve_code_reference(config.output_schema)
|
||||
kwargs['output_schema'] = resolve_code_reference(config.output_schema)
|
||||
if config.output_key:
|
||||
agent.output_key = config.output_key
|
||||
kwargs['output_key'] = config.output_key
|
||||
if config.tools:
|
||||
agent.tools = cls._resolve_tools(config.tools, config_abs_path)
|
||||
kwargs['tools'] = cls._resolve_tools(config.tools, config_abs_path)
|
||||
if config.before_model_callbacks:
|
||||
agent.before_model_callback = resolve_callbacks(
|
||||
kwargs['before_model_callback'] = resolve_callbacks(
|
||||
config.before_model_callbacks
|
||||
)
|
||||
if config.after_model_callbacks:
|
||||
agent.after_model_callback = resolve_callbacks(
|
||||
kwargs['after_model_callback'] = resolve_callbacks(
|
||||
config.after_model_callbacks
|
||||
)
|
||||
if config.before_tool_callbacks:
|
||||
agent.before_tool_callback = resolve_callbacks(
|
||||
kwargs['before_tool_callback'] = resolve_callbacks(
|
||||
config.before_tool_callbacks
|
||||
)
|
||||
if config.after_tool_callbacks:
|
||||
agent.after_tool_callback = resolve_callbacks(config.after_tool_callbacks)
|
||||
return agent
|
||||
kwargs['after_tool_callback'] = resolve_callbacks(
|
||||
config.after_tool_callbacks
|
||||
)
|
||||
if config.generate_content_config:
|
||||
kwargs['generate_content_config'] = config.generate_content_config
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
Agent: TypeAlias = LlmAgent
|
||||
|
||||
@@ -19,9 +19,10 @@ from typing import List
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
|
||||
from google.genai import types
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from ..tools.base_tool import ToolConfig
|
||||
from ..tools.tool_configs import ToolConfig
|
||||
from .base_agent_config import BaseAgentConfig
|
||||
from .common_configs import CodeConfig
|
||||
|
||||
@@ -138,3 +139,6 @@ class LlmAgentConfig(BaseAgentConfig):
|
||||
|
||||
after_tool_callbacks: Optional[List[CodeConfig]] = None
|
||||
"""Optional. LlmAgent.after_tool_callbacks."""
|
||||
|
||||
generate_content_config: Optional[types.GenerateContentConfig] = None
|
||||
"""Optional. LlmAgent.generate_content_config."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user