Merge branch 'main' into feature/google-api-toolset-additional-headers-3105

This commit is contained in:
Parham MohammadAlizadeh
2025-10-17 16:23:25 +01:00
committed by GitHub
58 changed files with 3396 additions and 870 deletions
@@ -69,8 +69,10 @@ def get_pull_request_details(pr_number: int) -> str:
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
id
number
title
body
state
author {
login
}
@@ -13,6 +13,7 @@
# limitations under the License.
import asyncio
import logging
import time
from adk_pr_triaging_agent import agent
@@ -21,11 +22,14 @@ from adk_pr_triaging_agent.settings import PULL_REQUEST_NUMBER
from adk_pr_triaging_agent.settings import REPO
from adk_pr_triaging_agent.utils import call_agent_async
from adk_pr_triaging_agent.utils import parse_number_string
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
APP_NAME = "adk_pr_triaging_app"
USER_ID = "adk_pr_triaging_user"
logs.setup_adk_logger(level=logging.DEBUG)
async def main():
runner = InMemoryRunner(
@@ -17,7 +17,7 @@ import random
from dotenv import load_dotenv
from google.adk import Agent
from google.adk.tools.google_search_tool import google_search
from google.adk.tools.google_search_tool import GoogleSearchTool
from google.adk.tools.tool_context import ToolContext
from google.adk.tools.vertex_ai_search_tool import VertexAiSearchTool
@@ -57,7 +57,9 @@ root_agent = Agent(
""",
tools=[
roll_die,
VertexAiSearchTool(data_store_id=VERTEXAI_DATASTORE_ID),
google_search,
VertexAiSearchTool(
data_store_id=VERTEXAI_DATASTORE_ID, bypass_multi_tools_limit=True
),
GoogleSearchTool(bypass_multi_tools_limit=True),
],
)
@@ -0,0 +1,96 @@
# Computer Use Agent
This directory contains a computer use agent that can operate a browser to complete user tasks. The agent uses Playwright to control a Chromium browser and can interact with web pages by taking screenshots, clicking, typing, and navigating.
This agent is to demo the usage of ComputerUseToolset.
## Overview
The computer use agent consists of:
- `agent.py`: Main agent configuration using Google's gemini-2.5-computer-use-preview-10-2025 model
- `playwright.py`: Playwright-based computer implementation for browser automation
- `requirements.txt`: Python dependencies
## Setup
### 1. Install Python Dependencies
Install the required Python packages from the requirements file:
```bash
uv pip install -r internal/samples/computer_use/requirements.txt
```
### 2. Install Playwright Dependencies
Install Playwright's system dependencies for Chromium:
```bash
playwright install-deps chromium
```
### 3. Install Chromium Browser
Install the Chromium browser for Playwright:
```bash
playwright install chromium
```
## Usage
### Running the Agent
To start the computer use agent, run the following command from the project root:
```bash
adk web internal/samples
```
This will start the ADK web interface where you can interact with the computer_use agent.
### Example Queries
Once the agent is running, you can send queries like:
```
find me a flight from SF to Hawaii on next Monday, coming back on next Friday. start by navigating directly to flights.google.com
```
The agent will:
1. Open a browser window
2. Navigate to the specified website
3. Interact with the page elements to complete your task
4. Provide updates on its progress
### Other Example Tasks
- Book hotel reservations
- Search for products online
- Fill out forms
- Navigate complex websites
- Research information across multiple pages
## Technical Details
- **Model**: Uses Google's `gemini-2.5-computer-use-preview-10-2025` model for computer use capabilities
- **Browser**: Automated Chromium browser via Playwright
- **Screen Size**: Configured for 600x800 resolution
- **Tools**: Uses ComputerUseToolset for screen capture, clicking, typing, and scrolling
## Troubleshooting
If you encounter issues:
1. **Playwright not found**: Make sure you've run both `playwright install-deps chromium` and `playwright install chromium`
2. **Dependencies missing**: Verify all packages from `requirements.txt` are installed
3. **Browser crashes**: Check that your system supports Chromium and has sufficient resources
4. **Permission errors**: Ensure your user has permission to run browser automation tools
## Notes
- The agent operates in a controlled browser environment
- Screenshots are taken to help the agent understand the current state
- The agent will provide updates on its actions as it works
- Be patient as complex tasks may take some time to complete
+35
View File
@@ -0,0 +1,35 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk.models.google_llm import Gemini
from google.adk.tools.computer_use.computer_use_toolset import ComputerUseToolset
from typing_extensions import override
from .playwright import PlaywrightComputer
root_agent = Agent(
model='gemini-2.5-computer-use-preview-10-2025',
name='hello_world_agent',
description=(
'computer use agent that can operate a browser on a computer to finish'
' user tasks'
),
instruction="""
you are a computer use agent
""",
tools=[
ComputerUseToolset(computer=PlaywrightComputer(screen_size=(1280, 936)))
],
)
@@ -0,0 +1,317 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import time
from typing import Literal
from google.adk.tools.computer_use.base_computer import BaseComputer
from google.adk.tools.computer_use.base_computer import ComputerEnvironment
from google.adk.tools.computer_use.base_computer import ComputerState
from playwright.async_api import async_playwright
import termcolor
from typing_extensions import override
# Define a mapping from the user-friendly key names to Playwright's expected key names.
# Playwright is generally good with case-insensitivity for these, but it's best to be canonical.
# See: https://playwright.dev/docs/api/class-keyboard#keyboard-press
# Keys like 'a', 'b', '1', '$' are passed directly.
PLAYWRIGHT_KEY_MAP = {
"backspace": "Backspace",
"tab": "Tab",
"return": "Enter", # Playwright uses 'Enter'
"enter": "Enter",
"shift": "Shift",
"control": "Control", # Or 'ControlOrMeta' for cross-platform Ctrl/Cmd
"alt": "Alt",
"escape": "Escape",
"space": "Space", # Can also just be " "
"pageup": "PageUp",
"pagedown": "PageDown",
"end": "End",
"home": "Home",
"left": "ArrowLeft",
"up": "ArrowUp",
"right": "ArrowRight",
"down": "ArrowDown",
"insert": "Insert",
"delete": "Delete",
"semicolon": ";", # For actual character ';'
"equals": "=", # For actual character '='
"multiply": "Multiply", # NumpadMultiply
"add": "Add", # NumpadAdd
"separator": "Separator", # Numpad specific
"subtract": "Subtract", # NumpadSubtract, or just '-' for character
"decimal": "Decimal", # NumpadDecimal, or just '.' for character
"divide": "Divide", # NumpadDivide, or just '/' for character
"f1": "F1",
"f2": "F2",
"f3": "F3",
"f4": "F4",
"f5": "F5",
"f6": "F6",
"f7": "F7",
"f8": "F8",
"f9": "F9",
"f10": "F10",
"f11": "F11",
"f12": "F12",
"command": "Meta", # 'Meta' is Command on macOS, Windows key on Windows
}
class PlaywrightComputer(BaseComputer):
"""Conputer that controls Chromium via Playwright."""
def __init__(
self,
screen_size: tuple[int, int],
initial_url: str = "https://www.google.com",
search_engine_url: str = "https://www.google.com",
highlight_mouse: bool = False,
):
self._initial_url = initial_url
self._screen_size = screen_size
self._search_engine_url = search_engine_url
self._highlight_mouse = highlight_mouse
@override
async def initialize(self):
print("Creating session...")
self._playwright = await async_playwright().start()
self._browser = await self._playwright.chromium.launch(
args=["--disable-blink-features=AutomationControlled"],
headless=False,
)
self._context = await self._browser.new_context(
viewport={
"width": self._screen_size[0],
"height": self._screen_size[1],
}
)
self._page = await self._context.new_page()
await self._page.goto(self._initial_url)
termcolor.cprint(
f"Started local playwright.",
color="green",
attrs=["bold"],
)
@override
async def environment(self):
return ComputerEnvironment.ENVIRONMENT_BROWSER
@override
async def close(self, exc_type, exc_val, exc_tb):
if self._context:
self._context.close()
try:
self._browser.close()
except Exception as e:
# Browser was already shut down because of SIGINT or such.
if (
"Browser.close: Connection closed while reading from the driver"
in str(e)
):
pass
else:
raise
self._playwright.stop()
async def open_web_browser(self) -> ComputerState:
return await self.current_state()
async def click_at(self, x: int, y: int):
await self.highlight_mouse(x, y)
await self._page.mouse.click(x, y)
await self._page.wait_for_load_state()
return await self.current_state()
async def hover_at(self, x: int, y: int):
await self.highlight_mouse(x, y)
await self._page.mouse.move(x, y)
await self._page.wait_for_load_state()
return await self.current_state()
async def type_text_at(
self,
x: int,
y: int,
text: str,
press_enter: bool = True,
clear_before_typing: bool = True,
) -> ComputerState:
await self.highlight_mouse(x, y)
await self._page.mouse.click(x, y)
await self._page.wait_for_load_state()
if clear_before_typing:
await self.key_combination(["Control", "A"])
await self.key_combination(["Delete"])
await self._page.keyboard.type(text)
await self._page.wait_for_load_state()
if press_enter:
await self.key_combination(["Enter"])
await self._page.wait_for_load_state()
return await self.current_state()
async def _horizontal_document_scroll(
self, direction: Literal["left", "right"]
) -> ComputerState:
# Scroll by 50% of the viewport size.
horizontal_scroll_amount = await self.screen_size()[0] // 2
if direction == "left":
sign = "-"
else:
sign = ""
scroll_argument = f"{sign}{horizontal_scroll_amount}"
# Scroll using JS.
await self._page.evaluate(f"window.scrollBy({scroll_argument}, 0); ")
await self._page.wait_for_load_state()
return await self.current_state()
async def scroll_document(
self, direction: Literal["up", "down", "left", "right"]
) -> ComputerState:
if direction == "down":
return await self.key_combination(["PageDown"])
elif direction == "up":
return await self.key_combination(["PageUp"])
elif direction in ("left", "right"):
return await self._horizontal_document_scroll(direction)
else:
raise ValueError("Unsupported direction: ", direction)
async def scroll_at(
self,
x: int,
y: int,
direction: Literal["up", "down", "left", "right"],
magnitude: int,
) -> ComputerState:
await self.highlight_mouse(x, y)
await self._page.mouse.move(x, y)
await self._page.wait_for_load_state()
dx = 0
dy = 0
if direction == "up":
dy = -magnitude
elif direction == "down":
dy = magnitude
elif direction == "left":
dx = -magnitude
elif direction == "right":
dx = magnitude
else:
raise ValueError("Unsupported direction: ", direction)
await self._page.mouse.wheel(dx, dy)
await self._page.wait_for_load_state()
return await self.current_state()
async def wait(self, seconds: int) -> ComputerState:
await asyncio.sleep(seconds)
return await self.current_state()
async def go_back(self) -> ComputerState:
await self._page.go_back()
await self._page.wait_for_load_state()
return await self.current_state()
async def go_forward(self) -> ComputerState:
await self._page.go_forward()
await self._page.wait_for_load_state()
return await self.current_state()
async def search(self) -> ComputerState:
return await self.navigate(self._search_engine_url)
async def navigate(self, url: str) -> ComputerState:
await self._page.goto(url)
await self._page.wait_for_load_state()
return await self.current_state()
async def key_combination(self, keys: list[str]) -> ComputerState:
# Normalize all keys to the Playwright compatible version.
keys = [PLAYWRIGHT_KEY_MAP.get(k.lower(), k) for k in keys]
for key in keys[:-1]:
await self._page.keyboard.down(key)
await self._page.keyboard.press(keys[-1])
for key in reversed(keys[:-1]):
await self._page.keyboard.up(key)
return await self.current_state()
async def drag_and_drop(
self, x: int, y: int, destination_x: int, destination_y: int
) -> ComputerState:
await self.highlight_mouse(x, y)
await self._page.mouse.move(x, y)
await self._page.wait_for_load_state()
await self._page.mouse.down()
await self._page.wait_for_load_state()
await self.highlight_mouse(destination_x, destination_y)
await self._page.mouse.move(destination_x, destination_y)
await self._page.wait_for_load_state()
await self._page.mouse.up()
return await self.current_state()
async def current_state(self) -> ComputerState:
await self._page.wait_for_load_state()
# Even if Playwright reports the page as loaded, it may not be so.
# Add a manual sleep to make sure the page has finished rendering.
time.sleep(0.5)
screenshot_bytes = await self._page.screenshot(type="png", full_page=False)
return ComputerState(screenshot=screenshot_bytes, url=self._page.url)
async def screen_size(self) -> tuple[int, int]:
return self._screen_size
async def highlight_mouse(self, x: int, y: int):
if not self._highlight_mouse:
return
await self._page.evaluate(f"""
() => {{
const element_id = "playwright-feedback-circle";
const div = document.createElement('div');
div.id = element_id;
div.style.pointerEvents = 'none';
div.style.border = '4px solid red';
div.style.borderRadius = '50%';
div.style.width = '20px';
div.style.height = '20px';
div.style.position = 'fixed';
div.style.zIndex = '9999';
document.body.appendChild(div);
div.hidden = false;
div.style.left = {x} - 10 + 'px';
div.style.top = {y} - 10 + 'px';
setTimeout(() => {{
div.hidden = true;
}}, 2000);
}}
""")
# Wait a bit for the user to see the cursor.
time.sleep(1)
@@ -0,0 +1,4 @@
termcolor==3.1.0
playwright==1.52.0
browserbase==1.3.0
rich
@@ -0,0 +1,8 @@
This agent connects to a local MCP server via Streamable HTTP and provides
custom per-request headers to the MCP server.
To run this agent, start the local MCP server first by running:
```bash
uv run header_server.py
```
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,34 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
root_agent = LlmAgent(
model='gemini-2.0-flash',
name='tenant_agent',
instruction="""You are a helpful assistant that helps users get tenant
information. Call the get_tenant_data tool when the user asks for tenant data.""",
tools=[
McpToolset(
connection_params=StreamableHTTPConnectionParams(
url='http://localhost:3000/mcp',
),
tool_filter=['get_tenant_data'],
header_provider=lambda ctx: {'X-Tenant-ID': 'tenant1'},
)
],
)
@@ -0,0 +1,50 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from fastapi import Request
from mcp.server.fastmcp import Context
from mcp.server.fastmcp import FastMCP
mcp = FastMCP('Header Check Server', host='localhost', port=3000)
TENANT_DATA = {
'tenant1': {'name': 'Tenant 1', 'data': 'Data for tenant 1'},
'tenant2': {'name': 'Tenant 2', 'data': 'Data for tenant 2'},
}
@mcp.tool(
description='Returns tenant specific data based on X-Tenant-ID header.'
)
def get_tenant_data(context: Context) -> dict:
"""Return tenant specific data."""
if context.request_context and context.request_context.request:
headers = context.request_context.request.headers
tenant_id = headers.get('x-tenant-id')
if tenant_id in TENANT_DATA:
return TENANT_DATA[tenant_id]
else:
return {'error': f'Tenant {tenant_id} not found'}
else:
return {'error': 'Could not get request context'}
if __name__ == '__main__':
try:
print('Starting Header Check MCP server on http://localhost:3000')
mcp.run(transport='streamable-http')
except KeyboardInterrupt:
print('\nServer stopped.')
@@ -46,8 +46,30 @@ You can run the agent with:
$ adk web contributing/samples/plugin_reflect_tool_retry
```
You can provide the following prompt to see the agent retrying tool calls:
Select "basic" and provide the following prompt to see the agent retrying tool
calls:
```
Please guess a number! Tell me what number you guess and how is it.
```
### Hallucinating tool calls
The "hallucinating_func_name" agent is an example to show the plugin can retry
hallucinating tool calls.
For example, we used the `after_model_callback` to hack a tool call with the
wrong name then the agent can retry calling with the right tool name.
You can run the agent with:
```bash
$ adk web contributing/samples/plugin_reflect_tool_retry
```
Select "hallucinating_func_name" and provide the following prompt to see the
agent retrying tool calls:
```
Roll a 6 sided die
```
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,81 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import random
from google.adk.agents import LlmAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.apps.app import App
from google.adk.models.llm_response import LlmResponse
from google.adk.plugins import ReflectAndRetryToolPlugin
from google.adk.tools.tool_context import ToolContext
APP_NAME = "hallucinating_func_name"
USER_ID = "test_user"
hallucinated = False # Whether the tool name is hallucinated
def roll_die(sides: int, tool_context: ToolContext) -> int:
"""Roll a die and return the rolled result.
Args:
sides: The integer number of sides the die has.
Returns:
An integer of the result of rolling the die.
"""
result = random.randint(1, sides)
if not "rolls" in tool_context.state:
tool_context.state["rolls"] = []
tool_context.state["rolls"] = tool_context.state["rolls"] + [result]
return result
def after_model_callback(
callback_context: CallbackContext, llm_response: LlmResponse
):
"""After model callback to produce one hallucinating tool call."""
global hallucinated
if hallucinated:
return None
if (
llm_response.content
and llm_response.content.parts[0].function_call.name == "roll_die"
):
llm_response.content.parts[0].function_call.name = "roll_die_wrong_name"
hallucinated = True
return None
root_agent = LlmAgent(
name="hello_world",
description="Helpful agent",
instruction="""Use guess_number_tool to guess a number.""",
model="gemini-2.5-flash",
tools=[roll_die],
after_model_callback=after_model_callback,
)
app = App(
name=APP_NAME,
root_agent=root_agent,
plugins=[
ReflectAndRetryToolPlugin(max_retries=3),
],
)
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,71 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
async def update_state(tool_context: ToolContext, key: str, value: str) -> dict:
"""Updates a state value."""
tool_context.state[key] = value
return {"status": f"Updated state '{key}' to '{value}'"}
async def load_state(tool_context: ToolContext, key: str) -> dict:
"""Loads a state value."""
return {key: tool_context.state.get(key)}
async def save_artifact(
tool_context: ToolContext, filename: str, content: str
) -> dict:
"""Saves an artifact with the given filename and content."""
artifact_bytes = content.encode("utf-8")
artifact_part = types.Part(
inline_data=types.Blob(mime_type="text/plain", data=artifact_bytes)
)
version = await tool_context.save_artifact(filename, artifact_part)
return {"status": "success", "filename": filename, "version": version}
async def load_artifact(tool_context: ToolContext, filename: str) -> dict:
"""Loads an artifact with the given filename."""
artifact = await tool_context.load_artifact(filename)
if not artifact:
return {"error": f"Artifact '{filename}' not found"}
content = artifact.inline_data.data.decode("utf-8")
return {"filename": filename, "content": content}
# Create the agent
root_agent = Agent(
name="state_agent",
model="gemini-2.0-flash",
instruction="""You are an agent that manages state and artifacts.
You can:
- Update state value
- Load state value
- Save artifact
- Load artifact
Use the appropriate tool based on what the user asks for.""",
tools=[
update_state,
load_state,
save_artifact,
load_artifact,
],
)
+101 -101
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -92,7 +92,7 @@ from google.adk.tools import google_search
root_agent = Agent(
name="search_assistant",
model="gemini-2.0-flash", # Or your preferred Gemini model
model="gemini-2.5-flash", # Or your preferred Gemini model
instruction="You are a helpful assistant. Answer user questions using Google Search when needed.",
description="An assistant that can search the web.",
tools=[google_search]
@@ -109,13 +109,13 @@ Define a multi-agent system with coordinator agent, greeter agent, and task exec
from google.adk.agents import LlmAgent, BaseAgent
# Define individual agents
greeter = LlmAgent(name="greeter", model="gemini-2.0-flash", ...)
task_executor = LlmAgent(name="task_executor", model="gemini-2.0-flash", ...)
greeter = LlmAgent(name="greeter", model="gemini-2.5-flash", ...)
task_executor = LlmAgent(name="task_executor", model="gemini-2.5-flash", ...)
# Create parent agent and assign children via sub_agents
coordinator = LlmAgent(
name="Coordinator",
model="gemini-2.0-flash",
model="gemini-2.5-flash",
description="I coordinate greetings and tasks.",
sub_agents=[ # Assign sub_agents here
greeter,
@@ -14,6 +14,7 @@
from __future__ import annotations
from collections.abc import Callable
from datetime import datetime
from datetime import timezone
import logging
@@ -57,6 +58,34 @@ DEFAULT_ERROR_MESSAGE = "An error occurred during processing"
logger = logging.getLogger("google_adk." + __name__)
AdkEventToA2AEventsConverter = Callable[
[
Event,
InvocationContext,
Optional[str],
Optional[str],
GenAIPartToA2APartConverter,
],
List[A2AEvent],
]
"""A callable that converts an ADK Event into a list of A2A events.
This interface allows for custom logic to map ADK's event structure to the
event structure expected by the A2A server.
Args:
event: The source ADK Event to convert.
invocation_context: The context of the ADK agent invocation.
task_id: The ID of the A2A task being processed.
context_id: The context ID from the A2A request.
part_converter: A function to convert GenAI content parts to A2A
parts.
Returns:
A list of A2A events.
"""
def _serialize_metadata_value(value: Any) -> str:
"""Safely serializes metadata values to string format.
@@ -14,8 +14,12 @@
from __future__ import annotations
from collections.abc import Callable
import sys
from typing import Any
from typing import Optional
from pydantic import BaseModel
try:
from a2a.server.agent_execution import RequestContext
@@ -35,6 +39,39 @@ from .part_converter import A2APartToGenAIPartConverter
from .part_converter import convert_a2a_part_to_genai_part
@a2a_experimental
class AgentRunRequest(BaseModel):
"""Data model for arguments passed to the ADK runner."""
user_id: Optional[str] = None
session_id: Optional[str] = None
invocation_id: Optional[str] = None
new_message: Optional[genai_types.Content] = None
state_delta: Optional[dict[str, Any]] = None
run_config: Optional[RunConfig] = None
A2ARequestToAgentRunRequestConverter = Callable[
[
RequestContext,
A2APartToGenAIPartConverter,
],
AgentRunRequest,
]
"""A callable that converts an A2A RequestContext to RunnerRequest for ADK runner.
This interface allows for custom logic to map an incoming A2A RequestContext to the
structured arguments expected by the ADK runner's `run_async` method.
Args:
request: The incoming request context from the A2A server.
part_converter: A function to convert A2A content parts to GenAI parts.
Returns:
An RunnerRequest object containing the keyword arguments for ADK runner's run_async method.
"""
def _get_user_id(request: RequestContext) -> str:
# Get user from call context if available (auth is enabled on a2a server)
if (
@@ -49,20 +86,32 @@ def _get_user_id(request: RequestContext) -> str:
@a2a_experimental
def convert_a2a_request_to_adk_run_args(
def convert_a2a_request_to_agent_run_request(
request: RequestContext,
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
) -> dict[str, Any]:
) -> AgentRunRequest:
"""Converts an A2A RequestContext to an AgentRunRequest model.
Args:
request: The incoming request context from the A2A server.
part_converter: A function to convert A2A content parts to GenAI parts.
Returns:
A AgentRunRequest object ready to be used as arguments for the ADK runner.
Raises:
ValueError: If the request message is None.
"""
if not request.message:
raise ValueError('Request message cannot be None')
return {
'user_id': _get_user_id(request),
'session_id': request.context_id,
'new_message': genai_types.Content(
return AgentRunRequest(
user_id=_get_user_id(request),
session_id=request.context_id,
new_message=genai_types.Content(
role='user',
parts=[part_converter(part) for part in request.message.parts],
),
'run_config': RunConfig(),
}
run_config=RunConfig(),
)

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