Compare commits

..

41 Commits

Author SHA1 Message Date
Shangjie Chen 20c30d5819 chore: Bump version number to 1.10.0
PiperOrigin-RevId: 792206610
2025-08-07 10:05:01 -07:00
Yeesian Ng 83e5df7862 fix: Set the agent_framework when initializing module-based agent engine
PiperOrigin-RevId: 792170095
2025-08-07 08:27:15 -07:00
Google Team Member 25b2806301 fix: accommodate for open api schema that do not have any 'properties'
PiperOrigin-RevId: 792028582
2025-08-07 00:30:50 -07:00
Xuan Yang f6a022cda3 chore: only run ADK Answering Agent automatically when Q&A discussion is created
PiperOrigin-RevId: 792025646
2025-08-07 00:19:30 -07:00
Liang Wu e0a8355219 chore(config): add the public URL of JSON schema file to template
PiperOrigin-RevId: 791983175
2025-08-06 21:50:20 -07:00
Liang Wu d9ce2e691c feat(config): implement config and from_config for MCPToolset
The connection_params argument in the constructor is split into four arguments in the config class because some of them have identical fields. In order to identify which is which, a separate name is more convenient.

PiperOrigin-RevId: 791965995
2025-08-06 20:45:58 -07:00
Liang Wu dc193f7969 fix(config): fix adk create --type=config
Previously click didn't convert the input into the enum type.

PiperOrigin-RevId: 791922529
2025-08-06 18:10:21 -07:00
Xuan Yang 6277dae749 chore: add a disclaimer for the response from the answering agent
PiperOrigin-RevId: 791915465
2025-08-06 17:49:08 -07:00
Liang Wu ef837015f3 refactor(config): move BaseToolConfig to a separate file
PiperOrigin-RevId: 791841562
2025-08-06 14:18:20 -07:00
Google Team Member 54cc849de7 feat: Add metadata field to ADK BaseTool
PiperOrigin-RevId: 791790030
2025-08-06 12:11:42 -07:00
Liang Wu e73d71d324 feat(config): implement config and from_config for ExampleTool
Only list[Example] is supported in config. BaseExampleProvider will need to be used in code.

PiperOrigin-RevId: 791763913
2025-08-06 11:05:54 -07:00
Xiang (Sean) Zhou e528749a1c fix: lazy import VertexAiRagRetrieval
original codes try to eagerly import VertexAiRagRetrieval while it doesn't want to raise error if client try to import other names in this package and dependencies of VertexAiRagRetrieval is missing. so it swallow the import error which doesn't make sense, given vertex sdk is a must have for VertexAiRagRetrieval, we should fail fast.

this fix achieve the same purpose but fail fast if client try to import  VertexAiRagRetrieval from this package and miss certain dependencies (e.g. vertex sdk)

PiperOrigin-RevId: 791759776
2025-08-06 10:54:57 -07:00
Liang Wu 1686cc57c2 feat(config): implement configs and from_config() for CrewaiTool and LangchainTool
PiperOrigin-RevId: 791742964
2025-08-06 10:19:12 -07:00
Wei Sun (Jack) 53803522b6 refactor(config): Makes BaseAgent.from_config a final method and let sub-class to optionally override _parse_config to update kwargs if needed
This ensures that the pydantic hooks (e.g. model_validators) are triggered correctly.

PiperOrigin-RevId: 791545704
2025-08-05 23:53:25 -07:00
Liang Wu e3c2bf3062 chore: remove unused Example-related classes
PiperOrigin-RevId: 791538058
2025-08-05 23:26:06 -07:00
Liang Wu 2fff882fb0 feat(config): implement from_config() for BaseTool
PiperOrigin-RevId: 791520708
2025-08-05 22:26:23 -07:00
Xuan Yang a3b31ca950 chore: add the missing name for the ADK Answering Agent workflow
PiperOrigin-RevId: 791413949
2025-08-05 16:24:24 -07:00
Xuan Yang 8dc0c949af chore: add Github workflow config for the ADK Answering agent
PiperOrigin-RevId: 791407331
2025-08-05 16:07:09 -07:00
Hangfei Lin 71fbc9275b feat: Implement Live Session Resumption
Previous implementation doesn't pass the actual handle to server. Now we cache the handle and pass it over when reconnection happens.

To enable:
    run_config = RunConfig(
        session_resumption=types.SessionResumptionConfig(transparent=True)
    )

PiperOrigin-RevId: 791308462
2025-08-05 11:50:54 -07:00
Xiang (Sean) Zhou 423542a43f fix: shared default plugin manager and cost manager instances among multiple invocations
PiperOrigin-RevId: 791303349
2025-08-05 11:38:52 -07:00
Xiang (Sean) Zhou 37dae9b631 chore: Import AGENT_CARD_WELL_KNOWN_PATH from adk instead of from a2a directly
thus let adk handle import problem for a2a, e.g. python version need to be > 3.10 etc.

PiperOrigin-RevId: 791273137
2025-08-05 10:28:37 -07:00
Liang Wu 0e28d64712 feat(tools): create enterprise_web_search_tool as a tool instance
There is no argument for the tool, so just like google_search, we should make it an easy-to-use tool instance.

PiperOrigin-RevId: 791266806
2025-08-05 10:11:37 -07:00
Xiang (Sean) Zhou 6da6c2a44c fix: using async lock for accessing shared object in parallel executions and update tests for testing various type of functions
1. given we are running parallel functions in one event loop (one thread) , we should use async lock instead of thread lock
2. test three kind of functions:
  a. sync function
  b. async function that doesn't yield
  c. async function that yield

PiperOrigin-RevId: 791255012
2025-08-05 09:45:04 -07:00
Wei Sun (Jack) 8ef2177658 test: Fixes adk cli options and method parameters mismatching and adds a unit test for future proof checking
The test will fail if `@option` list and method parameter don't match.

Future proof test for #2328

PiperOrigin-RevId: 791022512
2025-08-04 21:08:57 -07:00
Google Team Member 97318bcd19 fix: correct type annotation
Overridden `supported_models` should be a `classmethod` rather than a `staticmethod`.

PiperOrigin-RevId: 790989895
2025-08-04 19:25:52 -07:00
Xuan Yang 283303032a chore: update the prompt to make the ADK Answering Agent more objective
PiperOrigin-RevId: 790882938
2025-08-04 13:59:59 -07:00
nikkie e369c283b3 fix: typo againt (in adk run --replay help)
Merge https://github.com/google/adk-python/pull/2327

`adk run --help` (adk 1.9.0)

```
  --replay FILE      The json file that contains the initial state of the
                     session and user queries. A new session will be created
                     using this state. And user queries are run againt the
                     newly created session. Users cannot continue to interact
                     with the agent.
```

```
$ git grep againt
src/google/adk/cli/cli_tools_click.py:        " queries are run againt the newly created session. Users cannot"
```

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2327 from ftnext:fix-typo-run-replay-help 77cae65a235d9119810fe3d209910562672713c8
PiperOrigin-RevId: 790872246
2025-08-04 13:32:41 -07:00
Xuan Yang 74589a1db7 chore: make LlmRequest.LiveConnectConfig field default to a factory to avoid sharing a mutable instance
PiperOrigin-RevId: 790854215
2025-08-04 12:43:34 -07:00
Wei Sun (Jack) e41dbccf7f fix(cli): Fixes adk deploy cloud_run cli
Fixes #2328

PiperOrigin-RevId: 790775592
2025-08-04 09:16:26 -07:00
Carol Zheng d620bcb384 fix: Remove thoughts from contents in llm requests
Merge https://github.com/google/adk-python/pull/2320

Fix #843

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2320 from CAROLZXYZXY:cazheng/fix-843 5b4a4b256928cb766a44a3e18d4300b7ee5f779f
PiperOrigin-RevId: 790592793
2025-08-03 22:48:25 -07:00
Xiang (Sean) Zhou 90b9193a20 chore: Add sample agent for testing parallel functions execution
PiperOrigin-RevId: 790208057
2025-08-02 14:53:03 -07:00
Xiang (Sean) Zhou 57cd41f424 feat: Support parallel execution of parallel function calls
PiperOrigin-RevId: 790182046
2025-08-02 12:28:19 -07:00
Divyansh Shukla 7556ebc76a feat: Allow max tokens to be customizable in Claude
PiperOrigin-RevId: 789901925
2025-08-01 14:43:57 -07:00
Wei Sun (Jack) 2bb20411f4 feat(config): Adds BaseAgent.config_type field to indicate the config for the current agent and removes if-else branches against LlmAgent/LoopAgent/... in config_agent_utils::from_config
This makes the logic work with any user-defined agent with user-defined XxxAgentConfig.

PiperOrigin-RevId: 789845354
2025-08-01 12:02:07 -07:00
Xiang (Sean) Zhou 86a44873e9 fix: Annotate response type as None for transfer_to_agent tool and set empty Schema as response schema when tool has no response annotation
1. if a function has no return type annotation, we should treat it as returning any type
2. we use empty schema (with `type` as None) to indicate no type constraints and this is already supported by model server

PiperOrigin-RevId: 789808104
2025-08-01 10:22:28 -07:00
Xiang (Sean) Zhou faadef167e fix: incompatible a2a sdk changes
a. camel case to snake case
b. A2ACardResolver moved to different module

PiperOrigin-RevId: 789807686
2025-08-01 10:20:37 -07:00
Google Team Member bead607364 chore: Hide the ask_data_insights tool until the API is publicly available
PiperOrigin-RevId: 789806535
2025-08-01 10:17:13 -07:00
Xuan Yang 041f04e89c chore: change LlmRequest.config's default value to be types.GenerateContentConfig() instead of None
PiperOrigin-RevId: 789792582
2025-08-01 09:36:20 -07:00
Google Team Member 16a15c8709 docs: fix typos
PiperOrigin-RevId: 789660536
2025-08-01 01:31:41 -07:00
Liang Wu 9656ccc407 feat(config): add GenerateContentConfig to LlmAgentConfig
PiperOrigin-RevId: 789631181
2025-07-31 23:33:36 -07:00
Liang Wu db975dfe2a chore: prevent triggering of _load_from_yaml_config in AgentLoader
PiperOrigin-RevId: 789502695
2025-07-31 15:59:41 -07:00
74 changed files with 6560 additions and 590 deletions
@@ -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
+35
View File
@@ -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)
+1 -1
View File
@@ -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.
-10
View File
@@ -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
View File
@@ -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
View File
@@ -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
]
+4 -4
View File
@@ -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)
+55 -7
View File
@@ -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
+40 -17
View File
@@ -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
+9 -2
View File
@@ -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.
"""
+30 -26
View File
@@ -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
+5 -1
View File
@@ -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