Compare commits

..

35 Commits

Author SHA1 Message Date
Yeesian Ng 69eb2b50c5 feat: Add --agent_engine_config_file option to adk deploy agent_engine
PiperOrigin-RevId: 800171071
2025-08-27 14:19:43 -07:00
Wei Sun (Jack) 2dd432cc1f fix: add the missing from_config class method in BaseToolset
PiperOrigin-RevId: 799893557
2025-08-26 23:48:05 -07:00
Hangfei Lin 3b997a0a07 fix: change LlmResponse to use Content for transcriptions
The transcription change breaks the multi-agent transfer during live/bidi.

Updates `GeminiLlmConnection` to populate the `content` field of `LlmResponse` with `types.Content` and `types.Part` objects for both input and output transcriptions, instead of using dedicated transcription fields. Also removes a debug print from `audio_cache_manager.py`.

the transcription is not fully ready to be used yet so roll back the transcription change.

PiperOrigin-RevId: 799851950
2025-08-26 21:12:59 -07:00
Google Team Member bcf0dda8bc fix: AgentTool returns last content, instead of the content in the last event
This fixes the issue when inner agent has after_agent_callback updating state only, AgentTool will always return empty.

PiperOrigin-RevId: 799814933
2025-08-26 19:15:23 -07:00
Wei Sun (Jack) 4c70606129 chore: Renames MCPTool and MCPToolset to McpTool and McpToolset
- Keep original class names for backward-compatibility.
- Log warning if user instantiate the classes with original names.
- New names are more aligned with MCP SDKs convention.

PiperOrigin-RevId: 799777320
2025-08-26 17:02:41 -07:00
Ankur Sharma 216cb7aec0 fix: Update routes for run_eval, get_eval_result, list_eval_results and list_metrics_info
PiperOrigin-RevId: 799718430
2025-08-26 14:27:00 -07:00
Mofi Rahman ad81aa54de fix: Fix adk deploy docker file permission
Merge https://github.com/google/adk-python/pull/2563

Currently in adk deploy cloud_run or gke, the dockerfile copies the agent code after the file permission is set. This can lead to file permission not being set correctly for the container to open and read the file.

This PR will make sure the files are copied with the permission of the user that is set in the container.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2563 from moficodes:deploy-docker d7f6df4d893af75b360e6d96ffd2640ce6076ca2
PiperOrigin-RevId: 799371070
2025-08-25 20:03:37 -07:00
Google Team Member 47b88d2b06 feat: Add the ask_data_insights tool for natural language queries on BigQuery data
PiperOrigin-RevId: 799267061
2025-08-25 14:23:10 -07:00
Jack Wotherspoon 6806deaf88 feat: passthrough extra args for adk deploy cloud_run as Cloud Run args
Merge https://github.com/google/adk-python/pull/2544

The command `adk deploy cloud_run` supports limited `gcloud run deploy` args 😢.

Which makes the command fine for simple deployments...

It should support all current and future Cloud Run deployment args for the command to be widely adopted.

This can easily be done by passing through all extra args passed to `adk deploy cloud_run` to gcloud...

This PR assumes any extra args/flags passed after `AGENT_PATH` are gcloud flags.

## Example

```sh
# ADK flags
adk deploy cloud_run \
--project=$GOOGLE_CLOUD_PROJECT \
--region=$GOOGLE_CLOUD_LOCATION \
$AGENT_PATH \
# Use the -- separator for gcloud args
-- \
--min-instances=2 \
--no-allow-unauthenticated
```

This gives full Cloud Run feature support to ADK users 🤖 🚀

## Test Plan

To test you can just build locally or pip install feature branch directly:

```
uv venv
uv pip install git+https://github.com/jackwotherspoon/adk-python.git
```

Deploy to Cloud Run using additional arguments following `AGENT_PATH`, such as `--min-instance=2` or `--description="Cloud Run test"`:

```sh
uv run adk deploy cloud_run \
--project=$GOOGLE_CLOUD_PROJECT \
--region=$GOOGLE_CLOUD_LOCATION \
--with_ui \
$AGENT_PATH \
-- \
--labels=test-label=adk \
--min-instances=2
```

You can click on the Cloud Run service after deployment and check the service yaml, you should see the additional label etc.

<img width="1612" height="622" alt="image" src="https://github.com/user-attachments/assets/596a260a-0052-460b-9642-c18900ccf7c9" />

Fixes https://github.com/google/adk-python/issues/2351

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2544 from jackwotherspoon:main 184a4d73f8dbe6f565ff92cf1c1fe69bb163de5e
PiperOrigin-RevId: 799252544
2025-08-25 13:45:21 -07:00
Wei Sun (Jack) 2b2f0b52d8 chore: Fixes a2a tests for python 3.9
A more common approach is to just skip entire module in pytest.

PiperOrigin-RevId: 799164877
2025-08-25 10:03:19 -07:00
Wei Sun (Jack) 0eb65c07d5 chore: Ignore hidden files in autoformat.sh
PiperOrigin-RevId: 798490378
2025-08-23 00:38:42 -07:00
akshaypachpute-1998 0b89f1882d chore: replaced hard coded value for user_id to the value from the tool context from parent agent. Fixes google/adk-python#2407
Merge https://github.com/google/adk-python/pull/2409

Description:

This PR Fixes: #2407

The AgentTool in /google/adk/tools/agent_tool.py uses a hardcoded user_id='tmp_user' when creating a new session for the agent it wraps. This happens within the run_async method.

code snippet
... @override async def run_async( self, *, args: dict[str, Any], tool_context: ToolContext, ) -> Any: ... session = await runner.session_service.create_session( app_name=self.agent.name, user_id='tmp_user',  # <-- This is hardcoded state=tool_context.state.to_dict(), ) ...

Why is this a problem?
This hardcoding breaks the chain of user identity. When a parent agent calls a sub-agent via the AgentTool, the original user_id is lost. Any tool or logic inside the sub-agent that needs to perform user-specific actions (e.g., accessing user data from a database, retrieving user-specific memory, checking permissions) will fail or operate on the wrong context because it receives 'tmp_user' instead of the actual user's ID.

Impact:
This prevents the creation of robust, multi-agent applications where user context must be maintained across different agents and tools. It limits the utility of AgentTool to only stateless sub-agents that do not require user-specific information.

Suggested Fix:
The user_id should be retrieved from the parent context, which is available via the tool_context parameter passed into run_async. The create_session call should be updated to use the dynamic user_id from the parent session.For example, the fix might involve accessing the user ID from the tool_context.

code-snippet
session = await runner.session_service.create_session( app_name=self.agent.name, user_id=tool_context._invocation_context.user_id, state=tool_context.state.to_dict(), )

To Reproduce
Steps to reproduce the behavior:
To reproduce this bug, we need to set up a two-agent system: a ParentAgent that calls a ChildAgent using the AgentTool. The ChildAgent will have a tool designed to simply return the user_id it receives from its context.

Expected behavior
It should return the user_id of the user calling the agent,

but, in current situation we are getting tmp_user

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2409 from akshaypachpute-1998:fix-issue-2407-agent-tool-context-propogation 0c3e8656fdf11386e3ab13a3a1f2df99a396dbd1
PiperOrigin-RevId: 798315832
2025-08-22 13:10:07 -07:00
Xiang (Sean) Zhou 124fee64e5 chore: Remove unnecessary import in runners
PiperOrigin-RevId: 798285019
2025-08-22 11:38:37 -07:00
Wei Sun (Jack) a360bc2542 docs: Clean up docs in sample
PiperOrigin-RevId: 798284758
2025-08-22 11:36:58 -07:00
Ankur Sharma 75ba9689e3 fix: Update routes for list_eval_sets, add_session_to_eval_set, get_eval, update_eval and delete_eval
PiperOrigin-RevId: 798241789
2025-08-22 09:41:59 -07:00
Wei Sun (Jack) 2c088acc9b docs: Fixes root_agent.yaml in tool_mcp_stdio_notion_config for Agent Config sample and add README.md
PiperOrigin-RevId: 798241478
2025-08-22 09:40:31 -07:00
rcsantana777 7fe60641e3 feat: Add custom User-Agent header to RestApiTool requests
Merge https://github.com/google/adk-python/pull/2641

This PR adds a custom `User-Agent` header to all requests made via `RestApiTool`. This allows backend services to identify traffic originating from the ADK. The header format is `google-adk/<version> (tool: <tool_name>)`, where `<version>` is the current version of the `google-adk` package, fetched dynamically from `google.adk.version`, and `<tool_name>` is the name of the specific `RestApiTool` instance.

**Associated Issue**

Fixes #2676

**Testing Plan**

**Unit Tests**

I ran the full suite of unit tests locally to ensure the changes did not introduce any regressions. All tests passed successfully.
```bash
$ pytest ./tests/unittests
================================= 4202 passed, 2459 warnings in 44.68s ====================
```
The warnings are related to existing experimental features and are not affected by this change.

**Manual End-to-End (E2E) Test**

I performed a manual test to ensure the integrated flow works as expected.

*   **Setup:** Created a clean virtual environment (`~/venvs/adk-e2e-test`) and installed the locally built `google-adk` package using `uv venv --seed` and `pip install dist/google_adk-*.whl`. Ran a custom `mock_server.py` script (using Flask) in one terminal to listen for requests and print headers. Ran a custom `run_test_tool.py` script in a second terminal to send a request using the modified `RestApiTool`.

*   **Execution:** The `run_test_tool.py` script successfully sent a POST request to the mock server's `/test` endpoint. The mock server received the request and printed the headers.

    **`run_test_tool.py` Output:**
    ```json
    {
      "message": "Request received successfully!"
    }
    ```

    **`mock_server.py` Output (Headers):**
    ```text
    --- Request Received ---
    Headers:
      Host: 127.0.0.1:8000
      User-Agent: google-adk/1.12.0 (tool: TestTool)
      Accept-Encoding: gzip, deflate
      Accept: */*
      Connection: keep-alive
      Content-Type: application/json
      Content-Length: 2
    ------------------------
    ```
    The `User-Agent: google-adk/1.12.0 (tool: TestTool)` header confirms the change is working as intended, dynamically fetching the package version and including the tool name.

**Checklist**

*   [x] Associated issue linked
*   [x] Unit tests passed
*   [x] End-to-end test performed and results documented
*   [x] Code formatted with `./autoformat.sh`

---

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2641 from rcsantana777:feat/add-user-agent a9a9375306c18bb7ba501276cbf76693e70a87ad
PiperOrigin-RevId: 798232385
2025-08-22 09:11:16 -07:00
Xiang (Sean) Zhou 395a50b5dc chore: add triaging instructions for mcp
PiperOrigin-RevId: 798017380
2025-08-21 20:06:51 -07:00
Hangfei Lin ddb070ff07 feat: Add support for store audio and transcription into events/sessions and artifacts
PiperOrigin-RevId: 798011660
2025-08-21 19:50:26 -07:00
Ankur Sharma f660180854 fix: Update create_eval_set API to return the created EvalSet and it route
PiperOrigin-RevId: 797974571
2025-08-21 17:13:46 -07:00
Xiang (Sean) Zhou 157f73181d feat: Allow user to specify protocol for A2A RPC URL in to_a2a utility
this fixes https://github.com/google/adk-python/issues/2405

PiperOrigin-RevId: 797958771
2025-08-21 16:26:57 -07:00
Joe Fernandez ccab076ace docs: Add What's new section to README.md
Merge https://github.com/google/adk-python/pull/2668

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2668 from joefernandez:patch-1 7bc04963755af76fe289d1855341553057348a55
PiperOrigin-RevId: 797958558
2025-08-21 16:25:30 -07:00
Wei Sun (Jack) 80d188199b chore: Fixes a few lint error for tools
PiperOrigin-RevId: 797945021
2025-08-21 15:49:21 -07:00
Google Team Member 29bb75f975 fix: updating BaseAgent.clone() and LlmAgent.clone() to properly clone fields that are lists
PiperOrigin-RevId: 797855214
2025-08-21 11:52:31 -07:00
Google Team Member 167182be01 fix: make tool description for bigquery execute_sql for various write modes self contained
So far we had a default docstring for the `execute-sql` tool and for non-default write modes we were concatenating more content to it. This was working fine in Python 3.9-3.12 but broke in Python 3.13 because of a nuanced difference in the string concatenation to the `__doc__` property of a function b/433914562#comment4. This change makes the docstring management more robust and readable.

PiperOrigin-RevId: 797843736
2025-08-21 11:26:19 -07:00
Xiang (Sean) Zhou 3f3aa7b32d fix: Set invocation_id and branch for event generated when both output_schema and tools are used
fixes https://github.com/google/adk-python/issues/2631

PiperOrigin-RevId: 797843460
2025-08-21 11:24:43 -07:00
Kacper Jawoszek 826f554789 fix: rework parallel_agent.py to always aclose async generators
See https://github.com/google/adk-python/issues/1670#issuecomment-3115891100

PiperOrigin-RevId: 797838268
2025-08-21 11:12:35 -07:00
Kacper Jawoszek 9645cee3a6 chore: add test for parallel agent to verify correct handling of exceptions
PiperOrigin-RevId: 797825924
2025-08-21 10:43:16 -07:00
Kacper Jawoszek 70f50db653 chore: add test for parallel agent to verify correct ordering of agents
PiperOrigin-RevId: 797817063
2025-08-21 10:21:37 -07:00
Google Team Member 81a53b53d6 fix: add table metadata info into Spanner tool get_table_schema and fix the key usage info
This can help to provide more context and information about the table, like parent-child relationship, and row deletion policy etc.

PiperOrigin-RevId: 797562858
2025-08-20 19:30:52 -07:00
Xiang (Sean) Zhou 52a3d6cb8a chore: Bump version number to 1.12.0
PiperOrigin-RevId: 797542097
2025-08-20 18:07:13 -07:00
Wei Sun (Jack) 331978772a fix: Fixes deps in test extra dep
PiperOrigin-RevId: 797509357
2025-08-20 16:25:08 -07:00
Wei Sun (Jack) e93f861cde chore: Removes unused to_agent_config method in BaseAgentConfig
PiperOrigin-RevId: 797502656
2025-08-20 16:08:36 -07:00
Google Team Member 54ed079100 fix: Fix Spanner DatabaseSessionService support
Introduce `DynamicPickleType` to handle session actions, using sqlalchemy-spanner `SpannerPickleType` when the database dialect is Spanner.

Connects to a Spanner database to store session data persistently in tables.
# Example using Spanner database:
`session_service = DatabaseSessionService(db_url="spanner+spanner:///projects/project-id/instances/instance-id/databases/database-id")`

# Example adk web command:
`adk web --session_service_uri="spanner+spanner:///projects/project-id/instances/instance-id/databases/database-id"`

PiperOrigin-RevId: 797416610
2025-08-20 12:26:53 -07:00
Thiago Salvatore c144b5347c fix: Add support for required params
Merge https://github.com/google/adk-python/pull/2212

This PR closes issue #2202

ADK was not parsing the required attribute when using LiteLLM, letting the LLM decide what is required vs not, not respecting function definitions.

## Test Plan

There's a fork of adk-python that is being running live for over 2 weeks in our production environment with millions of requests per day.

Below you can find a screenshot of the unit tests passing. I've also added one change to the test cases to cover this scenario

<img width="1904" height="483" alt="image" src="https://github.com/user-attachments/assets/5a6eb069-63ae-45a3-baca-6b01543f56fb" />

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2212 from thiagosalvatore:main 7de4037d8016389313f3fb22df40c12bac578523
PiperOrigin-RevId: 797393698
2025-08-20 11:31:53 -07:00
69 changed files with 3579 additions and 501 deletions
+48
View File
@@ -1,5 +1,53 @@
# Changelog
## 1.12.0 (2025-08-21)
### Features
**[Agent Config]** 🌟 **NEW FEATURE**: Support using config file (YAML) to author agents in addition to python code. See the [documentation](https://google.github.io/adk-docs/agents/config/) for details.
* [Agent Config] Support deploying config agent to Agent Engine in CLI ([b3b7003](https://github.com/google/adk-python/commit/b3b70035c432670a5f0b5cdd1e9467f43b80495c))
* [Tools] Add a dedicated Bigtable toolset to provide an easier, integrated way to interact
with Bigtable for building AI Agent applications(experimental feature) ([a953807](https://github.com/google/adk-python/commit/a953807cce341425ba23e3f0a85eae58d6b0630f))
* [Tools] Support custom tool_name_prefix in auto-generated GoogleApiToolset ([a2832d5](https://github.com/google/adk-python/commit/a2832d5ac7ba5264ee91f6d5a6a0058cfe4c9e8a)) See [oauth_calendar_agent](https://github.com/google/adk-python/tree/main/contributing/samples/oauth_calendar_agent) as an example.
* [CLI] Add `build_image` option for `adk deploy cloud_run` CLI ([c843503](https://github.com/google/adk-python/commit/c84350345af0ea6a232e0818b20c4262b228b103))
* [Services] Add setdefault method to the ADK State object ([77ed1f5](https://github.com/google/adk-python/commit/77ed1f5f15ed3f009547ed0e20f86d949de12ec2))
### Bug Fixes
* Lazy load VertexAiCodeExecutor and ContainerCodeExecutor ([018db79](https://github.com/google/adk-python/commit/018db79d1354f93b8328abb8416f63070b25f9f1))
* Fix the path for agent card in A2A demo ([fa64545](https://github.com/google/adk-python/commit/fa64545a9de216312a69f93126cfd37f1016c14b))
* Fix the path for agent card in A2A demo ([a117cf0](https://github.com/google/adk-python/commit/a117cf0af335c5e316ae9d61336a433052316462))
* litellm-test due to breaking change in dep library of extension extra ([004a0a0](https://github.com/google/adk-python/commit/004a0a0f2d9a4f7ae6bff42a7cad96c11a99acaf))
* Using base event's invocation id when merge multiple function response event ([279e4fe](https://github.com/google/adk-python/commit/279e4fedd0b1c0d1499c0f9a4454357af7da490e))
* Avoid crash when there is no candidates_token_count, which is Optional ([22f34e9](https://github.com/google/adk-python/commit/22f34e9d2c552fbcfa15a672ef6ff0c36fa32619))
* Fix the packaging version comparison logic in adk cli ([a2b7909](https://github.com/google/adk-python/commit/a2b7909fc36e7786a721f28e2bf75a1e86ad230d))
* Add Spanner admin scope to Spanner tool default Oauth scopes ([b66054d](https://github.com/google/adk-python/commit/b66054dd0d8c5b3d6f6ad58ac1fbd8128d1da614))
* Fixes SequentialAgent.config_type type hint ([8a9a271](https://github.com/google/adk-python/commit/8a9a271141678996c9b84b8c55d4b539d011391c))
* Fixes the host in the ansi bracket of adk web ([cd357bf](https://github.com/google/adk-python/commit/cd357bf5aeb01f1a6ae2a72349a73700ca9f1ed2))
* Add spanner tool name prefix ([a27927d](https://github.com/google/adk-python/commit/a27927dc8197c391c80acb8b2c23d610fba2f887))
### Improvements
* Support `ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS` as environment variable to suppress experimental warnings ([4afc9b2](https://github.com/google/adk-python/commit/4afc9b2f33d63381583cea328f97c02213611529))
* Uses pydantic `Field` for Agent configs, so that the generated AgentConfig.json json schema can carry field description ([5b999ed](https://github.com/google/adk-python/commit/5b999ed6fd23a0fc1da56ccff4c09621f433846b))
* Update `openai` dependency version, based on correct OPENAI release ([bb8ebd1](https://github.com/google/adk-python/commit/bb8ebd15f90768b518cd0e21a59b269e30d6d944))
* Add the missing license header for core_callback_config init file ([f8fd6a4](https://github.com/google/adk-python/commit/f8fd6a4f09ab520b8ecdbd8f9fe48228dbff7ebe))
* Creates yaml_utils.py in utils to allow adk dump yaml in the same style ([1fd58cb](https://github.com/google/adk-python/commit/1fd58cb3633992cd88fa7e09ca6eda0f9b34236f))
* Return explict None type for DELETE endpoints ([f03f167](https://github.com/google/adk-python/commit/f03f1677790c0a9e59b6ba6f46010d0b7b64be50))
* Add _config suffix to all yaml-based agent examples ([43f302c](https://github.com/google/adk-python/commit/43f302ce1ab53077ee8f1486d5294540678921e6))
* Rename run related method and request to align with the conventions ([ecaa7b4](https://github.com/google/adk-python/commit/ecaa7b4c9847b478c7cdc37185b1525f733bb403))
* Update models in samples/ folder to be gemini 2.0+ ([6c217ba](https://github.com/google/adk-python/commit/6c217bad828edf62b41ec06b168f8a6cb7ece2ed))
* Remove the "one commit" requirement from the contributing guide ([c32cb6e](https://github.com/google/adk-python/commit/c32cb6eef9ce320ea5a1f3845fc57b83762c237e))
* Bump version to 1.11.0 ([8005270](https://github.com/google/adk-python/commit/80052700f6cee947322080ae6c415d3a428b6c91))
### Documentation
* Add contributing bigtable sample ([fef5318](https://github.com/google/adk-python/commit/fef5318a22f3dcaadb7ecb858725eb61a0350140))
* Fix core_callback example ([ba6e85e](https://github.com/google/adk-python/commit/ba6e85eb3fb06f58ce9077574eac193298e18bea))
* Adds a minimal sample to demo how to use Agent Config to create a multi-agent setup ([1328e6e](https://github.com/google/adk-python/commit/1328e6ef62e9e6260048c0078579edb85a0440bc))
## [1.11.0](https://github.com/google/adk-python/compare/v1.10.0...v1.11.0) (2025-08-14)
### Features
+5
View File
@@ -27,6 +27,11 @@ Agent Development Kit (ADK) is a flexible and modular framework for developing a
---
## ✨ What's new
- **Agent Config**: Build agents without code. Check out the
[Agent Config](https://google.github.io/adk-docs/agents/config/) feature.
## ✨ Key Features
- **Rich Tool Ecosystem**: Utilize pre-built tools, custom functions,
+3 -3
View File
@@ -52,16 +52,16 @@ echo '---------------------------------------'
echo '| Auto-formatting src/...'
echo '---------------------------------------'
find -L src/ -type f -name "*.py" -exec pyink --config pyproject.toml {} +
find -L src/ -not -path "*/.*" -type f -name "*.py" -exec pyink --config pyproject.toml {} +
echo '---------------------------------------'
echo '| Auto-formatting tests/...'
echo '---------------------------------------'
find -L tests/ -type f -name "*.py" -exec pyink --config pyproject.toml {} +
find -L tests/ -not -path "*/.*" -type f -name "*.py" -exec pyink --config pyproject.toml {} +
echo '---------------------------------------'
echo '| Auto-formatting contributing/...'
echo '---------------------------------------'
find -L contributing/ -type f -name "*.py" -exec pyink --config pyproject.toml {} +
find -L contributing/ -not -path "*/.*" -type f -name "*.py" -exec pyink --config pyproject.toml {} +
@@ -32,6 +32,7 @@ LABEL_TO_OWNER = {
"documentation": "polong-lin",
"services": "DeanChensj",
"tools": "seanzhou1023",
"mcp": "seanzhou1023",
"eval": "ankursharmas",
"live": "hangfei",
"models": "genquan9",
@@ -267,6 +268,7 @@ root_agent = Agent(
- If it's about model support(non-Gemini, like Litellm, Ollama, OpenAI models), label it with "models".
- If it's about tracing, label it with "tracing".
- If it's agent orchestration, agent definition, label it with "core".
- If it's about Model Context Protocol (e.g. MCP tool, MCP toolset, MCP session management etc.), label it with "mcp".
- If you can't find a appropriate labels for the PR, follow the previous instruction that starts with "IMPORTANT:".
Here is the contribution guidelines:
@@ -31,6 +31,7 @@ LABEL_TO_OWNER = {
"documentation": "polong-lin",
"services": "DeanChensj",
"question": "",
"mcp": "seanzhou1023",
"tools": "seanzhou1023",
"eval": "ankursharmas",
"live": "hangfei",
@@ -182,6 +183,7 @@ root_agent = Agent(
- If it's about tracing, label it with "tracing".
- If it's agent orchestration, agent definition, label it with "core".
- If it's about agent engine, label it with "agent engine".
- If it's about Model Context Protocol (e.g. MCP tool, MCP toolset, MCP session management etc.), label it with "mcp".
- If you can't find a appropriate labels for the issue, follow the previous instruction that starts with "IMPORTANT:".
Call the `add_label_and_owner_to_issue` tool to label the issue, which will also assign the issue to the owner of the label.
+10
View File
@@ -25,6 +25,16 @@ 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
@@ -65,8 +65,8 @@ async def check_prime(nums: list[int]) -> str:
root_agent = Agent(
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-2.0-flash-live-preview-04-09', # for Vertex project
model='gemini-2.0-flash-live-001', # for AI studio key
name='hello_world_agent',
description=(
'hello world agent that can roll a dice of 8 sides and check prime'
@@ -1,3 +1,3 @@
# Config-based Agent Sample - LLM multi-agent
http://google3/third_party/py/google/adk/open_source_workspace/contributing/samples/hello_world_ma/
From contributing/samples/hello_world_ma/
+1 -1
View File
@@ -23,7 +23,7 @@ distributed via the `google.adk.tools.spanner` module. These tools include:
1. `get_table_schema`
Fetches Spanner database table schema.
Fetches Spanner database table schema and metadata information.
1. `execute_sql`
@@ -1,3 +1,3 @@
# Config-based Agent Sample - Human-In-The-Loop
http://google3/third_party/py/google/adk/open_source_workspace/contributing/samples/human_in_loop/
From contributing/samples/human_in_loop/
@@ -0,0 +1,48 @@
# Config-based Agent Sample - MCP Toolset with Notion MCP Server
This sample demonstrates how to configure an ADK agent to use the Notion MCP server for interacting with Notion pages and databases.
## Setup Instructions
### 1. Create a Notion Integration
1. Go to [Notion Integrations](https://www.notion.so/my-integrations)
2. Click "New integration"
3. Give it a name and select your workspace
4. Copy the "Internal Integration Secret" (starts with `ntn_`)
For detailed setup instructions, see the [Notion MCP Server documentation](https://www.npmjs.com/package/@notionhq/notion-mcp-server).
### 2. Configure the Agent
Replace `<your_notion_token>` in `root_agent.yaml` with your actual Notion integration token:
```yaml
env:
OPENAPI_MCP_HEADERS: '{"Authorization": "Bearer secret_your_actual_token_here", "Notion-Version": "2022-06-28"}'
```
### 3. Grant Integration Access
**Important**: After creating the integration, you must grant it access to specific pages and databases:
1. Go to `Access` tab in [Notion Integrations](https://www.notion.so/my-integrations) page
2. Click "Edit access"
3. Add pages or databases as needed
### 4. Run the Agent
Use the `adk web` to run the agent and interact with your Notion workspace.
## Example Queries
- "What can you do for me?"
- "Search for 'project' in my pages"
- "Create a new page called 'Meeting Notes'"
- "List all my databases"
## Troubleshooting
- If you get "Unauthorized" errors, check that your token is correct
- If you get "Object not found" errors, ensure you've granted the integration access to the specific pages/databases
- Make sure the Notion API version in the headers matches what the MCP server expects
@@ -13,4 +13,4 @@ tools:
- "-y"
- "@notionhq/notion-mcp-server"
env:
OPENAPI_MCP_HEADERS: "{'Authorization': 'Bearer fake_notion_api_key', 'Notion-Version': '2022-06-28'}"
OPENAPI_MCP_HEADERS: '{"Authorization": "Bearer <your_notion_token>", "Notion-Version": "2022-06-28"}'
+35 -32
View File
@@ -25,37 +25,38 @@ classifiers = [ # List of https://pypi.org/classifiers/
]
dependencies = [
# go/keep-sorted start
"PyYAML>=6.0.2, <7.0.0", # For APIHubToolset.
"absolufy-imports>=0.3.1, <1.0.0", # For Agent Engine deployment.
"anyio>=4.9.0, <5.0.0;python_version>='3.10'", # For MCP Session Manager
"authlib>=1.5.1, <2.0.0", # For RestAPI Tool
"click>=8.1.8, <9.0.0", # For CLI tools
"fastapi>=0.115.0, <1.0.0", # FastAPI framework
"google-api-python-client>=2.157.0, <3.0.0", # Google API client discovery
"google-cloud-bigtable>=2.32.0", # For Bigtable database
"PyYAML>=6.0.2, <7.0.0", # For APIHubToolset.
"absolufy-imports>=0.3.1, <1.0.0", # For Agent Engine deployment.
"anyio>=4.9.0, <5.0.0;python_version>='3.10'", # For MCP Session Manager
"authlib>=1.5.1, <2.0.0", # For RestAPI Tool
"click>=8.1.8, <9.0.0", # For CLI tools
"fastapi>=0.115.0, <1.0.0", # FastAPI framework
"google-api-python-client>=2.157.0, <3.0.0", # Google API client discovery
"google-cloud-bigtable>=2.32.0", # For Bigtable database
"google-cloud-aiplatform[agent_engines]>=1.95.1, <2.0.0", # For VertexAI integrations, e.g. example store.
"google-cloud-secret-manager>=2.22.0, <3.0.0", # Fetching secrets in RestAPI Tool
"google-cloud-spanner>=3.56.0, <4.0.0", # For Spanner database
"google-cloud-speech>=2.30.0, <3.0.0", # For Audio Transcription
"google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
"google-genai>=1.21.1, <2.0.0", # Google GenAI SDK
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset
"opentelemetry-api>=1.31.0, <2.0.0", # OpenTelemetry
"google-cloud-secret-manager>=2.22.0, <3.0.0", # Fetching secrets in RestAPI Tool
"google-cloud-spanner>=3.56.0, <4.0.0", # For Spanner database
"google-cloud-speech>=2.30.0, <3.0.0", # For Audio Transcription
"google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
"google-genai>=1.21.1, <2.0.0", # Google GenAI SDK
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset
"opentelemetry-api>=1.31.0, <2.0.0", # OpenTelemetry
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
"opentelemetry-sdk>=1.31.0, <2.0.0",
"pydantic>=2.0, <3.0.0", # For data validation/models
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
"pydantic>=2.0, <3.0.0", # For data validation/models
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
"requests>=2.32.4, <3.0.0",
"sqlalchemy>=2.0, <3.0.0", # SQL database ORM
"starlette>=0.46.2, <1.0.0", # For FastAPI CLI
"tenacity>=8.0.0, <9.0.0", # For Retry management
"sqlalchemy-spanner>=1.14.0", # Spanner database session service
"sqlalchemy>=2.0, <3.0.0", # SQL database ORM
"starlette>=0.46.2, <1.0.0", # For FastAPI CLI
"tenacity>=8.0.0, <9.0.0", # For Retry management
"typing-extensions>=4.5, <5",
"tzlocal>=5.3, <6.0", # Time zone utilities
"uvicorn>=0.34.0, <1.0.0", # ASGI server for FastAPI
"watchdog>=6.0.0, <7.0.0", # For file change detection and hot reload
"websockets>=15.0.1, <16.0.0", # For BaseLlmFlow
"tzlocal>=5.3, <6.0", # Time zone utilities
"uvicorn>=0.34.0, <1.0.0", # ASGI server for FastAPI
"watchdog>=6.0.0, <7.0.0", # For file change detection and hot reload
"websockets>=15.0.1, <16.0.0", # For BaseLlmFlow
# go/keep-sorted end
]
dynamic = ["version"]
@@ -98,18 +99,20 @@ eval = [
test = [
# go/keep-sorted start
"anthropic>=0.43.0", # For anthropic model tests
"a2a-sdk>=0.3.0,<0.4.0;python_version>='3.10'",
"anthropic>=0.43.0", # For anthropic model tests
"langchain-community>=0.3.17",
# langgraph 0.5 removed langgraph.graph.graph which we depend on
"langgraph>=0.2.60, <= 0.4.10", # For LangGraphAgent
"litellm>=1.75.5, <2.0.0", # For LiteLLM tests
"llama-index-readers-file>=0.4.0", # For retrieval tests
"openai>=1.100.2", # For LiteLLM
"langgraph>=0.2.60, <= 0.4.10", # For LangGraphAgent
"litellm>=1.75.5, <2.0.0", # For LiteLLM tests
"llama-index-readers-file>=0.4.0", # For retrieval tests
"openai>=1.100.2", # For LiteLLM
"pytest-asyncio>=0.25.0",
"pytest-mock>=3.14.0",
"pytest-xdist>=3.6.1",
"pytest>=8.3.4",
"python-multipart>=0.0.9",
"rouge-score>=0.1.2",
"tabulate>=0.9.0",
# go/keep-sorted end
]
+10 -3
View File
@@ -39,11 +39,17 @@ from ...memory.in_memory_memory_service import InMemoryMemoryService
from ...runners import Runner
from ...sessions.in_memory_session_service import InMemorySessionService
from ..executor.a2a_agent_executor import A2aAgentExecutor
from ..experimental import a2a_experimental
from .agent_card_builder import AgentCardBuilder
@a2a_experimental
def to_a2a(
agent: BaseAgent, *, host: str = "localhost", port: int = 8000
agent: BaseAgent,
*,
host: str = "localhost",
port: int = 8000,
protocol: str = "http",
) -> Starlette:
"""Convert an ADK agent to a A2A Starlette application.
@@ -51,13 +57,14 @@ def to_a2a(
agent: The ADK agent to convert
host: The host for the A2A RPC URL (default: "localhost")
port: The port for the A2A RPC URL (default: 8000)
protocol: The protocol for the A2A RPC URL (default: "http")
Returns:
A Starlette application that can be run with uvicorn
Example:
agent = MyAgent()
app = to_a2a(agent, host="localhost", port=8000)
app = to_a2a(agent, host="localhost", port=8000, protocol="http")
# Then run with: uvicorn module:app --host localhost --port 8000
"""
# Set up ADK logging to ensure logs are visible when using uvicorn directly
@@ -87,7 +94,7 @@ def to_a2a(
)
# Build agent card
rpc_url = f"http://{host}:{port}/"
rpc_url = f"{protocol}://{host}:{port}/"
card_builder = AgentCardBuilder(
agent=agent,
rpc_url=rpc_url,
+12
View File
@@ -181,6 +181,18 @@ class BaseAgent(BaseModel):
cloned_agent = self.model_copy(update=update)
# If any field is stored as list and not provided in the update, need to
# shallow copy it for the cloned agent to avoid sharing the same list object
# with the original agent.
for field_name in cloned_agent.__class__.model_fields:
if field_name == 'sub_agents':
continue
if update is not None and field_name in update:
continue
field = getattr(cloned_agent, field_name)
if isinstance(field, list):
setattr(cloned_agent, field_name, field.copy())
if update is None or 'sub_agents' not in update:
# If `sub_agents` is not provided in the update, need to recursively clone
# the sub-agents to avoid sharing the sub-agents with the original agent.
@@ -79,12 +79,3 @@ Example:
default=None,
description='Optional. The after_agent_callbacks of the agent.',
)
def to_agent_config(
self, custom_agent_config_cls: Type[TBaseAgentConfig]
) -> TBaseAgentConfig:
"""Converts this config to the concrete agent config type.
NOTE: this is for ADK framework use only.
"""
return custom_agent_config_cls.model_validate(self.model_dump())
@@ -40,6 +40,25 @@ class LlmCallsLimitExceededError(Exception):
"""Error thrown when the number of LLM calls exceed the limit."""
class RealtimeCacheEntry(BaseModel):
"""Store audio data chunks for caching before flushing."""
model_config = ConfigDict(
arbitrary_types_allowed=True,
extra="forbid",
)
"""The pydantic model config."""
role: str
"""The role that created this audio data, typically "user" or "model"."""
data: types.Blob
"""The audio data chunk."""
timestamp: float
"""Timestamp when the audio chunk was received."""
class _InvocationCostManager(BaseModel):
"""A container to keep track of the cost of invocation.
@@ -156,6 +175,12 @@ class InvocationContext(BaseModel):
live_session_resumption_handle: Optional[str] = None
"""The handle for live session resumption."""
input_realtime_cache: Optional[list[RealtimeCacheEntry]] = None
"""Caches input audio chunks before flushing to session and artifact services."""
output_realtime_cache: Optional[list[RealtimeCacheEntry]] = None
"""Caches output audio chunks before flushing to session and artifact services."""
run_config: Optional[RunConfig] = None
"""Configurations for live agents under this invocation."""
+106 -28
View File
@@ -17,11 +17,9 @@
from __future__ import annotations
import asyncio
from typing import Any
import sys
from typing import AsyncGenerator
from typing import ClassVar
from typing import Dict
from typing import Type
from typing_extensions import override
@@ -49,6 +47,70 @@ def _create_branch_ctx_for_sub_agent(
return invocation_context
# TODO - remove once Python <3.11 is no longer supported.
async def _merge_agent_run_pre_3_11(
agent_runs: list[AsyncGenerator[Event, None]],
) -> AsyncGenerator[Event, None]:
"""Merges the agent run event generator.
This version works in Python 3.9 and 3.10 and uses custom replacement for
asyncio.TaskGroup for tasks cancellation and exception handling.
This implementation guarantees for each agent, it won't move on until the
generated event is processed by upstream runner.
Args:
agent_runs: A list of async generators that yield events from each agent.
Yields:
Event: The next event from the merged generator.
"""
sentinel = object()
queue = asyncio.Queue()
def propagate_exceptions(tasks):
# Propagate exceptions and errors from tasks.
for task in tasks:
if task.done():
# Ignore the result (None) of correctly finished tasks and re-raise
# exceptions and errors.
task.result()
# Agents are processed in parallel.
# Events for each agent are put on queue sequentially.
async def process_an_agent(events_for_one_agent):
try:
async for event in events_for_one_agent:
resume_signal = asyncio.Event()
await queue.put((event, resume_signal))
# Wait for upstream to consume event before generating new events.
await resume_signal.wait()
finally:
# Mark agent as finished.
await queue.put((sentinel, None))
tasks = []
try:
for events_for_one_agent in agent_runs:
tasks.append(asyncio.create_task(process_an_agent(events_for_one_agent)))
sentinel_count = 0
# Run until all agents finished processing.
while sentinel_count < len(agent_runs):
propagate_exceptions(tasks)
event, resume_signal = await queue.get()
# Agent finished processing.
if event is sentinel:
sentinel_count += 1
else:
yield event
# Signal to agent that event has been processed by runner and it can
# continue now.
resume_signal.set()
finally:
for task in tasks:
task.cancel()
async def _merge_agent_run(
agent_runs: list[AsyncGenerator[Event, None]],
) -> AsyncGenerator[Event, None]:
@@ -63,30 +125,37 @@ async def _merge_agent_run(
Yields:
Event: The next event from the merged generator.
"""
tasks = [
asyncio.create_task(events_for_one_agent.__anext__())
for events_for_one_agent in agent_runs
]
pending_tasks = set(tasks)
sentinel = object()
queue = asyncio.Queue()
while pending_tasks:
done, pending_tasks = await asyncio.wait(
pending_tasks, return_when=asyncio.FIRST_COMPLETED
)
for task in done:
try:
yield task.result()
# Agents are processed in parallel.
# Events for each agent are put on queue sequentially.
async def process_an_agent(events_for_one_agent):
try:
async for event in events_for_one_agent:
resume_signal = asyncio.Event()
await queue.put((event, resume_signal))
# Wait for upstream to consume event before generating new events.
await resume_signal.wait()
finally:
# Mark agent as finished.
await queue.put((sentinel, None))
# Find the generator that produced this event and move it on.
for i, original_task in enumerate(tasks):
if task == original_task:
new_task = asyncio.create_task(agent_runs[i].__anext__())
tasks[i] = new_task
pending_tasks.add(new_task)
break # stop iterating once found
async with asyncio.TaskGroup() as tg:
for events_for_one_agent in agent_runs:
tg.create_task(process_an_agent(events_for_one_agent))
except StopAsyncIteration:
continue
sentinel_count = 0
# Run until all agents finished processing.
while sentinel_count < len(agent_runs):
event, resume_signal = await queue.get()
# Agent finished processing.
if event is sentinel:
sentinel_count += 1
else:
yield event
# Signal to agent that it should generate next event.
resume_signal.set()
class ParallelAgent(BaseAgent):
@@ -112,10 +181,19 @@ class ParallelAgent(BaseAgent):
)
for sub_agent in self.sub_agents
]
async with Aclosing(_merge_agent_run(agent_runs)) as agen:
async for event in agen:
yield event
try:
# TODO remove if once Python <3.11 is no longer supported.
if sys.version_info >= (3, 11):
async with Aclosing(_merge_agent_run(agent_runs)) as agen:
async for event in agen:
yield event
else:
async with Aclosing(_merge_agent_run_pre_3_11(agent_runs)) as agen:
async for event in agen:
yield event
finally:
for sub_agent_run in agent_runs:
await sub_agent_run.aclose()
@override
async def _run_live_impl(
+180 -25
View File
@@ -45,6 +45,7 @@ from opentelemetry.sdk.trace import TracerProvider
from pydantic import Field
from pydantic import ValidationError
from starlette.types import Lifespan
from typing_extensions import deprecated
from typing_extensions import override
from watchdog.observers import Observer
@@ -66,6 +67,7 @@ from ..evaluation.eval_metrics import EvalMetricResult
from ..evaluation.eval_metrics import EvalMetricResultPerInvocation
from ..evaluation.eval_metrics import MetricInfo
from ..evaluation.eval_result import EvalSetResult
from ..evaluation.eval_set import EvalSet
from ..evaluation.eval_set_results_manager import EvalSetResultsManager
from ..evaluation.eval_sets_manager import EvalSetsManager
from ..events.event import Event
@@ -171,7 +173,18 @@ class AddSessionToEvalSetRequest(common.BaseModel):
class RunEvalRequest(common.BaseModel):
eval_ids: list[str] # if empty, then all evals in the eval set are run.
eval_ids: list[str] = Field(
deprecated=True,
default_factory=list,
description="This field is deprecated, use eval_case_ids instead.",
)
eval_case_ids: list[str] = Field(
default_factory=list,
description=(
"List of eval case ids to evaluate. if empty, then all eval cases in"
" the eval set are run."
),
)
eval_metrics: list[EvalMetric]
@@ -193,10 +206,38 @@ class RunEvalResult(common.BaseModel):
session_id: str
class RunEvalResponse(common.BaseModel):
run_eval_results: list[RunEvalResult]
class GetEventGraphResult(common.BaseModel):
dot_src: str
class CreateEvalSetRequest(common.BaseModel):
eval_set: EvalSet
class ListEvalSetsResponse(common.BaseModel):
eval_set_ids: list[str]
class EvalResult(EvalSetResult):
"""This class has no field intentionally.
The goal here is to just give a new name to the class to align with the API
endpoint.
"""
class ListEvalResultsResponse(common.BaseModel):
eval_result_ids: list[str]
class ListMetricsInfoResponse(common.BaseModel):
metrics_info: list[MetricInfo]
class AdkWebServer:
"""Helper class for setting up and running the ADK web server on FastAPI.
@@ -466,36 +507,78 @@ class AdkWebServer:
)
@app.post(
"/apps/{app_name}/eval_sets/{eval_set_id}",
"/apps/{app_name}/eval-sets",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def create_eval_set(
app_name: str,
eval_set_id: str,
):
"""Creates an eval set, given the id."""
app_name: str, create_eval_set_request: CreateEvalSetRequest
) -> EvalSet:
try:
self.eval_sets_manager.create_eval_set(app_name, eval_set_id)
return self.eval_sets_manager.create_eval_set(
app_name=app_name,
eval_set_id=create_eval_set_request.eval_set.eval_set_id,
)
except ValueError as ve:
raise HTTPException(
status_code=400,
detail=str(ve),
) from ve
@deprecated(
"Please use create_eval_set instead. This will be removed in future"
" releases."
)
@app.post(
"/apps/{app_name}/eval_sets/{eval_set_id}",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def create_eval_set_legacy(
app_name: str,
eval_set_id: str,
):
"""Creates an eval set, given the id."""
await create_eval_set(
app_name=app_name,
create_eval_set_request=CreateEvalSetRequest(
eval_set=EvalSet(eval_set_id=eval_set_id, eval_cases=[])
),
)
@app.get(
"/apps/{app_name}/eval-sets",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def list_eval_sets(app_name: str) -> ListEvalSetsResponse:
"""Lists all eval sets for the given app."""
eval_sets = []
try:
eval_sets = self.eval_sets_manager.list_eval_sets(app_name)
except NotFoundError as e:
logger.warning(e)
return ListEvalSetsResponse(eval_set_ids=eval_sets)
@deprecated(
"Please use list_eval_sets instead. This will be removed in future"
" releases."
)
@app.get(
"/apps/{app_name}/eval_sets",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def list_eval_sets(app_name: str) -> list[str]:
"""Lists all eval sets for the given app."""
try:
return self.eval_sets_manager.list_eval_sets(app_name)
except NotFoundError as e:
logger.warning(e)
return []
async def list_eval_sets_legacy(app_name: str) -> list[str]:
list_eval_sets_response = await list_eval_sets(app_name)
return list_eval_sets_response.eval_set_ids
@app.post(
"/apps/{app_name}/eval-sets/{eval_set_id}/add-session",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
@app.post(
"/apps/{app_name}/eval_sets/{eval_set_id}/add_session",
response_model_exclude_none=True,
@@ -555,6 +638,11 @@ class AdkWebServer:
return sorted([x.eval_id for x in eval_set_data.eval_cases])
@app.get(
"/apps/{app_name}/eval-sets/{eval_set_id}/eval-cases/{eval_case_id}",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
@app.get(
"/apps/{app_name}/eval_sets/{eval_set_id}/evals/{eval_case_id}",
response_model_exclude_none=True,
@@ -578,6 +666,11 @@ class AdkWebServer:
),
)
@app.put(
"/apps/{app_name}/eval-sets/{eval_set_id}/eval-cases/{eval_case_id}",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
@app.put(
"/apps/{app_name}/eval_sets/{eval_set_id}/evals/{eval_case_id}",
response_model_exclude_none=True,
@@ -610,6 +703,10 @@ class AdkWebServer:
except NotFoundError as nfe:
raise HTTPException(status_code=404, detail=str(nfe)) from nfe
@app.delete(
"/apps/{app_name}/eval-sets/{eval_set_id}/eval-cases/{eval_case_id}",
tags=[TAG_EVALUATION],
)
@app.delete(
"/apps/{app_name}/eval_sets/{eval_set_id}/evals/{eval_case_id}",
tags=[TAG_EVALUATION],
@@ -624,14 +721,30 @@ class AdkWebServer:
except NotFoundError as nfe:
raise HTTPException(status_code=404, detail=str(nfe)) from nfe
@deprecated(
"Please use run_eval instead. This will be removed in future releases."
)
@app.post(
"/apps/{app_name}/eval_sets/{eval_set_id}/run_eval",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def run_eval(
async def run_eval_legacy(
app_name: str, eval_set_id: str, req: RunEvalRequest
) -> list[RunEvalResult]:
run_eval_response = await run_eval(
app_name=app_name, eval_set_id=eval_set_id, req=req
)
return run_eval_response.run_eval_results
@app.post(
"/apps/{app_name}/eval-sets/{eval_set_id}/run",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def run_eval(
app_name: str, eval_set_id: str, req: RunEvalRequest
) -> RunEvalResponse:
"""Runs an eval given the details in the eval request."""
# Create a mapping from eval set file to all the evals that needed to be
# run.
@@ -661,7 +774,7 @@ class AdkWebServer:
inference_request = InferenceRequest(
app_name=app_name,
eval_set_id=eval_set.eval_set_id,
eval_case_ids=req.eval_ids,
eval_case_ids=req.eval_case_ids or req.eval_ids,
inference_config=InferenceConfig(),
)
inference_results = await _collect_inferences(
@@ -694,18 +807,41 @@ class AdkWebServer:
)
)
return run_eval_results
return RunEvalResponse(run_eval_results=run_eval_results)
@app.get(
"/apps/{app_name}/eval_results/{eval_result_id}",
"/apps/{app_name}/eval-results/{eval_result_id}",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def get_eval_result(
app_name: str,
eval_result_id: str,
) -> EvalSetResult:
) -> EvalResult:
"""Gets the eval result for the given eval id."""
try:
eval_set_result = self.eval_set_results_manager.get_eval_set_result(
app_name, eval_result_id
)
return EvalResult(**eval_set_result.model_dump())
except ValueError as ve:
raise HTTPException(status_code=404, detail=str(ve)) from ve
except ValidationError as ve:
raise HTTPException(status_code=500, detail=str(ve)) from ve
@deprecated(
"Please use get_eval_result instead. This will be removed in future"
" releases."
)
@app.get(
"/apps/{app_name}/eval_results/{eval_result_id}",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def get_eval_result_legacy(
app_name: str,
eval_result_id: str,
) -> EvalSetResult:
try:
return self.eval_set_results_manager.get_eval_set_result(
app_name, eval_result_id
@@ -715,28 +851,47 @@ class AdkWebServer:
except ValidationError as ve:
raise HTTPException(status_code=500, detail=str(ve)) from ve
@app.get(
"/apps/{app_name}/eval-results",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def list_eval_results(app_name: str) -> ListEvalResultsResponse:
"""Lists all eval results for the given app."""
eval_result_ids = self.eval_set_results_manager.list_eval_set_results(
app_name
)
return ListEvalResultsResponse(eval_result_ids=eval_result_ids)
@deprecated(
"Please use list_eval_results instead. This will be removed in future"
" releases."
)
@app.get(
"/apps/{app_name}/eval_results",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def list_eval_results(app_name: str) -> list[str]:
"""Lists all eval results for the given app."""
return self.eval_set_results_manager.list_eval_set_results(app_name)
async def list_eval_results_legacy(app_name: str) -> list[str]:
list_eval_results_response = await list_eval_results(app_name)
return list_eval_results_response.eval_result_ids
@app.get(
"/apps/{app_name}/eval_metrics",
"/apps/{app_name}/metrics-info",
response_model_exclude_none=True,
tags=[TAG_EVALUATION],
)
async def list_eval_metrics(app_name: str) -> list[MetricInfo]:
async def list_metrics_info(app_name: str) -> ListMetricsInfoResponse:
"""Lists all eval metrics for the given app."""
try:
from ..evaluation.metric_evaluator_registry import DEFAULT_METRIC_EVALUATOR_REGISTRY
# Right now we ignore the app_name as eval metrics are not tied to the
# app_name, but they could be moving forward.
return DEFAULT_METRIC_EVALUATOR_REGISTRY.get_registered_metrics()
metrics_info = (
DEFAULT_METRIC_EVALUATOR_REGISTRY.get_registered_metrics()
)
return ListMetricsInfoResponse(metrics_info=metrics_info)
except ModuleNotFoundError as e:
logger.exception("%s\n%s", MISSING_EVAL_DEPENDENCIES_MESSAGE, e)
raise HTTPException(
+154 -36
View File
@@ -13,6 +13,7 @@
# limitations under the License.
from __future__ import annotations
import json
import os
import shutil
import subprocess
@@ -28,9 +29,6 @@ WORKDIR /app
# Create a non-root user
RUN adduser --disabled-password --gecos "" myuser
# Change ownership of /app to myuser
RUN chown -R myuser:myuser /app
# Switch to the non-root user
USER myuser
@@ -49,8 +47,8 @@ RUN pip install google-adk=={adk_version}
# Copy agent - Start
COPY "agents/{app_name}/" "/app/agents/{app_name}/"
{install_agent_deps}
# Set permission
COPY --chown=myuser:myuser "agents/{app_name}/" "/app/agents/{app_name}/"
# Copy agent - End
@@ -95,6 +93,52 @@ def _resolve_project(project_in_option: Optional[str]) -> str:
return project
def _validate_gcloud_extra_args(
extra_gcloud_args: Optional[tuple[str, ...]], adk_managed_args: set[str]
) -> None:
"""Validates that extra gcloud args don't conflict with ADK-managed args.
This function dynamically checks for conflicts based on the actual args
that ADK will set, rather than using a hardcoded list.
Args:
extra_gcloud_args: User-provided extra arguments for gcloud.
adk_managed_args: Set of argument names that ADK will set automatically.
Should include '--' prefix (e.g., '--project').
Raises:
click.ClickException: If any conflicts are found.
"""
if not extra_gcloud_args:
return
# Parse user arguments into a set of argument names for faster lookup
user_arg_names = set()
for arg in extra_gcloud_args:
if arg.startswith('--'):
# Handle both '--arg=value' and '--arg value' formats
arg_name = arg.split('=')[0]
user_arg_names.add(arg_name)
# Check for conflicts with ADK-managed args
conflicts = user_arg_names.intersection(adk_managed_args)
if conflicts:
conflict_list = ', '.join(f"'{arg}'" for arg in sorted(conflicts))
if len(conflicts) == 1:
raise click.ClickException(
f"The argument {conflict_list} conflicts with ADK's automatic"
' configuration. ADK will set this argument automatically, so please'
' remove it from your command.'
)
else:
raise click.ClickException(
f"The arguments {conflict_list} conflict with ADK's automatic"
' configuration. ADK will set these arguments automatically, so'
' please remove them from your command.'
)
def _get_service_option_by_adk_version(
adk_version: str,
session_uri: Optional[str],
@@ -141,6 +185,7 @@ def to_cloud_run(
artifact_service_uri: Optional[str] = None,
memory_service_uri: Optional[str] = None,
a2a: bool = False,
extra_gcloud_args: Optional[tuple[str, ...]] = None,
):
"""Deploys an agent to Google Cloud Run.
@@ -234,26 +279,56 @@ def to_cloud_run(
click.echo('Deploying to Cloud Run...')
region_options = ['--region', region] if region else []
project = _resolve_project(project)
subprocess.run(
[
'gcloud',
'run',
'deploy',
service_name,
'--source',
temp_folder,
'--project',
project,
*region_options,
'--port',
str(port),
'--verbosity',
log_level.lower() if log_level else verbosity,
'--labels',
'created-by=adk',
],
check=True,
)
# Build the set of args that ADK will manage
adk_managed_args = {'--source', '--project', '--port', '--verbosity'}
if region:
adk_managed_args.add('--region')
# Validate that extra gcloud args don't conflict with ADK-managed args
_validate_gcloud_extra_args(extra_gcloud_args, adk_managed_args)
# Build the command with extra gcloud args
gcloud_cmd = [
'gcloud',
'run',
'deploy',
service_name,
'--source',
temp_folder,
'--project',
project,
*region_options,
'--port',
str(port),
'--verbosity',
log_level.lower() if log_level else verbosity,
]
# Handle labels specially - merge user labels with ADK label
user_labels = []
extra_args_without_labels = []
if extra_gcloud_args:
for arg in extra_gcloud_args:
if arg.startswith('--labels='):
# Extract user-provided labels
user_labels_value = arg[9:] # Remove '--labels=' prefix
user_labels.append(user_labels_value)
else:
extra_args_without_labels.append(arg)
# Combine ADK label with user labels
all_labels = ['created-by=adk']
all_labels.extend(user_labels)
labels_arg = ','.join(all_labels)
gcloud_cmd.extend(['--labels', labels_arg])
# Add any remaining extra passthrough args
gcloud_cmd.extend(extra_args_without_labels)
subprocess.run(gcloud_cmd, check=True)
finally:
click.echo(f'Cleaning up the temp folder: {temp_folder}')
shutil.rmtree(temp_folder)
@@ -274,6 +349,7 @@ def to_agent_engine(
description: Optional[str] = None,
requirements_file: Optional[str] = None,
env_file: Optional[str] = None,
agent_engine_config_file: Optional[str] = None,
):
"""Deploys an agent to Vertex AI Agent Engine.
@@ -323,6 +399,9 @@ def to_agent_engine(
variables. If not specified, the `.env` file in the `agent_folder` will be
used. The values of `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`
will be overridden by `project` and `region` if they are specified.
agent_engine_config_file (str): The filepath to the agent engine config file
to use. If not specified, the `.agent_engine_config.json` file in the
`agent_folder` will be used.
"""
app_name = os.path.basename(agent_folder)
agent_src_path = os.path.join(temp_folder, app_name)
@@ -353,6 +432,34 @@ def to_agent_engine(
project = _resolve_project(project)
click.echo('Resolving files and dependencies...')
agent_config = {}
if not agent_engine_config_file:
# Attempt to read the agent engine config from .agent_engine_config.json in the dir (if any).
agent_engine_config_file = os.path.join(
agent_folder, '.agent_engine_config.json'
)
if os.path.exists(agent_engine_config_file):
click.echo(f'Reading agent engine config from {agent_engine_config_file}')
with open(agent_engine_config_file, 'r') as f:
agent_config = json.load(f)
if display_name:
if 'display_name' in agent_config:
click.echo(
'Overriding display_name in agent engine config with'
f' {display_name}'
)
agent_config['display_name'] = display_name
if description:
if 'description' in agent_config:
click.echo(
f'Overriding description in agent engine config with {description}'
)
agent_config['description'] = description
if agent_config.get('extra_packages'):
agent_config['extra_packages'].append(temp_folder)
else:
agent_config['extra_packages'] = [temp_folder]
if not requirements_file:
# Attempt to read requirements from requirements.txt in the dir (if any).
requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt')
@@ -361,7 +468,18 @@ def to_agent_engine(
with open(requirements_txt_path, 'w', encoding='utf-8') as f:
f.write('google-cloud-aiplatform[adk,agent_engines]')
click.echo(f'Created {requirements_txt_path}')
requirements_file = requirements_txt_path
agent_config['requirements'] = agent_config.get(
'requirements',
requirements_txt_path,
)
else:
if 'requirements' in agent_config:
click.echo(
'Overriding requirements in agent engine config with '
f'{requirements_file}'
)
agent_config['requirements'] = requirements_file
env_vars = None
if not env_file:
# Attempt to read the env variables from .env in the dir (if any).
@@ -395,6 +513,14 @@ def to_agent_engine(
else:
region = env_region
click.echo(f'{region=} set by GOOGLE_CLOUD_LOCATION in {env_file}')
if env_vars:
if 'env_vars' in agent_config:
click.echo(
f'Overriding env_vars in agent engine config with {env_vars}'
)
agent_config['env_vars'] = env_vars
# Set env_vars in agent_config to None if it is not set.
agent_config['env_vars'] = agent_config.get('env_vars', env_vars)
vertexai.init(
project=project,
@@ -406,7 +532,7 @@ def to_agent_engine(
is_config_agent = False
config_root_agent_file = os.path.join(agent_src_path, 'root_agent.yaml')
if os.path.exists(config_root_agent_file):
click.echo('Config agent detected.')
click.echo(f'Config agent detected: {config_root_agent_file}')
is_config_agent = True
adk_app_file = os.path.join(temp_folder, f'{adk_app}.py')
@@ -439,7 +565,7 @@ def to_agent_engine(
click.echo(f'The following exception was raised: {e}')
click.echo('Deploying to agent engine...')
agent_engine = agent_engines.ModuleAgent(
agent_config['agent_engine'] = agent_engines.ModuleAgent(
module_name=adk_app,
agent_name='adk_app',
register_operations={
@@ -461,14 +587,6 @@ def to_agent_engine(
sys_paths=[temp_folder[1:]],
agent_framework='google-adk',
)
agent_config = dict(
agent_engine=agent_engine,
requirements=requirements_file,
display_name=display_name,
description=description,
env_vars=env_vars,
extra_packages=[temp_folder],
)
if not agent_engine_id:
agent_engines.create(**agent_config)

Some files were not shown because too many files have changed in this diff Show More