You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69eb2b50c5 | |||
| 2dd432cc1f | |||
| 3b997a0a07 | |||
| bcf0dda8bc | |||
| 4c70606129 | |||
| 216cb7aec0 | |||
| ad81aa54de | |||
| 47b88d2b06 | |||
| 6806deaf88 | |||
| 2b2f0b52d8 | |||
| 0eb65c07d5 | |||
| 0b89f1882d | |||
| 124fee64e5 | |||
| a360bc2542 | |||
| 75ba9689e3 | |||
| 2c088acc9b | |||
| 7fe60641e3 | |||
| 395a50b5dc | |||
| ddb070ff07 | |||
| f660180854 | |||
| 157f73181d | |||
| ccab076ace | |||
| 80d188199b | |||
| 29bb75f975 | |||
| 167182be01 | |||
| 3f3aa7b32d | |||
| 826f554789 | |||
| 9645cee3a6 | |||
| 70f50db653 | |||
| 81a53b53d6 | |||
| 52a3d6cb8a | |||
| 331978772a | |||
| e93f861cde | |||
| 54ed079100 | |||
| c144b5347c |
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
@@ -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
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user