chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: adk-agent-builder
|
||||
description: Central hub for building, testing, and iterating on ADK agents. Trigger this skill when the user wants to create a new agent, configure modes (task, single-turn), or build graph-based workflows.
|
||||
---
|
||||
|
||||
# ADK Agent Builder
|
||||
|
||||
This file serves as a directory of specialized reference guides for developing
|
||||
agents with ADK. To avoid context pollution, read only the relevant reference
|
||||
file based on your current task.
|
||||
|
||||
## Core Concepts Directory
|
||||
|
||||
Refer to these files for foundational knowledge:
|
||||
|
||||
- **Getting Started & Basic Agents**: [getting-started.md](references/getting-started.md)
|
||||
- Environment setup, API key configuration, and minimal agent definitions.
|
||||
- **Tool Catalog**: [tool-catalog.md](references/tool-catalog.md)
|
||||
- How to bind function tools, MCP tools, OpenAPI specs, and Google API tools.
|
||||
- **Agent Modes (Task / Single-Turn)**: [task-mode.md](references/task-mode.md)
|
||||
- Multi-turn structured delegation and autonomous single-turn execution patterns.
|
||||
- **Import Paths**: [import-paths.md](references/import-paths.md)
|
||||
- Canonical and verbose import paths for core components, tools, and
|
||||
events.
|
||||
|
||||
## Workflow & Graph Orchestration
|
||||
|
||||
Refer to these files when building complex graphs:
|
||||
|
||||
- **Function Nodes**: [function-nodes.md](references/function-nodes.md)
|
||||
- How to use functions as nodes, type resolution, and generators.
|
||||
- **Routing & Conditions**: [routing-and-conditions.md](references/routing-and-conditions.md)
|
||||
- Edge patterns, dict-based routing, self-loops, and conditional execution.
|
||||
- **LLM Agent Nodes**: [llm-agent-nodes.md](references/llm-agent-nodes.md)
|
||||
- How to use LLM agents as workflow nodes, task wrappers, and handling output schemas.
|
||||
- **Advanced Patterns**:
|
||||
[advanced-patterns.md](references/advanced-patterns.md)
|
||||
- Nested workflows, custom node types, and graph validation rules.
|
||||
|
||||
## Advanced Orchestration Patterns
|
||||
|
||||
- **Parallel Processing & Fan-Out**: [parallel-and-fanout.md](references/parallel-and-fanout.md)
|
||||
- `ParallelWorker` for list splitting and concurrent processing, fan-out/join patterns.
|
||||
- **Human-in-the-Loop**: [human-in-the-loop.md](references/human-in-the-loop.md)
|
||||
- Pausing execution for user input, resumable workflows, and AuthConfig on nodes.
|
||||
- **Dynamic Nodes**: [dynamic-nodes.md](references/dynamic-nodes.md)
|
||||
- Scheduling nodes at runtime dynamically via `ctx.run_node()`.
|
||||
|
||||
## Infrastructure & Utilities
|
||||
|
||||
- **State & Events**: [state-and-events.md](references/state-and-events.md)
|
||||
- Using context API, sharing global state, and yield event structures.
|
||||
- **Session & Memory**:
|
||||
[session-and-state.md](references/session-and-state.md)
|
||||
- Session state mutation, scope conventions, and database session
|
||||
services.
|
||||
- **Callbacks & Plugins**:
|
||||
[callbacks-and-plugins.md](references/callbacks-and-plugins.md)
|
||||
- Implementing callbacks, plugin manager integration, and override
|
||||
behavior.
|
||||
- **Multi-Agent Systems**: [multi-agent.md](references/multi-agent.md)
|
||||
- Hierarchical execution (e.g., `SequentialAgent`, `LoopAgent`, `ParallelAgent`).
|
||||
- **Testing Strategies**: [testing.md](references/testing.md)
|
||||
- Automated queries with `adk run`, unit tests, and integration testing with sample agents.
|
||||
|
||||
## Standards & Guidelines
|
||||
|
||||
- **Best Practices**: [best-practices.md](references/best-practices.md)
|
||||
- Critical rules (Pydantic schemas, content events, state-based data flow).
|
||||
@@ -0,0 +1,308 @@
|
||||
# Advanced Workflow Patterns Reference
|
||||
|
||||
Nested workflows, dynamic nodes, retry configuration, custom node types, and graph construction.
|
||||
|
||||
## 📋 Agent Verification Checklist (Advanced Patterns)
|
||||
Use this checklist when implementing complex workflows:
|
||||
|
||||
- [ ] **Validation**: Does your graph follow all 7 validation rules? (e.g., no unconditional cycles)
|
||||
- [ ] **Custom Nodes**: If creating a custom node, did you override `get_name()` and `run()`?
|
||||
- [ ] **Dynamic Execution**: If using `run_node`, did you follow the rules in the dedicated dynamic-nodes reference?
|
||||
- [ ] **Waiting State**: Did you use `wait_for_output=True` if the node should stay in WAITING state until output is yielded?
|
||||
|
||||
## 💡 Quick Reference
|
||||
|
||||
- **Retry**: `RetryConfig(max_attempts=5, initial_delay=1.0)`
|
||||
- **Custom Node Fields**: `rerun_on_resume`, `wait_for_output`, `retry_config`, `timeout`
|
||||
|
||||
## Nested Workflows
|
||||
|
||||
A `Workflow` is both an agent and a node. Use one workflow inside another:
|
||||
|
||||
```python
|
||||
from google.adk.workflow import Workflow
|
||||
|
||||
# Inner workflow
|
||||
inner = Workflow(
|
||||
name="inner_pipeline",
|
||||
edges=[
|
||||
('START', step_a),
|
||||
(step_a, step_b),
|
||||
],
|
||||
)
|
||||
|
||||
# Outer workflow using inner as a node
|
||||
outer = Workflow(
|
||||
name="outer_pipeline",
|
||||
edges=[
|
||||
('START', pre_process),
|
||||
(pre_process, inner), # Nested workflow
|
||||
(inner, post_process),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
The inner workflow receives the predecessor's output as its START input and its terminal output flows to the next node in the outer workflow.
|
||||
|
||||
## Dynamic Node Scheduling
|
||||
|
||||
Schedule nodes at runtime using `ctx.run_node()`.
|
||||
|
||||
See the dedicated [Dynamic Node Scheduling Reference](dynamic-nodes.md) for
|
||||
detailed rules, examples, and best practices.
|
||||
|
||||
## Retry Configuration
|
||||
|
||||
Configure automatic retry for nodes that may fail:
|
||||
|
||||
```python
|
||||
from google.adk.workflow import RetryConfig
|
||||
from google.adk.workflow import FunctionNode
|
||||
|
||||
retry = RetryConfig(
|
||||
max_attempts=5, # Max attempts (default: 5). 0 or 1 = no retry
|
||||
initial_delay=1.0, # Seconds before first retry (default: 1.0)
|
||||
max_delay=60.0, # Max seconds between retries (default: 60.0)
|
||||
backoff_factor=2.0, # Delay multiplier per attempt (default: 2.0)
|
||||
jitter=1.0, # Randomness factor (default: 1.0, 0.0 = none)
|
||||
exceptions=None, # Exception types to retry (None = all)
|
||||
)
|
||||
|
||||
node = FunctionNode(
|
||||
flaky_api_call,
|
||||
name="api_call",
|
||||
retry_config=retry,
|
||||
)
|
||||
```
|
||||
|
||||
### Retry delay formula
|
||||
|
||||
```
|
||||
delay = initial_delay * (backoff_factor ^ attempt)
|
||||
delay = min(delay, max_delay)
|
||||
delay = delay * (1 + random(0, jitter))
|
||||
```
|
||||
|
||||
### Accessing the attempt count
|
||||
|
||||
```python
|
||||
def my_node(ctx: Context, node_input: str) -> str:
|
||||
# attempt_count is 1 on the first try, ≥2 on retries
|
||||
if ctx.attempt_count > 1:
|
||||
print(f"Retry attempt {ctx.attempt_count}")
|
||||
return "result"
|
||||
```
|
||||
|
||||
## Custom Node Types
|
||||
|
||||
Subclass `BaseNode` for custom behavior:
|
||||
|
||||
```python
|
||||
from google.adk.workflow import BaseNode
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.agents.context import Context
|
||||
from pydantic import ConfigDict, Field
|
||||
from typing import Any, AsyncGenerator
|
||||
from typing_extensions import override
|
||||
|
||||
class BatchProcessorNode(BaseNode):
|
||||
"""Processes items in batches."""
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
name: str = Field(default="batch_processor")
|
||||
batch_size: int = Field(default=10)
|
||||
|
||||
def __init__(self, *, name: str = "batch_processor", batch_size: int = 10):
|
||||
super().__init__()
|
||||
object.__setattr__(self, 'name', name)
|
||||
object.__setattr__(self, 'batch_size', batch_size)
|
||||
|
||||
@override
|
||||
def get_name(self) -> str:
|
||||
return self.name
|
||||
|
||||
@override
|
||||
async def run(
|
||||
self,
|
||||
*,
|
||||
ctx: Context,
|
||||
node_input: Any,
|
||||
) -> AsyncGenerator[Any, None]:
|
||||
items = node_input if isinstance(node_input, list) else [node_input]
|
||||
results = []
|
||||
for i in range(0, len(items), self.batch_size):
|
||||
batch = items[i:i + self.batch_size]
|
||||
batch_result = await process_batch(batch)
|
||||
results.extend(batch_result)
|
||||
yield Event(output=results)
|
||||
```
|
||||
|
||||
### BaseNode Fields
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `rerun_on_resume` | `False` | Whether to rerun after HITL interrupt |
|
||||
| `wait_for_output` | `False` | Node stays in WAITING state until it yields output (see below) |
|
||||
| `retry_config` | `None` | Retry configuration on failure |
|
||||
| `timeout` | `None` | Max seconds for node to complete |
|
||||
|
||||
### wait_for_output
|
||||
|
||||
When `wait_for_output=True`, a node that finishes without yielding an `Event` with output moves to **WAITING** state instead of COMPLETED. Downstream nodes are **not** triggered. The node can then be re-triggered by upstream predecessors.
|
||||
|
||||
This is how `JoinNode` works internally — it runs once per predecessor, storing partial inputs, and only yields output (triggering downstream) when all predecessors have completed. `LlmAgentWrapper` in `task` mode also sets `wait_for_output=True` automatically.
|
||||
|
||||
```python
|
||||
from google.adk.workflow import BaseNode
|
||||
|
||||
class CollectorNode(BaseNode):
|
||||
wait_for_output: bool = True # Stay in WAITING until output is yielded
|
||||
|
||||
async def run(self, *, ctx, node_input):
|
||||
# Store partial input, don't yield output yet
|
||||
collected = ctx.state.get("collected", [])
|
||||
collected.append(node_input)
|
||||
yield Event(state={"collected": collected})
|
||||
|
||||
# Only yield output when we have enough
|
||||
if len(collected) >= 3:
|
||||
yield Event(output=collected)
|
||||
# Now node transitions to COMPLETED and triggers downstream
|
||||
```
|
||||
|
||||
Nodes with `wait_for_output=True` default:
|
||||
|
||||
- `JoinNode`: `True` (waits for all predecessors)
|
||||
- `LlmAgentWrapper` (task mode): `True` (set in `model_post_init`)
|
||||
- All other nodes: `False`
|
||||
|
||||
### Required Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `get_name() -> str` | Return the node name |
|
||||
| `run(*, ctx, node_input) -> AsyncGenerator` | Execute the node, yield events |
|
||||
|
||||
## ToolNode
|
||||
|
||||
Wrap an ADK tool as a workflow node:
|
||||
|
||||
```python
|
||||
from google.adk.workflow._tool_node import _ToolNode as ToolNode
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
|
||||
def search(query: str) -> str:
|
||||
"""Search for information."""
|
||||
return f"Results for: {query}"
|
||||
|
||||
tool = FunctionTool(search)
|
||||
tool_node = ToolNode(tool, name="search_node")
|
||||
|
||||
agent = Workflow(
|
||||
name="with_tool",
|
||||
edges=[
|
||||
('START', prepare_query),
|
||||
(prepare_query, tool_node), # Input must be dict (tool args) or None
|
||||
(tool_node, process_results),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**Important**: ToolNode input must be a dictionary of tool arguments or None.
|
||||
|
||||
## AgentNode
|
||||
|
||||
Wrap any `BaseAgent` (not just LlmAgent) as a workflow node:
|
||||
|
||||
```python
|
||||
from google.adk.workflow._agent_node import AgentNode
|
||||
from google.adk.agents.loop_agent import LoopAgent
|
||||
|
||||
loop = LoopAgent(
|
||||
name="refine_loop",
|
||||
sub_agents=[writer, reviewer],
|
||||
max_iterations=3,
|
||||
)
|
||||
|
||||
loop_node = AgentNode(agent=loop, name="refinement")
|
||||
|
||||
agent = Workflow(
|
||||
name="with_loop",
|
||||
edges=[
|
||||
('START', loop_node),
|
||||
(loop_node, final_step),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Graph Validation Rules
|
||||
|
||||
The workflow graph is validated on construction. These rules are enforced:
|
||||
|
||||
1. START node must exist
|
||||
2. START node must not have incoming edges
|
||||
3. All non-START nodes must be reachable (appear as `to_node` in some edge)
|
||||
4. No duplicate node names
|
||||
5. No duplicate edges
|
||||
6. At most one `__DEFAULT__` route per node
|
||||
7. No unconditional cycles (cycles must have at least one routed edge)
|
||||
|
||||
## Edge Construction Patterns
|
||||
|
||||
```python
|
||||
from google.adk.workflow import Edge
|
||||
from google.adk.workflow._workflow_graph import WorkflowGraph
|
||||
|
||||
# Tuple syntax (most common)
|
||||
edges = [
|
||||
('START', node_a), # Simple edge
|
||||
(node_a, node_b, "route"), # Routed edge
|
||||
(node_a, (node_b, node_c)), # Fan-out
|
||||
((node_b, node_c), join_node), # Fan-in
|
||||
]
|
||||
|
||||
# Sequence shorthand (tuple with 3+ elements creates chain)
|
||||
edges = [('START', node_a, node_b, node_c)]
|
||||
# Equivalent to: [('START', node_a), (node_a, node_b), (node_b, node_c)]
|
||||
|
||||
# Routing map (dict syntax)
|
||||
edges = [
|
||||
(classifier, {"success": handler_a, "error": handler_b}),
|
||||
]
|
||||
|
||||
# Edge objects (explicit)
|
||||
edges = [
|
||||
Edge(START, node_a),
|
||||
Edge(node_a, node_b, route="success"),
|
||||
]
|
||||
|
||||
# Edge.chain helper
|
||||
edges = Edge.chain('START', node_a, node_b, node_c)
|
||||
# Returns: [(START, node_a), (node_a, node_b), (node_b, node_c)]
|
||||
|
||||
# WorkflowGraph.from_edge_items
|
||||
graph = WorkflowGraph.from_edge_items([
|
||||
('START', node_a),
|
||||
(node_a, node_b),
|
||||
])
|
||||
agent = Workflow(name="my_workflow", graph=graph)
|
||||
```
|
||||
|
||||
## Source File Locations
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Workflow | `src/google/adk/workflow/_workflow.py` |
|
||||
| WorkflowGraph, Edge | `src/google/adk/workflow/_workflow_graph.py` |
|
||||
| Context | `src/google/adk/agents/context.py` |
|
||||
| FunctionNode | `src/google/adk/workflow/_function_node.py` |
|
||||
| _LlmAgentWrapper | `src/google/adk/workflow/_llm_agent_wrapper.py` |
|
||||
| AgentNode | `src/google/adk/workflow/_agent_node.py` |
|
||||
| _ToolNode | `src/google/adk/workflow/_tool_node.py` |
|
||||
| JoinNode | `src/google/adk/workflow/_join_node.py` |
|
||||
| ParallelWorker | `src/google/adk/workflow/_parallel_worker.py` |
|
||||
| BaseNode, START | `src/google/adk/workflow/_base_node.py` |
|
||||
| @node decorator | `src/google/adk/workflow/_node.py` |
|
||||
| RetryConfig | `src/google/adk/workflow/_retry_config.py` |
|
||||
| Event | `src/google/adk/events/event.py` |
|
||||
| RequestInput | `src/google/adk/events/request_input.py` |
|
||||
@@ -0,0 +1,179 @@
|
||||
# ADK Workflow Best Practices
|
||||
|
||||
This document outlines the critical best practices and rules for developing reliable and maintainable workflows with the ADK.
|
||||
|
||||
## 📋 Agent Code Verification Checklist
|
||||
Use this checklist to verify your code before submitting or finalizing changes:
|
||||
- [ ] **Schemas**: Are Pydantic `BaseModel` classes used for all inputs/outputs? (No raw dicts)
|
||||
- [ ] **UI Output**: Do user-visible messages use `Event(message=...)`? (Not `output=`)
|
||||
- [ ] **State Data Flow**: Is data stored in state and read via `{var}` or param names?
|
||||
- [ ] **State Updates**: Are state updates done via `Event(state=...)`? (Avoid direct `ctx.state` mutation)
|
||||
- [ ] **Outputs**: Does each node execution yield at most **one** `event.output`?
|
||||
- [ ] **Semantics**: Are `yield` and `return` never mixed in the same function?
|
||||
- [ ] **Instructions**: Are `{node_input}` templates NOT used in agent instructions?
|
||||
- [ ] **HITL**: Are `interrupt_id`s unique per iteration in loops?
|
||||
|
||||
## Best Practices (MUST FOLLOW)
|
||||
|
||||
### Use Pydantic Models, Not Raw Dicts
|
||||
|
||||
**Always define Pydantic `BaseModel` classes** for function node inputs, outputs, LLM `output_schema`, and structured data. Never use `dict[str, Any]` when the shape is known:
|
||||
|
||||
```python
|
||||
# ❌ WRONG: raw dicts
|
||||
def lookup_flights(node_input: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"flight_cost": 500, "details": "Economy"}
|
||||
|
||||
# ✅ CORRECT: typed schemas
|
||||
class FlightInfo(BaseModel):
|
||||
flight_cost: int
|
||||
details: str
|
||||
|
||||
def lookup_flights(node_input: Itinerary) -> FlightInfo:
|
||||
return FlightInfo(flight_cost=500, details="Economy")
|
||||
```
|
||||
|
||||
This applies to ALL data flowing through the graph: node inputs, node outputs, JoinNode results, LLM output schemas, and HITL response schemas.
|
||||
|
||||
### Emit Content Events for Web UI Display
|
||||
|
||||
`event.output` is internal — only `event.content` renders in the ADK web UI. For user-visible output, use `Event(message=...)`:
|
||||
|
||||
```python
|
||||
def final_output(node_input: str):
|
||||
yield Event(message=node_input) # message= renders in web UI
|
||||
yield Event(output=node_input) # output= passes data to downstream nodes
|
||||
|
||||
# State-only event (no output, no message — just side-effect state update)
|
||||
def store_data(node_input: str):
|
||||
yield Event(state={"user_input": node_input})
|
||||
|
||||
> [!TIP]
|
||||
> Function nodes can stream user-visible messages by yielding `Event(message="chunk", partial=True)`.
|
||||
```
|
||||
|
||||
LLM agents emit content events automatically. Add them explicitly for function nodes that produce user-facing results.
|
||||
|
||||
### Prefer State-Based Data Flow with LLM Agents
|
||||
|
||||
Store data in state via `Event(state={...})` or `output_key`, then read it via instruction templates `{var}` or function parameter name injection. This is more robust than passing data through `node_input`, especially for routing workflows where multiple branches need the same data.
|
||||
|
||||
```python
|
||||
# ✅ State-based: store early, read anywhere via {var} or param name
|
||||
def process_input(node_input: str):
|
||||
yield Event(state={"topic": node_input})
|
||||
|
||||
writer = Agent(name="writer", instruction='Write about "{topic}".', output_key="draft")
|
||||
def send(draft: str): # draft resolved from ctx.state["draft"]
|
||||
yield Event(message=draft)
|
||||
|
||||
# ❌ Fragile: threading data through node_input breaks at routing/loops
|
||||
```
|
||||
|
||||
### Set State via Event, Not ctx.state
|
||||
|
||||
**Prefer `Event(state=...)` over `ctx.state[key] = ...`** for writing state. Event-based state is persisted in event history and replayable during non-resumable HITL. Direct `ctx.state` mutations are side effects that may be lost on replay.
|
||||
|
||||
```python
|
||||
# ✅ Preferred
|
||||
def save(node_input: str):
|
||||
return Event(output=node_input, state={"user_request": node_input})
|
||||
|
||||
# ❌ Avoid
|
||||
def save(ctx: Context, node_input: str) -> str:
|
||||
ctx.state["user_request"] = node_input
|
||||
return node_input
|
||||
```
|
||||
|
||||
### One Output Event Per Node
|
||||
|
||||
Each node execution can yield many events, but **at most one should have `event.output`**. This applies to function nodes, LLM agents (including `task` and `single_turn` mode), and nested workflows. Multiple output events get silently merged into a list, which changes the downstream `node_input` type and usually causes errors. Similarly, at most one event can have `route` — multiple routed events raise `ValueError`.
|
||||
|
||||
```python
|
||||
# ✅ Correct: one output event, other events for messages/state
|
||||
def my_node(node_input: str):
|
||||
yield Event(message="Processing...") # display only
|
||||
yield Event(state={"status": "done"}) # state update only
|
||||
yield Event(output="final result") # the single output
|
||||
|
||||
# ❌ Wrong: multiple output events
|
||||
def my_node(node_input: str):
|
||||
yield Event(output="first") # these get merged into ["first", "second"]
|
||||
yield Event(output="second") # downstream expects str, gets list → TypeError
|
||||
```
|
||||
|
||||
### Don't Mix yield and return Event
|
||||
|
||||
A function is either a **generator** (uses `yield`) or a **regular function** (uses `return`). Never mix them — in Python, a function with `yield` becomes a generator and any `return value` is silently ignored:
|
||||
|
||||
```python
|
||||
# ✅ Generator: use yield for all events
|
||||
def my_node(node_input: str):
|
||||
yield Event(state={"key": "value"})
|
||||
yield Event(output="result")
|
||||
|
||||
# ✅ Regular function: use return for a single value/event
|
||||
def my_node(node_input: str):
|
||||
return Event(output="result", state={"key": "value"})
|
||||
|
||||
# ✅ Regular function: return plain value (auto-wrapped in Event)
|
||||
def my_node(node_input: str) -> str:
|
||||
return "result"
|
||||
|
||||
# ❌ Wrong: mixing yield and return — the return is silently ignored
|
||||
def my_node(node_input: str):
|
||||
yield Event(state={"key": "value"})
|
||||
return Event(output="result") # IGNORED — Python generator semantics
|
||||
```
|
||||
|
||||
Use generators (`yield`) when you need multiple events (state + output + message). Use regular functions (`return`) for simple single-value output.
|
||||
|
||||
### Never Put node_input in LLM Agent Instructions
|
||||
|
||||
`{var}` templates in `instruction` resolve **only** from `ctx.state`. `node_input` is NOT available as a template variable — it is automatically sent as the user message to the LLM. Do not try to reference it in the instruction:
|
||||
|
||||
```python
|
||||
# ❌ Wrong: {node_input} is not in state, raises KeyError
|
||||
agent = Agent(
|
||||
name="summarizer",
|
||||
instruction="Summarize this: {node_input}",
|
||||
)
|
||||
|
||||
# ✅ Correct: node_input already becomes the user message, just instruct
|
||||
agent = Agent(
|
||||
name="summarizer",
|
||||
instruction="Summarize the following text in one sentence.",
|
||||
)
|
||||
|
||||
# ✅ Correct: use state for data that needs to be in the instruction
|
||||
agent = Agent(
|
||||
name="writer",
|
||||
instruction='Write about "{topic}". Previous feedback: {feedback?}',
|
||||
output_key="draft",
|
||||
)
|
||||
```
|
||||
|
||||
### Workflow Cannot Be a Sub-Agent of LlmAgent
|
||||
|
||||
`Workflow`, `SequentialAgent`, `LoopAgent`, and `ParallelAgent` cannot be added as `sub_agents` of an `LlmAgent`. Agent transfer to workflow agents is not supported.
|
||||
|
||||
### Workflow Data Rules
|
||||
|
||||
- **`Event.output` must be JSON-serializable.** FunctionNode auto-converts BaseModel returns via `model_dump()`. Never store `types.Content` or other non-serializable objects in `Event.output`.
|
||||
- **`output_key` stores dicts, not BaseModel instances.** LLM agents with `output_schema` run `validate_schema()` → `model_dump()`, so `ctx.state[output_key]` is a plain dict.
|
||||
- **`ctx.state.get(key)` returns a dict.** Use dict access (`data["field"]`) or reconstruct (`MyModel(**data)`) for typed access.
|
||||
|
||||
## Human-in-the-Loop (HITL) Rules
|
||||
|
||||
### Unique interrupt_id in Loops
|
||||
|
||||
When a node requests input (yields `RequestInput`) inside a loop (e.g., a review-revise loop), you **MUST use a unique `interrupt_id` per iteration** (e.g., `review_{count}`).
|
||||
|
||||
If you reuse the same `interrupt_id`, the event-based state reconstruction will confuse responses from earlier iterations with the current one, leading to infinite restart loops!
|
||||
|
||||
```python
|
||||
# ✅ Correct: unique ID per iteration
|
||||
review_count = ctx.state.get('review_count', 0)
|
||||
interrupt_id = f'review_{review_count}'
|
||||
yield RequestInput(interrupt_id=interrupt_id, message="Approve?")
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
# Callbacks and Plugins
|
||||
|
||||
## 📋 Agent Verification Checklist (Callbacks)
|
||||
Use this checklist when implementing callbacks or plugins:
|
||||
- [ ] **Override Behavior**: Remember that returning a non-`None` value in a callback *overrides* the default behavior (e.g., skips model call or tool execution). Is that intentional?
|
||||
- [ ] **Context Type**: Remember that `CallbackContext` is an alias for `Context`.
|
||||
|
||||
## 💡 Quick Reference (Callback Returns)
|
||||
- **Continue Normal Flow**: Return `None`.
|
||||
- **Override Model**: Return `LlmResponse` in `before_model`.
|
||||
- **Override Tool**: Return `dict` in `before_tool`.
|
||||
|
||||
## Agent Callbacks
|
||||
|
||||
```python
|
||||
root_agent = Agent(
|
||||
before_agent_callback=my_before_cb, # Before agent runs
|
||||
after_agent_callback=my_after_cb, # After agent runs
|
||||
before_model_callback=my_before_model, # Before LLM call
|
||||
after_model_callback=my_after_model, # After LLM call
|
||||
before_tool_callback=my_before_tool, # Before tool call
|
||||
after_tool_callback=my_after_tool, # After tool call
|
||||
on_model_error_callback=my_error_cb, # On LLM error
|
||||
on_tool_error_callback=my_tool_error_cb, # On tool error
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** `CallbackContext` is a backward-compatible alias for `Context`. Both work identically.
|
||||
|
||||
## Callback Signatures
|
||||
|
||||
```python
|
||||
# before_agent / after_agent
|
||||
def callback(callback_context: CallbackContext):
|
||||
return None # Continue normal flow
|
||||
# OR return ModelContent to override
|
||||
|
||||
# before_model
|
||||
def callback(callback_context, llm_request: LlmRequest):
|
||||
return None # Continue to LLM
|
||||
# OR return LlmResponse to skip LLM
|
||||
|
||||
# after_model
|
||||
def callback(callback_context, llm_response):
|
||||
return None # Use actual response
|
||||
# OR return LlmResponse to override
|
||||
|
||||
# before_tool
|
||||
def callback(tool, args, tool_context):
|
||||
return None # Call tool normally
|
||||
# OR return dict to skip tool
|
||||
|
||||
# after_tool
|
||||
def callback(tool, args, tool_context, tool_response):
|
||||
return None # Use actual response
|
||||
# OR return dict to override
|
||||
```
|
||||
|
||||
**Multiple callbacks:** Pass a list. They execute in order until one
|
||||
returns non-None.
|
||||
|
||||
## Plugins (App-Level Callbacks)
|
||||
|
||||
```python
|
||||
from google.adk.plugins.base_plugin import BasePlugin
|
||||
|
||||
class MyPlugin(BasePlugin):
|
||||
def __init__(self):
|
||||
super().__init__(name='my_plugin')
|
||||
|
||||
async def before_agent_callback(self, *, agent, callback_context):
|
||||
pass
|
||||
|
||||
async def before_model_callback(self, *, callback_context, llm_request):
|
||||
pass
|
||||
```
|
||||
|
||||
## Built-in Plugins
|
||||
|
||||
| Plugin | Import | Purpose |
|
||||
|--------|--------|---------|
|
||||
| `ContextFilterPlugin` | `from google.adk.plugins.context_filter_plugin import ContextFilterPlugin` | Limit history in context |
|
||||
| `SaveFilesAsArtifactsPlugin` | `from google.adk.plugins.save_files_as_artifacts_plugin import SaveFilesAsArtifactsPlugin` | Auto-save file outputs |
|
||||
| `GlobalInstructionPlugin` | `from google.adk.plugins.global_instruction_plugin import GlobalInstructionPlugin` | Inject global instructions |
|
||||
|
||||
Usage with App:
|
||||
|
||||
```python
|
||||
from google.adk.apps import App
|
||||
from google.adk.plugins.context_filter_plugin import ContextFilterPlugin
|
||||
|
||||
app = App(
|
||||
name='my_app',
|
||||
root_agent=root_agent,
|
||||
plugins=[ContextFilterPlugin(num_invocations_to_keep=3)],
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,95 @@
|
||||
# Dynamic Node Scheduling Reference
|
||||
|
||||
Schedule nodes at runtime using `ctx.run_node()`. This allows a node within a workflow to trigger the run of another node (or a callable that can be built into a node) and asynchronously wait for its result.
|
||||
|
||||
## 📋 Agent Verification Checklist (Dynamic Nodes)
|
||||
Use this checklist when scheduling nodes dynamically:
|
||||
- [ ] **Rerun on Resume**: Does the parent node calling `run_node` have `rerun_on_resume=True`?
|
||||
- [ ] **Run ID**: If using an explicit `run_id`, does it contain non-numeric characters?
|
||||
- [ ] **Param Name**: If passing input directly to a raw function via `node_input=...`, is that function's parameter named `node_input`?
|
||||
- [ ] **Nesting**: If the child node *also* calls `run_node`, is it wrapped in `FunctionNode(..., rerun_on_resume=True)`?
|
||||
|
||||
## 💡 Quick Reference
|
||||
- **Call**: `await ctx.run_node(node_like, node_input=...)`
|
||||
- **Output Delegation**: Set `use_as_output=True` to make child output be the parent's output.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```python
|
||||
from google.adk import Agent, Context, Event, Workflow
|
||||
from google.adk.workflow import node
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Feedback(BaseModel):
|
||||
grade: str
|
||||
|
||||
generate_headline = Agent(
|
||||
name="generate_headline",
|
||||
instruction='Write a headline about the topic "{topic}".',
|
||||
)
|
||||
|
||||
evaluate_headline = Agent(
|
||||
name="evaluate_headline",
|
||||
instruction="Grade whether the headline is tech-related.",
|
||||
output_schema=Feedback,
|
||||
mode="single_turn",
|
||||
)
|
||||
|
||||
@node(rerun_on_resume=True)
|
||||
async def orchestrate(ctx: Context, node_input: str) -> str:
|
||||
yield Event(state={"topic": node_input})
|
||||
while True:
|
||||
headline = await ctx.run_node(generate_headline)
|
||||
feedback = Feedback.model_validate(
|
||||
await ctx.run_node(evaluate_headline, node_input=headline)
|
||||
)
|
||||
if feedback.grade == "tech-related":
|
||||
yield headline
|
||||
break
|
||||
|
||||
root_agent = Workflow(
|
||||
name="root_agent",
|
||||
edges=[("START", orchestrate)],
|
||||
)
|
||||
```
|
||||
|
||||
## Requirements & Rules
|
||||
|
||||
- **`rerun_on_resume=True`**: The parent node calling `ctx.run_node()` must have `rerun_on_resume=True`. This is required because dynamically scheduled nodes might be interrupted (e.g., for HITL), and the workflow needs to wake up and re-run the parent node to get the child node's response.
|
||||
- **Unique Instance Names**: Each dynamic instance needs a unique name (auto-generated for Agent nodes).
|
||||
- **Node-Like Acceptable**: `ctx.run_node()` accepts any node-like object (function, Agent, BaseNode).
|
||||
- **Explicit `run_id` Constraint**: If you provide an explicit `run_id`, it **must contain non-numeric characters** (e.g., `"run_a"` instead of `"1"`) to prevent collision with auto-generated numeric IDs.
|
||||
- **`use_as_output=True`**: Suppresses the parent node's own output and uses the child's output as the parent's output. This is achieved via `outputFor` annotation in events. This can only be called ONCE per parent node execution.
|
||||
- **`use_sub_branch`**: (Optional) If set to `True`, attaches a branch segment (`node_name@run_id`) to the current execution branch to ensure event isolation for parallel or sub-agent runs.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Always `await` `ctx.run_node()` directly. Wrapping it in `asyncio.create_task()` means the task runs unsupervised — errors are silently swallowed and the task is not cancelled if the parent node is interrupted.
|
||||
|
||||
## Imperative Workflow Construction
|
||||
|
||||
As an alternative to defining static graph edges, you can use dynamic nodes to construct workflows in an imperative style using standard Python control flow. This approach can sometimes be more intuitive for complex conditional logic or parallel execution.
|
||||
|
||||
### Replacing Graph Patterns
|
||||
|
||||
#### 1. Sequences & Branching
|
||||
Instead of defining edges with routes, use standard Python `if/else`:
|
||||
```python
|
||||
async def orchestrator(ctx: Context, node_input: str):
|
||||
res_a = await ctx.run_node(step_a, node_input=node_input)
|
||||
if "success" in res_a:
|
||||
return await ctx.run_node(step_b, node_input=res_a)
|
||||
else:
|
||||
return await ctx.run_node(step_c, node_input=res_a)
|
||||
```
|
||||
|
||||
|
||||
### Important Pits & Best Practices
|
||||
|
||||
- **Function Parameter Mapping**: When passing a raw function to `run_node`, ADK defaults to `'state'` binding mode. If you want to pass input directly via `node_input=...` in `run_node`, **the function parameter MUST be named `node_input`**!
|
||||
```python
|
||||
def my_worker(node_input: str): # MUST be named 'node_input'
|
||||
return f"Done: {node_input}"
|
||||
```
|
||||
- **Nested Dynamic Nodes**: If a dynamically scheduled node *itself* calls `run_node`, it acts as a parent node and **MUST have `rerun_on_resume=True`**! Since raw functions passed to `run_node` default to `False`, you must manually wrap the inner parent function in `FunctionNode(..., rerun_on_resume=True)`!
|
||||
- **Generator Returns**: In nodes that use `yield` (generators), you cannot use `return value` to produce the final output (Python syntax error in async generators). You must yield `Event(output=...)` instead.
|
||||
@@ -0,0 +1,320 @@
|
||||
# Function Nodes Reference
|
||||
|
||||
Function nodes are the most common node type. Any Python function becomes a workflow node.
|
||||
|
||||
## 📋 Agent Verification Checklist (Function Nodes)
|
||||
Use this checklist to verify your Function Node configuration:
|
||||
- [ ] **Input Type**: If following an LLM agent without schema, is `node_input` typed as `Any` or `types.Content`? (Not `str`)
|
||||
- [ ] **UI Output**: Do you yield `Event(message=...)` for results that should appear in the Web UI?
|
||||
- [ ] **Outputs**: Does the function yield or return at most **one** `event.output`?
|
||||
- [ ] **Union Types**: If using Union types for `node_input`, did you add `isinstance` checks in the body for actual validation?
|
||||
|
||||
## 💡 Quick Reference (Param Resolution)
|
||||
- **`ctx`**: Workflow `Context` object.
|
||||
- **`node_input`**: Output from the predecessor node.
|
||||
- **Any other name**: Auto-resolved from `ctx.state[param_name]`.
|
||||
|
||||
## Imports
|
||||
|
||||
```python
|
||||
from google.adk.workflow import FunctionNode
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.agents.context import Context
|
||||
from google.adk.workflow import node # @node decorator
|
||||
```
|
||||
|
||||
## Basic Functions
|
||||
|
||||
A function returning a value automatically wraps it in an `Event`:
|
||||
|
||||
```python
|
||||
def process(node_input: str) -> str:
|
||||
return f"Processed: {node_input}"
|
||||
|
||||
# Async functions work too
|
||||
async def fetch_data(node_input: str) -> dict:
|
||||
result = await some_api_call(node_input)
|
||||
return {"data": result}
|
||||
```
|
||||
|
||||
## Function Signatures
|
||||
|
||||
FunctionNode inspects the function signature to resolve parameters:
|
||||
|
||||
| Parameter Name | Source |
|
||||
|---------------|--------|
|
||||
| `ctx` | Workflow `Context` object |
|
||||
| `node_input` | Output from predecessor node |
|
||||
| Any other name | Looked up from `ctx.state[param_name]` |
|
||||
|
||||
```python
|
||||
# Receives both context and input
|
||||
def my_node(ctx: Context, node_input: str) -> str:
|
||||
session_id = ctx.session.id
|
||||
return f"Session {session_id}: {node_input}"
|
||||
|
||||
# Receives only input
|
||||
def simple(node_input: str) -> str:
|
||||
return node_input.upper()
|
||||
|
||||
# Reads from state (other params resolved from ctx.state)
|
||||
def uses_state(node_input: str, user_name: str) -> str:
|
||||
# user_name read from ctx.state['user_name']
|
||||
return f"{user_name}: {node_input}"
|
||||
|
||||
# No parameters at all
|
||||
def constant() -> str:
|
||||
return "hello"
|
||||
```
|
||||
|
||||
## Generator Functions
|
||||
|
||||
Yield multiple events from a single node:
|
||||
|
||||
```python
|
||||
# Async generator
|
||||
async def multi_output(ctx: Context) -> AsyncGenerator[Any, None]:
|
||||
yield Event(output="first output")
|
||||
yield Event(output="second output")
|
||||
|
||||
# Sync generator
|
||||
def sync_multi(node_input: str):
|
||||
yield Event(output="step 1")
|
||||
yield Event(output="step 2")
|
||||
```
|
||||
|
||||
**At most one event should have `output`.** Multiple output events get silently merged into a list, changing the downstream type. Similarly, at most one event can have `route` (multiple raise `ValueError`). Use separate events for messages, state updates, and the single output.
|
||||
|
||||
## Yielding Raw Values
|
||||
|
||||
Yield raw values instead of Event objects. They are wrapped automatically:
|
||||
|
||||
```python
|
||||
async def raw_yield(node_input: str):
|
||||
yield "output value" # Wrapped in Event(output="output value")
|
||||
```
|
||||
|
||||
## Returning None
|
||||
|
||||
If a function returns `None`, no event is emitted and no downstream node is triggered:
|
||||
|
||||
```python
|
||||
def maybe_output(node_input: str) -> str | None:
|
||||
if not node_input:
|
||||
return None # No downstream trigger
|
||||
return f"Got: {node_input}"
|
||||
```
|
||||
|
||||
## Auto Type Conversion
|
||||
|
||||
FunctionNode automatically converts `dict` inputs to Pydantic models based on type hints:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Order(BaseModel):
|
||||
item: str
|
||||
quantity: int
|
||||
|
||||
def process_order(node_input: Order) -> str:
|
||||
# If node_input is {'item': 'widget', 'quantity': 3},
|
||||
# it's auto-converted to Order(item='widget', quantity=3)
|
||||
return f"Order: {node_input.quantity}x {node_input.item}"
|
||||
```
|
||||
|
||||
This works recursively for `list[Model]` and `dict[str, Model]` too.
|
||||
|
||||
### Pydantic Schemas with LLM Agents (Recommended Pattern)
|
||||
|
||||
Use `output_schema` on LLM agents to get structured, JSON-serializable output. This avoids `types.Content` serialization issues and enables auto-conversion in downstream function nodes:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
|
||||
class ReviewResult(BaseModel):
|
||||
score: int
|
||||
feedback: str
|
||||
approved: bool
|
||||
|
||||
reviewer = LlmAgent(
|
||||
name="reviewer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Review the code and provide structured feedback.",
|
||||
output_schema=ReviewResult,
|
||||
)
|
||||
|
||||
# Downstream function node receives dict, auto-converted to Pydantic model
|
||||
def process_review(node_input: ReviewResult) -> str:
|
||||
if node_input.approved:
|
||||
return f"Approved with score {node_input.score}"
|
||||
return f"Rejected: {node_input.feedback}"
|
||||
```
|
||||
|
||||
**Why use `output_schema`:**
|
||||
- LLM agent output becomes a `dict` (JSON-serializable) instead of `types.Content`
|
||||
- Fixes `TypeError` when SQLite session service serializes JoinNode state
|
||||
- Enables auto type conversion in downstream function nodes
|
||||
- Provides structured data for programmatic access
|
||||
|
||||
## Explicit FunctionNode
|
||||
|
||||
For more control, create a `FunctionNode` explicitly:
|
||||
|
||||
```python
|
||||
from google.adk.workflow import FunctionNode
|
||||
from google.adk.workflow import RetryConfig
|
||||
|
||||
node = FunctionNode(
|
||||
my_func,
|
||||
name="custom_name", # Override inferred name
|
||||
rerun_on_resume=True, # Rerun after HITL interrupt
|
||||
retry_config=RetryConfig( # Retry on failure
|
||||
max_attempts=3,
|
||||
initial_delay=1.0,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## @node Decorator
|
||||
|
||||
The `@node` decorator provides syntactic sugar:
|
||||
|
||||
```python
|
||||
from google.adk.workflow import node
|
||||
|
||||
@node
|
||||
def my_func(node_input: str) -> str:
|
||||
return node_input
|
||||
|
||||
@node(name="custom_name", rerun_on_resume=True)
|
||||
async def my_async_func(node_input: str) -> str:
|
||||
return node_input
|
||||
|
||||
# As a function call
|
||||
my_node = node(some_func, name="renamed")
|
||||
|
||||
# Wrap as ParallelWorker
|
||||
parallel = node(some_func, parallel_worker=True)
|
||||
```
|
||||
|
||||
## Prefer Typed Schemas Over Raw Dicts
|
||||
|
||||
Use Pydantic models for node inputs, outputs, and state instead of raw `dict`. This gives you validation, IDE autocomplete, and self-documenting code:
|
||||
|
||||
```python
|
||||
# ❌ Avoid: raw dicts are error-prone and opaque
|
||||
def process(node_input: dict) -> dict:
|
||||
return {"status": "done", "count": node_input["items"]}
|
||||
|
||||
# ✅ Prefer: typed schemas
|
||||
class TaskInput(BaseModel):
|
||||
items: list[str]
|
||||
priority: str = "normal"
|
||||
|
||||
class TaskResult(BaseModel):
|
||||
status: str
|
||||
count: int
|
||||
|
||||
def process(node_input: TaskInput) -> TaskResult:
|
||||
return TaskResult(status="done", count=len(node_input.items))
|
||||
```
|
||||
|
||||
This applies to:
|
||||
- **Function node inputs/outputs**: Use Pydantic models as `node_input` type hints and return types
|
||||
- **LLM agent `output_schema`**: Always set `output_schema=MyModel` to get structured dict output instead of `types.Content`
|
||||
- **`RequestInput.response_schema`**: Pass a Pydantic `BaseModel` class directly (e.g., `response_schema=MyModel`)
|
||||
- **State values**: Store Pydantic model dicts (via `.model_dump()`) rather than hand-built dicts
|
||||
|
||||
FunctionNode auto-converts `dict` inputs to Pydantic models based on type hints (see [Auto Type Conversion](#auto-type-conversion) above), so typed schemas work seamlessly across the graph.
|
||||
|
||||
## Emitting Content Events for Web UI Display
|
||||
|
||||
In the ADK web UI, only `event.content` is rendered to the user — `event.output` is internal and not displayed. When a function node produces user-facing output, yield a content event in addition to the output event:
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
from google.adk.events.event import Event
|
||||
|
||||
async def summarize(ctx: Context, node_input: str):
|
||||
result = f"Summary: {node_input}"
|
||||
|
||||
# Content event: rendered in the web UI
|
||||
yield Event(content=types.ModelContent(result))
|
||||
|
||||
# Output event: passed to downstream nodes
|
||||
yield Event(output=result)
|
||||
```
|
||||
|
||||
LLM agents emit content events automatically. For function nodes that are terminal (no downstream edges) or produce user-visible intermediate results, add the content event so users see output in the web UI.
|
||||
|
||||
## Events with Routes
|
||||
|
||||
Return an `Event` with a `route` for conditional branching:
|
||||
|
||||
```python
|
||||
def classify(node_input: str):
|
||||
if "urgent" in node_input:
|
||||
return Event(output=node_input, route="urgent")
|
||||
return Event(output=node_input, route="normal")
|
||||
```
|
||||
|
||||
## Events with State Updates
|
||||
|
||||
Update shared workflow state via the `state` constructor parameter:
|
||||
|
||||
```python
|
||||
def update_counter(node_input: str):
|
||||
return Event(
|
||||
output=node_input,
|
||||
state={"counter": 1, "last_input": node_input},
|
||||
)
|
||||
```
|
||||
|
||||
Or use `ctx.state` directly:
|
||||
|
||||
```python
|
||||
def update_via_context(ctx: Context, node_input: str) -> str:
|
||||
ctx.state["counter"] = ctx.state.get("counter", 0) + 1
|
||||
return node_input
|
||||
```
|
||||
|
||||
## Type Validation (Important)
|
||||
|
||||
FunctionNode strictly type-checks `node_input` against the type hint. A `TypeError` is raised if the actual type doesn't match.
|
||||
|
||||
**Union types:** `node_input: list | dict` silently skips validation (FunctionNode detects Union via `get_origin()` and sets `is_instance = True`). This means Union hints won't crash, but they also won't catch wrong types — any value passes. Use `isinstance` checks inside the function body for actual validation.
|
||||
|
||||
**Common pitfall: LLM agent -> function node.** LlmAgentWrapper outputs `types.Content` (not `str`). If your function node follows an LLM agent and declares `node_input: str`, it will fail with:
|
||||
|
||||
```
|
||||
TypeError: Parameter "node_input" expects type <class 'str'>
|
||||
but received type <class 'google.genai.types.Content'>
|
||||
```
|
||||
|
||||
**Fix:** Use `Any` for `node_input` and extract text manually:
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
from google.genai import types
|
||||
|
||||
def process(node_input: Any) -> str:
|
||||
# Handle types.Content from LLM agents
|
||||
if isinstance(node_input, types.Content):
|
||||
return ''.join(p.text for p in (node_input.parts or []) if p.text)
|
||||
return str(node_input) if node_input is not None else ''
|
||||
```
|
||||
|
||||
**Output type summary by predecessor:**
|
||||
|
||||
| Predecessor Node Type | `node_input` Type |
|
||||
|----------------------|-------------------|
|
||||
| Function returning `str` | `str` |
|
||||
| Function returning `dict` | `dict` |
|
||||
| Function returning `Event(output=X)` | type of `X` |
|
||||
| `LlmAgentWrapper` (no `output_schema`) | `types.Content` |
|
||||
| `LlmAgentWrapper` (with `output_schema`) | `dict` |
|
||||
| `JoinNode` | `dict[str, Any]` (keyed by predecessor names) |
|
||||
| `ParallelWorker` | `list` |
|
||||
| `START` (no `input_schema`) | `types.Content` (user's message) |
|
||||
| `START` (with `input_schema`) | parsed schema type |
|
||||
@@ -0,0 +1,434 @@
|
||||
# Getting Started: Creating ADK Agents
|
||||
|
||||
Step-by-step guide covering environment setup, basic LLM agents, and workflow agents.
|
||||
|
||||
## 📋 New Agent Checklist
|
||||
Use this checklist when creating a new agent to ensure it follows convention:
|
||||
|
||||
- [ ] **Directory**: Is there a directory for the agent?
|
||||
- [ ] **__init__.py**: Does it contain `from . import agent`?
|
||||
- [ ] **agent.py**: Does it define `root_agent` or `app`?
|
||||
- [ ] **.env**: Is there a `.env` file with the appropriate API keys? (Do not commit to git)
|
||||
|
||||
## 💡 Quick Reference (CLI Commands)
|
||||
|
||||
- **Create**: `adk create <agent_name>` (Scaffolds a new agent project)
|
||||
- **Web UI**: `adk web <path_to_agent_dir>` (Starts dev server at localhost:8000)
|
||||
- **Run CLI**: `adk run <path_to_agent_dir>` (Interactive or query mode)
|
||||
|
||||
## 1. Set Up the Environment
|
||||
|
||||
Create a virtual environment and install the ADK:
|
||||
|
||||
```bash
|
||||
# Create and activate virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # macOS/Linux
|
||||
|
||||
# Install the ADK package
|
||||
pip install google-adk
|
||||
```
|
||||
|
||||
Or with `uv`:
|
||||
|
||||
```bash
|
||||
uv venv --python "python3.11" ".venv"
|
||||
source .venv/bin/activate
|
||||
uv pip install google-adk
|
||||
```
|
||||
|
||||
## 2. Configure API Keys
|
||||
|
||||
### Google AI Studio (recommended for getting started)
|
||||
|
||||
Obtain an API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
|
||||
|
||||
Create a `.env` file in the agent directory:
|
||||
|
||||
```
|
||||
GOOGLE_GENAI_USE_ENTERPRISE=FALSE
|
||||
GOOGLE_API_KEY=YOUR_API_KEY
|
||||
```
|
||||
|
||||
### Vertex AI
|
||||
|
||||
For production use with Google Cloud:
|
||||
|
||||
```
|
||||
GOOGLE_GENAI_USE_ENTERPRISE=TRUE
|
||||
GOOGLE_CLOUD_PROJECT=your-project-id
|
||||
GOOGLE_CLOUD_LOCATION=us-central1
|
||||
```
|
||||
|
||||
Run `gcloud auth application-default login` to authenticate.
|
||||
|
||||
### Vertex AI Express Mode
|
||||
|
||||
Combines Vertex AI with API key authentication:
|
||||
|
||||
```
|
||||
GOOGLE_GENAI_USE_ENTERPRISE=TRUE
|
||||
GOOGLE_API_KEY=YOUR_EXPRESS_MODE_KEY
|
||||
```
|
||||
|
||||
## 3. Agent Directory Structure
|
||||
|
||||
The ADK CLI discovers agents by directory convention. Each agent directory must have:
|
||||
|
||||
```
|
||||
my_agent/
|
||||
├── __init__.py # Must import the agent module
|
||||
├── agent.py # Must define root_agent
|
||||
└── .env # API keys (not committed to git)
|
||||
```
|
||||
|
||||
### __init__.py
|
||||
|
||||
```python
|
||||
from . import agent
|
||||
```
|
||||
|
||||
Or generate the project with the CLI:
|
||||
|
||||
```bash
|
||||
adk create my_agent
|
||||
```
|
||||
|
||||
## 4. Basic LLM Agent with Tools
|
||||
|
||||
Before building workflow agents, understand the basic LLM agent pattern. An `LlmAgent` (also aliased as `Agent`) connects an LLM to tools and instructions:
|
||||
|
||||
### agent.py
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
|
||||
def get_weather(city: str) -> dict:
|
||||
"""Returns the current weather for a specified city."""
|
||||
# In production, call a real weather API
|
||||
return {
|
||||
"status": "success",
|
||||
"city": city,
|
||||
"weather": "sunny",
|
||||
"temperature": "72F",
|
||||
}
|
||||
|
||||
def get_current_time(city: str) -> dict:
|
||||
"""Returns the current time in a specified city."""
|
||||
import datetime
|
||||
return {
|
||||
"status": "success",
|
||||
"city": city,
|
||||
"time": datetime.datetime.now().strftime("%I:%M %p"),
|
||||
}
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-flash",
|
||||
name="root_agent",
|
||||
description="An assistant that provides weather and time information.",
|
||||
instruction="""You are a helpful assistant.
|
||||
Use the get_weather tool to look up weather and
|
||||
get_current_time to check the time in any city.
|
||||
Always be friendly and concise.""",
|
||||
tools=[get_weather, get_current_time],
|
||||
)
|
||||
```
|
||||
|
||||
### Key concepts
|
||||
|
||||
- **`model`**: The LLM to use (e.g., `"gemini-2.5-flash"`, `"gemini-2.5-pro"`)
|
||||
- **`instruction`**: System prompt guiding the agent's behavior
|
||||
- **`tools`**: Python functions the LLM can call. The function name, docstring, and type hints are sent to the LLM as the tool schema
|
||||
- **`description`**: Used when this agent is a sub-agent (for transfer routing)
|
||||
- **`output_key`**: Store the agent's final text output in session state under this key
|
||||
|
||||
### Tool function conventions
|
||||
|
||||
- Use clear function names and docstrings — the LLM sees these
|
||||
- Type-hint all parameters — they define the tool's input schema
|
||||
- Return a `dict` or `str` — the return value becomes the tool response
|
||||
|
||||
## 5. Run the Agent
|
||||
|
||||
### Web UI (primary debugging tool)
|
||||
|
||||
```bash
|
||||
adk web my_agent/
|
||||
```
|
||||
|
||||
Open `http://localhost:8000`. Select the agent from the dropdown, type a message, and see events in the Events tab.
|
||||
|
||||
**Note**: `adk web` is for development only, not production.
|
||||
|
||||
### CLI mode
|
||||
|
||||
```bash
|
||||
adk run my_agent/
|
||||
```
|
||||
|
||||
### API server
|
||||
|
||||
```bash
|
||||
adk api_server my_agent/
|
||||
```
|
||||
|
||||
### Programmatic execution
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.genai import types
|
||||
|
||||
async def main():
|
||||
from my_agent import agent
|
||||
|
||||
runner = InMemoryRunner(
|
||||
app_name="my_app",
|
||||
agent=agent.root_agent,
|
||||
)
|
||||
|
||||
session = await runner.session_service.create_session(
|
||||
app_name="my_app", user_id="user1"
|
||||
)
|
||||
|
||||
content = types.Content(
|
||||
role="user", parts=[types.Part.from_text(text="What's the weather in Paris?")]
|
||||
)
|
||||
|
||||
async for event in runner.run_async(
|
||||
user_id="user1",
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content and event.content.parts:
|
||||
if event.content.parts[0].text:
|
||||
print(f"{event.author}: {event.content.parts[0].text}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## 6. From LLM Agent to Workflow Agent
|
||||
|
||||
A `Workflow` extends the basic agent pattern with graph-based execution. Instead of a single LLM deciding what to do, define explicit nodes and edges:
|
||||
|
||||
### agent.py — Minimal Workflow
|
||||
|
||||
```python
|
||||
from google.adk.workflow import Workflow
|
||||
|
||||
def greet(node_input: str) -> str:
|
||||
return f"Hello! You said: {node_input}"
|
||||
|
||||
root_agent = Workflow(
|
||||
name="my_workflow",
|
||||
edges=[
|
||||
('START', greet),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## 5. Sample: Sequential Pipeline with LLM Agents
|
||||
|
||||
A code write-review-refactor pipeline using `SequentialAgent`:
|
||||
|
||||
### agent.py
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.agents.sequential_agent import SequentialAgent
|
||||
|
||||
code_writer_agent = LlmAgent(
|
||||
name="CodeWriterAgent",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="""You are a Python Code Generator.
|
||||
Based *only* on the user's request, write Python code that fulfills the requirement.
|
||||
Output *only* the complete Python code block.
|
||||
""",
|
||||
description="Writes initial Python code based on a specification.",
|
||||
output_key="generated_code",
|
||||
)
|
||||
|
||||
code_reviewer_agent = LlmAgent(
|
||||
name="CodeReviewerAgent",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="""You are an expert Python Code Reviewer.
|
||||
Review the following code:
|
||||
|
||||
```python
|
||||
{generated_code}
|
||||
```
|
||||
|
||||
Provide feedback as a concise, bulleted list.
|
||||
If the code is excellent, state: "No major issues found."
|
||||
""",
|
||||
description="Reviews code and provides feedback.",
|
||||
output_key="review_comments",
|
||||
)
|
||||
|
||||
code_refactorer_agent = LlmAgent(
|
||||
name="CodeRefactorerAgent",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="""You are a Python Code Refactoring AI.
|
||||
Improve the code based on the review comments.
|
||||
|
||||
**Original Code:**
|
||||
```python
|
||||
{generated_code}
|
||||
```
|
||||
|
||||
**Review Comments:**
|
||||
{review_comments}
|
||||
|
||||
If no issues found, return the original code unchanged.
|
||||
Output *only* the final Python code block.
|
||||
""",
|
||||
description="Refactors code based on review comments.",
|
||||
output_key="refactored_code",
|
||||
)
|
||||
|
||||
root_agent = SequentialAgent(
|
||||
name="CodePipelineAgent",
|
||||
sub_agents=[code_writer_agent, code_reviewer_agent, code_refactorer_agent],
|
||||
description="Executes a sequence of code writing, reviewing, and refactoring.",
|
||||
)
|
||||
```
|
||||
|
||||
### Key patterns in this sample
|
||||
|
||||
- **`output_key`**: Each agent stores its output in session state, making it available to later agents
|
||||
- **`{generated_code}`**: Instruction placeholders are resolved from session state at runtime
|
||||
- **`SequentialAgent`**: Convenience wrapper that auto-generates `START -> agent1 -> agent2 -> agent3` edges
|
||||
|
||||
## 6. Sample: Graph Workflow with Functions and Routing
|
||||
|
||||
A data processing pipeline with conditional routing:
|
||||
|
||||
### agent.py
|
||||
|
||||
```python
|
||||
from google.adk.workflow import Workflow
|
||||
from google.adk.events.event import Event
|
||||
from google.adk.agents.context import Context
|
||||
|
||||
def parse_input(node_input: str) -> dict:
|
||||
"""Parse the user's input into a structured format."""
|
||||
words = node_input.strip().split()
|
||||
return {"text": node_input, "word_count": len(words)}
|
||||
|
||||
def classify(node_input: dict):
|
||||
"""Route based on input length."""
|
||||
if node_input["word_count"] > 10:
|
||||
return Event(output=node_input, route="long")
|
||||
return Event(output=node_input, route="short")
|
||||
|
||||
def handle_short(node_input: dict) -> str:
|
||||
return f"Short input ({node_input['word_count']} words): {node_input['text']}"
|
||||
|
||||
def handle_long(node_input: dict) -> str:
|
||||
return f"Long input ({node_input['word_count']} words). Summary: {node_input['text'][:50]}..."
|
||||
|
||||
root_agent = Workflow(
|
||||
name="classifier_workflow",
|
||||
input_schema=str,
|
||||
edges=[
|
||||
('START', parse_input),
|
||||
(parse_input, classify),
|
||||
(classify, handle_short, "short"),
|
||||
(classify, handle_long, "long"),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## 7. Sample: Parallel Processing
|
||||
|
||||
Process a list of items concurrently:
|
||||
|
||||
### agent.py
|
||||
|
||||
```python
|
||||
from google.adk.workflow import Workflow
|
||||
from google.adk.workflow import node
|
||||
|
||||
def split_input(node_input: str) -> list:
|
||||
"""Split comma-separated input into a list."""
|
||||
return [item.strip() for item in node_input.split(",")]
|
||||
|
||||
@node(parallel_worker=True)
|
||||
def process_item(node_input: str) -> dict:
|
||||
"""Process a single item (runs in parallel for each list item)."""
|
||||
return {"item": node_input, "length": len(node_input), "upper": node_input.upper()}
|
||||
|
||||
def format_results(node_input: list) -> str:
|
||||
"""Format the parallel results into a readable summary."""
|
||||
lines = [f"- {r['item']}: {r['length']} chars -> {r['upper']}" for r in node_input]
|
||||
return "Results:\n" + "\n".join(lines)
|
||||
|
||||
root_agent = Workflow(
|
||||
name="parallel_processor",
|
||||
input_schema=str,
|
||||
edges=[
|
||||
('START', split_input),
|
||||
(split_input, process_item),
|
||||
(process_item, format_results),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## 8. Sample: Workflow with LLM Agent and Tools
|
||||
|
||||
Combine function nodes with an LLM agent that has tools:
|
||||
|
||||
### agent.py
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.workflow import Workflow
|
||||
from google.adk.agents.context import Context
|
||||
|
||||
def get_weather(city: str) -> dict:
|
||||
"""Get the current weather for a city."""
|
||||
# In production, call a real API
|
||||
return {"city": city, "temp": "72F", "condition": "sunny"}
|
||||
|
||||
def extract_city(node_input: str) -> str:
|
||||
"""Extract city name from user input."""
|
||||
# Simple extraction; in production, use NLP or LLM
|
||||
return node_input.strip()
|
||||
|
||||
weather_agent = LlmAgent(
|
||||
name="weather_reporter",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="""You are a friendly weather reporter.
|
||||
Use the get_weather tool to look up the weather, then give
|
||||
a natural-language weather report for the city.""",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
def format_output(ctx: Context, node_input: str) -> str:
|
||||
"""Add a friendly sign-off."""
|
||||
return f"{node_input}\n\nHave a great day!"
|
||||
|
||||
root_agent = Workflow(
|
||||
name="weather_workflow",
|
||||
input_schema=str,
|
||||
edges=[
|
||||
('START', extract_city),
|
||||
(extract_city, weather_agent),
|
||||
(weather_agent, format_output),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No module named 'google.adk'"
|
||||
Ensure the virtual environment is activated and `google-adk` is installed.
|
||||
|
||||
### Agent not showing in `adk web`
|
||||
Check that `__init__.py` contains `from . import agent` and `agent.py` defines `root_agent`.
|
||||
|
||||
### API key errors
|
||||
Verify `.env` is in the agent directory (not the parent) and contains a valid `GOOGLE_API_KEY`.
|
||||
|
||||
### Model not found
|
||||
Check the model name. Common models: `gemini-2.5-flash`, `gemini-2.5-pro`. The ADK also supports non-Google models (Anthropic, LiteLLM) with extra dependencies.
|
||||
@@ -0,0 +1,279 @@
|
||||
# Human-in-the-Loop (HITL) Reference
|
||||
|
||||
Pause workflow execution to request user input and resume with their response.
|
||||
|
||||
## 📋 Agent Verification Checklist (HITL)
|
||||
Use this checklist when implementing human-in-the-loop logic:
|
||||
- [ ] **Unique ID**: Is the `interrupt_id` unique per iteration in loops? (Critical to prevent infinite loops)
|
||||
- [ ] **Resumability**: For multi-step HITL, did you export an `App` with `is_resumable=True`?
|
||||
- [ ] **Resume Inputs**: If `rerun_on_resume=True` (default for LLM nodes), does the node handle `ctx.resume_inputs`?
|
||||
|
||||
## 💡 Quick Reference
|
||||
- **Request Input**: `yield RequestInput(message="Question", response_schema=Schema)`
|
||||
- **Resumable Config**: `ResumabilityConfig(is_resumable=True)`
|
||||
|
||||
HITL works in two modes:
|
||||
|
||||
### Resumable mode (recommended for multi-step HITL)
|
||||
|
||||
Export an `App` with resumability. The workflow checkpoints state and resumes at the interrupted node:
|
||||
|
||||
```python
|
||||
from google.adk.apps.app import App, ResumabilityConfig
|
||||
|
||||
app = App(
|
||||
name="my_app",
|
||||
root_agent=workflow_agent,
|
||||
resumability_config=ResumabilityConfig(is_resumable=True),
|
||||
)
|
||||
```
|
||||
|
||||
The agent loader checks for `app` before `root_agent`, so export both from `agent.py`.
|
||||
|
||||
### Non-resumable mode (simpler, no App needed)
|
||||
|
||||
The workflow replays from START on each user response, reconstructing state from session events. No `App` or `ResumabilityConfig` needed — just define `root_agent`. This works for simple single-interrupt HITL but replays all nodes up to the interrupt point on each resume.
|
||||
|
||||
## Imports
|
||||
|
||||
```python
|
||||
from google.adk.events.request_input import RequestInput
|
||||
from google.adk.agents.context import Context
|
||||
from google.adk.workflow import Workflow
|
||||
from google.adk.apps.app import App, ResumabilityConfig
|
||||
```
|
||||
|
||||
## Basic Request Input
|
||||
|
||||
Yield or return a `RequestInput` to pause execution and ask the user for input:
|
||||
|
||||
```python
|
||||
# Yield from a generator
|
||||
async def approval_gate(ctx: Context, node_input: str):
|
||||
yield RequestInput(
|
||||
message="Please approve this action:",
|
||||
response_schema={"type": "string"},
|
||||
)
|
||||
|
||||
# Or return directly from a regular function (no generator needed)
|
||||
def evaluate_request(request: TimeOffRequest):
|
||||
if request.days <= 1:
|
||||
return TimeOffDecision(approved=True) # Auto-approve
|
||||
return RequestInput(
|
||||
interrupt_id="manager_approval",
|
||||
message="Please review this time off request.",
|
||||
payload=request,
|
||||
response_schema=TimeOffDecision,
|
||||
)
|
||||
```
|
||||
|
||||
The workflow pauses and emits a function call event to the user. When the user responds, the workflow resumes.
|
||||
|
||||
## RequestInput Fields
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ApprovalResponse(BaseModel):
|
||||
approved: bool
|
||||
comment: str
|
||||
|
||||
RequestInput(
|
||||
interrupt_id="custom_id", # Auto-generated UUID if omitted
|
||||
message="Question for user", # Display message
|
||||
payload={"key": "value"}, # Custom data to include
|
||||
response_schema=ApprovalResponse, # Pydantic class, Python type, or JSON schema dict
|
||||
)
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `interrupt_id` | `str` | Unique ID for this interrupt (auto-generated UUID) |
|
||||
| `message` | `str` | Message shown to the user |
|
||||
| `payload` | `Any` | Custom payload sent with the request |
|
||||
| `response_schema` | `type \| dict` | Expected response format (Pydantic BaseModel class, Python type, or JSON schema dict) |
|
||||
|
||||
## Resume Behavior: rerun_on_resume
|
||||
|
||||
When a node is interrupted and the user responds, the `rerun_on_resume` flag controls what happens:
|
||||
|
||||
### rerun_on_resume=False (default for FunctionNode)
|
||||
|
||||
The user's response becomes the node's output. The node is NOT re-executed:
|
||||
|
||||
```python
|
||||
from google.adk.workflow import FunctionNode
|
||||
|
||||
async def ask_approval(ctx: Context, node_input: str):
|
||||
yield RequestInput(message="Approve?")
|
||||
|
||||
# Node won't rerun; user's response is passed as output to next node
|
||||
approval_node = FunctionNode(ask_approval, rerun_on_resume=False)
|
||||
```
|
||||
|
||||
### rerun_on_resume=True (default for LlmAgentWrapper)
|
||||
|
||||
The node is re-executed with the user's response available in `ctx.resume_inputs`:
|
||||
|
||||
```python
|
||||
async def interactive_node(ctx: Context, node_input: str):
|
||||
if ctx.resume_inputs:
|
||||
# Second run: user responded
|
||||
user_answer = list(ctx.resume_inputs.values())[0]
|
||||
yield Event(output=f"User said: {user_answer}")
|
||||
else:
|
||||
# First run: ask the user
|
||||
yield RequestInput(message="What should I do?")
|
||||
```
|
||||
|
||||
## HITL with LLM Agents
|
||||
|
||||
LLM agents support HITL via `LongRunningFunctionTool`:
|
||||
|
||||
```python
|
||||
from google.adk.tools.long_running_tool import LongRunningFunctionTool
|
||||
|
||||
def approval_tool(request: str) -> str:
|
||||
"""Request human approval for an action."""
|
||||
return f"Approved: {request}"
|
||||
|
||||
llm_agent = LlmAgent(
|
||||
name="agent_with_approval",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="When you need approval, use the approval_tool.",
|
||||
tools=[LongRunningFunctionTool(func=approval_tool)],
|
||||
)
|
||||
|
||||
# LlmAgentWrapper has rerun_on_resume=True by default
|
||||
agent = Workflow(
|
||||
name="hitl_workflow",
|
||||
edges=[
|
||||
('START', llm_agent),
|
||||
(llm_agent, next_step),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Multi-Step HITL
|
||||
|
||||
A node can request input multiple times by checking `ctx.resume_inputs`:
|
||||
|
||||
```python
|
||||
async def multi_step_form(ctx: Context, node_input: str):
|
||||
if not ctx.resume_inputs:
|
||||
# Step 1: Ask for name
|
||||
yield RequestInput(
|
||||
interrupt_id="ask_name",
|
||||
message="What is your name?",
|
||||
)
|
||||
return
|
||||
|
||||
if "ask_name" in ctx.resume_inputs and "ask_email" not in ctx.resume_inputs:
|
||||
# Step 2: Ask for email
|
||||
yield RequestInput(
|
||||
interrupt_id="ask_email",
|
||||
message="What is your email?",
|
||||
)
|
||||
return
|
||||
|
||||
# All inputs collected
|
||||
name = ctx.resume_inputs["ask_name"]
|
||||
email = ctx.resume_inputs["ask_email"]
|
||||
yield Event(output={"name": name, "email": email})
|
||||
```
|
||||
|
||||
## HITL in Loops (Unique interrupt_id)
|
||||
|
||||
When a HITL node can fire multiple times in a loop (e.g. reject → revise → re-approve), you **must use a unique `interrupt_id` per iteration**. Reusing the same ID causes event-based state reconstruction to confuse earlier responses with the current interrupt, resulting in an infinite restart loop.
|
||||
|
||||
```python
|
||||
async def review(ctx: Context, node_input: Any):
|
||||
# Counter-based unique ID per review cycle
|
||||
review_count = ctx.state.get('review_count', 0)
|
||||
interrupt_id = f'review_{review_count}'
|
||||
|
||||
response = ctx.resume_inputs.get(interrupt_id)
|
||||
if response:
|
||||
route = 'approved' if response.get('approved') else 'rejected'
|
||||
yield Event(
|
||||
output=response,
|
||||
route=route,
|
||||
state={'review_count': review_count + 1},
|
||||
)
|
||||
return
|
||||
|
||||
yield RequestInput(
|
||||
interrupt_id=interrupt_id,
|
||||
message="Approve this plan?",
|
||||
response_schema=ApprovalSchema,
|
||||
)
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Store a counter in `ctx.state` and increment on each response
|
||||
- Use the counter in the `interrupt_id` (e.g. `review_0`, `review_1`, ...)
|
||||
- Look up `ctx.resume_inputs` with the same counter-based ID
|
||||
- This applies to both resumable and non-resumable modes
|
||||
|
||||
## Resumability Configuration
|
||||
|
||||
### Resumable mode (recommended for multi-step HITL)
|
||||
|
||||
```python
|
||||
from google.adk.apps.app import App, ResumabilityConfig
|
||||
|
||||
# Export BOTH root_agent and app from agent.py
|
||||
root_agent = Workflow(name="my_workflow", edges=[...])
|
||||
|
||||
app = App(
|
||||
name="my_app",
|
||||
root_agent=root_agent,
|
||||
resumability_config=ResumabilityConfig(is_resumable=True),
|
||||
)
|
||||
```
|
||||
|
||||
When `is_resumable=True`:
|
||||
- Workflow state is checkpointed in session's `agent_states` map
|
||||
- On resume, the workflow loads checkpointed state and resumes at the interrupted node
|
||||
- Required for multi-step HITL, `LongRunningFunctionTool`, and complex workflows
|
||||
|
||||
### Non-resumable mode (simpler)
|
||||
|
||||
When `is_resumable=False` (default) or no `App` is exported:
|
||||
- No state checkpointing — the workflow replays from START on each user response
|
||||
- State is reconstructed from session events during replay
|
||||
- Completed nodes are skipped; execution resumes at the interrupted node
|
||||
- Works for simple single-interrupt HITL without needing `App` or `ResumabilityConfig`
|
||||
- For multi-step HITL or complex workflows, use resumable mode instead
|
||||
|
||||
## Responding to HITL Requests
|
||||
|
||||
From the client side, respond to function calls:
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
|
||||
# Extract function_call_id from the interrupt event
|
||||
function_call_id = interrupt_event.content.parts[0].function_call.id
|
||||
|
||||
# Create response
|
||||
response = types.Content(
|
||||
role="user",
|
||||
parts=[types.Part(
|
||||
function_response=types.FunctionResponse(
|
||||
id=function_call_id,
|
||||
name="adk_request_input",
|
||||
response={"result": "User's answer here"},
|
||||
)
|
||||
)],
|
||||
)
|
||||
|
||||
# Send response to resume the workflow
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=response,
|
||||
):
|
||||
# Process resumed workflow events
|
||||
pass
|
||||
```
|
||||
@@ -0,0 +1,148 @@
|
||||
# ADK Import Paths Quick Reference
|
||||
|
||||
## 📋 Agent Verification Checklist (Imports)
|
||||
Use this checklist to ensure you are using the most idiomatic import paths:
|
||||
|
||||
- [ ] **Canonical Imports**: Did you use the short canonical imports where available (e.g., `from google.adk import Agent`) instead of the verbose ones?
|
||||
- [ ] **Avoid Deprecated**: Are you avoiding deprecated paths (e.g., use `McpToolset` instead of `MCPToolset`)?
|
||||
|
||||
## Canonical Imports (preferred, used by all samples)
|
||||
|
||||
```python
|
||||
from google.adk import Agent, Context, Event, Workflow
|
||||
from google.adk.events import RequestInput
|
||||
from google.adk.workflow import node, RetryConfig, Edge, JoinNode
|
||||
```
|
||||
|
||||
## Core Agents
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `Agent` (canonical) | `from google.adk import Agent` |
|
||||
| `Agent` (verbose) | `from google.adk.agents.llm_agent import Agent` |
|
||||
| `LlmAgent` | `from google.adk.agents.llm_agent import LlmAgent` |
|
||||
| `SequentialAgent` | `from google.adk.agents.sequential_agent import SequentialAgent` |
|
||||
| `ParallelAgent` | `from google.adk.agents.parallel_agent import ParallelAgent` |
|
||||
| `LoopAgent` | `from google.adk.agents.loop_agent import LoopAgent` |
|
||||
|
||||
## Workflow Agents (Experimental)
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `Workflow` | `from google.adk.workflow import Workflow` |
|
||||
| `Edge` | `from google.adk.workflow import Edge` |
|
||||
| `Agent` (supports task/single_turn mode) | `from google.adk import Agent` |
|
||||
|
||||
## Workflow Nodes
|
||||
|
||||
| Component | Import |
|
||||
| ----------------------------------- | -------------------------------------- |
|
||||
| `FunctionNode` | `from google.adk.workflow import |
|
||||
: : FunctionNode` :
|
||||
| `_LlmAgentWrapper` (private, | `from |
|
||||
: auto-used) : google.adk.workflow._llm_agent_wrapper :
|
||||
: : import _LlmAgentWrapper` :
|
||||
| `AgentNode` | `from google.adk.workflow._agent_node |
|
||||
: : import AgentNode` :
|
||||
| `_ToolNode` (private) | `from google.adk.workflow._tool_node |
|
||||
: : import _ToolNode` :
|
||||
| `JoinNode` | `from google.adk.workflow import |
|
||||
: : JoinNode` :
|
||||
| Parallel-worker behavior (no public | Set `parallel_worker=True` on `@node` |
|
||||
: class) : or `LlmAgent`; the framework wraps :
|
||||
: : with an internal `_ParallelWorker` :
|
||||
| `BaseNode`, `START` | `from google.adk.workflow import |
|
||||
: : BaseNode, START` :
|
||||
| `@node` decorator | `from google.adk.workflow import node` |
|
||||
|
||||
## Workflow Events and Context
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `Event` | `from google.adk.events.event import Event` |
|
||||
| `RequestInput` | `from google.adk.events.request_input import RequestInput` |
|
||||
| `Context` | `from google.adk.agents.context import Context` |
|
||||
| `WorkflowGraph` | `from google.adk.workflow._workflow_graph import WorkflowGraph` |
|
||||
| `RetryConfig` | `from google.adk.workflow import RetryConfig` |
|
||||
|
||||
## Task Mode
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `RequestTaskTool` | `from google.adk.agents.llm.task._request_task_tool import RequestTaskTool` |
|
||||
| `FinishTaskTool` | `from google.adk.agents.llm.task._finish_task_tool import FinishTaskTool` |
|
||||
| `TaskRequest`, `TaskResult` | `from google.adk.agents.llm.task._task_models import TaskRequest, TaskResult` |
|
||||
|
||||
## Tools
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `FunctionTool` | `from google.adk.tools.function_tool import FunctionTool` |
|
||||
| `BaseTool` | `from google.adk.tools.base_tool import BaseTool` |
|
||||
| `BaseToolset` | `from google.adk.tools.base_toolset import BaseToolset` |
|
||||
| `ToolContext` | `from google.adk.tools.tool_context import ToolContext` |
|
||||
| `LongRunningFunctionTool` | `from google.adk.tools.long_running_tool import LongRunningFunctionTool` |
|
||||
| `McpToolset` | `from google.adk.tools.mcp_tool.mcp_toolset import McpToolset` |
|
||||
| `StdioConnectionParams` | `from google.adk.tools.mcp_tool import StdioConnectionParams` |
|
||||
| `SseConnectionParams` | `from google.adk.tools.mcp_tool import SseConnectionParams` |
|
||||
| `OpenAPIToolset` | `from google.adk.tools.openapi_tool import OpenAPIToolset` |
|
||||
|
||||
## Built-in Tools
|
||||
|
||||
| Tool | Import |
|
||||
|------|--------|
|
||||
| `google_search` | `from google.adk.tools import google_search` |
|
||||
| `load_artifacts` | `from google.adk.tools import load_artifacts` |
|
||||
| `load_memory` | `from google.adk.tools import load_memory` |
|
||||
| `exit_loop` | `from google.adk.tools import exit_loop` |
|
||||
| `transfer_to_agent` | `from google.adk.tools import transfer_to_agent` |
|
||||
| `get_user_choice` | `from google.adk.tools import get_user_choice` |
|
||||
|
||||
## Runner and Session
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `Runner` | `from google.adk.runners import Runner` |
|
||||
| `InMemoryRunner` | `from google.adk.runners import InMemoryRunner` |
|
||||
| `InMemorySessionService` | `from google.adk.sessions import InMemorySessionService` |
|
||||
| `DatabaseSessionService` | `from google.adk.sessions import DatabaseSessionService` |
|
||||
|
||||
## App and Plugins
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `App` | `from google.adk.apps import App` |
|
||||
| `ResumabilityConfig` | `from google.adk.apps.app import ResumabilityConfig` |
|
||||
| `BasePlugin` | `from google.adk.plugins.base_plugin import BasePlugin` |
|
||||
| `ContextFilterPlugin` | `from google.adk.plugins.context_filter_plugin import ContextFilterPlugin` |
|
||||
|
||||
## Models
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `LiteLlm` | `from google.adk.models.lite_llm import LiteLlm` |
|
||||
| `LlmRequest` | `from google.adk.models.llm_request import LlmRequest` |
|
||||
| `LlmResponse` | `from google.adk.models.llm_response import LlmResponse` |
|
||||
|
||||
## Callbacks
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `CallbackContext` | `from google.adk.agents.callback_context import CallbackContext` |
|
||||
| `ReadonlyContext` | `from google.adk.agents.readonly_context import ReadonlyContext` |
|
||||
|
||||
## Code Executors
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `BuiltInCodeExecutor` | `from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor` |
|
||||
|
||||
## Google GenAI Types
|
||||
|
||||
| Component | Import |
|
||||
|-----------|--------|
|
||||
| `types` | `from google.genai import types` |
|
||||
| `Content` | `from google.genai.types import Content` |
|
||||
| `ModelContent` | `from google.genai.types import ModelContent` |
|
||||
| `Part` | `from google.genai.types import Part` |
|
||||
| `GenerateContentConfig` | `from google.genai.types import GenerateContentConfig` |
|
||||
@@ -0,0 +1,462 @@
|
||||
# LLM Agent Nodes Reference
|
||||
|
||||
Embed LLM-powered agents as nodes in workflow graphs.
|
||||
|
||||
## 📋 Agent Verification Checklist (LLM Nodes)
|
||||
Use this checklist to verify your LLM agent configuration:
|
||||
|
||||
- [ ] **Output Type**: If no `output_schema` is set, downstream now receives `str` (auto-extracted from `types.Content`). You can safely type-hint `node_input: str`.
|
||||
- [ ] **State Serialization**: If this agent feeds into a `JoinNode`, did you set `output_schema` to avoid non-serializable `types.Content` errors?
|
||||
- [ ] **Instructions**: Are `{var}` templates used in instructions resolving ONLY from `ctx.state`? (Not `node_input`)
|
||||
- [ ] **Config**: Are instructions, tools, and response schema set on the `LlmAgent` directly, and NOT in `generate_content_config`?
|
||||
|
||||
## 💡 Quick Reference
|
||||
|
||||
- **Chat Mode**: Default. Multi-turn, keeps session history.
|
||||
- **Single-Turn Mode**: Isolated. Set `mode="single_turn"` or rely on auto-wrapping defaults.
|
||||
- **Task Mode**: Multi-turn within a task. Set `mode="task"`.
|
||||
- **Stateless**: Set `include_contents="none"` to ignore session history.
|
||||
|
||||
## Imports
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.workflow._llm_agent_wrapper import _LlmAgentWrapper # private
|
||||
from google.adk.workflow import Workflow
|
||||
```
|
||||
|
||||
## Choosing the Right LLM Agent
|
||||
|
||||
**Use `google.adk.agents.llm_agent.LlmAgent`** in workflow edges. It is auto-wrapped as `LlmAgentWrapper`, which emits `Event(output=...)` for downstream data passing. This is required for any LLM agent that needs to pass output to downstream function nodes via `node_input`.
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
|
||||
writer = LlmAgent(
|
||||
name="writer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Write a short story.",
|
||||
output_schema=Story,
|
||||
)
|
||||
|
||||
# writer is auto-wrapped as _LlmAgentWrapper — downstream gets Event(output=...)
|
||||
agent = Workflow(
|
||||
name="pipeline",
|
||||
edges=[('START', writer), (writer, process_story)],
|
||||
)
|
||||
```
|
||||
|
||||
## Basic LLM Node
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
|
||||
writer = LlmAgent(
|
||||
name="writer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Write a short story based on the user's prompt.",
|
||||
)
|
||||
|
||||
reviewer = LlmAgent(
|
||||
name="reviewer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Review the following story and provide feedback.",
|
||||
)
|
||||
|
||||
agent = Workflow(
|
||||
name="story_pipeline",
|
||||
edges=[
|
||||
('START', writer), # Auto-wrapped as LlmAgentWrapper
|
||||
(writer, reviewer),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## LLM Agent Output Types
|
||||
|
||||
When an `LlmAgent` runs as a workflow node, `process_llm_agent_output` (in
|
||||
`_llm_agent_wrapper.py`) sets `event.output` to:
|
||||
|
||||
- The **concatenated text** of the model's response (a `str`) — when
|
||||
`output_schema` is not set.
|
||||
- The **validated dict** (`model_dump()` of the Pydantic model) — when
|
||||
`output_schema=MyModel` is set.
|
||||
|
||||
A downstream function node typed `node_input: str` therefore works in the
|
||||
default case, and `node_input: dict` works when `output_schema` is set.
|
||||
|
||||
**Observability caveat:** the value above is set on the event internally and
|
||||
forwarded to the next node, but `event.output` is **`None`** when you observe it
|
||||
from `runner.run_async(...)` for the LLM agent's own event — the framework
|
||||
clears it before the event reaches user code. Don't write tests that assert on
|
||||
`event.output` for an LLM agent's event; assert on the downstream node's output,
|
||||
on `session.state[output_key]`, or on `event.content.parts[*].text` instead.
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class CodeOutput(BaseModel):
|
||||
code: str
|
||||
language: str
|
||||
|
||||
writer = LlmAgent(
|
||||
name="writer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Write code. Return JSON with 'code' and 'language' fields.",
|
||||
output_schema=CodeOutput,
|
||||
)
|
||||
|
||||
# Downstream node receives a dict: {"code": "...", "language": "python"}
|
||||
def process_code(node_input: dict) -> str:
|
||||
return node_input["code"]
|
||||
```
|
||||
|
||||
**Summary of LLM agent node output types:**
|
||||
|
||||
LLM Agent Config | `node_input` Type for Next Node
|
||||
-------------------- | -----------------------------------
|
||||
No `output_schema` | `str` (concatenated model text)
|
||||
With `output_schema` | `dict` (parsed from Pydantic model)
|
||||
|
||||
**Prefer `output_schema` when downstream nodes need structured access.** Strings
|
||||
are fine for pass-through text, but a typed dict is easier to consume and is
|
||||
required when the predecessor feeds a `JoinNode` whose results land in a
|
||||
persistent session service (raw text is fine; objects that aren't
|
||||
JSON-serializable break `DatabaseSessionService`).
|
||||
|
||||
## Auto-Wrapping Behavior
|
||||
|
||||
When you place an `LlmAgent` in workflow edges, it is auto-wrapped as `_LlmAgentWrapper`. The wrapper:
|
||||
|
||||
- Defaults to `single_turn` mode (agent sees only current input, not session history)
|
||||
- Sets `rerun_on_resume=True` (reruns after HITL interrupts)
|
||||
- Creates a content branch for isolation between parallel LLM agents
|
||||
|
||||
The mode is set on the `LlmAgent` itself, not the wrapper:
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
|
||||
# single_turn (default when auto-wrapped): isolated, no session history
|
||||
classifier = LlmAgent(
|
||||
name="classifier",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Classify the input as positive, negative, or neutral.",
|
||||
output_schema=ClassificationResult,
|
||||
)
|
||||
|
||||
# task mode: supports HITL, multi-turn within the task
|
||||
task_agent = LlmAgent(
|
||||
name="task_agent",
|
||||
model="gemini-2.5-flash",
|
||||
mode="task",
|
||||
instruction="Process the request.",
|
||||
)
|
||||
```
|
||||
|
||||
## LlmAgent Configuration
|
||||
|
||||
### Instructions
|
||||
|
||||
Dynamic instructions with placeholders resolved from session state. **`{var}` templates only resolve from `ctx.state` — `node_input` is NOT available in templates.** To use predecessor data in instructions, store it in state first (via `Event(state={...})` or `output_key`):
|
||||
|
||||
```python
|
||||
agent = LlmAgent(
|
||||
name="personalized",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="""You are helping {user_name}.
|
||||
Their preferences are: {preferences}.
|
||||
Respond in {language}.""",
|
||||
)
|
||||
# {user_name}, {preferences}, {language} resolved from session state
|
||||
# Missing variables raise KeyError at runtime — use {var?} for optional:
|
||||
# instruction="Current mood: {mood?}" # empty string if 'mood' not in state
|
||||
```
|
||||
|
||||
**Template variable behavior:**
|
||||
|
||||
| Syntax | Missing Key Behavior |
|
||||
|--------|---------------------|
|
||||
| `{var}` | Raises `KeyError` at LLM call time |
|
||||
| `{var?}` | Substitutes empty string, logs debug message |
|
||||
| `{not.an" identifier}` | Left as-is (not substituted) |
|
||||
|
||||
Instruction provider function for fully dynamic instructions:
|
||||
|
||||
```python
|
||||
from google.adk.agents.readonly_context import ReadonlyContext
|
||||
|
||||
def build_instruction(ctx: ReadonlyContext) -> str:
|
||||
agents = ctx.state.get("active_agents", [])
|
||||
return f"Coordinate these agents: {', '.join(agents)}"
|
||||
|
||||
agent = LlmAgent(
|
||||
name="coordinator",
|
||||
model="gemini-2.5-flash",
|
||||
instruction=build_instruction,
|
||||
)
|
||||
```
|
||||
|
||||
### Output Schema
|
||||
|
||||
Structure LLM output with Pydantic models:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ReviewResult(BaseModel):
|
||||
score: int
|
||||
feedback: str
|
||||
approved: bool
|
||||
|
||||
reviewer = LlmAgent(
|
||||
name="reviewer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Review the code and provide structured feedback.",
|
||||
output_schema=ReviewResult,
|
||||
)
|
||||
```
|
||||
|
||||
When used as a workflow node, the output becomes a `dict` (via `model_dump()`) as `node_input` for the next node.
|
||||
|
||||
### Output Key
|
||||
|
||||
Store agent output in session state:
|
||||
|
||||
```python
|
||||
agent = LlmAgent(
|
||||
name="writer",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Write a draft.",
|
||||
output_key="draft", # Stores output in state['draft']
|
||||
)
|
||||
```
|
||||
|
||||
### include_contents
|
||||
|
||||
Control conversation history:
|
||||
|
||||
```python
|
||||
agent = LlmAgent(
|
||||
name="stateless",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Process this input independently.",
|
||||
include_contents="none", # Don't include session history
|
||||
)
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
Add tools to LLM agents:
|
||||
|
||||
```python
|
||||
def search_database(query: str) -> str:
|
||||
"""Search the database for relevant records."""
|
||||
return f"Results for: {query}"
|
||||
|
||||
def send_email(to: str, subject: str, body: str) -> str:
|
||||
"""Send an email to the specified address."""
|
||||
return "Email sent"
|
||||
|
||||
agent = LlmAgent(
|
||||
name="assistant",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Help the user with their request.",
|
||||
tools=[search_database, send_email],
|
||||
)
|
||||
```
|
||||
|
||||
Tools can be:
|
||||
|
||||
- Python functions (auto-wrapped as `FunctionTool`)
|
||||
- `BaseTool` instances
|
||||
- `BaseToolset` instances (e.g., MCP toolsets)
|
||||
|
||||
## Callbacks
|
||||
|
||||
### Before Model Callback
|
||||
|
||||
Intercept or modify LLM requests. Return an `LlmResponse` to skip the LLM call; return `None` to proceed:
|
||||
|
||||
```python
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.models.llm_request import LlmRequest
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
|
||||
def guard_callback(
|
||||
callback_context: CallbackContext,
|
||||
llm_request: LlmRequest,
|
||||
) -> LlmResponse | None:
|
||||
for content in llm_request.contents:
|
||||
if content.parts:
|
||||
for part in content.parts:
|
||||
if part.text and "unsafe" in part.text:
|
||||
return LlmResponse(
|
||||
content=types.ModelContent("I cannot process that.")
|
||||
)
|
||||
return None # Proceed with normal LLM call
|
||||
|
||||
agent = LlmAgent(
|
||||
name="guarded",
|
||||
model="gemini-2.5-flash",
|
||||
before_model_callback=guard_callback,
|
||||
)
|
||||
```
|
||||
|
||||
### After Model Callback
|
||||
|
||||
Transform LLM responses. Return an `LlmResponse` to replace; return `None` to keep original:
|
||||
|
||||
```python
|
||||
def log_response(
|
||||
callback_context: CallbackContext,
|
||||
llm_response: LlmResponse,
|
||||
) -> LlmResponse | None:
|
||||
print(f"LLM responded: {llm_response.content}")
|
||||
return None # Use original response
|
||||
|
||||
agent = LlmAgent(
|
||||
name="logged",
|
||||
model="gemini-2.5-flash",
|
||||
after_model_callback=log_response,
|
||||
)
|
||||
```
|
||||
|
||||
### Before/After Tool Callbacks
|
||||
|
||||
Intercept tool calls. Return a `dict` to use as tool response (skipping actual execution); return `None` to proceed:
|
||||
|
||||
```python
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
def audit_tool(
|
||||
tool: BaseTool,
|
||||
args: dict[str, Any],
|
||||
tool_context: ToolContext,
|
||||
) -> dict | None:
|
||||
print(f"Calling tool {tool.name} with args: {args}")
|
||||
return None # Proceed with tool call
|
||||
|
||||
def validate_tool_result(
|
||||
tool: BaseTool,
|
||||
args: dict[str, Any],
|
||||
tool_context: ToolContext,
|
||||
tool_response: dict,
|
||||
) -> dict | None:
|
||||
if "error" in tool_response:
|
||||
return {"result": "Tool execution failed, please try again."}
|
||||
return None # Use original result
|
||||
|
||||
agent = LlmAgent(
|
||||
name="audited",
|
||||
model="gemini-2.5-flash",
|
||||
tools=[my_tool],
|
||||
before_tool_callback=audit_tool,
|
||||
after_tool_callback=validate_tool_result,
|
||||
)
|
||||
```
|
||||
|
||||
### Multiple Callbacks
|
||||
|
||||
Pass a list of callbacks. They execute in order until one returns non-None:
|
||||
|
||||
```python
|
||||
agent = LlmAgent(
|
||||
name="multi_callback",
|
||||
model="gemini-2.5-flash",
|
||||
before_model_callback=[safety_check, rate_limiter, logger],
|
||||
)
|
||||
```
|
||||
|
||||
### Error Callbacks
|
||||
|
||||
Handle LLM or tool errors gracefully:
|
||||
|
||||
```python
|
||||
def handle_model_error(
|
||||
callback_context: CallbackContext,
|
||||
llm_request: LlmRequest,
|
||||
error: Exception,
|
||||
) -> LlmResponse | None:
|
||||
return LlmResponse(
|
||||
content=types.ModelContent("Service temporarily unavailable.")
|
||||
)
|
||||
|
||||
def handle_tool_error(
|
||||
tool: BaseTool,
|
||||
args: dict[str, Any],
|
||||
tool_context: ToolContext,
|
||||
error: Exception,
|
||||
) -> dict | None:
|
||||
return {"error": str(error), "fallback": True}
|
||||
|
||||
agent = LlmAgent(
|
||||
name="resilient",
|
||||
model="gemini-2.5-flash",
|
||||
on_model_error_callback=handle_model_error,
|
||||
on_tool_error_callback=handle_tool_error,
|
||||
)
|
||||
```
|
||||
|
||||
## All Callback Types
|
||||
|
||||
| Callback | Signature | Return to Override |
|
||||
|----------|-----------|-------------------|
|
||||
| `before_model_callback` | `(CallbackContext, LlmRequest) -> LlmResponse?` | Return `LlmResponse` to skip LLM |
|
||||
| `after_model_callback` | `(CallbackContext, LlmResponse) -> LlmResponse?` | Return `LlmResponse` to replace |
|
||||
| `on_model_error_callback` | `(CallbackContext, LlmRequest, Exception) -> LlmResponse?` | Return `LlmResponse` to suppress error |
|
||||
| `before_tool_callback` | `(BaseTool, dict, ToolContext) -> dict?` | Return `dict` to skip tool |
|
||||
| `after_tool_callback` | `(BaseTool, dict, ToolContext, dict) -> dict?` | Return `dict` to replace result |
|
||||
| `on_tool_error_callback` | `(BaseTool, dict, ToolContext, Exception) -> dict?` | Return `dict` to suppress error |
|
||||
|
||||
All callbacks can be sync or async. All accept a single callback or a list.
|
||||
|
||||
## Generate Content Config
|
||||
|
||||
Fine-tune LLM generation:
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
|
||||
agent = LlmAgent(
|
||||
name="creative",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Write creative stories.",
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
temperature=0.9,
|
||||
top_p=0.95,
|
||||
max_output_tokens=2048,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Agent Transfer
|
||||
|
||||
Agents can transfer control to sub-agents:
|
||||
|
||||
```python
|
||||
specialist = LlmAgent(
|
||||
name="specialist",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Handle specialized requests.",
|
||||
)
|
||||
|
||||
coordinator = LlmAgent(
|
||||
name="coordinator",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="Route requests to the specialist when needed.",
|
||||
sub_agents=[specialist],
|
||||
)
|
||||
```
|
||||
|
||||
Control transfer behavior:
|
||||
|
||||
```python
|
||||
agent = LlmAgent(
|
||||
name="isolated",
|
||||
model="gemini-2.5-flash",
|
||||
disallow_transfer_to_parent=True,
|
||||
disallow_transfer_to_peers=True,
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,142 @@
|
||||
# Multi-Agent Patterns
|
||||
|
||||
## 📋 Agent Verification Checklist (Multi-Agent)
|
||||
Use this checklist when setting up multi-agent systems:
|
||||
- [ ] **Description**: Does every sub-agent have a clear `description`? (Used by LLM for routing or tool generation)
|
||||
- [ ] **Model Inheritance**: Did you let sub-agents inherit the model from the coordinator to avoid duplication?
|
||||
- [ ] **Loop Termination**: If using `LoopAgent`, is there a clear way to call `exit_loop` to prevent infinite loops?
|
||||
|
||||
## 💡 Quick Reference
|
||||
- **Sequential**: `SequentialAgent(sub_agents=[a, b, c])`
|
||||
- **Parallel**: `ParallelAgent(sub_agents=[a, b, c])`
|
||||
- **Loop**: `LoopAgent(sub_agents=[a, b], max_iterations=5)`
|
||||
|
||||
## LLM-Based Multi-Agent (Chat Transfer)
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
|
||||
researcher = Agent(
|
||||
name='researcher',
|
||||
description='Researches topics.',
|
||||
instruction='You research topics and provide findings.',
|
||||
tools=[search_tool],
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name='writer',
|
||||
description='Writes content.',
|
||||
instruction='You write content based on research.',
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-2.5-flash',
|
||||
name='coordinator',
|
||||
instruction=(
|
||||
'Delegate research to the researcher and '
|
||||
'writing to the writer.'
|
||||
),
|
||||
sub_agents=[researcher, writer],
|
||||
)
|
||||
```
|
||||
|
||||
**Key rules:**
|
||||
- Only the root agent needs `model=`. Sub-agents inherit it.
|
||||
- Each sub-agent needs a `description` (used for routing).
|
||||
- Transfer between agents is automatic via LLM reasoning.
|
||||
- `disallow_transfer_to_parent=True` prevents back-transfer.
|
||||
- `disallow_transfer_to_peers=True` prevents peer-transfer.
|
||||
|
||||
## Task-Based Multi-Agent (Structured Delegation)
|
||||
|
||||
For structured input/output, use task mode instead of chat transfer. See **`task-mode.md`** for full details.
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
|
||||
worker = Agent(
|
||||
name='worker',
|
||||
mode='task', # or 'single_turn'
|
||||
input_schema=WorkerInput,
|
||||
output_schema=WorkerOutput,
|
||||
instruction='Do work, then call finish_task.',
|
||||
description='Performs structured work.',
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name='coordinator',
|
||||
model='gemini-2.5-flash',
|
||||
sub_agents=[worker],
|
||||
instruction='Delegate to worker via request_task_worker.',
|
||||
)
|
||||
```
|
||||
|
||||
## Non-LLM Orchestration Agents
|
||||
|
||||
### SequentialAgent
|
||||
|
||||
Runs sub-agents in order, one after another:
|
||||
|
||||
```python
|
||||
from google.adk.agents.sequential_agent import SequentialAgent
|
||||
|
||||
root_agent = SequentialAgent(
|
||||
name='pipeline',
|
||||
sub_agents=[step1_agent, step2_agent, step3_agent],
|
||||
)
|
||||
```
|
||||
|
||||
### ParallelAgent
|
||||
|
||||
Runs sub-agents concurrently:
|
||||
|
||||
```python
|
||||
from google.adk.agents.parallel_agent import ParallelAgent
|
||||
|
||||
root_agent = ParallelAgent(
|
||||
name='fan_out',
|
||||
sub_agents=[task_a, task_b, task_c],
|
||||
)
|
||||
```
|
||||
|
||||
### LoopAgent
|
||||
|
||||
Repeats sub-agents until `exit_loop` is called:
|
||||
|
||||
```python
|
||||
from google.adk.tools import exit_loop
|
||||
from google.adk.agents.loop_agent import LoopAgent
|
||||
|
||||
looping_agent = Agent(
|
||||
name='checker',
|
||||
tools=[exit_loop],
|
||||
instruction='Check the result and call exit_loop if done.',
|
||||
)
|
||||
|
||||
root_agent = LoopAgent(
|
||||
name='retry_loop',
|
||||
sub_agents=[worker_agent, looping_agent],
|
||||
max_iterations=5,
|
||||
)
|
||||
```
|
||||
|
||||
## Model Configuration
|
||||
|
||||
- Default model: `gemini-2.5-flash`
|
||||
- Override globally: `Agent.set_default_model('gemini-2.5-pro')`
|
||||
- Model inheritance: sub-agents inherit parent's model if not set
|
||||
- Non-Gemini models via LiteLlm:
|
||||
```python
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
root_agent = Agent(model=LiteLlm(model='anthropic/claude-sonnet-4-20250514'), ...)
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Agent stuck in sub-agent:** Sub-agent has no path back to parent.
|
||||
Set `disallow_transfer_to_parent=False` (default) or add explicit
|
||||
transfer instructions.
|
||||
- **Wrong agent handles request:** Ambiguous `description` fields. Make
|
||||
each agent's description clearly differentiate its scope.
|
||||
- **Circular imports:** Define all agents in a single `agent.py` file,
|
||||
or use a shared module for sub-agents.
|
||||
@@ -0,0 +1,227 @@
|
||||
# Parallel Execution, Fan-Out, and Fan-In Reference
|
||||
|
||||
Execute multiple nodes concurrently and collect their results.
|
||||
|
||||
## 📋 Agent Verification Checklist (Parallel & Fan-Out)
|
||||
Use this checklist when implementing parallel patterns:
|
||||
|
||||
- [ ] **JoinNode Serialization**: If LLM agents feed into a `JoinNode`, did you set `output_schema` on them to prevent JSON serialization errors?
|
||||
- [ ] **ParallelWorker Usage**: Did you avoid using `parallel_worker=True` on fan-out nodes? (It expects a list input)
|
||||
- [ ] **Multi-Trigger vs Join**: Do you understand that Multi-Trigger fires downstream once per branch, while JoinNode waits and fires once with merged dict?
|
||||
|
||||
## 💡 Quick Reference
|
||||
|
||||
- **Fan-Out (Tuple)**: `('START', (node_a, node_b))`
|
||||
- **Fan-In (JoinNode)**: `((node_a, node_b), join_node)`
|
||||
- **List Worker**: `@node(parallel_worker=True)` (Takes list, outputs list)
|
||||
|
||||
## Imports
|
||||
|
||||
```python
|
||||
from google.adk.workflow import Workflow, JoinNode, node
|
||||
```
|
||||
|
||||
Parallel-worker behavior is opted into via the `parallel_worker=True` flag on
|
||||
`@node` or `LlmAgent`. The underlying wrapper class is internal — don't import
|
||||
it directly.
|
||||
|
||||
## Fan-Out: Multiple Branches
|
||||
|
||||
Send output to multiple nodes simultaneously using tuple syntax:
|
||||
|
||||
```python
|
||||
def analyze_text(node_input: str) -> str:
|
||||
return f"Analysis: {node_input}"
|
||||
|
||||
def translate_text(node_input: str) -> str:
|
||||
return f"Translation: {node_input}"
|
||||
|
||||
def summarize_text(node_input: str) -> str:
|
||||
return f"Summary: {node_input}"
|
||||
|
||||
agent = Workflow(
|
||||
name="fan_out",
|
||||
edges=[
|
||||
('START', (analyze_text, translate_text, summarize_text)),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Each branch receives the same input and runs concurrently.
|
||||
|
||||
## Fan-In: JoinNode
|
||||
|
||||
Collect outputs from multiple branches before continuing:
|
||||
|
||||
```python
|
||||
join = JoinNode(name="collect_results")
|
||||
|
||||
agent = Workflow(
|
||||
name="fan_out_fan_in",
|
||||
edges=[
|
||||
('START', (analyze_text, translate_text, summarize_text)),
|
||||
((analyze_text, translate_text, summarize_text), join),
|
||||
(join, final_processor),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### JoinNode Output Format
|
||||
|
||||
JoinNode outputs a dictionary mapping predecessor names to their outputs:
|
||||
|
||||
```python
|
||||
# JoinNode output:
|
||||
# {
|
||||
# "analyze_text": "Analysis: hello",
|
||||
# "translate_text": "Translation: hello",
|
||||
# "summarize_text": "Summary: hello",
|
||||
# }
|
||||
|
||||
def final_processor(node_input: dict) -> str:
|
||||
analysis = node_input["analyze_text"]
|
||||
translation = node_input["translate_text"]
|
||||
summary = node_input["summarize_text"]
|
||||
return f"Combined: {analysis}, {translation}, {summary}"
|
||||
```
|
||||
|
||||
### JoinNode Behavior
|
||||
|
||||
- Waits for **all** predecessor nodes to complete
|
||||
- Emits intermediate events while still waiting (downstream not triggered until all inputs received)
|
||||
- Only triggers downstream when all inputs are received
|
||||
- Stores partial inputs in workflow state
|
||||
|
||||
**Serialization warning:** JoinNode stores partial inputs in session state while waiting. If predecessors are LLM agents without `output_schema`, the stored values are `types.Content` objects which are **not JSON-serializable**. This causes `TypeError` with SQLite/database session services. Fix: use `output_schema` on LLM agents feeding into a JoinNode.
|
||||
|
||||
## Parallel workers: process lists in parallel
|
||||
|
||||
Apply the same node to each item in a list concurrently by setting the
|
||||
`parallel_worker=True` flag. The framework wraps the node internally — there is
|
||||
no public `ParallelWorker` class to import.
|
||||
|
||||
```python
|
||||
from google.adk.workflow import node, Workflow
|
||||
|
||||
@node(parallel_worker=True)
|
||||
def process_item(node_input: int) -> int:
|
||||
return node_input * 2
|
||||
|
||||
def produce_list(node_input: str) -> list:
|
||||
return [1, 2, 3, 4, 5]
|
||||
|
||||
agent = Workflow(
|
||||
name="parallel_processing",
|
||||
edges=[
|
||||
('START', produce_list),
|
||||
(produce_list, process_item),
|
||||
],
|
||||
)
|
||||
# Output: [2, 4, 6, 8, 10]
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- Input: a **list** (or single item, which gets wrapped in a list)
|
||||
- Output: a **list** of results in the same order as inputs
|
||||
- Empty list input produces empty list output
|
||||
- Each item is processed by a dynamically created worker node
|
||||
- Default `rerun_on_resume=True`
|
||||
|
||||
### Parallel workers with Agents
|
||||
|
||||
Set `parallel_worker=True` directly on an Agent — no extra wrapping needed:
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
|
||||
explain_topic = Agent(
|
||||
name="explain_topic",
|
||||
instruction="Explain how this topic relates to the original topic: \"{topic}\".",
|
||||
output_schema=TopicExplanation,
|
||||
parallel_worker=True, # Each list item processed by a cloned agent
|
||||
)
|
||||
|
||||
agent = Workflow(
|
||||
name="parallel_analysis",
|
||||
edges=[
|
||||
('START', process_input, find_related_topics, explain_topic, aggregate),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**Do NOT use `parallel_worker=True` on fan-out nodes.** Fan-out edges `(a, (b, c, d))` already run nodes in parallel. Adding `parallel_worker=True` makes the node expect a list input and iterate over it — if it receives a single value or None, it produces no output and the JoinNode gets nothing.
|
||||
|
||||
## Multi-Trigger (Fan-Out to Shared Downstream)
|
||||
|
||||
Fan-out branches that all feed a single downstream node. The downstream node is triggered once per branch:
|
||||
|
||||
```python
|
||||
async def send_message(node_input: Any):
|
||||
yield Event(message=f"Triggered for input: {node_input}")
|
||||
|
||||
agent = Workflow(
|
||||
name="root_agent",
|
||||
edges=[(
|
||||
"START",
|
||||
(make_uppercase, count_characters, reverse_string),
|
||||
send_message,
|
||||
)],
|
||||
input_schema=str,
|
||||
)
|
||||
```
|
||||
|
||||
This differs from JoinNode: here `send_message` fires 3 times (once per branch), while JoinNode waits for all branches and fires once with a merged dict.
|
||||
|
||||
## Diamond Pattern
|
||||
|
||||
Fan-out then fan-in (diamond shape):
|
||||
|
||||
```python
|
||||
def splitter(node_input: str) -> str:
|
||||
return node_input
|
||||
|
||||
def branch_a(node_input: str) -> str:
|
||||
return f"A: {node_input}"
|
||||
|
||||
def branch_b(node_input: str) -> str:
|
||||
return f"B: {node_input}"
|
||||
|
||||
join = JoinNode(name="merge")
|
||||
|
||||
def combiner(node_input: dict) -> str:
|
||||
return f"Combined: {node_input['branch_a']} + {node_input['branch_b']}"
|
||||
|
||||
agent = Workflow(
|
||||
name="diamond",
|
||||
edges=[
|
||||
('START', splitter),
|
||||
(splitter, (branch_a, branch_b)),
|
||||
((branch_a, branch_b), join),
|
||||
(join, combiner),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## SequentialAgent and ParallelAgent
|
||||
|
||||
Convenience subclasses for common patterns:
|
||||
|
||||
```python
|
||||
from google.adk.agents.sequential_agent import SequentialAgent
|
||||
from google.adk.agents.parallel_agent import ParallelAgent
|
||||
|
||||
# Sequential: runs sub_agents in order
|
||||
pipeline = SequentialAgent(
|
||||
name="pipeline",
|
||||
sub_agents=[writer_agent, reviewer_agent, editor_agent],
|
||||
)
|
||||
# Equivalent to: START -> writer -> reviewer -> editor
|
||||
|
||||
# Parallel: runs sub_agents concurrently
|
||||
parallel = ParallelAgent(
|
||||
name="concurrent",
|
||||
sub_agents=[analyzer_agent, translator_agent, summarizer_agent],
|
||||
)
|
||||
# Equivalent to: START -> (analyzer, translator, summarizer)
|
||||
```
|
||||
@@ -0,0 +1,232 @@
|
||||
# Routing and Conditional Branching Reference
|
||||
|
||||
Route workflow execution along different paths based on node outputs.
|
||||
|
||||
## 📋 Agent Verification Checklist (Routing)
|
||||
Use this checklist when implementing routing logic:
|
||||
- [ ] **Syntax**: Is the preferred dict syntax used for mapping routes to targets? (Avoid verbose individual edges)
|
||||
- [ ] **Loops**: Are cycles (loops) routed? (Unconditional cycles are rejected during validation)
|
||||
- [ ] **Triggering**: If a node has conditional routing, do ALL outgoing edges have routes? (To avoid unintended triggering by unconditional edges)
|
||||
|
||||
## 💡 Quick Reference
|
||||
- **Dict Routing**: `(source_node, {"route_a": target_a, "route_b": target_b})`
|
||||
- **Sequence**: `("START", step_a, step_b, step_c)`
|
||||
- **Default**: `"__DEFAULT__"` (Fallback route)
|
||||
|
||||
## Basic Routing
|
||||
|
||||
A node emits an `Event` with a `route` value. Use **dict syntax** to map routes to target nodes:
|
||||
|
||||
```python
|
||||
from google.adk import Event, Workflow
|
||||
|
||||
def classify(node_input: str):
|
||||
if "error" in node_input:
|
||||
return Event(output=node_input, route="error")
|
||||
return Event(output=node_input, route="success")
|
||||
|
||||
def handle_success(node_input: str) -> str:
|
||||
return f"Success: {node_input}"
|
||||
|
||||
def handle_error(node_input: str) -> str:
|
||||
return f"Error: {node_input}"
|
||||
|
||||
agent = Workflow(
|
||||
name="router",
|
||||
edges=[
|
||||
('START', classify),
|
||||
(classify, {"success": handle_success, "error": handle_error}),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Routing Map (Dict Syntax) — Preferred
|
||||
|
||||
The dict syntax is the idiomatic way to express routing. It maps route values to target nodes in a single edge tuple:
|
||||
|
||||
```python
|
||||
edges = [
|
||||
("START", process_input, classifier, route_on_category),
|
||||
(route_on_category, {
|
||||
"question": answer_question,
|
||||
"statement": comment_on_statement,
|
||||
"other": handle_other,
|
||||
}),
|
||||
]
|
||||
```
|
||||
|
||||
This replaces verbose individual routed edges:
|
||||
```python
|
||||
# ❌ Verbose — avoid
|
||||
(classifier, answer_question, "question"),
|
||||
(classifier, comment_on_statement, "statement"),
|
||||
(classifier, handle_other, "other"),
|
||||
|
||||
# ✅ Preferred — dict syntax
|
||||
(classifier, {"question": answer_question, "statement": comment_on_statement, "other": handle_other}),
|
||||
```
|
||||
|
||||
## Sequence Shorthand (Tuple Chains)
|
||||
|
||||
A tuple with more than 2 elements creates a sequential chain:
|
||||
|
||||
```python
|
||||
# Shorthand: tuple creates chain edges
|
||||
edges = [("START", step_a, step_b, step_c)]
|
||||
# Equivalent to: [("START", step_a), (step_a, step_b), (step_b, step_c)]
|
||||
```
|
||||
|
||||
Combine with dict routing:
|
||||
```python
|
||||
edges = [
|
||||
("START", process_input, classify, route_on_result),
|
||||
(route_on_result, {"approved": send, "rejected": discard}),
|
||||
]
|
||||
```
|
||||
|
||||
## Route Value Types
|
||||
|
||||
Routes can be `str`, `bool`, or `int`:
|
||||
|
||||
```python
|
||||
# String routes (most common)
|
||||
(decision_node, {"approve": path_a, "reject": path_b})
|
||||
|
||||
# Boolean routes
|
||||
(decision_node, {True: yes_path, False: no_path})
|
||||
|
||||
# Integer routes
|
||||
(decision_node, {0: path_0, 1: path_1})
|
||||
```
|
||||
|
||||
## Default Route
|
||||
|
||||
Use `'__DEFAULT__'` as a fallback when no other route matches:
|
||||
|
||||
```python
|
||||
edges = [
|
||||
("START", classify),
|
||||
(classify, {
|
||||
"success": handler_a,
|
||||
"error": handler_b,
|
||||
"__DEFAULT__": fallback_handler,
|
||||
}),
|
||||
]
|
||||
```
|
||||
|
||||
Only one default route per node is allowed.
|
||||
|
||||
**No duplicate edges:** Two edges from the same source to the same target are rejected, even with different routes. If you need both a named route and `__DEFAULT__` to reach the same destination, use a thin wrapper function for the default path.
|
||||
|
||||
## Dynamic Routing with Functions
|
||||
|
||||
A function node that emits different routes based on runtime data:
|
||||
|
||||
```python
|
||||
from google.adk import Context, Event
|
||||
|
||||
def route_on_score(ctx: Context, node_input: dict):
|
||||
score = node_input.get("score", 0)
|
||||
if score > 0.8:
|
||||
return Event(output=node_input, route="high")
|
||||
elif score > 0.5:
|
||||
return Event(output=node_input, route="medium")
|
||||
else:
|
||||
return Event(output=node_input, route="low")
|
||||
|
||||
agent = Workflow(
|
||||
name="scored_router",
|
||||
edges=[
|
||||
("START", compute_score, route_on_score),
|
||||
(route_on_score, {
|
||||
"high": premium_handler,
|
||||
"medium": standard_handler,
|
||||
"low": basic_handler,
|
||||
}),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Multi-Route (Fan-Out via Route)
|
||||
|
||||
A node can output multiple routes to trigger multiple downstream paths simultaneously:
|
||||
|
||||
```python
|
||||
def fan_out_router(node_input: str):
|
||||
return Event(output=node_input, route=["path_a", "path_b"])
|
||||
|
||||
agent = Workflow(
|
||||
name="multi_route",
|
||||
edges=[
|
||||
("START", fan_out_router),
|
||||
(fan_out_router, {"path_a": branch_a, "path_b": branch_b}),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## List of Routes on a Single Edge
|
||||
|
||||
An edge can match multiple routes by passing a list as the route value. The edge fires if the node output matches **any** route in the list:
|
||||
|
||||
```python
|
||||
edges = [
|
||||
("START", classifier),
|
||||
(classifier, {"route_z": handler_b}),
|
||||
# handler_a fires on either route_x or route_y
|
||||
(classifier, handler_a, ["route_x", "route_y"]),
|
||||
]
|
||||
```
|
||||
|
||||
This is useful when multiple route values should lead to the same downstream node without duplicating edges. Note: list-of-routes on a single edge uses the 3-tuple syntax since dict syntax maps one route to one target.
|
||||
|
||||
## Self-Loop
|
||||
|
||||
A node can route back to itself:
|
||||
|
||||
```python
|
||||
def guess_number(target_number: int):
|
||||
guess = random.randint(0, 10)
|
||||
yield Event(message=f'Guessing {guess}...')
|
||||
if guess == target_number:
|
||||
yield Event(message='Correct!')
|
||||
else:
|
||||
yield Event(route='guessed_wrong')
|
||||
|
||||
agent = Workflow(
|
||||
name='root_agent',
|
||||
edges=[
|
||||
('START', validate_input, guess_number),
|
||||
(guess_number, {'guessed_wrong': guess_number}),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Revision Loop
|
||||
|
||||
A common pattern: route back to an earlier node for revision, or forward for approval:
|
||||
|
||||
```python
|
||||
edges = [
|
||||
("START", process_input, draft_email, human_review),
|
||||
(human_review, {
|
||||
"revise": draft_email,
|
||||
"approved": send,
|
||||
"rejected": discard,
|
||||
}),
|
||||
]
|
||||
```
|
||||
|
||||
**Important**: Cycles must have at least one routed edge (unconditional cycles are rejected during graph validation).
|
||||
|
||||
## Unconditional Edges
|
||||
|
||||
Edges without a route value are unconditional — they always fire:
|
||||
|
||||
```python
|
||||
edges = [
|
||||
('START', node_a), # Unconditional
|
||||
(node_a, node_b), # Unconditional (always fires)
|
||||
]
|
||||
```
|
||||
|
||||
**Important**: Unrouted edges always fire, regardless of whether the output event has a route. If a node has conditional routing, ALL outgoing edges should have routes to avoid unintended triggering.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Session, Memory, and Artifact Patterns
|
||||
|
||||
## 📋 Agent Verification Checklist (Session & State)
|
||||
Use this checklist when managing state and artifacts:
|
||||
- [ ] **State Mutation**: Did you use `ctx.state['key'] = value` instead of reassigning `state = {...}`?
|
||||
- [ ] **Instruction Placeholders**: Did you use `{var?}` for variables that might not be in state yet?
|
||||
- [ ] **Key Collisions**: In parallel workflows, do state keys have unique names or appropriate prefixes (e.g., `app:`) to prevent overwrites?
|
||||
|
||||
## 💡 Quick Reference (State Keys)
|
||||
- **Required**: `{key}` in instructions (raises error if missing).
|
||||
- **Optional**: `{key?}` in instructions (empty string if missing).
|
||||
- **App Scope**: `app:key` (Shared across agents).
|
||||
- **Agent Scope**: `key` (Default, scoped to current agent).
|
||||
|
||||
## Session State
|
||||
|
||||
Session state is a dict that persists across turns within a session.
|
||||
Access via `tool_context.state` or instruction placeholders:
|
||||
|
||||
```python
|
||||
# In instruction (template variable substitution)
|
||||
instruction = 'Current user: {user_name}'
|
||||
|
||||
# In tool
|
||||
def my_tool(tool_context: ToolContext):
|
||||
tool_context.state['user_name'] = 'Alice'
|
||||
|
||||
# In callback
|
||||
def before_agent(callback_context):
|
||||
callback_context.state['_time'] = datetime.now().isoformat()
|
||||
```
|
||||
|
||||
**State key conventions:**
|
||||
- `app:key` -- app-level state (shared across agents)
|
||||
- `key` -- agent-level state (scoped to current agent)
|
||||
- `_key` -- convention for internal/framework state
|
||||
- `{key?}` in instruction -- optional placeholder (empty if missing)
|
||||
- `{key}` in instruction -- required placeholder (error if missing)
|
||||
|
||||
## Session Services
|
||||
|
||||
| Service | Use Case |
|
||||
|---------|----------|
|
||||
| `InMemorySessionService` | Local dev, testing (default) |
|
||||
| `DatabaseSessionService` | Production (SQLite, PostgreSQL) |
|
||||
| `VertexAiSessionService` | Vertex AI Agent Engine |
|
||||
|
||||
```python
|
||||
from google.adk import Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
|
||||
runner = Runner(
|
||||
agent=root_agent,
|
||||
app_name='my_app',
|
||||
session_service=InMemorySessionService(),
|
||||
)
|
||||
```
|
||||
|
||||
## Artifacts
|
||||
|
||||
Artifacts store non-textual data (files, images) associated with sessions:
|
||||
|
||||
```python
|
||||
from google.genai import types
|
||||
|
||||
# Save from tool
|
||||
async def save_chart(tool_context: ToolContext):
|
||||
chart_bytes = generate_chart()
|
||||
part = types.Part.from_bytes(data=chart_bytes, mime_type='image/png')
|
||||
version = await tool_context.save_artifact('chart.png', part)
|
||||
|
||||
# Load from tool
|
||||
async def get_chart(tool_context: ToolContext):
|
||||
part = await tool_context.load_artifact('chart.png')
|
||||
return part.inline_data.data
|
||||
```
|
||||
|
||||
## Memory Services
|
||||
|
||||
Long-term recall across sessions:
|
||||
|
||||
```python
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
|
||||
runner = Runner(
|
||||
agent=root_agent,
|
||||
memory_service=InMemoryMemoryService(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
Use `load_memory` and `preload_memory` tools to access memory from
|
||||
within agents.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **State not persisting:** Assigning to `state` instead of mutating.
|
||||
Use `tool_context.state['key'] = value` (not `state = {'key': value}`).
|
||||
- **State overwritten by parallel tools:** Multiple tools modifying same
|
||||
key concurrently. Use unique keys per tool, or `app:` prefix for shared
|
||||
state.
|
||||
@@ -0,0 +1,187 @@
|
||||
# State and Events Reference
|
||||
|
||||
Manage shared state across workflow nodes and understand the event system.
|
||||
|
||||
## 📋 Agent Verification Checklist (State & Events)
|
||||
Use this checklist when working with state and events:
|
||||
|
||||
- [ ] **State Updates**: Did you use `Event(state=...)` for state updates? (Captures delta in event history)
|
||||
- [ ] **Parameter Resolution**: Are custom parameters named after keys in `ctx.state`?
|
||||
- [ ] **Output Serialization**: Is `event.output` JSON-serializable? (Required for DB session services)
|
||||
- [ ] **Web UI Display**: Did you use `Event(message=...)` for output meant for users?
|
||||
|
||||
## 💡 Quick Reference (Resolution Order)
|
||||
|
||||
1. **`ctx`**: Workflow `Context` object.
|
||||
2. **`node_input`**: Predecessor output.
|
||||
3. **Other names**: Looked up from `ctx.state[param_name]`.
|
||||
|
||||
## Workflow Context
|
||||
|
||||
Every node receives a `Context` object (when declaring a `ctx` parameter):
|
||||
|
||||
```python
|
||||
from google.adk.agents.context import Context
|
||||
|
||||
def my_node(ctx: Context, node_input: str) -> str:
|
||||
# Access shared state
|
||||
value = ctx.state.get("key", "default")
|
||||
|
||||
# Write to state
|
||||
ctx.state["key"] = "new_value"
|
||||
|
||||
# Access session info
|
||||
session_id = ctx.session.id
|
||||
invocation_id = ctx.invocation_id
|
||||
|
||||
# Get node metadata
|
||||
node_path = ctx.node_path # e.g., "MyWorkflow/my_node"
|
||||
run_id = ctx.run_id # this node-run's identifier
|
||||
attempt = ctx.attempt_count # 1 on first attempt, ≥1 thereafter
|
||||
|
||||
return f"Processed: {value}"
|
||||
```
|
||||
|
||||
## Context Properties
|
||||
|
||||
### Common Properties (available everywhere)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `state` | `State` | Delta-aware session state (read/write like a dict) |
|
||||
| `session` | `Session` | Current session (with local events merged in workflows) |
|
||||
| `invocation_id` | `str` | Current invocation ID |
|
||||
| `user_content` | `types.Content` | The user content that started this invocation (read-only) |
|
||||
| `agent_name` | `str` | Name of the agent currently running |
|
||||
| `user_id` | `str` | The user ID (read-only) |
|
||||
| `run_config` | `RunConfig \| None` | Run configuration for this invocation (read-only) |
|
||||
| `actions` | `EventActions` | Event actions for state/artifact deltas |
|
||||
|
||||
### Workflow-Only Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ---------------- | ------------------------------------- |
|
||||
| `node_path` | `str` | Full path of current node (e.g., |
|
||||
: : : "WorkflowA/node1") :
|
||||
| `run_id` | `str` | Identifier for this node-run (e.g., |
|
||||
: : : `"1"`, `"2"`) :
|
||||
| `attempt_count` | `int` | Retry attempt number (1 on first try) |
|
||||
| `resume_inputs` | `dict[str, Any]` | Inputs for resuming (keyed by |
|
||||
: : : interrupt_id) :
|
||||
|
||||
### Workflow-Only Methods
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `run_node(node, node_input, *, name)` | `Any` | Execute a node dynamically (requires `rerun_on_resume=True`) |
|
||||
|
||||
## State Management
|
||||
|
||||
State is shared across all nodes in a workflow invocation. **Prefer `Event(state=...)` over `ctx.state[...] =`** for setting state:
|
||||
|
||||
```python
|
||||
# ✅ Preferred: set state via Event (persisted in event history, replayable)
|
||||
def node_a(node_input: str):
|
||||
return Event(
|
||||
output="done",
|
||||
state={"user_data": {"name": "Alice", "score": 95}},
|
||||
)
|
||||
|
||||
# ❌ Avoid: direct ctx.state mutation (not captured in event history)
|
||||
def node_a(ctx: Context, node_input: str) -> str:
|
||||
ctx.state["user_data"] = {"name": "Alice", "score": 95}
|
||||
return "done"
|
||||
```
|
||||
|
||||
**Why `Event(state=...)` is preferred:**
|
||||
|
||||
- State deltas are persisted in event history as `event.actions.state_delta`
|
||||
- Non-resumable HITL can reconstruct state by replaying events
|
||||
- Makes state changes explicit and traceable
|
||||
- `ctx.state` mutations are side effects that may be lost on replay
|
||||
|
||||
Reading state is always done via `ctx.state`:
|
||||
|
||||
```python
|
||||
def node_b(ctx: Context, node_input: str) -> str:
|
||||
user = ctx.state["user_data"]
|
||||
return f"User {user['name']} scored {user['score']}"
|
||||
```
|
||||
|
||||
The `state` dict is stored as `event.actions.state_delta` and applied to the session.
|
||||
|
||||
## State as Function Parameters
|
||||
|
||||
FunctionNode automatically resolves parameters from state:
|
||||
|
||||
```python
|
||||
# If ctx.state["user_name"] = "Alice" and ctx.state["threshold"] = 0.5
|
||||
def my_node(node_input: str, user_name: str, threshold: float) -> str:
|
||||
# user_name = "Alice" (from state)
|
||||
# threshold = 0.5 (from state)
|
||||
return f"{user_name}: {node_input} (threshold={threshold})"
|
||||
```
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. `ctx` -> Context object
|
||||
2. `node_input` -> predecessor output
|
||||
3. Other names -> `ctx.state[param_name]` (with auto type conversion)
|
||||
4. Default values if not in state
|
||||
|
||||
## Event Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `output` | `Any` | Output data passed to downstream nodes |
|
||||
| `route` | `str\|bool\|int\|list` | Routing signal for conditional edges (convenience kwarg → `actions.route`) |
|
||||
| `state` | `dict` (constructor only) | State delta to apply (convenience kwarg → `actions.state_delta`) |
|
||||
| `message` | `ContentUnion` (constructor only) | User-facing content (convenience kwarg → `content`) |
|
||||
| `content` | `types.Content` | Content for display (set directly or via `message=`) |
|
||||
| `node_path` | `str` | Set by workflow (convenience kwarg → `node_info.path`) |
|
||||
|
||||
## Workflow Data Rules
|
||||
|
||||
- **`Event.output` must be JSON-serializable.** FunctionNode auto-converts Pydantic `BaseModel` returns via `model_dump()`, so returning a model is safe. But `types.Content` and other non-serializable objects will fail with SQLite/database session services.
|
||||
- **`output_key` stores dicts, not BaseModel instances.** LLM agents with `output_schema` use `validate_schema()` → `model_dump()` internally, so `ctx.state[output_key]` is always a plain dict.
|
||||
- **`ctx.state.get(key)` returns a dict.** Use dict access (`data["field"]`) or reconstruct the model (`MyModel(**data)`) if you need typed access.
|
||||
|
||||
```python
|
||||
# Reading output_key from state — it's a dict, not a BaseModel
|
||||
def use_plan(ctx: Context, node_input: Any) -> str:
|
||||
plan = ctx.state.get('task_plan', {}) # dict, not TaskPlan
|
||||
return plan['project_name'] # dict access
|
||||
|
||||
# Or reconstruct if you need typed access:
|
||||
plan_model = TaskPlan(**plan)
|
||||
return plan_model.project_name
|
||||
```
|
||||
|
||||
## Content Events (User-Visible Output)
|
||||
|
||||
In the ADK web UI, only `event.content` is rendered — `event.output` is internal and not displayed. Emit content events for any user-facing output:
|
||||
|
||||
```python
|
||||
# Simple text message
|
||||
yield Event(message="Processing step 1...")
|
||||
|
||||
# Multimodal message (text + image)
|
||||
from google.genai import types
|
||||
yield Event(
|
||||
message=[
|
||||
types.Part.from_text(text="Here is the result:"),
|
||||
types.Part.from_bytes(data=image_bytes, mime_type="image/png"),
|
||||
]
|
||||
)
|
||||
|
||||
# Streaming: multiple messages from same node
|
||||
async def verbose_node(ctx: Context, node_input: str):
|
||||
yield Event(message="Processing step 1...")
|
||||
await asyncio.sleep(1.0)
|
||||
yield Event(message="Processing step 2...")
|
||||
yield Event(output="final result")
|
||||
```
|
||||
|
||||
## Workflow Output
|
||||
|
||||
The Workflow emits its own output Event in `_finalize_workflow` after all nodes complete. Terminal nodes (nodes with no outgoing edges) have their data collected and emitted as the workflow's output. This output event has `author=workflow.name` and `node_path=workflow's own path`.
|
||||
@@ -0,0 +1,275 @@
|
||||
# Task Mode: Structured Delegation
|
||||
|
||||
Delegate structured tasks to sub-agents with typed input/output schemas.
|
||||
|
||||
## 📋 Agent Verification Checklist (Task Mode)
|
||||
Use this checklist to verify your Task Mode configuration:
|
||||
- [ ] **Mode Setting**: Did you explicitly set `mode='task'` or `mode='single_turn'` on the sub-agent?
|
||||
- [ ] **Description**: Does the sub-agent have a clear `description`? (Crucial for the auto-generated tool's description)
|
||||
- [ ] **Schemas**: Are `input_schema` and `output_schema` defined as Pydantic models? (If not, defaults are used)
|
||||
- [ ] **Completion**: Does the sub-agent know it must call `finish_task` to return results to the coordinator?
|
||||
|
||||
## 💡 Quick Reference (Generated Tools)
|
||||
- **`request_task_{agent_name}`**: Generated on the **coordinator** to delegate tasks.
|
||||
- **`finish_task`**: Generated on the **sub-agent** to return results and complete the task.
|
||||
|
||||
## Overview
|
||||
|
||||
ADK agents support three delegation modes via the `mode` parameter on `Agent`:
|
||||
|
||||
| Mode | Tool Generated | User Interaction | Completion |
|
||||
|------|---------------|------------------|------------|
|
||||
| `chat` (default) | `transfer_to_agent` | Full conversational | Agent transfers back |
|
||||
| `task` | `request_task_{name}` | Multi-turn (can chat with user) | Calls `finish_task` |
|
||||
| `single_turn` | `request_task_{name}` | None (autonomous) | Calls `finish_task` |
|
||||
|
||||
## Imports
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
from pydantic import BaseModel
|
||||
```
|
||||
|
||||
**Note**: Task mode uses `Agent` (aliased from `LlmAgent`) from `google.adk`. Both task sub-agents and coordinators use the same `Agent` class — set `mode='task'` or `mode='single_turn'` on sub-agents.
|
||||
|
||||
## Task Mode (`mode='task'`)
|
||||
|
||||
A task agent receives structured input via `request_task_{name}`, can interact with the user for clarification, and returns structured output via `finish_task`.
|
||||
|
||||
### Delegation Lifecycle
|
||||
|
||||
1. User asks the coordinator to do something
|
||||
2. Coordinator calls `request_task_{agent_name}(...)` with structured input
|
||||
3. Task agent receives the input, works on it (may use tools, may chat with user)
|
||||
4. Task agent calls `finish_task(...)` with structured output
|
||||
5. Coordinator receives the result and responds to the user
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ResearchInput(BaseModel):
|
||||
topic: str
|
||||
depth: str = 'standard'
|
||||
|
||||
class ResearchOutput(BaseModel):
|
||||
summary: str
|
||||
key_findings: str
|
||||
confidence: str
|
||||
|
||||
def search_web(query: str) -> str:
|
||||
"""Search the web for information."""
|
||||
return f'Results for "{query}": ...'
|
||||
|
||||
def analyze_sources(sources: str) -> str:
|
||||
"""Analyze and synthesize source material."""
|
||||
return f'Analysis of {len(sources.split())} words complete.'
|
||||
|
||||
researcher = Agent(
|
||||
name='researcher',
|
||||
mode='task',
|
||||
input_schema=ResearchInput,
|
||||
output_schema=ResearchOutput,
|
||||
instruction=(
|
||||
'You are a research assistant. When given a topic:\n'
|
||||
'1. Use search_web to find information.\n'
|
||||
'2. Use analyze_sources to synthesize findings.\n'
|
||||
'3. If the user asks for changes, adjust your research.\n'
|
||||
'4. Call finish_task with summary, key_findings, and confidence.'
|
||||
),
|
||||
description='Researches topics using web search and analysis.',
|
||||
tools=[search_web, analyze_sources],
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name='coordinator',
|
||||
model='gemini-2.5-flash',
|
||||
sub_agents=[researcher],
|
||||
instruction=(
|
||||
'When the user asks you to research something, delegate to'
|
||||
' the researcher using request_task_researcher. After the'
|
||||
' researcher completes, summarize the results for the user.'
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Single-Turn Mode (`mode='single_turn'`)
|
||||
|
||||
A single-turn agent completes autonomously with no user interaction. It receives input, does its work, and returns a result.
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
class SummaryOutput(BaseModel):
|
||||
summary: str
|
||||
word_count: int
|
||||
key_points: str
|
||||
|
||||
def extract_text(url: str) -> str:
|
||||
"""Extract text from a URL."""
|
||||
return f'Extracted content from {url}: ...'
|
||||
|
||||
summarizer = Agent(
|
||||
name='summarizer',
|
||||
mode='single_turn',
|
||||
output_schema=SummaryOutput,
|
||||
instruction=(
|
||||
'Summarize the document:\n'
|
||||
'1. Use extract_text to get content.\n'
|
||||
'2. Call finish_task with summary, word_count, key_points.\n'
|
||||
'Complete autonomously without user interaction.'
|
||||
),
|
||||
description='Summarizes documents autonomously.',
|
||||
tools=[extract_text],
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name='coordinator',
|
||||
model='gemini-2.5-flash',
|
||||
sub_agents=[summarizer],
|
||||
instruction='Delegate summarization to summarizer via request_task_summarizer.',
|
||||
)
|
||||
```
|
||||
|
||||
## Input and Output Schemas
|
||||
|
||||
### Custom Schemas (Pydantic Models)
|
||||
|
||||
Define `input_schema` and/or `output_schema` with Pydantic `BaseModel`:
|
||||
|
||||
```python
|
||||
class TaskInput(BaseModel):
|
||||
query: str
|
||||
max_results: int = 10
|
||||
format: str = 'text'
|
||||
|
||||
class TaskOutput(BaseModel):
|
||||
results: str
|
||||
count: int
|
||||
status: str
|
||||
|
||||
agent = Agent(
|
||||
name='worker',
|
||||
mode='task',
|
||||
input_schema=TaskInput, # Validates request_task_worker args
|
||||
output_schema=TaskOutput, # Validates finish_task args
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Default Schemas
|
||||
|
||||
When no custom schema is provided:
|
||||
|
||||
**Default input** (used by `request_task_{name}`):
|
||||
```python
|
||||
class _DefaultTaskInput(BaseModel):
|
||||
goal: str | None = None
|
||||
background: str | None = None
|
||||
```
|
||||
|
||||
**Default output** (used by `finish_task`):
|
||||
```python
|
||||
class _DefaultTaskOutput(BaseModel):
|
||||
result: str
|
||||
```
|
||||
|
||||
## Auto-Generated Tools
|
||||
|
||||
### `request_task_{agent_name}`
|
||||
|
||||
Auto-generated on the **coordinator** for each `mode='task'` or `mode='single_turn'` sub-agent. The tool name is `request_task_{agent.name}`.
|
||||
|
||||
- Parameters come from `input_schema` (or default: `goal`, `background`)
|
||||
- Description includes the agent's `description` field
|
||||
- Validates input against the schema before delegating
|
||||
|
||||
### `finish_task`
|
||||
|
||||
Auto-generated on the **task agent** itself. Called by the task agent when work is complete.
|
||||
|
||||
- Parameters come from `output_schema` (or default: `result`)
|
||||
- Validates output against the schema before signaling completion
|
||||
- Sets `tool_context.actions.finish_task` with a `TaskResult`
|
||||
|
||||
## Mixed-Mode Patterns
|
||||
|
||||
Combine task and single-turn agents under one coordinator:
|
||||
|
||||
```python
|
||||
# Interactive: user can discuss options
|
||||
flight_searcher = Agent(
|
||||
name='flight_searcher',
|
||||
mode='task',
|
||||
input_schema=FlightSearchInput,
|
||||
output_schema=FlightSearchOutput,
|
||||
instruction='Search flights, discuss with user, then finish_task.',
|
||||
description='Searches and books flights interactively.',
|
||||
tools=[search_flights, book_flight],
|
||||
)
|
||||
|
||||
# Autonomous: no user interaction
|
||||
weather_checker = Agent(
|
||||
name='weather_checker',
|
||||
mode='single_turn',
|
||||
output_schema=WeatherOutput,
|
||||
instruction='Check weather and call finish_task. No user interaction.',
|
||||
description='Checks weather for a destination.',
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
# Autonomous: no user interaction
|
||||
hotel_finder = Agent(
|
||||
name='hotel_finder',
|
||||
mode='single_turn',
|
||||
output_schema=HotelOutput,
|
||||
instruction='Find hotels and call finish_task. No user interaction.',
|
||||
description='Finds hotels for a destination.',
|
||||
tools=[find_hotels],
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name='travel_planner',
|
||||
model='gemini-2.5-flash',
|
||||
sub_agents=[flight_searcher, weather_checker, hotel_finder],
|
||||
instruction=(
|
||||
'Help users plan trips:\n'
|
||||
'- request_task_weather_checker: autonomous weather check\n'
|
||||
'- request_task_hotel_finder: autonomous hotel search\n'
|
||||
'- request_task_flight_searcher: interactive flight booking'
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Both task sub-agents and coordinators use `Agent` from `google.adk`
|
||||
- Each sub-agent needs a `description` (used in the auto-generated tool description)
|
||||
- `input_schema` and `output_schema` are optional; defaults are provided
|
||||
- Sub-agents inherit model from the coordinator if not set
|
||||
- `finish_task` instructions are auto-injected into the task agent's LLM context
|
||||
- Single-turn agents receive an extra instruction telling them no user replies will come
|
||||
|
||||
## Task Mode vs Chat Mode
|
||||
|
||||
| Feature | Chat (`transfer_to_agent`) | Task (`request_task`) |
|
||||
|---------|---------------------------|----------------------|
|
||||
| Input | Free-form conversation | Structured (schema-validated) |
|
||||
| Output | Free-form conversation | Structured (schema-validated) |
|
||||
| Control flow | Agent decides when to transfer back | Agent calls `finish_task` |
|
||||
| User interaction | Full chat | `task`: multi-turn; `single_turn`: none |
|
||||
| Tool name | `transfer_to_agent` | `request_task_{name}` |
|
||||
| Parallel delegation | Not supported | Supported (multiple `request_task` calls) |
|
||||
|
||||
## Source File Locations
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Agent/LlmAgent (mode, schemas) | `src/google/adk/agents/llm_agent.py` |
|
||||
| BaseLlmFlow (base flow class) | `src/google/adk/flows/llm_flows/base_llm_flow.py` |
|
||||
| RequestTaskTool | `src/google/adk/agents/llm/task/_request_task_tool.py` |
|
||||
| FinishTaskTool | `src/google/adk/agents/llm/task/_finish_task_tool.py` |
|
||||
| TaskRequest, TaskResult | `src/google/adk/agents/llm/task/_task_models.py` |
|
||||
| Task samples | `contributing/task_samples/` |
|
||||
@@ -0,0 +1,315 @@
|
||||
# Testing Workflow Agents Reference
|
||||
|
||||
Write unit tests for workflow agents using `pytest` with async support and the
|
||||
public `InMemoryRunner` from `google.adk.runners`.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install ADK + pytest + pytest-asyncio
|
||||
pip install "google-adk>=2.0" pytest pytest-asyncio
|
||||
|
||||
# Or with uv
|
||||
uv add "google-adk>=2.0" pytest pytest-asyncio
|
||||
```
|
||||
|
||||
`pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
```
|
||||
|
||||
`asyncio_mode = "auto"` removes the need to mark every test with
|
||||
`@pytest.mark.asyncio`; if you'd rather mark each test explicitly, omit it.
|
||||
|
||||
## Imports
|
||||
|
||||
All imports below are from the published `google-adk` package — no test-internal
|
||||
helpers required.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from google.genai import types
|
||||
from google.adk import Workflow
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.apps import App
|
||||
from google.adk.apps.app import ResumabilityConfig
|
||||
from google.adk.events import Event, RequestInput
|
||||
from google.adk.runners import InMemoryRunner
|
||||
```
|
||||
|
||||
## A small `run` helper
|
||||
|
||||
Tests are tidier with a helper that drives one turn and collects events:
|
||||
|
||||
```python
|
||||
async def run(agent, text="hi", app_name="test_app"):
|
||||
runner = InMemoryRunner(agent=agent, app_name=app_name)
|
||||
session = await runner.session_service.create_session(
|
||||
app_name=app_name, user_id="u1"
|
||||
)
|
||||
msg = types.Content(role="user", parts=[types.Part(text=text)])
|
||||
events = []
|
||||
async for event in runner.run_async(
|
||||
user_id="u1", session_id=session.id, new_message=msg,
|
||||
):
|
||||
events.append(event)
|
||||
return runner, session, events
|
||||
|
||||
|
||||
def node_name(event):
|
||||
"""Extract the node name from event.node_info.path.
|
||||
|
||||
e.g. 'workflow@1/step@1' -> 'step'.
|
||||
"""
|
||||
if not event.node_info:
|
||||
return None
|
||||
return event.node_info.path.split("/")[-1].split("@")[0]
|
||||
```
|
||||
|
||||
In ADK 2.x, `event.author` is the enclosing workflow's name; the per-node
|
||||
identifier lives in `event.node_info.path`. Use `node_name(event)` to filter by
|
||||
the node that emitted an event.
|
||||
|
||||
## Basic Workflow Test
|
||||
|
||||
```python
|
||||
async def test_simple_workflow():
|
||||
def step_one(node_input: str) -> str:
|
||||
return "step 1 done"
|
||||
|
||||
def step_two(node_input: str) -> str:
|
||||
return "step 2 done"
|
||||
|
||||
agent = Workflow(
|
||||
name="test_workflow",
|
||||
edges=[
|
||||
("START", step_one),
|
||||
(step_one, step_two),
|
||||
],
|
||||
)
|
||||
|
||||
_, _, events = await run(agent)
|
||||
final = [e for e in events if node_name(e) == "step_two" and e.output][-1]
|
||||
assert final.output == "step 2 done"
|
||||
```
|
||||
|
||||
## Testing Conditional Routing
|
||||
|
||||
```python
|
||||
async def test_routing():
|
||||
def router(node_input: str):
|
||||
if "error" in node_input:
|
||||
return Event(output=node_input, route="error")
|
||||
return Event(output=node_input, route="success")
|
||||
|
||||
def success_handler(node_input: str) -> str:
|
||||
return f"OK: {node_input}"
|
||||
|
||||
def error_handler(node_input: str) -> str:
|
||||
return f"ERR: {node_input}"
|
||||
|
||||
agent = Workflow(
|
||||
name="routing_test",
|
||||
edges=[
|
||||
("START", router),
|
||||
(router, {"success": success_handler, "error": error_handler}),
|
||||
],
|
||||
)
|
||||
|
||||
_, _, evs_ok = await run(agent, text="all good")
|
||||
assert any(node_name(e) == "success_handler" for e in evs_ok)
|
||||
|
||||
_, _, evs_err = await run(agent, text="error case")
|
||||
assert any(node_name(e) == "error_handler" for e in evs_err)
|
||||
```
|
||||
|
||||
## Testing HITL (Pause and Resume)
|
||||
|
||||
```python
|
||||
async def test_hitl_workflow():
|
||||
async def ask_user(ctx, node_input: str):
|
||||
yield RequestInput(message="Approve?", interrupt_id="ask")
|
||||
|
||||
def after_approval(node_input) -> str:
|
||||
return f"Approved: {node_input}"
|
||||
|
||||
agent = Workflow(
|
||||
name="hitl_test",
|
||||
edges=[
|
||||
("START", ask_user),
|
||||
(ask_user, after_approval),
|
||||
],
|
||||
)
|
||||
|
||||
app = App(
|
||||
name="hitl_test_app",
|
||||
root_agent=agent,
|
||||
resumability_config=ResumabilityConfig(is_resumable=True),
|
||||
)
|
||||
runner = InMemoryRunner(app=app)
|
||||
session = await runner.session_service.create_session(
|
||||
app_name="hitl_test_app", user_id="u1"
|
||||
)
|
||||
|
||||
# First turn: should pause with a RequestInput function call
|
||||
msg = types.Content(role="user", parts=[types.Part(text="start")])
|
||||
pause_events = []
|
||||
async for event in runner.run_async(
|
||||
user_id="u1", session_id=session.id, new_message=msg,
|
||||
):
|
||||
pause_events.append(event)
|
||||
|
||||
fc_events = [e for e in pause_events if e.get_function_calls()]
|
||||
assert fc_events, "expected an interrupt function call"
|
||||
fc = fc_events[-1].get_function_calls()[0]
|
||||
|
||||
# Resume by responding to the function call
|
||||
response = types.Content(
|
||||
role="user",
|
||||
parts=[types.Part(function_response=types.FunctionResponse(
|
||||
id=fc.id, name=fc.name, response={"result": "yes"},
|
||||
))],
|
||||
)
|
||||
resumed = []
|
||||
async for event in runner.run_async(
|
||||
user_id="u1", session_id=session.id, new_message=response,
|
||||
):
|
||||
resumed.append(event)
|
||||
|
||||
final = [e for e in resumed if node_name(e) == "after_approval"][-1]
|
||||
assert final.output == "Approved: yes"
|
||||
```
|
||||
|
||||
## Testing State Updates
|
||||
|
||||
Prefer asserting on the post-run session's state rather than reading state
|
||||
mid-flight:
|
||||
|
||||
```python
|
||||
async def test_state_management():
|
||||
def writer(node_input: str):
|
||||
return Event(output=node_input, state={"counter": 1})
|
||||
|
||||
def reader(ctx, node_input):
|
||||
return f"counter={ctx.state['counter']}"
|
||||
|
||||
agent = Workflow(
|
||||
name="state_test",
|
||||
edges=[("START", writer, reader)],
|
||||
)
|
||||
|
||||
runner, session, events = await run(agent)
|
||||
final = [e for e in events if node_name(e) == "reader" and e.output][-1]
|
||||
assert final.output == "counter=1"
|
||||
|
||||
# Or read state directly off the session after the run
|
||||
final_session = await runner.session_service.get_session(
|
||||
app_name="test_app", user_id="u1", session_id=session.id
|
||||
)
|
||||
assert final_session.state["counter"] == 1
|
||||
```
|
||||
|
||||
## Testing Parallel Execution
|
||||
|
||||
```python
|
||||
from google.adk.workflow import node
|
||||
|
||||
async def test_parallel_worker():
|
||||
def produce(node_input: str) -> list:
|
||||
return [1, 2, 3]
|
||||
|
||||
@node(parallel_worker=True)
|
||||
def double(node_input: int) -> int:
|
||||
return node_input * 2
|
||||
|
||||
def collect(node_input: list) -> str:
|
||||
return f"results: {node_input}"
|
||||
|
||||
agent = Workflow(
|
||||
name="parallel_test",
|
||||
edges=[("START", produce, double, collect)],
|
||||
)
|
||||
|
||||
_, _, events = await run(agent)
|
||||
final = [e for e in events if node_name(e) == "collect" and e.output][-1]
|
||||
assert final.output == "results: [2, 4, 6]"
|
||||
```
|
||||
|
||||
## Mocking LLM Agents
|
||||
|
||||
For unit tests that don't hit the real API, pass a fake `BaseLlm` to the
|
||||
`LlmAgent` constructor. The framework only requires the abstract
|
||||
`generate_content_async` method.
|
||||
|
||||
```python
|
||||
from google.adk.models.base_llm import BaseLlm
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
from google.genai import types
|
||||
|
||||
|
||||
class FakeLlm(BaseLlm):
|
||||
def __init__(self, *, responses: list[str]):
|
||||
super().__init__(model="fake")
|
||||
self._responses = list(responses)
|
||||
|
||||
async def generate_content_async(self, llm_request, stream=False):
|
||||
text = self._responses.pop(0)
|
||||
yield LlmResponse(content=types.Content(
|
||||
role="model", parts=[types.Part(text=text)],
|
||||
))
|
||||
|
||||
|
||||
async def test_llm_agent_with_fake():
|
||||
agent = LlmAgent(
|
||||
name="x",
|
||||
model=FakeLlm(responses=["ok"]),
|
||||
instruction="Help.",
|
||||
)
|
||||
_, _, events = await run(agent, text="hi")
|
||||
final = events[-1]
|
||||
assert final.content and final.content.parts[0].text == "ok"
|
||||
```
|
||||
|
||||
If you only need to assert call shapes, `monkeypatch` the agent's
|
||||
`canonical_model.generate_content_async` with a mock instead.
|
||||
|
||||
## Integration tests with a real model
|
||||
|
||||
Tag tests that hit a real model and skip them by default:
|
||||
|
||||
```python
|
||||
import os
|
||||
import pytest
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def adk_env():
|
||||
if "GOOGLE_API_KEY" not in os.environ:
|
||||
pytest.skip("GOOGLE_API_KEY not set; skipping integration tests")
|
||||
os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "FALSE")
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_real_model():
|
||||
...
|
||||
```
|
||||
|
||||
Then `pytest -m integration` to run them, or `pytest -m "not integration"` to
|
||||
skip.
|
||||
|
||||
## Testing Tips
|
||||
|
||||
- Create a fresh `InMemoryRunner` and session per test — runners hold state
|
||||
and reuse causes cross-test interference.
|
||||
- Use a unique `app_name` per test (e.g. `request.node.name`) to avoid
|
||||
collisions across parallel pytest workers.
|
||||
- Assert on `event.node_info.path`, not `event.author`. `event.author` is the
|
||||
enclosing workflow's name; `event.node_info.path` identifies the exact node
|
||||
that emitted the event.
|
||||
- Use `event.is_final_response()` to filter for "the agent's final message"
|
||||
events.
|
||||
- For workflows with a `JoinNode`, make sure every LLM agent feeding into it
|
||||
has `output_schema=` set — otherwise the join buffer fails JSON
|
||||
serialization in tests that use `DatabaseSessionService`.
|
||||
- Run with `pytest -xvs` while iterating (`-x` stop on first failure, `-v`
|
||||
verbose, `-s` show prints) to debug event flow.
|
||||
@@ -0,0 +1,177 @@
|
||||
# ADK Tool Catalog
|
||||
|
||||
## 📋 Agent Verification Checklist (Tools)
|
||||
Use this checklist when creating or binding tools:
|
||||
- [ ] **Python Functions**: Do they have both **type hints** and a **docstring**? (Required for schema generation)
|
||||
- [ ] **Context Injection**: Is the special parameter named `tool_context` or `ctx` used for accessing state?
|
||||
- [ ] **MCP Tools**: Did you verify that `pip install mcp` is run if using MCP tools?
|
||||
- [ ] **Class Names**: Are you using `McpToolset` (the non-deprecated name)?
|
||||
|
||||
## 💡 Quick Reference (Built-in Tools)
|
||||
- **Google Search**: `from google.adk.tools import google_search`
|
||||
- **Load Artifacts**: `from google.adk.tools import load_artifacts`
|
||||
- **Agent Transfer**: `from google.adk.tools import transfer_to_agent`
|
||||
|
||||
## Python Function Tools (Most Common)
|
||||
|
||||
Any Python function with type annotations and a docstring becomes a tool:
|
||||
|
||||
```python
|
||||
def get_weather(city: str, unit: str = 'celsius') -> str:
|
||||
"""Get the current weather for a city.
|
||||
|
||||
Args:
|
||||
city: The city name to look up.
|
||||
unit: Temperature unit, 'celsius' or 'fahrenheit'.
|
||||
|
||||
Returns:
|
||||
A string with the weather information.
|
||||
"""
|
||||
return f"Sunny, 22 degrees {unit} in {city}"
|
||||
|
||||
root_agent = Agent(tools=[get_weather], ...)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Type hints required (they generate the JSON schema)
|
||||
- Docstring required (becomes the tool description)
|
||||
- Both sync and async functions supported
|
||||
- Special parameter `tool_context: ToolContext` is auto-injected (not in schema)
|
||||
|
||||
## ToolContext
|
||||
|
||||
`ToolContext` is a backward-compatible alias for `Context`. Both work identically.
|
||||
|
||||
```python
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
async def my_tool(query: str, tool_context: ToolContext) -> str:
|
||||
tool_context.state['key'] = 'value' # Session state
|
||||
await tool_context.save_artifact('f.txt', part) # Save artifact
|
||||
part = await tool_context.load_artifact('f.txt') # Load artifact
|
||||
results = await tool_context.search_memory('q') # Search memory
|
||||
return 'done'
|
||||
```
|
||||
|
||||
## MCP Tools (Model Context Protocol)
|
||||
|
||||
```python
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
from google.adk.tools.mcp_tool import StdioConnectionParams
|
||||
from mcp import StdioServerParameters
|
||||
|
||||
root_agent = Agent(
|
||||
tools=[
|
||||
McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command='npx',
|
||||
args=['-y', '@modelcontextprotocol/server-filesystem', '/path'],
|
||||
),
|
||||
timeout=5,
|
||||
),
|
||||
tool_filter=['read_file', 'list_directory'],
|
||||
)
|
||||
], ...
|
||||
)
|
||||
```
|
||||
|
||||
Connection types: `StdioConnectionParams`, `SseConnectionParams`,
|
||||
`StreamableHTTPConnectionParams`.
|
||||
|
||||
**Pitfalls:** Requires `pip install mcp`. Use `McpToolset` (not deprecated
|
||||
`MCPToolset`). `StdioServerParameters` is from the `mcp` package, not ADK.
|
||||
|
||||
## OpenAPI Tools
|
||||
|
||||
```python
|
||||
from google.adk.tools.openapi_tool import OpenAPIToolset
|
||||
|
||||
toolset = OpenAPIToolset(spec_str=open('openapi.yaml').read(), spec_str_type='yaml')
|
||||
root_agent = Agent(tools=[toolset], ...)
|
||||
```
|
||||
|
||||
Also: `from google.adk.tools.openapi_tool import RestApiTool` for individual endpoints.
|
||||
|
||||
## Google API Tools
|
||||
|
||||
```python
|
||||
from google.adk.tools.google_api_tool.google_api_toolsets import BigQueryToolset
|
||||
|
||||
bigquery = BigQueryToolset(client_id='...', client_secret='...',
|
||||
tool_filter=['bigquery_datasets_list'])
|
||||
root_agent = Agent(tools=[bigquery], ...)
|
||||
```
|
||||
|
||||
## Built-in Tools
|
||||
|
||||
| Tool | Import |
|
||||
|------|--------|
|
||||
| `google_search` | `from google.adk.tools import google_search` |
|
||||
| `load_artifacts` | `from google.adk.tools import load_artifacts` |
|
||||
| `load_memory` | `from google.adk.tools import load_memory` |
|
||||
| `exit_loop` | `from google.adk.tools import exit_loop` |
|
||||
| `transfer_to_agent` | `from google.adk.tools import transfer_to_agent` |
|
||||
| `get_user_choice` | `from google.adk.tools import get_user_choice` |
|
||||
| `url_context` | `from google.adk.tools import url_context` |
|
||||
|
||||
## LongRunningFunctionTool
|
||||
|
||||
```python
|
||||
from google.adk.tools.long_running_tool import LongRunningFunctionTool
|
||||
|
||||
def approve_expense(amount: float) -> dict:
|
||||
"""Submit expense for approval."""
|
||||
return {"status": "pending", "id": "exp-123"}
|
||||
|
||||
root_agent = Agent(tools=[LongRunningFunctionTool(approve_expense)], ...)
|
||||
```
|
||||
|
||||
## Code Execution
|
||||
|
||||
```python
|
||||
from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor
|
||||
|
||||
root_agent = Agent(code_executor=BuiltInCodeExecutor(), ...)
|
||||
```
|
||||
|
||||
Note: `code_executor` is a separate parameter from `tools`.
|
||||
|
||||
## Custom BaseTool
|
||||
|
||||
```python
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
from google.genai import types
|
||||
|
||||
class MyTool(BaseTool):
|
||||
def __init__(self):
|
||||
super().__init__(name='my_tool', description='Does something.')
|
||||
|
||||
def _get_declaration(self):
|
||||
return types.FunctionDeclaration(
|
||||
name=self.name, description=self.description,
|
||||
parameters_json_schema={
|
||||
'type': 'object',
|
||||
'properties': {'param': {'type': 'string'}},
|
||||
'required': ['param'],
|
||||
},
|
||||
)
|
||||
|
||||
async def run_async(self, *, args, tool_context):
|
||||
return {'result': args['param']}
|
||||
```
|
||||
|
||||
## BaseToolset (Tool Collections)
|
||||
|
||||
```python
|
||||
from google.adk.tools.base_toolset import BaseToolset
|
||||
|
||||
class MyToolset(BaseToolset):
|
||||
async def get_tools(self, readonly_context=None):
|
||||
return [ToolA(), ToolB()]
|
||||
|
||||
async def process_llm_request(self, *, tool_context, llm_request):
|
||||
llm_request.append_instructions(['Custom instruction'])
|
||||
```
|
||||
|
||||
Toolsets support `tool_filter`, `tool_name_prefix`, and `process_llm_request`.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: adk-architecture
|
||||
description: ADK architectural knowledge — graph orchestration, resumption, execution flow, node contracts, observability, and LLM context orchestration. Use this skill whenever you need to understand the architecture, event flow, or state management of the ADK system, or when designing or modifying core components. Triggers on "how does X work", "design of", "architecture of", "event flow", "resumption state", "checkpoint", "BaseNode", "NodeRunner".
|
||||
---
|
||||
|
||||
# ADK Architecture Guide
|
||||
|
||||
## Core Interfaces (references/interfaces/)
|
||||
- [BaseNode](references/interfaces/base-node.md) — node contract, output/streaming, state/routing, HITL, configuration
|
||||
- [Workflow](references/interfaces/workflow.md) — graph orchestration, dynamic nodes (tracking/dedup/resume), transitive dynamic nodes, interrupt propagation, design rules for node authors
|
||||
- [Runner](references/interfaces/runner.md) — The public interface for executing workflows and agents. Documents entrance methods `run` and `run_async`.
|
||||
- [Agent](references/interfaces/agent.md) — Blueprint defining identity, instructions, and tools. Documents that `run` is the preferred entrance method.
|
||||
- [BaseAgent](references/interfaces/base-agent.md) — Base class for all agents. Defines the contract for subclassing with `_run_impl` as the primary override point.
|
||||
- [Event](references/interfaces/event.md) — Core data structure for state reconstruction and communication. Represents a conversation turn, action, and state lifecycle immutability.
|
||||
|
||||
## Key Principles (references/principles/)
|
||||
- [API Principles](references/principles/api-principles.md) — stability, backward compatibility, and self-containment. Use when making design choices that affect the public API surface.
|
||||
|
||||
## Runtime Knowledge (references/architecture/)
|
||||
- [Context](references/architecture/context.md) — 1:1 node-context mapping, InvocationContext singleton, property reference
|
||||
- [NodeRunner](references/architecture/node-runner.md) — two communication channels, execution flow, output delegation. Internal runtime details.
|
||||
- [Runner Roles](references/architecture/runner-roles.md) — Runner vs NodeRunner vs Workflow separation. Explains why they are separate to avoid deadlocks.
|
||||
- [Checkpoint and Resume](references/architecture/checkpoint-resume.md) — HITL lifecycle, `rerun_on_resume`, `run_id`
|
||||
- [Observability](references/architecture/observability.md) — span-on-Context design, NodeRunner integration, correlated logs, metrics
|
||||
- [LLM Context Orchestration](references/architecture/llm-context-orchestration.md) — relationship between events and LLM context, task delegation translation, branch isolation. Use when modifying event processing, context preparation for LLMs, or debugging context pollution issues.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Checkpoint and Resume Lifecycle
|
||||
|
||||
HITL (Human-in-the-Loop) follows this pattern:
|
||||
|
||||
1. **Interrupt**: Node yields an event with `long_running_tool_ids`.
|
||||
Each ancestor propagates the interrupt upward via `ctx.interrupt_ids`.
|
||||
2. **Persist**: Only the leaf node's interrupt event is persisted to
|
||||
session. Workflow sets `ctx._interrupt_ids` directly (no internal
|
||||
event needed).
|
||||
3. **Resume**: User sends a `FunctionResponse` message. The Runner
|
||||
scans session events to find the matching `invocation_id`, then
|
||||
reconstructs node state from persisted events.
|
||||
4. **Continue**: The interrupted node receives the FR and continues
|
||||
execution. Downstream nodes receive the resumed node's output.
|
||||
|
||||
## run_id on resume
|
||||
|
||||
Resumed nodes reuse the same `run_id` from the original
|
||||
execution. From the node's perspective, the execution never paused
|
||||
— events before and after the resume share the same run_id.
|
||||
|
||||
Fresh dispatches (first run, loop re-trigger) get a new run_id.
|
||||
|
||||
## Resume behavior by `rerun_on_resume`
|
||||
|
||||
A node with multiple interrupt IDs may receive partial FRs (only
|
||||
some resolved). The behavior depends on `rerun_on_resume`:
|
||||
|
||||
**`rerun_on_resume=True`** (Workflow, orchestration nodes):
|
||||
|
||||
| FRs received | Status | Behavior |
|
||||
|---|---|---|
|
||||
| Partial | PENDING | Re-execute immediately with partial `resume_inputs`. Node handles remaining interrupts internally (e.g., Workflow dispatches resolved children, keeps unresolved as WAITING). |
|
||||
| All | PENDING | Re-execute with all `resume_inputs`. |
|
||||
|
||||
This is critical for Workflow — when one child's FR arrives, it
|
||||
re-runs immediately to dispatch that resolved child. It doesn't
|
||||
wait for all children's FRs.
|
||||
|
||||
**`rerun_on_resume=False`** (leaf nodes, simple HITL):
|
||||
|
||||
| FRs received | Status | Behavior |
|
||||
|---|---|---|
|
||||
| Partial | WAITING | Stay waiting. Need all FRs. |
|
||||
| All | COMPLETED | Auto-complete. Output = aggregated `resolved_responses`. No re-execution. |
|
||||
|
||||
## Resume with prior output and interrupts
|
||||
|
||||
A node can produce output AND interrupt in the same execution (e.g.,
|
||||
a Workflow where child A completes with output and child B interrupts).
|
||||
On resume:
|
||||
|
||||
- Some interrupt IDs are resolved (provided in `resume_inputs`)
|
||||
- Remaining interrupt IDs carry forward via `prior_interrupt_ids`
|
||||
- Prior output carries forward via `prior_output`
|
||||
- NodeRunner pre-populates ctx with these values before re-executing
|
||||
|
||||
```python
|
||||
runner = NodeRunner(
|
||||
node=node, parent_ctx=ctx,
|
||||
run_id=prior_run_id, # reuse
|
||||
prior_output=cached_output,
|
||||
prior_interrupt_ids={'fc-2'}, # still unresolved
|
||||
)
|
||||
child_ctx = await runner.run(
|
||||
node_input=input,
|
||||
resume_inputs={'fc-1': response},
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
# Context
|
||||
|
||||
## Architecture
|
||||
|
||||
The runtime uses two scoping objects:
|
||||
|
||||
- **InvocationContext** — singleton per invocation. Holds shared
|
||||
state (session, services, event queue) accessible by all nodes.
|
||||
Pydantic model at `agents/invocation_context.py`.
|
||||
- **Context** — one per node execution. Holds per-node results
|
||||
(output, route, interrupt_ids) and provides the API surface for
|
||||
node code. At `agents/context.py`.
|
||||
|
||||
Every Context holds a reference to the same InvocationContext
|
||||
(`_invocation_context`). Service access (artifacts, memory, auth)
|
||||
is delegated through it.
|
||||
|
||||
```
|
||||
Root Context ← created by Runner from IC
|
||||
└── Context [runner.node] ← the root node (e.g., Workflow)
|
||||
├── Context [child_a] ← child node A
|
||||
└── Context [child_b] ← child node B
|
||||
└── Context [grandchild] ← nested child
|
||||
```
|
||||
|
||||
The Runner creates `root_ctx = Context(ic)` as the tree root and
|
||||
passes it as `parent_ctx` to `NodeRunner(node=self.node)`. The
|
||||
root Context has no node_path or run_id — it exists solely
|
||||
as the parent for the Runner's root node. All Contexts in the tree
|
||||
share the same InvocationContext singleton.
|
||||
|
||||
InvocationContext contents:
|
||||
|
||||
- `session`, `agent`, `user_content`
|
||||
- `invocation_id`, `app_name`, `user_id`
|
||||
- Services: `artifact_service`, `memory_service`, `credential_service`
|
||||
- `run_config`, `live_request_queue`
|
||||
- `process_queue` — shared event queue consumed by the main loop
|
||||
|
||||
## 1:1 node-context mapping
|
||||
|
||||
Every node execution gets its own Context instance. The relationship
|
||||
is strictly 1:1: one node, one Context. The Context tree mirrors the
|
||||
node execution tree.
|
||||
|
||||
**NodeRunner** creates the child Context from the parent's Context
|
||||
via `_create_child_context()`. The child inherits:
|
||||
|
||||
- `_invocation_context` — same singleton (shared session, services)
|
||||
- `node_path` — parent path + node name (e.g., `wf/child_a`)
|
||||
- `run_id` — unique per execution (reused on resume)
|
||||
- `event_author` — inherited from parent
|
||||
- `schedule_dynamic_node_internal` — inherited from parent
|
||||
|
||||
The child does NOT inherit output, route, or interrupt_ids — those
|
||||
are per-execution results, starting fresh (unless resume carries
|
||||
forward `prior_output` / `prior_interrupt_ids`).
|
||||
|
||||
## Node result properties
|
||||
|
||||
These properties on Context are the primary mechanism for
|
||||
communicating results between nodes:
|
||||
|
||||
- **`ctx.output`** — the node's result value. Set once per
|
||||
execution. Can be set via `yield value` (framework sets it) or
|
||||
`ctx.output = X` directly. Second write raises `ValueError`.
|
||||
- **`ctx.route`** — routing value for conditional edges. Set
|
||||
independently of output. Workflow-specific.
|
||||
- **`ctx.interrupt_ids`** — accumulated interrupt IDs. Read-only
|
||||
for user code. Set by framework when node yields an Event with
|
||||
`long_running_tool_ids`.
|
||||
|
||||
Output and interrupts can coexist — the orchestrator's `_finalize`
|
||||
decides what to propagate. The orchestrator reads these properties
|
||||
after the child node finishes.
|
||||
|
||||
## Class hierarchy
|
||||
|
||||
```
|
||||
ReadonlyContext (agents/readonly_context.py)
|
||||
└── Context (agents/context.py)
|
||||
```
|
||||
|
||||
**ReadonlyContext** — read-only view used in callbacks and plugins:
|
||||
- `user_content`, `invocation_id`, `agent_name`
|
||||
- `state` (returns `MappingProxyType` — immutable view)
|
||||
- `session`, `user_id`, `run_config`
|
||||
|
||||
**Context(ReadonlyContext)** — full read-write context for node
|
||||
execution. Extends ReadonlyContext with mutable state, node results,
|
||||
workflow metadata, and service methods. See property reference below.
|
||||
|
||||
## Property reference
|
||||
|
||||
| Category | Properties |
|
||||
|---|---|
|
||||
| State & actions | `state` (mutable `State`), `actions` (EventActions) |
|
||||
| Node results | `output`, `route`, `interrupt_ids` (read-only) |
|
||||
| Workflow | `node_path`, `run_id`, `triggered_by`, `in_nodes`, `resume_inputs`, `retry_count`, `event_author` |
|
||||
| Methods | `run_node()`, `get_next_child_run_id()` |
|
||||
| Artifacts | `load_artifact()`, `save_artifact()`, `list_artifacts()` |
|
||||
| Memory | `search_memory()`, `add_session_to_memory()`, `add_events_to_memory()`, `add_memory()` |
|
||||
| Auth | `request_credential()`, `load_credential()`, `save_credential()` |
|
||||
| Tools | `request_confirmation()`, `function_call_id` |
|
||||
@@ -0,0 +1,42 @@
|
||||
# LLM Context Orchestration from Events
|
||||
|
||||
## Core Principle
|
||||
|
||||
In ADK, there is a clear distinction between the **Event Stream** and the **LLM Context**:
|
||||
|
||||
- **Events are the Ground Truth**: They are immutable records of what has happened in a session (user messages, model responses, tool calls, results). They serve as the audit log and persistence state.
|
||||
- **LLM Context is an Orchestrated View**: The context passed to an LLM is not merely a dump of the raw event log. It is a carefully orchestrated view, filtered and transformed to match the specific role, task, and branch of the agent currently executing.
|
||||
|
||||
## Orchestration Strategies
|
||||
|
||||
The framework orchestrates the translation of events into LLM context using several strategies:
|
||||
|
||||
### 1. Task Delegation Translation
|
||||
|
||||
When a coordinator agent delegates a task to a sub-agent (Task Agent) via a tool call:
|
||||
|
||||
- **Source Event**: Coordinator calls a tool like `request_task_<sub_agent_name>(args...)`.
|
||||
- **Orchestrated Context**:
|
||||
- The arguments in the `request_task_<sub_agent_name>` tool call are extracted and placed in the **System Instruction (SI)** or treated as the core instruction for the sub-agent.
|
||||
- The first user message presented to the sub-agent is synthesized to represent the goal (e.g., "Finish task of [sub_agent_name] with arguments [args]").
|
||||
- **Goal**: Isolate the sub-agent from the coordinator's full history and give it a crisp, clear starting point.
|
||||
|
||||
### 2. Branch Isolation
|
||||
|
||||
In complex workflows with parallel execution:
|
||||
|
||||
- **Source Events**: Events from all nodes and branches are stored in the same session chronologically.
|
||||
- **Orchestrated Context**: The framework filters events by `branch` (e.g., `node:path.name`). An agent only sees events that belong to its own execution path.
|
||||
- **Goal**: Prevent cross-node event pollution and ensure deterministic behavior in isolated tasks.
|
||||
|
||||
### 3. History Trimming and Compaction
|
||||
|
||||
To prevent context window overflow and stale instruction loops:
|
||||
|
||||
- **Source Events**: A long history of retries, tool calls, and interactions.
|
||||
- **Orchestrated Context**: The framework may trim older events or summarize them (event compaction). In task mode, it might keep only the essential setup events, ignoring stale retry loops that would otherwise confuse the LLM.
|
||||
- **Goal**: Maintain a focused and efficient context window for the LLM.
|
||||
|
||||
## Summary
|
||||
|
||||
The relationship is one of **Source vs. View**. Events are the source of truth for the session, while LLM context is a highly orchestrated view of that truth, tailored for the active agent.
|
||||
@@ -0,0 +1,76 @@
|
||||
# NodeRunner
|
||||
|
||||
NodeRunner is the per-node executor. It drives `BaseNode.run()`,
|
||||
creates the child Context, enriches events, and writes results
|
||||
to ctx.
|
||||
|
||||
## Two communication channels
|
||||
|
||||
The runtime has two distinct channels for data flow:
|
||||
|
||||
- **Context** — parent ↔ child communication. Output, route, state,
|
||||
resume_inputs, and interrupt_ids flow through ctx. The orchestrator
|
||||
reads ctx after the child completes to decide what to do next.
|
||||
- **Event** — persistence and streaming. Events are appended to the
|
||||
session and streamed to the caller. They carry message, state
|
||||
deltas, function calls, and interrupt markers.
|
||||
|
||||
A node writes to **ctx** to communicate with its parent. A node
|
||||
yields **Events** to persist data and stream messages to the user.
|
||||
|
||||
## Execution flow
|
||||
|
||||
```
|
||||
Orchestrator
|
||||
│
|
||||
├─ NodeRunner(node=child, parent_ctx=ctx)
|
||||
│ │
|
||||
│ ├─ _create_child_context() → child Context
|
||||
│ ├─ _execute_node() → iterate node.run()
|
||||
│ │ ├─ _track_event_in_context() → write to ctx
|
||||
│ │ └─ _enqueue_event() → enrich + persist
|
||||
│ ├─ _flush_output_and_deltas() → emit deferred output
|
||||
│ └─ return child ctx
|
||||
│
|
||||
└─ reads ctx.output, ctx.route, ctx.interrupt_ids
|
||||
```
|
||||
|
||||
1. **Create child Context** — inherits `_invocation_context` (shared
|
||||
singleton), builds `node_path` from parent, assigns `run_id`.
|
||||
|
||||
2. **Iterate `node.run()`** — for each yielded Event:
|
||||
|
||||
**Track in context** — `_track_event_in_context` writes output,
|
||||
route, and interrupt_ids from the event to ctx (source of truth).
|
||||
|
||||
**Enrich** — `_enrich_event` stamps metadata before persistence:
|
||||
- `event.author` — node name (or `event_author` override)
|
||||
- `event.invocation_id` — from InvocationContext
|
||||
- `event.node_info.path` — full path (e.g., `wf/child_a`)
|
||||
- `event.node_info.run_id` — unique per execution
|
||||
- `event.node_info.output_for` — ancestor paths when
|
||||
`use_as_output=True`
|
||||
|
||||
**Flush deltas** — for non-partial events, `_flush_deltas` moves
|
||||
pending state/artifact deltas from `ctx.actions` onto the event
|
||||
before enqueueing.
|
||||
|
||||
**Enqueue** — `ic.enqueue_event` puts the event on the shared
|
||||
process queue for session persistence.
|
||||
|
||||
3. **Flush deferred output** — if `ctx.output` was set directly
|
||||
(not via yield), `_flush_output_and_deltas` emits the output
|
||||
Event after `_run_impl` returns. Bundles any remaining
|
||||
state/artifact deltas onto the same Event.
|
||||
|
||||
4. **Return child ctx** — the orchestrator reads `ctx.output`,
|
||||
`ctx.route`, and `ctx.interrupt_ids`.
|
||||
|
||||
## Output delegation (`use_as_output`)
|
||||
|
||||
When a child is scheduled with `use_as_output=True`, its output
|
||||
Event also counts as the parent's output. NodeRunner:
|
||||
|
||||
- Sets `ctx._output_delegated = True` on the parent
|
||||
- Skips emitting the parent's own output Event
|
||||
- Stamps `event.node_info.output_for` with ancestor paths
|
||||
@@ -0,0 +1,164 @@
|
||||
# Observability
|
||||
|
||||
## Design: span on Context
|
||||
|
||||
Each Context carries a `_span` field. Since Context forms a 1:1
|
||||
parent-child tree with node executions (see [Context](context.md)),
|
||||
span hierarchy follows naturally — no separate span management
|
||||
needed.
|
||||
|
||||
```
|
||||
Root Context._span (invocation) ← Runner sets this
|
||||
└── ctx[workflow]._span ← NodeRunner creates
|
||||
├── ctx[child_a]._span ← NodeRunner creates
|
||||
│ ├── (call_llm span) ← auto-parented
|
||||
│ └── (execute_tool span) ← auto-parented
|
||||
├── ctx[child_b]._span ← NodeRunner creates
|
||||
│ └── ctx[grandchild]._span ← nested
|
||||
└── ctx[child_c]._span ← ctx.run_node()
|
||||
```
|
||||
|
||||
**Runner** creates `root_ctx` and the `invocation` span, storing
|
||||
it as `root_ctx._span`. This becomes the parent for all node spans.
|
||||
|
||||
**NodeRunner** creates each node's span, explicitly parented to
|
||||
`parent_ctx._span`, stores it on `child_ctx._span`, and closes it
|
||||
before returning (see [NodeRunner](node-runner.md) for the
|
||||
execution flow).
|
||||
|
||||
**Always use `ctx._span` explicitly** — never rely on OTel's
|
||||
implicit "current span" context. In a concurrent asyncio.Task
|
||||
runtime, implicit context can be unreliable across concurrent
|
||||
nodes. All tracing operations (attributes, logs, child spans)
|
||||
should go through `ctx._span`. When attaching or detaching OTel context explicitly (e.g., using `context.attach()` and `context.detach()`), **always pair them inside a `try...finally` block** to prevent context leaks across requests.
|
||||
|
||||
**Span lifecycle:**
|
||||
|
||||
1. `NodeRunner.run()` creates span via `tracer.start_span()`,
|
||||
parented to `parent_ctx._span`, stored on `ctx._span`
|
||||
2. Node executes; all tracing goes through `ctx._span` explicitly
|
||||
3. `NodeRunner.run()` calls `ctx._span.end()` before returning
|
||||
4. `BatchSpanProcessor` buffers ended spans, exports periodically
|
||||
5. `OTLPSpanExporter` sends batch to the OTLP endpoint
|
||||
|
||||
**Interrupted nodes:** Span ends immediately when NodeRunner
|
||||
returns — not left open waiting for resume. Otherwise the span
|
||||
would be invisible to the backend until resume (which could be
|
||||
minutes, hours, or never). The resumed execution starts a fresh
|
||||
span in a new `Runner.run_async()` call (same invocation_id,
|
||||
different trace — possibly on a different server).
|
||||
|
||||
## NodeRunner integration
|
||||
|
||||
**Context changes** — add `_span` field:
|
||||
|
||||
```python
|
||||
class Context(ReadonlyContext):
|
||||
_span: Span | None = None
|
||||
```
|
||||
|
||||
**NodeRunner.run():**
|
||||
|
||||
**NodeRunner.run() lifecycle:**
|
||||
|
||||
1. Create child ctx
|
||||
2. Create span, parented to `parent_ctx._span`
|
||||
3. Store on `ctx._span`
|
||||
4. Set node attributes (name, path, run_id, type)
|
||||
5. Execute node
|
||||
- Node can add custom attributes to `ctx._span` during
|
||||
execution (e.g., SingleAgentReactNode adds
|
||||
`gen_ai.agent.name`, `gen_ai.request_model`)
|
||||
- On interrupt: mark span `node.interrupted = True`
|
||||
- On error: set span status `ERROR`, record exception
|
||||
6. Set result attributes (has_output, interrupted, resumed)
|
||||
7. **Close span** (`ctx._span.end()`) — always, even on interrupt
|
||||
8. Return ctx
|
||||
|
||||
Key points:
|
||||
- Use `tracer.start_span()` with explicit parent context from
|
||||
`parent_ctx._span` — never rely on implicit OTel context in
|
||||
concurrent async code
|
||||
- Span always ends before `run()` returns, even on interrupt
|
||||
|
||||
## Span attributes and semantic conventions
|
||||
|
||||
Set at span creation (available for sampling decisions):
|
||||
|
||||
| Attribute | Source | Example |
|
||||
|---|---|---|
|
||||
| `node.name` | `self._node.name` | `"call_llm"` |
|
||||
| `node.path` | `ctx.node_path` | `"wf/child_a"` |
|
||||
| `node.run_id` | `self._run_id` | `"child_a_abc123"` |
|
||||
| `node.type` | `type(self._node).__name__` | `"CallLlmNode"` |
|
||||
|
||||
Set after execution (result attributes):
|
||||
|
||||
| Attribute | Source | Example |
|
||||
|---|---|---|
|
||||
| `node.has_output` | `ctx.output is not None` | `true` |
|
||||
| `node.interrupted` | `bool(ctx.interrupt_ids)` | `false` |
|
||||
| `node.resumed` | `bool(resume_inputs)` | `false` |
|
||||
|
||||
GenAI semantic conventions for node spans:
|
||||
|
||||
- `gen_ai.operation.name` = `"invoke_agent"` for agent nodes
|
||||
- `gen_ai.operation.name` = `"execute_tool"` for tool nodes
|
||||
- `gen_ai.agent.name`, `gen_ai.tool.name` as appropriate
|
||||
- Span kind: `INTERNAL` (in-process orchestration)
|
||||
|
||||
## Correlated logs
|
||||
|
||||
Use the OTel Logs API for point-in-time occurrences within a
|
||||
node's span. Context provides `emit_log()` for better DX —
|
||||
wraps `set_span_in_context(self._span)` internally so callers
|
||||
don't manage OTel context:
|
||||
|
||||
```python
|
||||
# On Context:
|
||||
def emit_log(self, body: str, **attributes):
|
||||
span_ctx = set_span_in_context(self._span)
|
||||
otel_logger.emit(
|
||||
LogRecord(body=body, attributes=attributes),
|
||||
context=span_ctx,
|
||||
)
|
||||
|
||||
# Usage:
|
||||
ctx.emit_log('node.event.yielded',
|
||||
has_output=event.output is not None,
|
||||
has_message=event.content is not None,
|
||||
)
|
||||
```
|
||||
|
||||
## Python logging
|
||||
|
||||
Use the `google_adk` logger namespace:
|
||||
|
||||
| Level | What to log |
|
||||
|---|---|
|
||||
| `DEBUG` | Node started, node completed, event enqueued |
|
||||
| `INFO` | Node interrupted, node resumed, dynamic node scheduled |
|
||||
| `WARNING` | Node timeout, retry triggered |
|
||||
| `ERROR` | Node failed, unhandled exception |
|
||||
|
||||
```python
|
||||
logger = logging.getLogger("google_adk." + __name__)
|
||||
|
||||
logger.debug(
|
||||
'Node %s started (run_id=%s, path=%s)',
|
||||
node.name, run_id, ctx.node_path,
|
||||
)
|
||||
```
|
||||
|
||||
Use `%`-style formatting (lazy evaluation) for logging, not
|
||||
f-strings.
|
||||
|
||||
## Metrics (future)
|
||||
|
||||
| Metric | Type | Description |
|
||||
|---|---|---|
|
||||
| `node.execution.duration` | Histogram | Per node type |
|
||||
| `node.execution.count` | Counter | Per node type and status |
|
||||
| `node.interrupt.count` | Counter | HITL interrupts |
|
||||
| `node.resume.count` | Counter | Resumed executions |
|
||||
| `workflow.active_nodes` | UpDownCounter | Currently executing |
|
||||
@@ -0,0 +1,12 @@
|
||||
# Runner vs NodeRunner vs Workflow
|
||||
|
||||
These three are deliberately separate:
|
||||
|
||||
- **Runner** = lifecycle orchestrator (InvocationContext, session,
|
||||
plugins, invocation boundaries)
|
||||
- **NodeRunner** = task scheduler (asyncio tasks, node execution,
|
||||
completions)
|
||||
- **Workflow** = graph engine (edges, traversal, node sequencing)
|
||||
|
||||
Merging Runner and NodeRunner would deadlock on nested workflows
|
||||
(inner workflow's NodeRunner would block the outer's Runner).
|
||||
@@ -0,0 +1,38 @@
|
||||
# Agent
|
||||
|
||||
The `Agent` (represented by `BaseAgent` in code) is a public interface in ADK that serves as a blueprint defining identity, instructions, and tools for an agentic entity. It inherits from `BaseNode` and can be part of a larger workflow or act as a standalone agent.
|
||||
|
||||
## Key Characteristics
|
||||
- **Name**: Unique identifier within the agent tree. Must be a valid Python identifier and cannot be "user".
|
||||
- **Description**: Capability description used by the model for delegation.
|
||||
- **Sub-agents**: Support for hierarchical agent structures.
|
||||
- **Callbacks**: Supports `before_agent_callback` and `after_agent_callback` for intercepting lifecycle events.
|
||||
|
||||
## Entrance Methods
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Since agents now extend `BaseNode`, the original `run_async` entrance method is considered **deprecated**. Developers should rely on the new `run` method from `BaseNode` to execute agents as workflow nodes.
|
||||
|
||||
### `run` (Preferred Entrance)
|
||||
The method inherited from `BaseNode` to execute the agent.
|
||||
|
||||
### `run_async` (Deprecated)
|
||||
Legacy entry method to run an agent via text-based conversation.
|
||||
|
||||
**Arguments:**
|
||||
- `parent_context`: `InvocationContext`, the invocation context of the parent agent.
|
||||
|
||||
**Yields:**
|
||||
- `Event`: The events generated by the agent.
|
||||
|
||||
### `run_live`
|
||||
Entry method to run an agent via video/audio-based conversation.
|
||||
|
||||
**Arguments:**
|
||||
- `parent_context`: `InvocationContext`, the invocation context of the parent agent.
|
||||
|
||||
**Yields:**
|
||||
- `Event`: The events generated by the agent.
|
||||
|
||||
### `from_config`
|
||||
Class method to create an agent from a configuration object.
|
||||
@@ -0,0 +1,31 @@
|
||||
# BaseAgent
|
||||
|
||||
`BaseAgent` is the base class for all agents in the ADK. Developers subclass `BaseAgent` to create custom agentic entities. It inherits from `BaseNode` and provides the core structure and lifecycle management for agents.
|
||||
|
||||
## Core Contract for Subclasses
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Since agents now extend `BaseNode`, the original `run_async` entrance method is considered **deprecated**. Developers should rely on the new `run` method from `BaseNode` and use `_run_impl` as the primary override point for custom logic.
|
||||
|
||||
When creating a custom agent by subclassing `BaseAgent`, you should focus on the following:
|
||||
|
||||
### `_run_impl` (Preferred Override Point)
|
||||
Core logic to run the agent as a workflow node.
|
||||
|
||||
**Arguments:**
|
||||
- `ctx`: `Context`, the node execution context.
|
||||
- `node_input`: `Any`, the input to the node.
|
||||
|
||||
**Yields:**
|
||||
- The results generated by the agent.
|
||||
|
||||
### Legacy Methods (Deprecated for Node Execution)
|
||||
The following methods were used for text and live conversations but are being superseded by the node-based execution model:
|
||||
- `_run_async_impl`: Core logic for text-based conversation.
|
||||
- `_run_live_impl`: Core logic for live conversation.
|
||||
|
||||
## Key Attributes to Configure
|
||||
|
||||
- **`name`**: The agent's name. Must be a valid Python identifier and unique within the agent tree. Cannot be "user".
|
||||
- **`description`**: A description of the agent's capability, used by the model for delegation choices.
|
||||
- **`sub_agents`**: A list of child agents to support hierarchical delegation.
|
||||
@@ -0,0 +1,137 @@
|
||||
# BaseNode
|
||||
|
||||
BaseNode is the primitive unit of execution in the workflow runtime.
|
||||
Every computation — LLM calls, tool execution, orchestration — is
|
||||
a node. It is a Pydantic `BaseModel` subclass.
|
||||
|
||||
## The node contract
|
||||
|
||||
Every node follows a two-method pattern:
|
||||
|
||||
- `run()` is `@final` — normalizes yields to Events. Never override.
|
||||
- `_run_impl()` is the extension point — subclasses implement their
|
||||
logic here as an async generator.
|
||||
|
||||
```python
|
||||
class MyNode(BaseNode):
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
result = do_work(node_input)
|
||||
yield result # becomes Event(output=result)
|
||||
```
|
||||
|
||||
**Why this split:** `run()` guarantees consistent normalization
|
||||
regardless of what the subclass does. The subclass only thinks
|
||||
about its domain logic.
|
||||
|
||||
**Normalization rules** (`run()` applies these to each yield):
|
||||
|
||||
- `None` → skipped
|
||||
- `Event` → pass through
|
||||
- `RequestInput` → interrupt Event
|
||||
- any other value → `Event(output=value)`
|
||||
|
||||
**Generator conventions:**
|
||||
|
||||
A node can yield three types of data:
|
||||
|
||||
- **Output** — the node's result value. Flows between nodes
|
||||
(parent reads `ctx.output` after child completes). At most one
|
||||
per execution (second raises `ValueError`).
|
||||
- **Message** — user-visible content streamed to the end user
|
||||
(e.g., progress text, partial responses). Multiple allowed.
|
||||
- **Route** — Workflow-specific concept. Triggers conditional
|
||||
edges in the graph. Set via `ctx.route` or `event.actions.route`.
|
||||
|
||||
Additional rules:
|
||||
|
||||
- Yielding nothing produces no output event
|
||||
- `yield None` is silently skipped
|
||||
|
||||
A custom node interacts with the runtime through two arguments:
|
||||
|
||||
- **`ctx`** (Context) — communicate results to the parent node
|
||||
- **`node_input`** — data passed by the parent/orchestrator
|
||||
|
||||
## Output and streaming
|
||||
|
||||
Three ways to produce output (pick one per execution):
|
||||
|
||||
```python
|
||||
# 1. Yield a value (most common)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
yield compute(node_input)
|
||||
|
||||
# 2. Set ctx.output directly
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.output = compute(node_input)
|
||||
return
|
||||
yield # generator contract
|
||||
|
||||
# 3. Yield an Event with output
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
yield Event(output=compute(node_input))
|
||||
```
|
||||
|
||||
A second output raises `ValueError` — at most one per execution.
|
||||
|
||||
**Streaming messages** — yield Events with `message` to send
|
||||
user-visible text (`message` is an alias for `content` on Event):
|
||||
|
||||
```python
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
yield Event(message='working...')
|
||||
yield final_result # this is the output
|
||||
```
|
||||
|
||||
## State and routing
|
||||
|
||||
**Mutating state:**
|
||||
|
||||
```python
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.state['key'] = 'value' # recorded as state_delta
|
||||
yield result
|
||||
```
|
||||
|
||||
**Setting route for conditional edges:**
|
||||
|
||||
```python
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.route = 'approve' if score > 0.8 else 'reject'
|
||||
yield node_input
|
||||
```
|
||||
|
||||
## Advanced: child nodes and HITL
|
||||
|
||||
**Running child nodes** via `ctx.run_node()`:
|
||||
|
||||
```python
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
child_result = await ctx.run_node(some_node, node_input)
|
||||
yield f'child said: {child_result}'
|
||||
```
|
||||
|
||||
Requires `rerun_on_resume = True` on the calling node.
|
||||
|
||||
**Requesting interrupt (HITL):**
|
||||
|
||||
```python
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
if ctx.resume_inputs and 'fc-1' in ctx.resume_inputs:
|
||||
yield f'approved: {ctx.resume_inputs["fc-1"]}'
|
||||
return
|
||||
yield Event(long_running_tool_ids={'fc-1'})
|
||||
```
|
||||
|
||||
## Configuration reference
|
||||
|
||||
| Field | Type | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `name` | `str` | required | Unique identifier |
|
||||
| `description` | `str` | `''` | Human-readable description |
|
||||
| `rerun_on_resume` | `bool` | `False` | Re-execute on resume (required for `ctx.run_node()`) |
|
||||
| `wait_for_output` | `bool` | `False` | Stay WAITING until output is yielded (for join nodes) |
|
||||
| `retry_config` | `RetryConfig \| None` | `None` | Retry on failure |
|
||||
| `timeout` | `float \| None` | `None` | Max execution time in seconds |
|
||||
| `input_schema` | `SchemaType \| None` | `None` | Validate/coerce input data |
|
||||
| `output_schema` | `SchemaType \| None` | `None` | Validate/coerce output data |
|
||||
@@ -0,0 +1,30 @@
|
||||
# Event
|
||||
|
||||
The `Event` class represents a single event in the conversation history or workflow execution in the ADK. It is the core data structure used for state reconstruction, communication, and persistence.
|
||||
|
||||
## Purpose
|
||||
- Stores conversation content between users and agents.
|
||||
- Captures actions taken by agents (e.g., function calls, function responses, state updates).
|
||||
- Holds metadata for workflow nodes, such as execution paths and run IDs.
|
||||
|
||||
## Key Fields
|
||||
|
||||
- **`invocation_id`**: The ID of the invocation this event belongs to. Non-empty before appending to a session.
|
||||
- **`author`**: 'user' or the name of the agent, indicating who created the event.
|
||||
- **`content`**: The actual content of the message (text, parts, etc.), inheriting from `LlmResponse`.
|
||||
- **`actions`**: `EventActions` containing function calls, responses, or state changes.
|
||||
- **`output`**: Generic data output from a workflow node.
|
||||
- **`node_info`**: `NodeInfo` containing the execution path in the workflow (e.g., "A/B").
|
||||
- **`branch`**: Used for branch-aware isolation when peer sub-agents shouldn't see each other's history.
|
||||
- **`id`**: Unique identifier for the event.
|
||||
- **`timestamp`**: The timestamp of the event.
|
||||
|
||||
## Methods of Interest
|
||||
- `get_function_calls()`: Returns function calls in the event.
|
||||
- `get_function_responses()`: Returns function responses in the event.
|
||||
- `is_final_response()`: Returns whether the event is the final response of an agent.
|
||||
|
||||
## State Lifecycle & Immutability
|
||||
- **Event Immutability**: Event history is immutable. Never assume that events are mutated or cleared after they are saved to a session.
|
||||
- **Signal & Action Persistence**: When checking if a signal or action is "pending" versus "resolved", do not rely on events being modified in place.
|
||||
- **Compaction Side-Effects**: Be aware that storing stateful flags on events (such as requested actions or transient status) can have permanent unintended effects on background compaction when those events age but remain in history.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Runner
|
||||
|
||||
The `Runner` is the public interface for executing agents and workflows in ADK. It manages the execution lifecycle, handling message processing, event generation, and interaction with services like artifacts, sessions, and memory.
|
||||
|
||||
## Entrance Methods
|
||||
|
||||
### `run_async`
|
||||
This is the main asynchronous entry method to run the agent in the runner. It should be used for production usage.
|
||||
|
||||
**Key Features:**
|
||||
- Supports event compaction if enabled in configuration.
|
||||
- Does not block subsequent concurrent calls for new user queries.
|
||||
- Yields events as they are generated.
|
||||
|
||||
**Arguments:**
|
||||
- `user_id`: The user ID of the session.
|
||||
- `session_id`: The session ID of the session.
|
||||
- `invocation_id`: Optional, set to resume an interrupted invocation.
|
||||
- `new_message`: A new message to append to the session.
|
||||
- `state_delta`: Optional state changes to apply to the session.
|
||||
- `run_config`: The run config for the agent.
|
||||
- `yield_user_message`: If True, yields the user message event before agent/node events.
|
||||
|
||||
### `run`
|
||||
This is a synchronous entry point provided for local testing and convenience purposes.
|
||||
|
||||
**Key Features:**
|
||||
- Runs the asynchronous execution in a background thread and re-yields events.
|
||||
- Production usage should prefer `run_async`.
|
||||
|
||||
**Arguments:**
|
||||
- `user_id`: The user ID of the session.
|
||||
- `session_id`: The session ID of the session.
|
||||
- `new_message`: A new message to append to the session.
|
||||
- `run_config`: The run config for the agent.
|
||||
@@ -0,0 +1,326 @@
|
||||
# Workflow
|
||||
|
||||
Workflow is a graph-based orchestration node. It extends BaseNode
|
||||
and implements `_run_impl()` as a scheduling loop that drives static
|
||||
graph nodes and tracks dynamic nodes spawned by `ctx.run_node()`.
|
||||
|
||||
## Two kinds of child nodes
|
||||
|
||||
Workflow manages two kinds of child nodes:
|
||||
|
||||
- **Static (graph) nodes** — declared in `edges`, compiled into a
|
||||
`WorkflowGraph`. Scheduled by the orchestration loop via triggers
|
||||
and `asyncio.Task`s. Tracked in `_LoopState.nodes` by node name.
|
||||
- **Dynamic nodes** — spawned at runtime via `ctx.run_node()` from
|
||||
inside a graph node's `_run_impl`. Tracked in
|
||||
`_LoopState.dynamic_nodes` by full `node_path`. Managed by
|
||||
`DynamicNodeScheduler`.
|
||||
|
||||
Static and dynamic nodes share the same `_LoopState.interrupt_ids`
|
||||
set, so the Workflow sees a unified view of all pending interrupts.
|
||||
|
||||
## Implementing a graph node
|
||||
|
||||
A graph node is a regular BaseNode placed in a Workflow's edges.
|
||||
The Workflow wraps it in a NodeRunner, creates a child Context, and
|
||||
reads `ctx.output`, `ctx.route`, and `ctx.interrupt_ids` after it
|
||||
completes.
|
||||
|
||||
**Output** — two paths. At most one per execution. The Workflow
|
||||
reads the output to pass downstream.
|
||||
|
||||
```python
|
||||
# Yield (persisted immediately)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
yield compute(node_input)
|
||||
|
||||
# ctx (deferred until node end)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.output = compute(node_input)
|
||||
return
|
||||
yield
|
||||
```
|
||||
|
||||
**Routing** — two paths. The Workflow uses the route to select
|
||||
conditional edges.
|
||||
|
||||
```python
|
||||
# Yield (persisted immediately)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
yield Event(route='approve' if node_input > 0.8 else 'reject')
|
||||
|
||||
# ctx (deferred until node end)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.route = 'approve' if node_input > 0.8 else 'reject'
|
||||
yield node_input
|
||||
```
|
||||
|
||||
**State** — two paths. `ctx.state` deltas are flushed onto the next
|
||||
yielded Event, or a final Event at node end.
|
||||
|
||||
```python
|
||||
# Yield (persisted immediately)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
yield Event(state={'count': 1})
|
||||
|
||||
# ctx (flushed onto next/final Event)
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.state['count'] = 1
|
||||
yield result
|
||||
```
|
||||
|
||||
**Interrupts** — yield only (`ctx.interrupt_ids` is read-only). The
|
||||
Workflow marks the node WAITING and propagates the interrupt IDs
|
||||
upward. On resume, if `rerun_on_resume=True` (default for Workflow),
|
||||
the node is re-executed with `ctx.resume_inputs` populated.
|
||||
|
||||
```python
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
if ctx.resume_inputs and 'fc-1' in ctx.resume_inputs:
|
||||
yield f'approved: {ctx.resume_inputs["fc-1"]}'
|
||||
return
|
||||
yield Event(long_running_tool_ids={'fc-1'})
|
||||
```
|
||||
|
||||
## Dynamic nodes via ctx.run_node()
|
||||
|
||||
A graph node can spawn child nodes at runtime:
|
||||
|
||||
```python
|
||||
class Orchestrator(BaseNode):
|
||||
rerun_on_resume: bool = True # required
|
||||
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
result = await ctx.run_node(some_node, input_data)
|
||||
yield f'child returned: {result}'
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
- The calling node **must** have `rerun_on_resume = True`. Without
|
||||
this, the Workflow cannot re-execute the node on resume to let it
|
||||
re-acquire its dynamic children's results.
|
||||
|
||||
### Tracking
|
||||
|
||||
Dynamic nodes are tracked by **full node_path**, not by name alone.
|
||||
The path is `parent_path/child_name`:
|
||||
|
||||
```
|
||||
wf/graph_node_a/dynamic_child ← dynamic node under graph_node_a
|
||||
wf/graph_node_a/dynamic_child/inner ← transitive dynamic node
|
||||
```
|
||||
|
||||
The `child_name` comes from either:
|
||||
- The `name` parameter on `ctx.run_node(node, name='explicit')`
|
||||
- The node's own `name` field (default)
|
||||
|
||||
Each unique `node_path` is tracked exactly once in
|
||||
`_LoopState.dynamic_nodes`. This enables:
|
||||
|
||||
- **Dedup** — if the same path is encountered again (after resume),
|
||||
the cached output is returned without re-execution.
|
||||
- **Resume** — if the node was interrupted, its state is
|
||||
reconstructed from session events via lazy scan.
|
||||
|
||||
### Dedup and resume protocol (DynamicNodeScheduler)
|
||||
|
||||
When `ctx.run_node()` is called, the scheduler checks three cases:
|
||||
|
||||
1. **Fresh** — no prior events for this `node_path`. Execute via
|
||||
NodeRunner, record output or interrupts in `_LoopState`.
|
||||
|
||||
2. **Completed** — prior events show the node produced output.
|
||||
Return cached output immediately. No re-execution.
|
||||
|
||||
3. **Waiting** — prior events show the node was interrupted:
|
||||
- Unresolved interrupts → propagate interrupt IDs to the caller
|
||||
(via `_LoopState.interrupt_ids`). The caller raises
|
||||
`NodeInterruptedError`.
|
||||
- All resolved → re-execute with `resume_inputs` from the
|
||||
resolved function responses.
|
||||
|
||||
State reconstruction is **lazy**: the scheduler scans session events
|
||||
only on the first `ctx.run_node()` call for a given path, not
|
||||
upfront. This avoids scanning for dynamic nodes that won't be
|
||||
re-invoked.
|
||||
|
||||
### Interrupt propagation
|
||||
|
||||
When a dynamic child interrupts:
|
||||
|
||||
1. `DynamicNodeScheduler._record_result` sets the child's status
|
||||
to WAITING and adds its interrupt IDs to
|
||||
`_LoopState.interrupt_ids`.
|
||||
2. `ctx.run_node()` checks `child_ctx.interrupt_ids`. If non-empty,
|
||||
it propagates them to the calling node's `ctx._interrupt_ids`
|
||||
and raises `NodeInterruptedError`.
|
||||
3. NodeRunner catches `NodeInterruptedError` in `_execute_node` and
|
||||
records the interrupt on the calling node's Context.
|
||||
4. The Workflow's `_handle_completion` sees the interrupt and marks
|
||||
the graph node as WAITING.
|
||||
|
||||
On resume, the Workflow re-executes the graph node (because
|
||||
`rerun_on_resume=True`). The graph node calls `ctx.run_node()`
|
||||
again, which hits the scheduler. The scheduler lazily scans events,
|
||||
finds the resolved FR, and either returns cached output or
|
||||
re-executes the dynamic child with `resume_inputs`.
|
||||
|
||||
### Output delegation (use_as_output)
|
||||
|
||||
`ctx.run_node(node, use_as_output=True)` makes the dynamic child's
|
||||
output count as the calling node's output:
|
||||
|
||||
```python
|
||||
class Delegator(BaseNode):
|
||||
rerun_on_resume: bool = True
|
||||
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
# child's output becomes this node's output
|
||||
await ctx.run_node(worker, node_input, use_as_output=True)
|
||||
```
|
||||
|
||||
- Sets `ctx._output_delegated = True` on the parent
|
||||
- NodeRunner stamps `event.node_info.output_for` with ancestor paths
|
||||
- Only one `use_as_output=True` per execution (second raises
|
||||
`ValueError`)
|
||||
|
||||
## Dynamic nodes from dynamic nodes (transitive)
|
||||
|
||||
A dynamic node can itself call `ctx.run_node()`, creating a
|
||||
transitive chain:
|
||||
|
||||
```python
|
||||
class Outer(BaseNode):
|
||||
rerun_on_resume: bool = True
|
||||
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
result = await ctx.run_node(Inner(name='inner'), 'data')
|
||||
yield result
|
||||
|
||||
class Inner(BaseNode):
|
||||
rerun_on_resume: bool = True
|
||||
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
sub = await ctx.run_node(Leaf(name='leaf'), node_input)
|
||||
yield f'inner got: {sub}'
|
||||
```
|
||||
|
||||
This works because:
|
||||
|
||||
- All dynamic nodes in the subtree are tracked by the **same**
|
||||
enclosing Workflow. The scheduler is inherited down the Context
|
||||
tree automatically.
|
||||
- Each level gets a unique `node_path`:
|
||||
`wf/graph_node/outer/inner/leaf`
|
||||
- Nested interrupts are correctly attributed — the scheduler
|
||||
matches events from any descendant under a given path.
|
||||
- Only a nested **orchestration node** (another Workflow or
|
||||
SingleAgentReactNode) takes over scheduling. Regular nodes
|
||||
inherit the enclosing Workflow's scheduler.
|
||||
|
||||
### Scoping
|
||||
|
||||
Each Workflow has its own `DynamicNodeScheduler` and `_LoopState`.
|
||||
A nested Workflow creates a new scheduler, so dynamic nodes within
|
||||
it are scoped to that inner Workflow — not mixed with the outer
|
||||
Workflow's state.
|
||||
|
||||
## event_author
|
||||
|
||||
Workflow sets `ctx.event_author = self.name` at the start of
|
||||
`_run_impl`. This propagates to all child Contexts via NodeRunner.
|
||||
All events emitted by children carry this author, giving the UI
|
||||
consistent attribution.
|
||||
|
||||
An inner orchestration node (nested Workflow, SingleAgentReactNode)
|
||||
overrides `event_author` with its own name, so events are attributed
|
||||
to the nearest orchestration ancestor.
|
||||
|
||||
## Orchestration loop lifecycle
|
||||
|
||||
```
|
||||
_run_impl
|
||||
├─ SETUP: resume from events OR seed start triggers
|
||||
├─ ctx._schedule_dynamic_node_internal = DynamicNodeScheduler
|
||||
├─ LOOP:
|
||||
│ ├─ _schedule_ready_nodes → pop triggers, create NodeRunners
|
||||
│ ├─ asyncio.wait(FIRST_COMPLETED)
|
||||
│ └─ _handle_completion → update state, buffer downstream
|
||||
├─ await dynamic_pending_tasks
|
||||
├─ _collect_remaining_interrupts
|
||||
└─ FINALIZE: set ctx.output or ctx._interrupt_ids
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
|
||||
- **Concurrency** — `max_concurrency` limits parallel graph nodes.
|
||||
Dynamic nodes are excluded (they run inline, throttling would
|
||||
deadlock).
|
||||
- **Terminal output** — nodes with no outgoing edges are terminal.
|
||||
Their output is delegated to the Workflow's own output via
|
||||
`output_for`. Only one terminal node may produce output.
|
||||
- **Loop edges** — a completed node can be re-triggered by a
|
||||
downstream edge pointing back to it. Its status resets to PENDING.
|
||||
|
||||
## Resume from session events
|
||||
|
||||
On resume (`ctx.resume_inputs` is non-empty), the Workflow
|
||||
reconstructs static node states from session events:
|
||||
|
||||
1. **Scan** — single forward pass through events for this
|
||||
invocation. For each direct child, track output, interrupts,
|
||||
and resolved FRs.
|
||||
2. **Derive status per child:**
|
||||
- Unresolved interrupts → WAITING
|
||||
- All interrupts resolved → PENDING (re-run with `resume_inputs`)
|
||||
- Has output → COMPLETED
|
||||
- **Partial resume across children:** if child A's interrupt is
|
||||
resolved but child B's is not, A becomes PENDING (re-runs)
|
||||
while B stays WAITING. The Workflow re-interrupts with B's
|
||||
remaining IDs.
|
||||
- **Partial resume within a child:** if a single child emitted
|
||||
multiple interrupts (e.g., fc-1 and fc-2) and only fc-1 is
|
||||
resolved:
|
||||
- `rerun_on_resume=True` (e.g., nested Workflow): re-run with
|
||||
partial `resume_inputs` so it can dispatch resolved
|
||||
grandchildren internally. Remaining interrupts propagate
|
||||
back up.
|
||||
- `rerun_on_resume=False`: stay WAITING until all interrupts
|
||||
are resolved.
|
||||
3. **Seed triggers** — PENDING nodes get triggers so the loop
|
||||
re-executes them with `resume_inputs`.
|
||||
|
||||
Dynamic node state is **not** scanned upfront — it's lazily
|
||||
reconstructed by `DynamicNodeScheduler` when `ctx.run_node()` is
|
||||
called during the re-execution.
|
||||
|
||||
## Key design rules for node authors
|
||||
|
||||
1. **Set `rerun_on_resume = True`** if your node calls
|
||||
`ctx.run_node()`. The Workflow must be able to re-execute your
|
||||
node so it can re-acquire dynamic children's results.
|
||||
|
||||
2. **Use deterministic names** for dynamic children. The `name`
|
||||
parameter on `ctx.run_node()` determines the `node_path`, which
|
||||
is the dedup/resume key. Non-deterministic names break resume.
|
||||
|
||||
3. **Always `await` ctx.run_node() directly.** Do not wrap in
|
||||
`asyncio.create_task()` — the task won't be tracked by the
|
||||
scheduler, errors are swallowed, and cancellation on interrupt
|
||||
won't work.
|
||||
|
||||
4. **Yield output after all dynamic children complete.** If your
|
||||
node calls `ctx.run_node()` and then yields, the output is
|
||||
emitted only after all children finish. This is the expected
|
||||
pattern.
|
||||
|
||||
5. **Handle `NodeInterruptedError` only if you need custom logic.**
|
||||
Normally, `ctx.run_node()` raises `NodeInterruptedError` when a
|
||||
child interrupts. NodeRunner catches it automatically. Only
|
||||
catch it yourself if you need to clean up or adjust state before
|
||||
the interrupt propagates.
|
||||
|
||||
6. **Don't set `ctx.event_author`** unless your node is an
|
||||
orchestration node (like Workflow or SingleAgentReactNode). The
|
||||
Workflow sets it for you and it propagates to all descendants.
|
||||
@@ -0,0 +1,42 @@
|
||||
# API Principles
|
||||
|
||||
Guidelines for designing and maintaining the ADK public API surface.
|
||||
|
||||
## Public API Surface
|
||||
|
||||
The public API surface of ADK includes:
|
||||
- All public classes, methods, and functions in the `google.adk` namespace.
|
||||
- The names, required parameters, and expected behavior of all built-in Tools.
|
||||
- The structure and schema of persisted data (Sessions, Memory, Evaluation datasets).
|
||||
- The JSON request/response format of the ADK API server.
|
||||
- The command-line interface (CLI) commands, arguments, and flags.
|
||||
- The expected file structure for agent definitions (e.g., `agent.py` convention loaded by CLI).
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Stability and Backward Compatibility
|
||||
- ADK adheres to Semantic Versioning 2.0.0.
|
||||
- Any change that forces a developer to alter their existing code to upgrade is a **breaking change** and necessitates a MAJOR version bump.
|
||||
- Avoid breaking changes whenever possible by using optional parameters and deprecation cycles.
|
||||
|
||||
### 2. Self-Containment
|
||||
- Each package should be as self-contained as possible to reduce coupling.
|
||||
- Within the ADK framework, importing from a package's `__init__.py` is **not allowed**. Import from the specific module directly.
|
||||
|
||||
### 3. Explicit Exports
|
||||
- The public API of a package must be explicitly exported in `__init__.py`.
|
||||
- **Only public names** should be imported into `__init__.py`. This keeps `__init__.py` minimal and prevents accidental exposure of internal implementation details.
|
||||
|
||||
### 4. Intuitive Naming
|
||||
- Public method and class names should be concise and intuitive.
|
||||
- Private method names can be longer and more self-explanatory to reduce the need for comments.
|
||||
|
||||
#### Examples
|
||||
|
||||
**Public Naming**
|
||||
- **Good**: `Runner.run()`, `Session.get_events()`
|
||||
- **Bad**: `Runner.orchestrate_agent_invocation_loop()`, `Session.retrieve_all_events_from_storage()`
|
||||
|
||||
**Private Naming**
|
||||
- **Good**: `_prepare_context_for_llm()`, `_should_trim_history()`
|
||||
- **Bad**: `_prep()`, `_trim()`
|
||||
@@ -0,0 +1,367 @@
|
||||
---
|
||||
name: adk-debug
|
||||
description: Use when debugging ADK agents, inspecting sessions, testing agent behavior, troubleshooting tool calls, event flow issues, or diagnosing LLM/model problems.
|
||||
---
|
||||
|
||||
# Debugging ADK Agents
|
||||
|
||||
Two debugging modes: `adk web` (browser UI + API) and `adk run` (CLI).
|
||||
|
||||
> [!NOTE]
|
||||
> **Preference**: For most development and debugging tasks, `adk run` (CLI) is preferred as it is faster and more convenient. **Within `adk run`, query mode is preferred over interactive mode** because it requires less human intervention. However, `adk web` is still required for UI-specific issues, session management visualization, or debugging the API server itself.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Mode 1: adk web (Browser UI + REST API)
|
||||
|
||||
Best for: visual inspection, session management, multi-turn testing.
|
||||
|
||||
### Dev server workflow
|
||||
|
||||
Before starting a server, ask the user:
|
||||
1. **Is there already a running `adk web` server?** If yes, use it
|
||||
(check with `curl -s http://localhost:8000/health`).
|
||||
2. **If not**, start one. Use `run_in_background` so it doesn't
|
||||
block. **Remember to shut it down when debugging is done.**
|
||||
|
||||
```bash
|
||||
# Check if server is already running
|
||||
curl -s http://localhost:8000/health
|
||||
|
||||
# Start server (if not running)
|
||||
adk web path/to/agents_dir # default: http://localhost:8000
|
||||
adk web -v path/to/agents_dir # verbose (DEBUG level)
|
||||
adk web --reload_agents path/to/agents_dir # auto-reload on file changes
|
||||
|
||||
# Shut down when done (if you started it)
|
||||
# Kill the background process or Ctrl+C
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **Coding Agent Friendly Setup**: To allow a coding agent to read the server logs, recommend the user to start the server and redirect output to a file in a location the agent can read (e.g., the conversation's artifact directory or a shared workspace folder):
|
||||
> ```bash
|
||||
> adk web -v path/to/agents_dir 2>&1 | tee path/to/agent_readable_log.log
|
||||
> ```
|
||||
> This ensures both the user and the agent can inspect the full debug logs.
|
||||
|
||||
Web UI: `http://localhost:8000/dev-ui/`
|
||||
|
||||
### Session inspection via curl
|
||||
|
||||
```bash
|
||||
# List sessions
|
||||
curl -s http://localhost:8000/apps/{app_name}/users/{user_id}/sessions | python3 -m json.tool
|
||||
|
||||
# Get full session with events
|
||||
curl -s http://localhost:8000/apps/{app_name}/users/{user_id}/sessions/{session_id} | python3 -m json.tool
|
||||
```
|
||||
|
||||
Do NOT delete sessions after debugging — the user may want to
|
||||
inspect them in the web UI.
|
||||
|
||||
### Summarize events
|
||||
|
||||
Fetch the session JSON and write a Python script to summarize
|
||||
it. Do NOT use hardcoded inline scripts — the JSON schema may
|
||||
change. Instead, fetch the raw JSON first:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8000/apps/{app_name}/users/{user_id}/sessions/{session_id} | python3 -m json.tool
|
||||
```
|
||||
|
||||
Then write a script based on the actual structure you see.
|
||||
Key fields to look for in each event: `author`, `branch`,
|
||||
`content.parts` (text, functionCall, functionResponse),
|
||||
`output`, `actions` (transferToAgent, requestTask, finishTask),
|
||||
`nodeInfo.path`.
|
||||
|
||||
### Send test messages via curl
|
||||
|
||||
```bash
|
||||
SESSION=$(curl -s -X POST http://localhost:8000/apps/{app_name}/users/test/sessions \
|
||||
-H "Content-Type: application/json" -d '{}' | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
|
||||
curl -N -X POST http://localhost:8000/run_sse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"app_name\":\"{app_name}\",\"user_id\":\"test\",\"session_id\":\"$SESSION\",
|
||||
\"new_message\":{\"role\":\"user\",\"parts\":[{\"text\":\"your message here\"}]},
|
||||
\"streaming\":false}"
|
||||
```
|
||||
|
||||
### Debug endpoints (traces)
|
||||
|
||||
```bash
|
||||
# Trace for a specific event
|
||||
curl -s http://localhost:8000/debug/trace/{event_id} | python3 -m json.tool
|
||||
|
||||
# All traces for a session
|
||||
curl -s http://localhost:8000/debug/trace/session/{session_id} | python3 -m json.tool
|
||||
|
||||
# Health check
|
||||
curl -s http://localhost:8000/health
|
||||
```
|
||||
|
||||
### Extract LLM content history
|
||||
|
||||
Fetch trace data and inspect the `call_llm` spans. The LLM
|
||||
request/response are in span attributes:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8000/debug/trace/session/{session_id} | python3 -m json.tool
|
||||
```
|
||||
|
||||
Look for spans with `name: "call_llm"` and inspect their
|
||||
`attributes.gcp.vertex.agent.llm_request` (JSON string of the
|
||||
full request including `contents`, `config`, `model`).
|
||||
|
||||
### Key span attributes
|
||||
|
||||
| Attribute | Description |
|
||||
|-----------|-------------|
|
||||
| `gcp.vertex.agent.llm_request` | Full LLM request JSON (contents, config, model) |
|
||||
| `gcp.vertex.agent.llm_response` | Full LLM response JSON |
|
||||
| `gcp.vertex.agent.event_id` | Event ID — correlate with session events |
|
||||
| `gen_ai.request.model` | Model name |
|
||||
| `gen_ai.usage.input_tokens` | Input token count |
|
||||
| `gen_ai.usage.output_tokens` | Output token count |
|
||||
| `gen_ai.response.finish_reasons` | Stop reason |
|
||||
|
||||
---
|
||||
|
||||
## Mode 2: adk run (CLI)
|
||||
|
||||
Best for: quick testing, scripting, CI/CD, headless debugging.
|
||||
|
||||
### Run interactively
|
||||
|
||||
```bash
|
||||
adk run path/to/my_agent # interactive prompts
|
||||
adk run -v path/to/my_agent # verbose logging
|
||||
```
|
||||
|
||||
### Run with query (automated)
|
||||
|
||||
```bash
|
||||
adk run path/to/my_agent "query" # run with query
|
||||
adk run --jsonl path/to/my_agent "query" # output structured JSONL (noise reduced)
|
||||
```
|
||||
|
||||
### When to use automated query mode
|
||||
|
||||
- **Fast & Lightweight**: Run tests quickly without starting the `adk web` dev server.
|
||||
- **Easy Automation**: Perfect for CI/CD pipelines and regression scripts.
|
||||
- **Highly Composable**: You can pipe the `--jsonl` output to standard tools like `jq`, `grep`, or `diff`.
|
||||
- **Parallel Execution**: Each run is an isolated process. You can run multiple tests concurrently without port conflicts.
|
||||
- **State Isolation**: Use `--in_memory` for fast, side-effect-free testing (no database updates).
|
||||
- **Multi-Turn Support**: Remember to set a session ID if you need to maintain conversation state across turns.
|
||||
|
||||
> [!TIP]
|
||||
> Always read the sample's `README.md` first to understand expected inputs and behaviors!
|
||||
|
||||
### Unit Tests vs. Sample Agents (When to use which)
|
||||
|
||||
Choosing the right testing strategy is crucial for efficiency and coverage:
|
||||
|
||||
- **Use Unit Tests when**:
|
||||
- Testing **isolated logic**, specific methods, or edge cases of a single component.
|
||||
- Verifying **data schemas**, Pydantic validations, or utility functions.
|
||||
- *Location*: `tests/unittests/`.
|
||||
|
||||
- **Use Sample Agents (Integration Testing) when**:
|
||||
- Developing features with **multi-level integration** (Runner + Agent + Workflow) or changes with wide impact.
|
||||
- Testing complex scenarios like **Human-in-the-Loop (HITL)** or long-running tools.
|
||||
- You need to verify the **real behavior** of the agent in a simulated environment.
|
||||
- *Location*: Create a sample under `contributing/agent_samples/` (refer to `adk-sample-creator`).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **AI Assistant Reminder**: If you create a temporary sample agent for testing, you **MUST delete it** after verification is complete, unless the user explicitly asks to keep it.
|
||||
|
||||
### Exit Codes & Details
|
||||
|
||||
- **Exit Code 0**: Success.
|
||||
- **Exit Code 1**: Error (e.g., API key missing, agent load failure).
|
||||
- **Exit Code 2**: Paused (Workflow is waiting for human input/HITL).
|
||||
|
||||
For more options and flags, run:
|
||||
```bash
|
||||
adk run --help
|
||||
```
|
||||
|
||||
### Event printing utility
|
||||
|
||||
```python
|
||||
from google.adk.utils._debug_output import print_event
|
||||
|
||||
print_event(event, verbose=False) # text responses only
|
||||
print_event(event, verbose=True) # tool calls, code execution, inline data
|
||||
```
|
||||
|
||||
Location: `src/google/adk/utils/_debug_output.py`
|
||||
|
||||
### Programmatic debugging
|
||||
|
||||
```python
|
||||
from google.adk import Agent, Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
|
||||
agent = Agent(name="test", model="gemini-2.5-flash", instruction="...")
|
||||
runner = Runner(app_name="test", agent=agent, session_service=InMemorySessionService())
|
||||
|
||||
session = runner.session_service.create_session_sync(app_name="test", user_id="u")
|
||||
for event in runner.run(user_id="u", session_id=session.id, new_message="hello"):
|
||||
print(f"{event.author}: {event.content}")
|
||||
if event.actions.transfer_to_agent:
|
||||
print(f" -> transfer to {event.actions.transfer_to_agent}")
|
||||
if event.output:
|
||||
print(f" -> output: {event.output}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Shared across both modes.
|
||||
|
||||
Set log level with `--log_level` (DEBUG, INFO, WARNING, ERROR, CRITICAL) or `-v` for DEBUG.
|
||||
Logs write to `/tmp/agents_log/`. Tail latest: `tail -F /tmp/agents_log/agent.latest.log`
|
||||
Logger name: `google_adk`. Setup: `src/google/adk/cli/utils/logs.py`
|
||||
|
||||
| Env Variable | Effect |
|
||||
|---|---|
|
||||
| `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS` | Include prompt/response in traces (default: `true`) |
|
||||
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | Enable prompt/response in OTEL spans |
|
||||
| `GOOGLE_CLOUD_PROJECT` | Required for `--trace_to_cloud` |
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### 1. Agent outputs raw JSON instead of calling tools
|
||||
|
||||
**Symptom:** Agent with `output_schema` dumps JSON text instead of calling tools.
|
||||
**Cause:** `output_schema` sets `response_schema` on the LLM config, activating controlled generation (JSON-only mode).
|
||||
**Check:** Look for `response_mime_type: "application/json"` in the LLM request.
|
||||
**Location:** `src/google/adk/flows/llm_flows/basic.py`
|
||||
|
||||
### 2. Events missing from session / not visible to plugins
|
||||
|
||||
**Symptom:** Events from sub-agents don't appear in plugin callbacks or runner event stream.
|
||||
**Cause:** Direct `append_event` calls inside components bypass the runner's event loop.
|
||||
**Check:** Only the runner (`runners.py`) should call `append_event`. Components should yield events.
|
||||
|
||||
### 3. `NameError: name 'X' is not defined` at runtime
|
||||
|
||||
**Symptom:** `{"error": "name 'SomeClass' is not defined"}`
|
||||
**Cause:** Class imported under `TYPE_CHECKING` but used at runtime (e.g., `isinstance()`).
|
||||
**Fix:** Move import outside `TYPE_CHECKING` or use a local import.
|
||||
|
||||
### 4. Sub-agent doesn't have context from parent conversation
|
||||
|
||||
**Symptom:** Sub-agent only sees its own input, not the parent's history.
|
||||
**Cause:** Branch isolation — sub-agents on a branch only see events on that branch.
|
||||
**Fix:** Write the sub-agent's `description` to prompt the parent to include context in delegation input.
|
||||
|
||||
### 5. Agent validation errors at startup
|
||||
|
||||
**Symptom:** `ValueError` on agent construction.
|
||||
**Common causes:**
|
||||
- `"All tools must be set via LlmAgent.tools."` — Don't pass tools via `generate_content_config`
|
||||
- `"System instruction must be set via LlmAgent.instruction."` — Don't set via `generate_content_config`
|
||||
- `"Response schema must be set via LlmAgent.output_schema."` — Don't set via `generate_content_config`
|
||||
**Location:** `src/google/adk/agents/llm_agent.py` — `validate_generate_content_config`
|
||||
|
||||
### 6. LLM calls exceeding limit
|
||||
|
||||
**Symptom:** `LlmCallsLimitExceededError: Max number of llm calls limit of N exceeded`
|
||||
**Cause:** `run_config.max_llm_calls` limit reached.
|
||||
**Fix:** Increase `max_llm_calls` in `RunConfig`, or investigate why the agent is looping.
|
||||
**Location:** `src/google/adk/agents/invocation_context.py`
|
||||
|
||||
### 7. Tool errors silently swallowed
|
||||
|
||||
**Symptom:** Tool call fails but agent continues without expected result.
|
||||
**Cause:** Errors are caught and returned as function response text. Set `on_tool_error_callback` to customize.
|
||||
**Check:** Look for error text in function response events.
|
||||
|
||||
### 8. Agent not loading / not discovered
|
||||
|
||||
**Symptom:** `adk web` doesn't list the agent, or returns 404.
|
||||
**Cause:** Agent directory must follow convention:
|
||||
```
|
||||
my_agent/
|
||||
__init__.py # MUST contain: from . import agent
|
||||
agent.py # MUST define: root_agent = Agent(...) OR app = App(...)
|
||||
```
|
||||
|
||||
### 9. Sync tool blocking the event loop
|
||||
|
||||
**Symptom:** Agent hangs or becomes very slow.
|
||||
**Cause:** Sync tools run in a thread pool (max 4 workers). All workers busy → new tool calls block.
|
||||
**Fix:** Make tools async if they do I/O.
|
||||
|
||||
---
|
||||
|
||||
## LLM Finish Reasons
|
||||
|
||||
- `STOP` — normal completion
|
||||
- `MAX_TOKENS` — output truncated (increase `max_output_tokens`)
|
||||
- `SAFETY` — blocked by safety filters
|
||||
- `RECITATION` — blocked for recitation
|
||||
|
||||
---
|
||||
|
||||
## Event Flow Architecture
|
||||
|
||||
```
|
||||
User message
|
||||
-> Runner.run_async()
|
||||
-> Runner._exec_with_plugin() # persists events, runs plugins
|
||||
-> agent.run_async() # yields events
|
||||
-> LlmAgent._run_async_impl()
|
||||
-> BaseLlmFlow.run_async() # Execution flow
|
||||
-> _AutoFlow or _SingleFlow # Flow implementations
|
||||
-> call_llm # LLM request + response
|
||||
-> execute_tools # tool dispatch (functions.py)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Callback Chain
|
||||
|
||||
**Before model call:** PluginManager `run_before_model_callback()` → agent `canonical_before_model_callbacks`
|
||||
**After model call:** PluginManager `run_after_model_callback()` → agent `canonical_after_model_callbacks`
|
||||
**Before/after tool call:** PluginManager `run_before_tool_callback()` / `run_after_tool_callback()` → agent callbacks
|
||||
|
||||
---
|
||||
|
||||
## Key Files for Debugging
|
||||
|
||||
| Area | File |
|
||||
|---|---|
|
||||
| Runner event loop | `src/google/adk/runners.py` |
|
||||
| LLM request building | `src/google/adk/flows/llm_flows/basic.py` |
|
||||
| Tool dispatch | `src/google/adk/flows/llm_flows/functions.py` |
|
||||
| Multi-agent orchestration | `src/google/adk/workflow/` |
|
||||
| Content/context building | `src/google/adk/flows/llm_flows/contents.py` |
|
||||
| Task support | `src/google/adk/agents/llm/task/` |
|
||||
| Agent config + validation | `src/google/adk/agents/llm_agent.py` |
|
||||
| Event model | `src/google/adk/events/event.py` |
|
||||
| Session services | `src/google/adk/sessions/` |
|
||||
| Invocation context | `src/google/adk/agents/invocation_context.py` |
|
||||
| Web server + debug endpoints | `src/google/adk/cli/adk_web_server.py` |
|
||||
| Debug output printer | `src/google/adk/utils/_debug_output.py` |
|
||||
|
||||
---
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
1. **Start with logs** — `-v` flag, check `/tmp/agents_log/agent.latest.log`
|
||||
2. **Inspect the session** — curl endpoints (`adk web`) or print events (`adk run`)
|
||||
3. **Check event actions** — `transfer_to_agent`, `request_task`, `finish_task`, `escalate`
|
||||
4. **Check event.output** — single_turn and task agents set output here
|
||||
5. **Check traces** — `/debug/trace/session/{id}` for model/token usage
|
||||
6. **Verify agent structure** — `__init__.py` imports, `root_agent` or `app` defined
|
||||
7. **Check tool responses** — look for error text in function response events
|
||||
8. **Check LLM finish reason** — `STOP`, `MAX_TOKENS`, `SAFETY`
|
||||
9. **Test in isolation** — create a minimal agent with just the problem tool/config
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: adk-git
|
||||
description: Use for any git operation (commit, push, pull, rebase, branch, PR, cherry-pick, etc.). Provides commit message format and conventions.
|
||||
---
|
||||
|
||||
# Git Operations for adk-python
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
Use **Conventional Commits**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation only
|
||||
- `style`: Formatting, no code change
|
||||
- `refactor`: Code restructure without behavior change
|
||||
- `perf`: Performance improvement
|
||||
- `test`: Adding/updating tests
|
||||
- `chore`: Build, config, dependencies
|
||||
- `ci`: CI/CD changes
|
||||
|
||||
### Description Phrasing
|
||||
|
||||
**CRITICAL**: The subject line must answer **why**, not just **what**.
|
||||
A reviewer reading only the subject should understand the motivation.
|
||||
|
||||
- **State the outcome**, not the mechanics:
|
||||
- Good: `Fix race condition when two agents write to same session`
|
||||
- Bad: `Update session.py to add lock`
|
||||
- **Name the capability added**, not the implementation:
|
||||
- Good: `Support parallel tool execution in workflows`
|
||||
- Bad: `Add asyncio.gather call in execute_tools_node`
|
||||
- **For refactors, state the reason**, not just the action:
|
||||
- Good: `Make graph public for dev UI serialization`
|
||||
- Bad: `Make graph a public field on new Workflow`
|
||||
- **For bug fixes, state what was broken**:
|
||||
- Good: `Prevent duplicate events when resuming HITL`
|
||||
- Bad: `Check interrupt_id before appending`
|
||||
|
||||
### Detailed Commit Messages
|
||||
|
||||
Promote detailed commit messages by including a short, concrete explanation in the body:
|
||||
- For **features**: Give a sample usage or explain the new capability.
|
||||
- For **fixes**: Explain what caused the error and how the fix addresses it.
|
||||
|
||||
**Example (Feature):**
|
||||
```
|
||||
feat(workflow): Support JSON string parsing in schema validation
|
||||
|
||||
Automatically parse JSON strings into dicts or Pydantic models when input_schema or output_schema is defined on a node.
|
||||
```
|
||||
|
||||
**Example (Fix):**
|
||||
```
|
||||
fix(sessions): Prevent duplicate events when resuming HITL
|
||||
|
||||
The interrupt_id was not checked before appending, causing duplicates if the user resumed multiple times. Added a check to ignore already processed interrupts.
|
||||
```
|
||||
|
||||
Self-check before committing: read your subject line and ask "does this tell me _why_ someone made this change?" If it only describes _what_ changed, rewrite it.
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Imperative mood** - "Add feature" not "Added feature".
|
||||
2. **Capitalize** first letter of description (for release-please changelog).
|
||||
3. **No period** at end of subject line.
|
||||
4. **50 char limit** on subject line when possible, max 72.
|
||||
5. **Use body for context** - Add a blank line then explain _why_,
|
||||
not _how_, when the subject alone isn't enough.
|
||||
6. **Reference GitHub issues** - If the commit fixes a GitHub issue, include "Fixes #<issue-number>" or "Closes #<issue-number>" (or the full issue URL if cross-repository) in the commit message body.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
feat(agents): Support App pattern with lifecycle plugins
|
||||
fix(sessions): Prevent memory leak on concurrent session cleanup
|
||||
refactor(tools): Unify env var checks across tool implementations
|
||||
docs: Add contributing guide for first-time contributors
|
||||
```
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Before performing any commit, check if `pre-commit` is installed and configured with the expected hooks (`isort`, `pyink`, `addlicense`, `mdformat`). If not, remind the user to set up pre-commit hooks using the `adk-setup` skill.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: adk-review
|
||||
description: Reviews all local changes in the repository for errors, styling compliance, unintended outcomes, and necessary documentation/test/sample updates. Generates a report and assists in fixing identified issues on-demand. Triggers on "adk-review", "review changes", "pr review", "check code style", "verify changes".
|
||||
---
|
||||
|
||||
# ADK Change Reviewer (adk-review)
|
||||
|
||||
This skill guides AI assistants in performing a comprehensive, rigorous review of local repository changes before they are committed or submitted. It evaluates code correctness, style guidelines, architectural impact, and checks if associated tests, samples, and documentation need updates. It generates a detailed report and, upon explicit user request, assists in automatically fixing the identified issues.
|
||||
|
||||
> [!NOTE]
|
||||
> Always read this skill and follow its steps when asked to review local changes or before finalizing a PR/commit.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist Dimensions
|
||||
|
||||
### 1. Code Correctness & Errors
|
||||
- **Syntax & Types**: Ensure the code is free of syntax errors and conforms to strong typing guidelines. Avoid using `Any`, and prefer specific/abstract types. Use `X | None` instead of `Optional[X]`.
|
||||
- **Imports**: Verify there are no circular imports. Ensure absolute imports are used where appropriate.
|
||||
- **Exception Handling**: Avoid bare `except:`. Always catch specific exceptions and log them properly with context.
|
||||
- **Visibility**: Ensure internal modules and package-private attributes use proper naming (e.g., prefixed with `_`) per ADK rules.
|
||||
- **Edge Cases & Defensive Programming**:
|
||||
- **Type & Attribute Discrimination**: Explicitly verify an object's type (e.g., using `isinstance`) before checking type-specific or custom attributes (e.g., checking if a node is an `LlmAgent` before inspecting its `mode`), avoiding errors on unexpected types.
|
||||
- **Boundary and Null Conditions**: Ensure robust handling for boundary conditions and null values (e.g., `None`, empty collections, zero, or empty strings) using validation or fallback defaults.
|
||||
- **Preconditions & Invariants**: Validate that preconditions and state invariants are checked before performing core logic.
|
||||
|
||||
### 2. Code Quality & Design
|
||||
- **Complexity & Readability**: Identify overly complex functions or classes. Suggest refactoring (e.g., splitting functions, extracting helper classes) to improve readability and maintainability. Ensure code is self-documenting.
|
||||
- **Design Patterns**: Check if appropriate design patterns are used. Avoid anti-patterns. Ensure high cohesion and low coupling.
|
||||
- **Performance & Efficiency**: Look for performance bottlenecks, such as unnecessary database queries, redundant computations, inefficient loops, or excessive memory allocation.
|
||||
- **Security & Privacy**: Verify that inputs are validated, sensitive data is handled securely, and there are no potential security vulnerabilities (like injection, resource exhaustion, or exposure of internal state).
|
||||
|
||||
### 3. Style and Convention Compliance
|
||||
- **ADK Style Guide**: Cross-reference all code changes with the guidelines in the `adk-style` skill (including Pydantic v2 patterns, lazy logging evaluation, and file structure).
|
||||
- **Pre-commit Hooks**: Ensure changed files are formatted and linted. Remind the user to run `pre-commit run --files <files>` if hooks like `isort`, `pyink`, `addlicense`, or `mdformat` are not configured automatically.
|
||||
|
||||
### 4. Architectural Integrity & Unintended Outcomes
|
||||
- **Public API Stability**: Verify whether changes modify, remove, or restrict public-facing interfaces, classes, methods, argument lists, or CLI structures (e.g., in the public package namespaces under `src/google/adk/`). Breaking changes are unacceptable without a formal deprecation cycle under Semantic Versioning.
|
||||
- **Execution & Resumption**: If changing workflows, nodes, or state management, ensure compatibility with the ADK 2.0 event execution lifecycle and session resumption (HITL/checkpoints).
|
||||
- **Concurrency & Safety**: Check for race conditions or resource leaks. Ensure long-running or shared resources (like plugins, exporters, and connections) are closed/disposed of safely.
|
||||
|
||||
### 5. Documentation Impact (`docs/design` and `docs/guides`)
|
||||
- **Design & Architecture**: Determine if the change updates a core design contract. If so, check if design docs under `docs/design/` require updates or new documents need to be written.
|
||||
- **Guides**: If the changes introduce a new feature or change a public API/workflow pattern, check if the guides under `docs/guides/` need updates.
|
||||
|
||||
### 6. Sample Compatibility & Updates
|
||||
- **Sample Integrity**: Verify if existing samples under `contributing/samples/` are affected by the change.
|
||||
- **New Samples**: If the changes introduce a key new capability, assess whether a new sample should be added to demonstrate the feature (following `adk-sample-creator` conventions).
|
||||
|
||||
### 7. Test Coverage & Quality
|
||||
- **Coverage**: Ensure that all modified or new code paths have corresponding unit or integration tests under `tests/`.
|
||||
- **ADK Test Rules**: Ensure test implementations adhere to the 9 rules in the `adk-style` testing reference (e.g., using deterministic IDs, event normalization, and clean up utilities).
|
||||
|
||||
---
|
||||
|
||||
## Execution Workflow
|
||||
|
||||
When the `adk-review` skill is triggered, you MUST execute the following steps:
|
||||
|
||||
### Step 1: Retrieve Local Changes
|
||||
Run `git status` and `git diff` to identify exactly which files have been modified, added, or deleted.
|
||||
|
||||
### Step 2: Perform the Multi-Dimensional Review
|
||||
Analyze the retrieved diffs file-by-file against the seven dimensions in the Checklist. Identify any errors, deviations, or missing files (such as docs, tests, or samples).
|
||||
|
||||
### Step 3: Generate and Present a Review Report
|
||||
Generate a clear, beautifully formatted Markdown report categorized by priority:
|
||||
- 🔴 **Critical Errors, Bugs, & Security**: Syntax, type safety violations, race conditions, resource leaks, or security vulnerabilities.
|
||||
- 🟠 **Code Quality & Design**: High complexity, poor readability, performance bottlenecks, or architectural misalignment.
|
||||
- 🟡 **Style & Conventions**: Lints, formatting issues, non-lazy logging, or minor typing mismatches.
|
||||
- 🔵 **Documentation, Tests, & Samples**: Missing or stale test coverage, design docs, or user guides.
|
||||
|
||||
Include the specific filename and line number/context for each finding.
|
||||
|
||||
### Step 4: Present Findings and Stop
|
||||
Stop execution here. Do **NOT** call any code editing tools or modify the codebase automatically. Present the generated review report clearly to the user, highlighting key takeaways, and stop.
|
||||
|
||||
Do **NOT** ask the user if they want you to fix the issues, and do **NOT** offer interactive fixing options by default. Simply stop and wait for the user to explicitly command or ask you to fix the changes.
|
||||
|
||||
### Step 5 (Optional): Implement Authorized Fixes & Verify
|
||||
If, and only if, the user explicitly instructs or requests you to apply a fix for some or all of the identified findings:
|
||||
1. Perform the necessary edits using precise code editing tools. Ensure all fixes strictly comply with the established `adk-style` and `adk-architecture` rules.
|
||||
2. Verify correctness by running associated unit and integration tests (e.g., via `pytest` or pre-commit hooks) before concluding.
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: adk-sample-creator
|
||||
description: Author new samples for the ADK Python repository. Use this skill when the user wants to create a new sample demonstrating a feature or agent pattern (e.g., dynamic nodes, standalone agents, fan-out/fan-in) or when adding examples to subdirectories under `contributing/`.
|
||||
---
|
||||
|
||||
# ADK Sample Creator
|
||||
|
||||
This skill helps you create new samples for the ADK Python repository. You should search for subdirectories under `contributing` (such as `new_workflow_samples`, `workflow_samples`, etc.) and confirm with the user which folder they want to use before creating the sample.
|
||||
|
||||
> [!TIP]
|
||||
|
||||
> Before creating samples, you can use the `adk-style` skill to learn about ADK 2.0 architecture knowledge and best practices.
|
||||
|
||||
A sample consists of:
|
||||
|
||||
1. A directory per sample.
|
||||
2. An `agent.py` file defining the agent or workflow logic.
|
||||
3. A `README.md` file explaining the sample.
|
||||
|
||||
## Guidelines
|
||||
|
||||
### 1. Folder Name
|
||||
|
||||
Use snake_case for the folder name (e.g., `dynamic_nodes`, `fan_out_fan_in`).
|
||||
|
||||
### 2. `agent.py` Content
|
||||
|
||||
The `agent.py` should focus on demonstrating a specific feature or agent pattern. Use absolute imports for testing convenience.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Model Selection**: Do not set the `model` parameter explicitly (e.g., `model="gemini-2.5-flash"`) on `Agent` instances in sample agents. Instead, let them default to the system-configured model, unless a specific model is explicitly requested by the user.
|
||||
|
||||
Choose one of the following patterns:
|
||||
|
||||
#### Pattern A: Workflows (for complex graphs)
|
||||
|
||||
Use this when you need multiple nodes, routing, or parallel execution.
|
||||
|
||||
**Imports:**
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
from google.adk import Context
|
||||
from google.adk.workflow import node
|
||||
from google.adk.workflow import JoinNode
|
||||
from google.adk.workflow._workflow_class import Workflow
|
||||
```
|
||||
|
||||
**Anatomy:**
|
||||
|
||||
```python
|
||||
my_agent = Agent(name="my_agent", ...)
|
||||
|
||||
@node()
|
||||
async def my_node(node_input: str):
|
||||
return "result"
|
||||
|
||||
root_agent = Workflow(
|
||||
name="root_wf",
|
||||
edges=[("START", my_node)],
|
||||
)
|
||||
```
|
||||
|
||||
#### Pattern B: Standalone Agents (for single-agent or simple tool use)
|
||||
|
||||
Use this when you don't need a graph and the agent handles the loop.
|
||||
|
||||
**Imports:**
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
from google.adk.tools import google_search # example
|
||||
```
|
||||
|
||||
**Anatomy:**
|
||||
|
||||
```python
|
||||
root_agent = Agent(
|
||||
name="standalone_assistant",
|
||||
instruction="You are a helpful assistant.",
|
||||
description="An assistant that can help with queries.",
|
||||
tools=[google_search],
|
||||
)
|
||||
```
|
||||
|
||||
### 3. `README.md` Content
|
||||
|
||||
Each sample should have a `README.md` with the following structure:
|
||||
|
||||
- **Overview**: What the sample does.
|
||||
- **Sample Inputs**: Examples of inputs to test with. Each prompt must be wrapped in backticks. If a prompt has an explanation, always add a blank line between the prompt and the explanation, and indent the explanation by two spaces.
|
||||
- **Graph**: Visualization of the graph flow (Mermaid recommended). For Workflow root agents, visualize the graph flow of nodes. For LlmAgent root agents that orchestrate tools or sub-agents, visualize the topology of the agent and its tools/sub-agents instead of internal workflow nodes.
|
||||
- **How To**: Explanation of key techniques used (e.g., `ctx.run_node`).
|
||||
- **Related Guides**: Links to relevant developer guides in `docs/guides/` that explain the concepts or classes used.
|
||||
|
||||
#### README Example Template:
|
||||
|
||||
````markdown
|
||||
# ADK Sample Name
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description.
|
||||
|
||||
## Sample Inputs
|
||||
|
||||
- `Prompt example 1`
|
||||
|
||||
- `Prompt example 2`
|
||||
|
||||
*Explanation or expected behavior*
|
||||
|
||||
## Graph
|
||||
|
||||
For Workflow root agents:
|
||||
```mermaid
|
||||
graph TD
|
||||
START --> MyNode
|
||||
```
|
||||
|
||||
For LlmAgent root agents:
|
||||
```mermaid
|
||||
graph TD
|
||||
MyAgent[my_agent] -->|calls| MyTool(my_tool)
|
||||
```
|
||||
|
||||
## How To
|
||||
|
||||
Explain the details.
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [Guide Title](../../docs/guides/path/to/guide.md) - Brief description of what the guide covers.
|
||||
````
|
||||
|
||||
## Examples
|
||||
|
||||
### Dynamic Nodes
|
||||
Snippet from `dynamic_nodes/agent.py`:
|
||||
```python
|
||||
@node(rerun_on_resume=True)
|
||||
async def orchestrate(ctx: Context, node_input: str) -> str:
|
||||
while True:
|
||||
headline = await ctx.run_node(generate_headline)
|
||||
# ...
|
||||
````
|
||||
|
||||
### Fan Out Fan In
|
||||
|
||||
Snippet from `fan_out_fan_in/agent.py`:
|
||||
|
||||
```python
|
||||
root_agent = Workflow(
|
||||
name="root_agent",
|
||||
edges=[("START", (node_a, node_b), join_node, aggregate)],
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: adk-setup
|
||||
description: Set up a local development environment for the ADK Python project. Use when the user wants to get started developing, set up their environment, install dependencies, or prepare for contributing.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Set up the local development environment for ADK Python.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Check the following before proceeding:
|
||||
|
||||
1. **Python 3.10+**
|
||||
|
||||
```bash
|
||||
python3 --version
|
||||
```
|
||||
|
||||
2. **uv package manager** (required — do not use pip/venv directly)
|
||||
```bash
|
||||
uv --version
|
||||
```
|
||||
If not installed:
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
## Setup Steps
|
||||
|
||||
Run these commands from the project root:
|
||||
|
||||
3. **Create and activate a virtual environment:**
|
||||
|
||||
```bash
|
||||
uv venv --python "python3.11" ".venv"
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
4. **Install all dependencies for development:**
|
||||
|
||||
```bash
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
5. **Install development tools:**
|
||||
|
||||
```bash
|
||||
uv tool install pre-commit
|
||||
uv tool install tox --with tox-uv
|
||||
```
|
||||
|
||||
6. **Install addlicense (requires Go):**
|
||||
|
||||
```bash
|
||||
go version && go install github.com/google/addlicense@latest
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If Go is not installed, tell the user:
|
||||
> "Go is required for the addlicense tool. Please install Go from https://go.dev/dl/ and then re-run the `adk-setup` skill to complete the setup."
|
||||
|
||||
7. **Set up pre-commit hooks:**
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
8. **Verify everything works by running tests locally:**
|
||||
```bash
|
||||
pytest tests/unittests -n auto
|
||||
```
|
||||
|
||||
## Key Commands Reference
|
||||
|
||||
| Task | Command |
|
||||
| :----------------------------------- | :------------------------------------------------ |
|
||||
| Run unit tests (Fast) | `pytest tests/unittests` |
|
||||
| Run tests across all Python versions | `tox` |
|
||||
| Format codebase | `pre-commit run --all-files` |
|
||||
| Run tests in parallel | `pytest tests/unittests -n auto` |
|
||||
| Run specific test file | `pytest tests/unittests/agents/test_llm_agent.py` |
|
||||
| Launch web UI | `adk web path/to/agents_dir` |
|
||||
| Run agent via CLI | `adk run path/to/my_agent` |
|
||||
| Build wheel | `uv build` |
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: adk-style
|
||||
description: ADK development style guide for routine nits — Python idioms, codebase conventions, imports, typing, Pydantic patterns, formatting, logging, async/concurrency, and file organization. Use this skill whenever writing code, tests, or reviewing PRs for the ADK project to ensure compliance with styling and coding conventions. Triggers on "code style", "how should I format", "naming convention", "lint", "nit", "imports", "typing", "Pydantic patterns", "testing rules", "async", "io".
|
||||
---
|
||||
|
||||
# ADK Style Guide
|
||||
|
||||
## Style Guide (references/)
|
||||
- [Visibility](references/visibility.md) — naming conventions for module-private, internal, and package-private visibility.
|
||||
- [Imports](references/imports.md) — relative vs absolute imports, `TYPE_CHECKING` patterns.
|
||||
- [Typing](references/typing.md) — strong typing, avoiding Any, bare type names, keyword-only arguments, `Optional` vs `| None`, abstract parameter types, mutable default avoidance, runtime type discrimination.
|
||||
- [Pydantic Patterns](references/pydantic.md) — Pydantic v2 usage, `Field()` constraints, `field_validator`, `model_validator`, private attributes, deprecation migration, post-init setup.
|
||||
- [Formatting](references/formatting.md) — indentation, line limits, and running pre-commit hooks.
|
||||
- [Documentation](references/documentation.md) — comments and docstrings.
|
||||
- [Logging](references/logging.md) — lazy evaluation and log levels.
|
||||
- [Async and Concurrency](references/async.md) — async I/O requirements, avoiding blocking the event loop.
|
||||
- [File Organization](references/file-organization.md) — file headers and class organization.
|
||||
|
||||
## Testing
|
||||
[references/testing.md](references/testing.md) — core principles, 9 rules for writing ADK tests, test structure template
|
||||
@@ -0,0 +1,19 @@
|
||||
# Async and Concurrency Style Guide
|
||||
|
||||
- **All I/O operations must be in async functions**: Any operation that
|
||||
performs I/O (network calls, file system access, database queries, etc.)
|
||||
must be defined in an `async def` function.
|
||||
- **Do not block the event loop**: Avoid calling blocking synchronous
|
||||
functions directly from async code.
|
||||
- **Wrap synchronous I/O**: If you must use a synchronous library for I/O
|
||||
(e.g., standard `open()`, `pathlib` file operations, or synchronous
|
||||
clients), wrap the blocking call in `asyncio.to_thread` to run it in a
|
||||
separate thread and prevent blocking the main event loop.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
async def save_data(path: Path, data: bytes) -> None:
|
||||
# Wrap blocking file write in asyncio.to_thread
|
||||
await asyncio.to_thread(path.write_bytes, data)
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
# Documentation and Comments
|
||||
|
||||
## Public API Documentation
|
||||
|
||||
- **Clear Usage**: For public interfaces, explain the intended usage clearly, with concise examples.
|
||||
- **Public Classes**: Explain all public attributes.
|
||||
- **Public Methods/Functions**: Explain all arguments, return values, and raised exceptions.
|
||||
|
||||
## Internal Implementation Comments
|
||||
|
||||
- **Explain Why, Not What**: For internal code and private methods, explain **why**, not **what** — the code itself should be self-documenting.
|
||||
- **Stale References**: Don't reference RFCs or design docs in source code (they become stale).
|
||||
@@ -0,0 +1,15 @@
|
||||
# File Organization
|
||||
|
||||
- One class per file in `workflow/`.
|
||||
- Private modules prefixed with `_` (e.g., `_base_node.py`).
|
||||
- Public API exported through `__init__.py`.
|
||||
- Unit tests must be placed in the same folder hierarchy under `tests/unittests/` as the original file in `src/`.
|
||||
- If a single source file has multiple test files (e.g. testing different classes or behaviors separately), use the source file name (without leading underscores or extension) as the prefix for the test file names.
|
||||
- Example: `src/google/adk/tools/environment/_tools.py` -> `tests/unittests/tools/environment/test_tools_edit_file.py`
|
||||
|
||||
## File Headers
|
||||
|
||||
Every source file must have:
|
||||
1. Apache 2.0 license header.
|
||||
2. `from __future__ import annotations`.
|
||||
3. Standard library imports, then third-party, then relative.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Formatting Style Guide
|
||||
|
||||
- 2-space indentation (never tabs).
|
||||
- 80-character line limit.
|
||||
- `pyink` formatter (Google-style).
|
||||
- `isort` with Google profile for import sorting.
|
||||
- Enforced automatically by pre-commit hooks (`isort`, `pyink`, `addlicense`, `mdformat`). Use the `adk-setup` skill to install and configure these tools.
|
||||
|
||||
## Running Formatter Manually
|
||||
|
||||
```bash
|
||||
# Format only staged files (runs automatically on commit)
|
||||
pre-commit run
|
||||
|
||||
# Format all changed files (staged + unstaged)
|
||||
pre-commit run --files $(git diff --name-only HEAD)
|
||||
|
||||
# Format all files in the repo
|
||||
pre-commit run --all-files
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# Imports Style Guide
|
||||
|
||||
## General Rules
|
||||
|
||||
- **Source code** (`src/`): Use relative imports.
|
||||
`from ..agents.llm_agent import LlmAgent`
|
||||
- **Tests** (`tests/`): Use absolute imports.
|
||||
`from google.adk.agents.llm_agent import LlmAgent`
|
||||
- **Import from module**: Import from the module file, not from `__init__.py`.
|
||||
`from ..agents.llm_agent import LlmAgent` (not `from ..agents import LlmAgent`)
|
||||
- **CLI package** (`cli/`):
|
||||
- Treat as an external package.
|
||||
- Use **relative imports** for files within the `cli/` package.
|
||||
- Use **absolute imports** for files outside of the `cli/` package.
|
||||
- **Dependency Direction**: Only `cli/` can import from the rest of the codebase. The other codebase must **STRICTLY NOT** import from `cli/`.
|
||||
|
||||
## TYPE_CHECKING Imports
|
||||
|
||||
Use `TYPE_CHECKING` for imports needed only by type hints to avoid circular imports at runtime:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..agents.invocation_context import InvocationContext
|
||||
```
|
||||
|
||||
This works because `from __future__ import annotations` makes all annotations strings (deferred evaluation), so the import is never needed at runtime.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Logging Style Guide
|
||||
|
||||
## General Rules
|
||||
|
||||
- **Lazy Evaluation**: Use lazy-evaluated `%`-based templates for logging to avoid overhead when the log level is not enabled.
|
||||
- **Good**: `logging.info("Processing item %s", item_id)`
|
||||
- **Bad**: `logging.info(f"Processing item {item_id}")`
|
||||
- **Contextual Logging**: Leverage structured logging and trace IDs when available to correlate logs across operations.
|
||||
- **No Secrets**: Never log sensitive information (API keys, user credentials, or PII).
|
||||
|
||||
## Log Levels
|
||||
|
||||
- **DEBUG**: Detailed information for diagnosing problems. Use generously in internal implementation but avoid cluttering production logs.
|
||||
- **INFO**: Confirmation that things are working as expected (e.g., workflow started, node completed).
|
||||
- **WARNING**: Indication that something unexpected happened or a problem might occur soon (e.g., retry triggered).
|
||||
- **ERROR**: A serious problem that prevented a function or operation from completing.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Pydantic Patterns
|
||||
|
||||
ADK models use Pydantic v2. This guide covers the key patterns used throughout the codebase.
|
||||
|
||||
## Basic Model Structure
|
||||
|
||||
- Use `Field()` for validation, defaults, and descriptions.
|
||||
- Use `PrivateAttr()` for internal state that shouldn't be serialized.
|
||||
- Use `model_post_init()` instead of `__init__` for setup logic.
|
||||
- Prefer `model_dump()` over `dict()` (Pydantic v2).
|
||||
|
||||
## On-Wire Models
|
||||
|
||||
For Pydantic models that cross network or system boundaries (e.g., API payloads, WebSocket messages, event persistence), inherit from `SerializedBaseModel` located in `google.adk.utils._serialized_base_model`.
|
||||
|
||||
This ensures:
|
||||
- camelCase serialization by default (via `alias_generator=to_camel`).
|
||||
|
||||
## Docstrings as Field Descriptions
|
||||
|
||||
To keep code Pythonic and ensure that generated schemas stay in sync with documentation, it is **strongly recommended** to use docstrings as field descriptions for all Pydantic models in the ADK codebase.
|
||||
|
||||
To enable this, add `use_attribute_docstrings=True` to your model's `ConfigDict`:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
class MyModel(BaseModel):
|
||||
model_config = ConfigDict(use_attribute_docstrings=True)
|
||||
|
||||
field_name: str
|
||||
"""Description of the field."""
|
||||
```
|
||||
|
||||
Note: If you are inheriting from `SerializedBaseModel`, this is already enabled by default.
|
||||
|
||||
## Summary of When to Use Each
|
||||
|
||||
| Need | Pattern |
|
||||
|---|---|
|
||||
| Simple numeric/string bounds | `Field(ge=0, le=100)` |
|
||||
| Single-field business logic | `@field_validator('field', mode='after')` |
|
||||
| Cross-field consistency | `@model_validator(mode='after')` |
|
||||
| Field deprecation/migration | `@model_validator(mode='before')` |
|
||||
| Internal mutable state | `PrivateAttr(default_factory=...)` |
|
||||
| Post-construction setup | `model_post_init()` |
|
||||
|
||||
## `Field()` with Constraints
|
||||
|
||||
Use `Field()` constraints for declarative validation directly on the field definition. This keeps validation close to the data declaration and avoids custom validator boilerplate.
|
||||
|
||||
|
||||
|
||||
## `field_validator` — Single-Field Validation
|
||||
|
||||
Use `@field_validator` for validation logic that goes beyond simple constraints. This is heavily used in ADK (36+ instances). Always use `mode='after'` unless you need to intercept raw input before Pydantic coercion.
|
||||
|
||||
|
||||
**Rules:**
|
||||
- Decorate with `@field_validator(...)`. While `@classmethod` is automatically applied by Pydantic v2, adding it is recommended in ADK for explicit visibility.
|
||||
- Return the (possibly transformed) value.
|
||||
- Raise `ValueError` with a descriptive message on failure.
|
||||
- Prefer `mode='after'` (validates after Pydantic's own parsing/coercion).
|
||||
|
||||
## `model_validator` — Cross-Field and Migration Validation
|
||||
|
||||
Use `@model_validator` when validation depends on multiple fields, or when handling deprecation/migration of field names.
|
||||
|
||||
### `mode='before'` — Deprecation and Field Migration
|
||||
|
||||
|
||||
### `mode='after'` — Cross-Field Consistency
|
||||
|
||||
|
||||
**Rules:**
|
||||
- `mode='before'`: receives raw `data` (usually `dict`). Use for field renaming, deprecation, and input normalization. Must return the (modified) data.
|
||||
- `mode='after'`: receives the fully constructed model instance (`self`). Use for cross-field consistency checks. Must return `self`.
|
||||
- Always guard `mode='before'` validators with `isinstance(data, dict)` since data could also come as an existing model instance.
|
||||
@@ -0,0 +1,232 @@
|
||||
# ADK Testing Style Guide
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Test through the public interface** — call what users call, assert what users see.
|
||||
- **Test behavior, not implementation** — verify outcomes (outputs, side effects, errors), not internal mechanics.
|
||||
- **Refactor-proof** — if an internal refactor preserves the same behavior, all tests should still pass.
|
||||
|
||||
## Rules
|
||||
|
||||
### 1. Test names describe the behavior, not the mechanism
|
||||
|
||||
```python
|
||||
# Good — describes what the caller observes
|
||||
def test_empty_queue_returns_none():
|
||||
def test_retry_stops_after_max_attempts():
|
||||
def test_missing_key_raises_key_error():
|
||||
|
||||
# Bad — describes implementation details
|
||||
def test_deque_popleft_called():
|
||||
def test_retry_counter_incremented():
|
||||
def test_dict_getitem_raises():
|
||||
```
|
||||
|
||||
### 2. Docstring: one-line summary, then setup/act/assert
|
||||
|
||||
The first line describes the expected behavior from the caller's
|
||||
perspective. For complex tests (multi-step, multi-invocation),
|
||||
follow with a structured breakdown of Setup, Act, and Assert.
|
||||
|
||||
```python
|
||||
# Good — simple test, one-liner is enough
|
||||
"""Getting from an empty cache returns the default value."""
|
||||
|
||||
# Good — complex test with structured breakdown
|
||||
"""Partial FR re-runs nested Workflow, resolved child completes
|
||||
while unresolved stays interrupted.
|
||||
|
||||
Setup: outer_wf → inner_wf → (child_a, child_b) → join.
|
||||
Both children interrupt on first run.
|
||||
Act:
|
||||
- Run 2: resolve only child_a's FR.
|
||||
- Run 3: resolve child_b's FR.
|
||||
Assert:
|
||||
- Run 2: child_a produces output, invocation still interrupted.
|
||||
- Run 3: child_b produces output, join completes, no interrupts.
|
||||
"""
|
||||
|
||||
# Bad — restates the implementation
|
||||
"""LRUCache._store.get returns sentinel when key missing."""
|
||||
"""ThreadPool._accept_tasks flag checked in submit()."""
|
||||
```
|
||||
|
||||
### 3. Each test covers one behavior
|
||||
|
||||
If a test checks multiple unrelated behaviors, split it. If you can't
|
||||
describe the test in one sentence, it's testing too much.
|
||||
|
||||
```python
|
||||
# Bad — tests capacity AND eviction AND default in one test
|
||||
def test_cache_behavior():
|
||||
assert cache.size == 0
|
||||
assert cache.get('x') is None
|
||||
cache.put('a', 1)
|
||||
assert cache.size == 1
|
||||
|
||||
# Good — split into focused tests
|
||||
def test_new_cache_is_empty():
|
||||
"""A freshly created cache has no entries."""
|
||||
|
||||
def test_cache_evicts_oldest_when_full():
|
||||
"""Adding to a full cache removes the least recently used entry."""
|
||||
```
|
||||
|
||||
### 4. Don't test internal state
|
||||
|
||||
```python
|
||||
# Bad — reaches into private attributes
|
||||
assert pool._workers[0].is_alive
|
||||
assert parser._state == 'HEADER'
|
||||
assert isinstance(router._handler, _FastHandler)
|
||||
|
||||
# Good — tests through the public interface
|
||||
assert pool.active_count == 1
|
||||
assert parser.parse('data') == expected
|
||||
assert router.route('/api') == handler
|
||||
```
|
||||
|
||||
### 5. Use real components, mock only boundaries
|
||||
|
||||
ADK tests should use real implementations as much as possible
|
||||
instead of mocking.
|
||||
|
||||
- Mock external dependencies: LLM APIs, cloud services, session stores
|
||||
- Use real ADK components: BaseNode subclasses, Event, Context
|
||||
- Mock InvocationContext when testing NodeRunner (it's a boundary)
|
||||
|
||||
### 6. Test fixtures should be minimal
|
||||
|
||||
Define the simplest possible setup that triggers the behavior:
|
||||
|
||||
```python
|
||||
# Good — minimal fixture, one purpose
|
||||
def make_user(role='viewer'):
|
||||
return User(name='test', email='t@t.com', role=role)
|
||||
|
||||
# Bad — kitchen-sink fixture with unrelated setup
|
||||
def make_full_test_env():
|
||||
db = create_database()
|
||||
user = create_user_with_billing()
|
||||
setup_notifications()
|
||||
...
|
||||
```
|
||||
|
||||
### 7. Keep arrange logic close to the test
|
||||
|
||||
When a helper class or fixture is used by only one test, define it
|
||||
inline inside the test function. This keeps the setup visible at the
|
||||
point of use and avoids scrolling to distant module-level definitions.
|
||||
Extract to module level only when 3+ tests share the same helper.
|
||||
|
||||
```python
|
||||
# Good — helper defined inline, right next to the test
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_delta_bundled_with_output():
|
||||
"""State set before yield is flushed onto the output event."""
|
||||
|
||||
class _Node(BaseNode):
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.state['color'] = 'blue'
|
||||
yield 'result'
|
||||
|
||||
ctx, events = _make_ctx()
|
||||
|
||||
await NodeRunner(node=_Node(name='n'), parent_ctx=ctx).run()
|
||||
|
||||
assert events[0].output == 'result'
|
||||
assert events[0].actions.state_delta['color'] == 'blue'
|
||||
|
||||
# Bad — helper defined 300 lines above, reader must scroll
|
||||
class _StateThenOutputNode(BaseNode):
|
||||
async def _run_impl(self, *, ctx, node_input):
|
||||
ctx.state['color'] = 'blue'
|
||||
yield 'result'
|
||||
|
||||
# ... 300 lines later ...
|
||||
async def test_state_delta_bundled_with_output():
|
||||
node = _StateThenOutputNode(name='n')
|
||||
...
|
||||
```
|
||||
|
||||
### 8. Assertions tell a story
|
||||
|
||||
```python
|
||||
# Good — reads like a specification
|
||||
assert queue.size == 0
|
||||
assert config.get('timeout') == 30
|
||||
assert response.status_code == 404
|
||||
|
||||
# Bad — overly defensive, tests framework behavior
|
||||
assert isinstance(queue, Queue)
|
||||
assert hasattr(config, 'get')
|
||||
assert len(response.headers) > 0
|
||||
```
|
||||
|
||||
### 9. Structure tests as arrange, act, assert
|
||||
|
||||
Every test has three distinct steps:
|
||||
|
||||
- **Arrange** — set up the external state specific to the scenario.
|
||||
General setup shared by many tests belongs in fixtures.
|
||||
- **Act** — call the system under test. Usually a single call.
|
||||
- **Assert** — verify return values or visible state changes. No
|
||||
further calls to the system under test here.
|
||||
|
||||
Keep steps distinct. Separate with blank lines. In simple tests where
|
||||
each step is a single statement, blank lines can be omitted. In
|
||||
complex tests, use descriptive comments like "Given [situation]",
|
||||
"When [action]", "Then [expectation]" — avoid bare labels that add
|
||||
no information.
|
||||
|
||||
```python
|
||||
# Good — clear visual separation
|
||||
def test_cache_returns_stored_value():
|
||||
cache = Cache()
|
||||
cache.put('key', 'value')
|
||||
|
||||
result = cache.get('key')
|
||||
|
||||
assert result == 'value'
|
||||
|
||||
# Good — simple test, blank lines omitted
|
||||
def test_new_cache_is_empty():
|
||||
assert Cache().size == 0
|
||||
|
||||
# Bad — steps interleaved
|
||||
def test_cache_behavior():
|
||||
cache = Cache()
|
||||
cache.put('key', 'value')
|
||||
result = cache.get('key')
|
||||
assert result == 'value'
|
||||
cache.put('key2', 'value2') # more setup after assert
|
||||
assert cache.size == 2
|
||||
```
|
||||
|
||||
### Test Structure Template
|
||||
|
||||
```python
|
||||
"""Tests for <ComponentName>.
|
||||
|
||||
Verifies that <component> correctly <high-level behavior>.
|
||||
"""
|
||||
|
||||
# --- Fixtures (minimal, one purpose each) ---
|
||||
|
||||
def _make_service():
|
||||
...
|
||||
|
||||
# --- Tests (one behavior per test) ---
|
||||
|
||||
def test_<behavior_description>():
|
||||
"""<One sentence: what the system does from the outside.>"""
|
||||
# Given a service with default config
|
||||
service = _make_service()
|
||||
input_data = 'hello'
|
||||
|
||||
# When the operation is performed
|
||||
result = service.do_something(input_data)
|
||||
|
||||
# Then the result matches expectations
|
||||
assert result == expected
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
# Type Hints and Strong Typing
|
||||
|
||||
## General Rules
|
||||
|
||||
- **Prefer Strong Typing**: Use type hints for all function arguments and return types. Avoid leaving types unspecified.
|
||||
- **Minimize `Any`**: Use specific types or `Generic` whenever possible. Avoid `Any` as it bypasses type checking.
|
||||
- **No double-quoted type hints**: When `from __future__ import annotations` is present, use bare type names (e.g., `list[str]` instead of `"list[str]"`).
|
||||
- **Always include `from __future__ import annotations`**: Every source file must include this immediately after the license header, before any other imports. This enables forward-referencing classes without quotes (PEP 563).
|
||||
|
||||
## `Optional[X]` vs `X | None`
|
||||
|
||||
The codebase uses both styles. Follow this convention:
|
||||
|
||||
- **New code** (especially in `workflow/`): Prefer `X | None` — it is more concise and modern.
|
||||
- **Existing files**: Match the style already used in the file for consistency.
|
||||
- **Both are acceptable** — do not refactor one to the other without reason.
|
||||
|
||||
|
||||
## Abstract Types for Function Parameters
|
||||
|
||||
Use abstract types from `collections.abc` for function parameter annotations. This accepts the widest range of inputs while remaining type-safe. Use concrete types for return annotations to give callers the most useful information.
|
||||
|
||||
|
||||
|
||||
## Keyword-Only Arguments
|
||||
|
||||
Use `*` to force keyword-only arguments on functions with multiple parameters of the same type, or where argument order is error-prone. This is a widely used pattern in ADK (16+ files).
|
||||
|
||||
|
||||
**When to use `*`:**
|
||||
- Constructors (`__init__`) with 2+ non-self parameters
|
||||
- Any function where swapping arguments would silently produce wrong results
|
||||
- Methods with multiple `str` or `int` parameters
|
||||
|
||||
## Mutable Default Arguments
|
||||
|
||||
**Never use mutable default arguments.** Use `None` as a sentinel and initialize in the function body. This is a well-followed pattern throughout ADK.
|
||||
|
||||
|
||||
This applies to `list`, `dict`, `set`, and any other mutable type.
|
||||
|
||||
## Runtime Type Discrimination with `isinstance()`
|
||||
|
||||
Use `isinstance()` for runtime type discrimination when handling polymorphic inputs. This is pervasive in ADK (700+ usages). Prefer exhaustive `if/elif` chains with a clear fallback.
|
||||
|
||||
|
||||
**Guidelines:**
|
||||
- Always include an `else` branch that raises `TypeError` or handles the unknown case.
|
||||
- Prefer `isinstance(x, SomeType)` over `type(x) is SomeType` — it handles subclasses correctly.
|
||||
- For checking multiple types: `isinstance(x, (TypeA, TypeB))`.
|
||||
|
||||
## No Asserts in Production Code
|
||||
|
||||
**Never use `assert` statements in production code.** They can be optimized away when Python runs with `-O` flags and provide poor error messages. Use specific exceptions like `ValueError`, `TypeError`, or `RuntimeError` instead.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Visibility Style Guide
|
||||
|
||||
Python does not have native access modifiers (like `public`, `private`, or `package-private`). ADK relies on naming conventions and module structure to define visibility boundaries.
|
||||
|
||||
## Conventions
|
||||
|
||||
### 1. Module-Private / Internal Files
|
||||
|
||||
- **Private by Default**: All new `.py` module files under `src/google/adk/` must be private by default (prefixed with `_`). This is enforced by a pre-commit hook (`check-new-py-prefix`).
|
||||
- Even if a file contains symbols intended for the public API, the file itself must have a leading underscore. The symbols are then exposed via the package's `__init__.py`.
|
||||
- Files intended for internal use within a package or subsystem must also be prefixed with a leading underscore (e.g., `_task_models.py`).
|
||||
- These files should **never** be imported directly by code outside of the ADK framework.
|
||||
|
||||
### 2. Class and Function Visibility
|
||||
|
||||
- **Public**: No leading underscore. Intended for use by consumers of the module or package.
|
||||
- **Internal/Private**: Leading underscore (e.g., `_private_method()`). Intended only for use within the defining class or module.
|
||||
|
||||
### 3. Package-Private (Subsystem Visibility)
|
||||
|
||||
Since Python lacks true package-private access, we simulate it by:
|
||||
|
||||
- **Not exporting** the symbol in the package's `__init__.py`.
|
||||
- Using `_`-prefixed modules for internal implementation details.
|
||||
- Code within the same package can import from these `_` modules, but code outside should not.
|
||||
- **Direct Imports Required**: Within the ADK framework, importing from `__init__.py` is **not allowed**. You must import from the specific module directly. This helps keep `__init__.py` minimal and keeps packages as self-contained as possible.
|
||||
|
||||
### 4. Public API Export
|
||||
|
||||
- The public API of a package must be explicitly exported in `__init__.py`.
|
||||
- **Use `__all__`**: The `__init__.py` file should define `__all__` to explicitly list the symbols that are part of the public API.
|
||||
- **Only public names** (symbols intended for use outside the package) should be imported into `__init__.py` and listed in `__all__`.
|
||||
- Users should be able to import public symbols directly from the package level, rather than digging into internal modules.
|
||||
|
||||
## Examples
|
||||
|
||||
### Exposing a Public Interface
|
||||
|
||||
```python
|
||||
# In src/google/adk/agents/llm/task/_task_agent.py (File is private by default)
|
||||
class TaskAgent: # Public symbol
|
||||
...
|
||||
|
||||
# In src/google/adk/agents/llm/task/__init__.py
|
||||
from ._task_agent import TaskAgent
|
||||
|
||||
__all__ = [
|
||||
'TaskAgent',
|
||||
]
|
||||
```
|
||||
|
||||
### Keeping Implementation Details Private
|
||||
|
||||
```python
|
||||
# In src/google/adk/agents/llm/task/_task_models.py (Internal file)
|
||||
class TaskRequest(BaseModel): # Public within the module, but module is private
|
||||
...
|
||||
|
||||
# In src/google/adk/agents/llm/task/__init__.py
|
||||
# We DO NOT export TaskRequest here if it is only for internal use within the task package.
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: adk-unit-design
|
||||
description: Creates or updates code unit design documents for source code documentation.
|
||||
---
|
||||
|
||||
# ADK Code Unit Design
|
||||
|
||||
This skill creates or updates a detailed software engineering design document for new or updated code file or specified code unit. The design document it generates is meant to explain the code to a developer who wants to modify or extend the code unit as part of the ADK development framework. Similar to a *unit test*, a *unit design* provides a generated software engineering design based on the *actual, implemented code* rather than any proposed code design or proposed software architecture.
|
||||
|
||||
## Input
|
||||
|
||||
- Code files containing new functionality
|
||||
- Names of new methods and classes (optional)
|
||||
- Code files for base classes or interfaces that the new functionality depends on (optional)
|
||||
- Code unit tests (optional)
|
||||
- Example code files (optional)
|
||||
|
||||
## Analysis
|
||||
|
||||
- Review specified code files for changes and named methods to determine:
|
||||
- Purpose and intended use of the new or updated code units
|
||||
- Any data flows handled by the new or updated code units
|
||||
- Dependencies required by the new or updated code units
|
||||
- Approaches for extending or customizing the code unit to add new capabilities
|
||||
- Classes that depend on the new or updated code units
|
||||
- Operational limitations of the new or updated code units
|
||||
|
||||
## Output
|
||||
|
||||
- Look for an existing design document in the `/docs/design/***` directory of this repository.
|
||||
- If a design already exists, update the existing design incrementally and prioritize preserving the previous content as much as possible.
|
||||
- If no design document exists, create a design file for the new code unit in the `/docs/design/***` directory of this repository, using the relative path of the code unit. For example, if the code unit is called `/topic/function/class.ext`, create a design document in the location `/docs/design/topic/function/class/index.md`.
|
||||
- Any links to local code files should be translated to URL links to the `google/adk-python` repository on GitHub. For example, if the local code unit path is `***/adk-python/topic/function/class.ext#L93`, the URL to the code file should be `https://github.com/google/adk-python/blob/main/topic/function/class.ext#L93`.
|
||||
|
||||
### Design document structure and content
|
||||
|
||||
Use the following structure and instructions to create the design document for the code unit:
|
||||
|
||||
```
|
||||
# (name of code unit or code file) - Code Unit Design
|
||||
|
||||
- 2-sentence summary of the code unit
|
||||
|
||||
## Introduction
|
||||
|
||||
- Paragraph(s) explaining:
|
||||
- The purpose and application of the code unit, including intended use cases
|
||||
- Developer problems solved by this code unit
|
||||
- Agent capabilities enabled by this code unit
|
||||
|
||||
## High-level architecture
|
||||
|
||||
- Describe the software architecture of this code unit and how it fits into the larger ADK framework
|
||||
- Explain general execution flow of this code unit
|
||||
- Describe any data flows handled by the code unit including inputs and outputs
|
||||
- Explain any cross-class dependencies of the code unit, including upstream dependencies and downstream dependencies
|
||||
|
||||
### Extension points
|
||||
|
||||
- Describe how the code unit could be extended or customized to add new features or capabilities
|
||||
- Note specific parts of the code unit that are designed to be extended or customized, including:
|
||||
- Abstract classes
|
||||
- Interfaces
|
||||
- Hooks
|
||||
- Callbacks
|
||||
- Configurable parameters
|
||||
- Plugin architecture
|
||||
- Other extension points
|
||||
|
||||
### Extension constraints
|
||||
|
||||
- Describe what parts of the code unit should not be modified, based on:
|
||||
- architectural constraints
|
||||
- implementation limitations
|
||||
- cross-class dependencies
|
||||
- other constraints
|
||||
|
||||
## Limitations
|
||||
|
||||
- Mention any limitations of the code unit, if known, such as:
|
||||
- input constraints
|
||||
- data structure constraints
|
||||
- output constraints
|
||||
- performance limitations
|
||||
- memory limitations
|
||||
- other limitations
|
||||
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: adk-unit-guide
|
||||
description: Creates detailed code unit guides for source code documentation.
|
||||
---
|
||||
|
||||
# ADK code unit guide
|
||||
This skill creates a detailed developer guide for new or updated code file or direct code input. The guide it generates is meant to explain the code to a developer who wants to use it in an application, but with a higher level of technical detail than what would appear in published developer documentation. Similar to a *unit test*, a *unit guide* provides generated, granular-level documentation for a unit of code, without worrying about bloating the actual developer documentation with too many details.
|
||||
|
||||
## Input
|
||||
|
||||
- Code files containing new functionality
|
||||
- Code unit tests (optional)
|
||||
- Code design files (optional)
|
||||
- Names of new methods and classes (optional)
|
||||
|
||||
## Analysis
|
||||
|
||||
- Review the code design files, if provided. Make note of:
|
||||
- Purpose and intended use of the new or updated code units
|
||||
- Classes that depend on the new or updated code units
|
||||
- Additional dependencies required by the new or updated code units
|
||||
- Limitations of the new or updated code units
|
||||
- Review specified code file for changes and named methods, if provided.
|
||||
- Determine what classes and code files may depend on the new or updated code units.
|
||||
|
||||
## Output
|
||||
|
||||
- Look for an existing guide in the `/docs/guides/***` directory of this repository.
|
||||
- If a guide already exists, update the existing guide incrementally and prioritize preserving the previous content as much as possible.
|
||||
- If no guide exists, create a guide file for the new code unit in the `/docs/guides/***` directory of this repository, using the relative path of the code unit. For example, if the code unit is called `/topic/function/class.ext`, create a guide in the location `/docs/guides/topic/function/class/index.md`.
|
||||
- **Update the Index**: Whenever a new guide is created, or an existing guide's title/summary changes, update the index file `/docs/guides/README.md`. Ensure the guide is listed under the correct category with a link and a brief summary.
|
||||
|
||||
### Guide structure and content
|
||||
|
||||
Use the following structure and instructions to create the guide for the code unit:
|
||||
|
||||
```
|
||||
# Title: name of the code file or code unit
|
||||
|
||||
- 2-sentence summary of the code unit
|
||||
|
||||
## Introduction
|
||||
|
||||
- Paragraph(s) explaining:
|
||||
- The purpose and application of the code unit
|
||||
- Key classes that depend on this code unit
|
||||
- Developer problems solved by this code unit
|
||||
|
||||
## Get started
|
||||
|
||||
- Present a single, minimum implementation of the code unit to demonstrate its use.
|
||||
- Show enough of the containing classes to make it clear where the code could be used.
|
||||
- Use unit test code as a starting point for the code example, if available.
|
||||
- When writing a sample agent, do not set the `model` attribute.
|
||||
- For workflow node samples, prefer using a simple Python function rather than extending `BaseNode` to demonstrate the node's logic, unless class extension is explicitly required for the use case.
|
||||
- When wrapping Python functions as workflow nodes, prefer using the `@node` decorator instead of `FunctionNode` directly, whenever possible.
|
||||
|
||||
## How it works
|
||||
|
||||
- Explain how the code unit accomplishes its purpose or solves a problem.
|
||||
- Mention key code classes that depend on this code unit.
|
||||
- Mention code classes that this code unit depends on.
|
||||
- Explain any cross-class dependencies of the code unit.
|
||||
|
||||
## Configuration options
|
||||
|
||||
- If the code unit has configuration options (e.g., settings, configuration objects), document them in a table detailing parameters, types, default values, and descriptions.
|
||||
- **Do NOT** list options inherited from base classes. Focus only on options introduced by the code unit itself.
|
||||
- Dive into each option to provide detailed description and usage patterns, rather than just repeating the type and a brief description.
|
||||
- **Do NOT** list references of all attributes or methods of the classes. Exhaustive API references belong in auto-generated reference documentation, not in guides. Guides should focus on how to use the code unit.
|
||||
|
||||
## Advanced applications
|
||||
|
||||
- Determine if there are advanced use cases for the code unit.
|
||||
- Add advanced applications of the code unit, including:
|
||||
- Problem solved
|
||||
- Implementations for special circumstances
|
||||
|
||||
## Limitations
|
||||
|
||||
- Mention any limitations of the code unit, if known.
|
||||
|
||||
## Related samples
|
||||
|
||||
- Link to relevant samples in the `contributing/` directory that demonstrate the use of this code unit.
|
||||
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: adk-verify-snippets
|
||||
description: >
|
||||
Extracts and verifies the runnability and code coverage of all Python code blocks inside a Markdown file.
|
||||
Generates a detailed compilation and execution report.
|
||||
metadata:
|
||||
author: Antigravity
|
||||
version: 1.4.0
|
||||
---
|
||||
|
||||
# Verify Markdown Snippets Skill
|
||||
|
||||
This skill extracts all ` ```python ` blocks from a Markdown file, executes each
|
||||
one in a process-isolated environment using the bundled `run.py` harness, and
|
||||
generates a structured report covering load status, run status, and line
|
||||
coverage.
|
||||
|
||||
> [!CAUTION] **STRICT READ-ONLY CONSTRAINT — READ THIS BEFORE DOING ANYTHING
|
||||
> ELSE**
|
||||
>
|
||||
> This skill is **read-only**. The agent **MUST NOT**: - **Modify** any file in
|
||||
> the repository (source, test, config, docs, or skill files — including this
|
||||
> SKILL.md). - **Delete** any file in the repository. - **Create** any new file
|
||||
> in the repository.
|
||||
>
|
||||
> The **only two write operations permitted** are: 1. Writing temporary `.py`
|
||||
> snippet files to a **system temp directory outside the repository**. 2.
|
||||
> Writing the final `<filename>_REPORT.md` into the **same directory as the
|
||||
> source Markdown file**.
|
||||
>
|
||||
> If in doubt, do not write. Any other mutation is a violation of this skill's
|
||||
> contract.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## 🔧 Prerequisites
|
||||
|
||||
1. **ADK Python environment**: Run from the repository root with the `uv`
|
||||
virtual environment active.
|
||||
2. **`coverage` package** *(optional)*: Enables per-snippet coverage reporting.
|
||||
Without it, coverage columns show `—`.
|
||||
|
||||
```bash
|
||||
uv pip install coverage
|
||||
```
|
||||
|
||||
3. **Gemini API key**: Required only for snippets that instantiate an `Agent`,
|
||||
`App`, or `Workflow` (which make live Gemini API calls). Set one of:
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="your-key-here"
|
||||
# or
|
||||
export GOOGLE_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
If both are set, `GEMINI_API_KEY` takes precedence.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## 🛠️ Usage
|
||||
|
||||
```bash
|
||||
uv run --no-sync python .agents/skills/adk-verify-snippets/scripts/verify_md.py <path_to_markdown_file.md>
|
||||
```
|
||||
|
||||
The script prints progress for each snippet, then writes a report to
|
||||
**`<filename>_REPORT.md`** in the same directory as the source file and prints
|
||||
the full path on completion.
|
||||
|
||||
**Report contents:** :- **Executive Summary table** — one row per snippet:
|
||||
preceding heading, Load phase status, Run phase status, coverage %, and error
|
||||
detail.
|
||||
|
||||
- **Detailed section** — for each snippet: the extracted code block, full
|
||||
execution logs (stdout + stderr/traceback), and the coverage report.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## 📝 How Snippets Are Classified
|
||||
|
||||
Each ` ```python ` block falls into one of these categories:
|
||||
|
||||
### 1. Runnability Test (has a module-level ADK component)
|
||||
|
||||
If the snippet assigns a `Workflow`, `Agent`, or `App` to a **module-level
|
||||
variable**, the runner executes it against the Gemini API.
|
||||
|
||||
- The variable name does not matter — the runner finds it automatically via
|
||||
`vars(module)`.
|
||||
- For multi-agent snippets, the runner identifies the root agent by excluding
|
||||
any agent that appears in another agent's `sub_agents` list.
|
||||
- To use a custom test prompt instead of the default `"Test input topic"`,
|
||||
define a module-level `test_input` string in the snippet.
|
||||
|
||||
If no module-level ADK component is found, the run phase is skipped and the
|
||||
report shows `➖ NO ADK COMPONENT`.
|
||||
|
||||
### 2. Loadability-Only (no ADK component)
|
||||
|
||||
The runner verifies the snippet compiles and imports without error. No API call
|
||||
is made.
|
||||
|
||||
### 3. Skipped (annotated with ignore)
|
||||
|
||||
Place `<!-- verify-snippets: ignore -->` immediately before the opening
|
||||
` ```python ` fence to exclude a block entirely. Use this for pseudo-code,
|
||||
illustrative examples, or snippets that require external setup.
|
||||
|
||||
````markdown
|
||||
<!-- verify-snippets: ignore -->
|
||||
```python
|
||||
# pseudo-code — not runnable as-is
|
||||
my_agent = Agent(model="gemini-ultra-hypothetical", ...)
|
||||
```
|
||||
````
|
||||
|
||||
The report shows these as `⏭️ SKIPPED`.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## ⚠️ Known Limitations
|
||||
|
||||
- **No shared state between snippets**: Each snippet runs in a fresh
|
||||
subprocess with no imports or variables carried over from previous snippets.
|
||||
A snippet that depends on code from an earlier block will fail with
|
||||
`NameError` or `ImportError`. Make each snippet self-contained, or annotate
|
||||
it with `<!-- verify-snippets: ignore -->`.
|
||||
- **120-second timeout**: Each snippet is killed after 120 seconds. Annotate
|
||||
long-running or blocking snippets with `<!-- verify-snippets: ignore -->`.
|
||||
- **Ignore annotation placement**: The `<!-- verify-snippets: ignore -->`
|
||||
annotation applies to the next ` ```python ` fence encountered. Blank lines
|
||||
between the annotation and the fence are tolerated, but any non-blank line
|
||||
(prose or a heading) cancels the annotation.
|
||||
- **Bare ` ``` ` closes the block**: The parser closes a Python block on the
|
||||
first bare ` ``` ` line (no language tag). A bare ` ``` ` appearing as
|
||||
content inside a snippet (e.g. to demonstrate Markdown syntax) will
|
||||
prematurely close the block. Annotate such snippets with
|
||||
`<!-- verify-snippets: ignore -->`.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## ⚠️ Behavioral Constraints (For AI Agents)
|
||||
|
||||
- **Read-only**: See the caution block at the top. The constraint is absolute.
|
||||
- **Report only, do not fix**: The agent MUST NOT rewrite the source Markdown,
|
||||
modify code blocks, or generate patches. Present the summary table to the
|
||||
user and stop.
|
||||
- **Present the summary table verbatim**: After the script completes, read the
|
||||
generated `_REPORT.md` and copy the Executive Summary table to the user
|
||||
**exactly as written** — same six columns, same order, no renaming or
|
||||
dropping: `Snippet | Preceding Heading | Load Phase | Run Phase | Coverage |
|
||||
Details`
|
||||
@@ -0,0 +1,356 @@
|
||||
# Copyright 2026 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
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# Sentinel string used by verify_md.py to locate and split the coverage section
|
||||
# out of run.py's stdout. Keep in sync with verify_md.py:COV_SECTION_HEADER.
|
||||
COV_SECTION_HEADER = "📊 Phase 4: Code Coverage Report"
|
||||
|
||||
# Structured exit codes — consumed by verify_md.py to classify results without
|
||||
# fragile string/emoji matching. Keep in sync with verify_md.py:EXIT_* constants.
|
||||
EXIT_SUCCESS = 0 # All phases passed
|
||||
EXIT_LOAD_FAILURE = 1 # Failed to compile / load the snippet
|
||||
EXIT_RUN_FAILURE = 2 # Loaded OK but the ADK component failed at runtime
|
||||
EXIT_NO_COMPONENT = 3 # Loaded OK, no runnable ADK component found (load-only)
|
||||
|
||||
# --- Optional Coverage Integration ---
|
||||
try:
|
||||
import coverage
|
||||
|
||||
HAS_COVERAGE = True
|
||||
except ImportError:
|
||||
HAS_COVERAGE = False
|
||||
|
||||
# --- Imports for ADK Inspection ---
|
||||
from google.adk.agents.base_agent import BaseAgent
|
||||
from google.adk.apps import App
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.workflow import Workflow
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def load_target_module(file_path: Path):
|
||||
"""Dynamically loads a Python file as a module, catching import/compilation/definition errors."""
|
||||
# Use the absolute path string as the key to avoid collisions when multiple
|
||||
# snippets share the same file stem or when the stem matches an installed package.
|
||||
module_name = str(file_path)
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(
|
||||
f"Could not resolve module spec for file '{file_path.name}'"
|
||||
)
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
|
||||
# Executing the module runs all top-level code, which will catch:
|
||||
# - SyntaxError / IndentationError
|
||||
# - ImportError (e.g. from google.adk.workflow import build_node)
|
||||
# - ValidationError (e.g. instantiating Workflow with invalid edges)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except Exception:
|
||||
# Remove the partially-initialised module so a broken entry is never
|
||||
# left in sys.modules for the lifetime of this process. This matters
|
||||
# when run.py is imported in-process (e.g. from a test harness) rather
|
||||
# than invoked as a subprocess.
|
||||
sys.modules.pop(module_name, None)
|
||||
raise
|
||||
return module
|
||||
|
||||
|
||||
def discover_adk_component(module):
|
||||
"""Scans the module namespace to discover runnable ADK components, prioritizing root components.
|
||||
|
||||
Uses two passes to correctly identify root agents regardless of the order
|
||||
in which names appear in ``vars(module)``:
|
||||
|
||||
* Pass 1 — collect every Workflow, Agent, and App in the module namespace.
|
||||
* Pass 2 — build the full set of sub-agent IDs from *all* collected agents,
|
||||
then filter to find agents that are not sub-agents of any other agent.
|
||||
|
||||
Without the two-pass approach, a root agent whose variable name is seen
|
||||
before its sub-agents (e.g. ``root`` defined above ``child`` in the file)
|
||||
would be encountered first, before ``child``'s own sub-agents are registered,
|
||||
causing incorrect root detection.
|
||||
"""
|
||||
workflows = []
|
||||
agents = []
|
||||
apps = []
|
||||
|
||||
# Pass 1: collect all candidate components.
|
||||
#
|
||||
# Use vars(module) rather than inspect.getmembers(module) because
|
||||
# getmembers() invokes every attribute getter and silently swallows any
|
||||
# Exception raised by broken descriptors or properties — a snippet that
|
||||
# defines an Agent behind a faulty @property would simply be missing from
|
||||
# the scan with no error or log entry. vars(module) reads the module's
|
||||
# __dict__ directly, which never triggers descriptors and never suppresses
|
||||
# exceptions, giving us an accurate view of module-level names.
|
||||
for obj in vars(module).values():
|
||||
if isinstance(obj, Workflow):
|
||||
workflows.append(obj)
|
||||
elif isinstance(obj, BaseAgent):
|
||||
agents.append(obj)
|
||||
elif isinstance(obj, App):
|
||||
apps.append(obj)
|
||||
|
||||
# 1. Prefer Workflow
|
||||
if workflows:
|
||||
return workflows[0], "Workflow"
|
||||
|
||||
# Pass 2: build the complete sub-agent ID set now that all agents are known,
|
||||
# then select the root (any agent not listed as a sub-agent of another).
|
||||
#
|
||||
# Read sub_agents into a local snapshot rather than calling the attribute
|
||||
# twice. Calling it twice is unsafe when sub_agents is a non-idempotent
|
||||
# property: the first call (guard) and the second call (iteration) could
|
||||
# return different objects, causing id() values to diverge and root
|
||||
# detection to silently misfire.
|
||||
sub_agent_ids: set[int] = set()
|
||||
for agent in agents:
|
||||
children = getattr(agent, "sub_agents", None) or []
|
||||
for sub in children:
|
||||
sub_agent_ids.add(id(sub))
|
||||
|
||||
# 2. Find root Agent (not a sub-agent of any other agent in the module)
|
||||
root_agents = [a for a in agents if id(a) not in sub_agent_ids]
|
||||
if root_agents:
|
||||
return root_agents[0], "Agent"
|
||||
|
||||
# 3. Fall back to App
|
||||
if apps:
|
||||
return apps[0], "App"
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
async def run_component(component, component_type, test_input):
|
||||
"""Unified runner to execute the discovered component."""
|
||||
print(f"\n🔍 Discovered ADK {component_type} in target file.")
|
||||
print(f"🚀 Running execution test with input: '{test_input}'...\n")
|
||||
|
||||
if component_type == "App":
|
||||
runnable_node = getattr(component, "root_agent", None)
|
||||
if runnable_node is None:
|
||||
raise AttributeError(
|
||||
f"App instance has no 'root_agent' attribute. "
|
||||
f"Ensure the App is constructed with a root_agent argument."
|
||||
)
|
||||
else:
|
||||
runnable_node = component
|
||||
|
||||
session_service = InMemorySessionService()
|
||||
runner = Runner(
|
||||
app_name="runnability_test",
|
||||
node=runnable_node,
|
||||
session_service=session_service,
|
||||
)
|
||||
session = await session_service.create_session(
|
||||
app_name="runnability_test", user_id="tester"
|
||||
)
|
||||
|
||||
user_message = types.Content(
|
||||
parts=[types.Part(text=str(test_input))], role="user"
|
||||
)
|
||||
|
||||
async for event in runner.run_async(
|
||||
user_id="tester", session_id=session.id, new_message=user_message
|
||||
):
|
||||
print(f"🎬 [Event] Author: {event.author}")
|
||||
if event.output:
|
||||
print(f"🔹 Output: {event.output}")
|
||||
if hasattr(event, "content") and event.content and event.content.parts:
|
||||
text = "".join(p.text for p in event.content.parts if p.text)
|
||||
if text:
|
||||
print(f"📝 Content Output:\n{'-'*40}\n{text}\n{'-'*40}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generalized ADK Runnability & Loadability Tester"
|
||||
)
|
||||
parser.add_argument(
|
||||
"file",
|
||||
type=str,
|
||||
help="Path to the python file containing the agent/workflow to test",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
file_path = Path(args.file).resolve()
|
||||
if not file_path.exists():
|
||||
print(f"❌ Error: File '{file_path}' does not exist.")
|
||||
sys.exit(EXIT_LOAD_FAILURE)
|
||||
|
||||
print(f"🔬 Testing file: {file_path.name}")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize coverage programmatically to track ONLY the target file.
|
||||
#
|
||||
# Implementation note: snippets are loaded via importlib/exec_module, which
|
||||
# CPython's sys.settrace-based tracer instruments correctly *only* if the
|
||||
# tracer is active before the module's code object is compiled and executed.
|
||||
# Starting coverage here — before load_target_module() — satisfies that
|
||||
# requirement. The `include` filter ensures no ADK library code is counted.
|
||||
cov = None
|
||||
if HAS_COVERAGE:
|
||||
cov = coverage.Coverage(
|
||||
branch=True,
|
||||
data_file=None, # Keep coverage data in-memory only, no .coverage file needed
|
||||
include=[str(file_path)], # Scope collection to the snippet file only
|
||||
)
|
||||
cov.start()
|
||||
else:
|
||||
print(
|
||||
"ℹ️ Install 'coverage' package to enable automated code coverage"
|
||||
" reporting."
|
||||
)
|
||||
|
||||
# exit_code is set by each phase and consumed inside the finally block so
|
||||
# that coverage reporting always runs before the process exits. Using a
|
||||
# mutable list as a simple cell lets the finally clause read the value set
|
||||
# by any code path (normal completion, early break-out via a flag, or an
|
||||
# unexpected exception) without requiring nonlocal or a class wrapper.
|
||||
exit_code = [EXIT_SUCCESS]
|
||||
|
||||
try:
|
||||
# 1. Test Loadability (Imports, Syntax, Instantiation/Validation)
|
||||
print("📋 Phase 1: Loading & Compiling...")
|
||||
try:
|
||||
module = load_target_module(file_path)
|
||||
print(
|
||||
f"✅ Load Success: '{file_path.name}' compiled and loaded without any"
|
||||
" issues."
|
||||
)
|
||||
except Exception:
|
||||
print(f"❌ Load Failure: Failed to compile/load '{file_path.name}':")
|
||||
print("-" * 60)
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
print("-" * 60)
|
||||
exit_code[0] = EXIT_LOAD_FAILURE
|
||||
# Do NOT return here. Fall through to the finally block so that
|
||||
# coverage is reported and sys.exit() is called with the correct code.
|
||||
# The module variable is not set, so we skip phases 2–3 via the flag.
|
||||
else:
|
||||
# 2. Discover Component (only reached when load succeeded)
|
||||
print("\n📋 Phase 2: Component Discovery...")
|
||||
component, comp_type = discover_adk_component(module)
|
||||
if not component:
|
||||
print(
|
||||
"➖ NO ADK COMPONENT: No module-level Workflow, Agent, or App"
|
||||
f" instance found in '{file_path.name}'."
|
||||
)
|
||||
print(
|
||||
" Runnability test skipped. To enable it, assign a Workflow,"
|
||||
" Agent, or App"
|
||||
)
|
||||
print(
|
||||
" to a module-level variable (e.g. `agent = Agent(...)`). The"
|
||||
" variable name"
|
||||
)
|
||||
print(
|
||||
" does not matter — the runner detects it automatically via"
|
||||
" vars(module)."
|
||||
)
|
||||
print(
|
||||
"ℹ️ Coverage below reflects load-time execution only (module-level"
|
||||
" statements)."
|
||||
)
|
||||
exit_code[0] = EXIT_NO_COMPONENT
|
||||
else:
|
||||
# Get test input from module, or fallback
|
||||
test_input = getattr(module, "test_input", "Test input topic")
|
||||
|
||||
# 3. Test Runnability
|
||||
print(f"\n📋 Phase 3: Executing {comp_type}...")
|
||||
try:
|
||||
# asyncio.run() creates a fresh event loop each time, so it will raise
|
||||
# RuntimeError if a loop is already running (e.g. the snippet called
|
||||
# asyncio.run() at module level without an __main__ guard).
|
||||
# We catch that specific case and report it clearly, rather than using
|
||||
# the deprecated asyncio.get_event_loop() API (removed in Python 3.12+).
|
||||
asyncio.run(run_component(component, comp_type, test_input))
|
||||
print(
|
||||
f"\n✅ Run Success: Component '{comp_type}' executed"
|
||||
" successfully."
|
||||
)
|
||||
except RuntimeError as e:
|
||||
if "event loop" in str(e).lower():
|
||||
print(
|
||||
f"\n❌ Run Failure: An event loop conflict was detected after"
|
||||
f" module load."
|
||||
)
|
||||
print(
|
||||
" The snippet likely called asyncio.run() at module level,"
|
||||
" which"
|
||||
)
|
||||
print(
|
||||
" conflicts with the runner's own event loop. Wrap top-level"
|
||||
" async"
|
||||
)
|
||||
print(
|
||||
" calls in an `if __name__ == '__main__':` guard, or"
|
||||
" annotate the"
|
||||
)
|
||||
print(" snippet with <!-- verify-snippets: ignore -->.")
|
||||
else:
|
||||
print(f"\n❌ Run Failure: Component failed during execution:")
|
||||
print("-" * 60)
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
print("-" * 60)
|
||||
exit_code[0] = EXIT_RUN_FAILURE
|
||||
except Exception:
|
||||
print(f"\n❌ Run Failure: Component failed during execution:")
|
||||
print("-" * 60)
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
print("-" * 60)
|
||||
exit_code[0] = EXIT_RUN_FAILURE
|
||||
|
||||
finally:
|
||||
# Coverage reporting runs here so it is guaranteed to execute on every
|
||||
# code path: normal completion, load failure, no-component, run failure.
|
||||
if cov:
|
||||
cov.stop()
|
||||
print(f"\n{COV_SECTION_HEADER} (Target File)")
|
||||
print("=" * 60)
|
||||
try:
|
||||
# Report coverage of the target file directly to stdout
|
||||
cov.report(morfs=[str(file_path)], file=sys.stdout)
|
||||
except coverage.exceptions.NoDataError:
|
||||
print(
|
||||
"⚠️ No coverage data collected (compilation or execution failed"
|
||||
" early)."
|
||||
)
|
||||
except Exception as ce:
|
||||
print(f"⚠️ Failed to generate coverage report: {ce}")
|
||||
print("=" * 60)
|
||||
# Only call sys.exit for non-zero codes. If exit_code is EXIT_SUCCESS
|
||||
# we return normally so that any exception currently propagating out of
|
||||
# the try block is not silently replaced by a SystemExit raised here.
|
||||
if exit_code[0] != EXIT_SUCCESS:
|
||||
sys.exit(exit_code[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,450 @@
|
||||
# Copyright 2026 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
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
SKIP_ANNOTATION = "<!-- verify-snippets: ignore -->"
|
||||
SNIPPET_TIMEOUT = 120 # seconds; adjust if snippets legitimately need longer
|
||||
|
||||
# Must match COV_SECTION_HEADER in run.py exactly — used to split coverage output
|
||||
# from the main execution log when parsing run.py's stdout.
|
||||
COV_SECTION_HEADER = "📊 Phase 4: Code Coverage Report"
|
||||
|
||||
# Structured exit codes from run.py — kept in sync with run.py:EXIT_* constants.
|
||||
# Using exit codes (not string/emoji matching) makes classification robust to
|
||||
# future changes in run.py's human-readable output text.
|
||||
EXIT_SUCCESS = 0 # All phases passed
|
||||
EXIT_LOAD_FAILURE = 1 # Failed to compile / load the snippet
|
||||
EXIT_RUN_FAILURE = 2 # Loaded OK but the ADK component failed at runtime
|
||||
EXIT_NO_COMPONENT = 3 # Loaded OK, no runnable ADK component found (load-only)
|
||||
|
||||
|
||||
def extract_snippets(md_path: Path):
|
||||
"""Parses a markdown file and extracts python code blocks along with their preceding headings.
|
||||
|
||||
A code block immediately preceded by the HTML comment
|
||||
``<!-- verify-snippets: ignore -->`` is recorded but marked as skipped so
|
||||
that illustrative / pseudo-code examples are excluded from execution.
|
||||
"""
|
||||
with open(md_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
lines = content.splitlines()
|
||||
snippets = []
|
||||
current_heading = "Top Level"
|
||||
in_code_block = False
|
||||
code_lines = []
|
||||
skip_next_block = False
|
||||
|
||||
for line in lines:
|
||||
# If we are inside a code block, handle it first to preserve comments starting with '#'
|
||||
if in_code_block:
|
||||
stripped = line.strip()
|
||||
# Close only on a bare closing fence (``` with no language specifier).
|
||||
# A fenced block of another language (e.g. ```bash) appearing *inside*
|
||||
# the Python block will not trigger this branch because it carries a
|
||||
# language tag, so it is appended to code_lines as literal content.
|
||||
if stripped == "```":
|
||||
in_code_block = False
|
||||
code_text = "\n".join(code_lines)
|
||||
snippets.append({
|
||||
"heading": current_heading,
|
||||
"code": code_text,
|
||||
"skip": skip_next_block,
|
||||
})
|
||||
skip_next_block = False
|
||||
else:
|
||||
code_lines.append(line)
|
||||
continue
|
||||
|
||||
# If we are outside a code block, check for headings or code block starts
|
||||
if line.startswith("#"):
|
||||
# Clean up heading markers (e.g., "## Get started" -> "Get started")
|
||||
current_heading = line.lstrip("#").strip()
|
||||
# A heading between the annotation and the fence cancels the skip.
|
||||
skip_next_block = False
|
||||
continue
|
||||
|
||||
if line.strip() == SKIP_ANNOTATION:
|
||||
skip_next_block = True
|
||||
continue
|
||||
|
||||
if line.strip().startswith("```python"):
|
||||
in_code_block = True
|
||||
code_lines = []
|
||||
continue
|
||||
|
||||
# Any other non-empty line (prose, blank-line-separated text, etc.) between
|
||||
# the annotation and the fence cancels the skip.
|
||||
if line.strip():
|
||||
skip_next_block = False
|
||||
|
||||
return snippets
|
||||
|
||||
|
||||
def run_snippet(run_py_path: Path, snippet_path: Path):
|
||||
"""Executes run.py on the isolated snippet and returns the result."""
|
||||
# Run using the same Python interpreter as this script (which will be the venv's python)
|
||||
cmd = [sys.executable, str(run_py_path), str(snippet_path)]
|
||||
|
||||
# Ensure GEMINI_API_KEY is preferred if both keys are set in the environment
|
||||
env = os.environ.copy()
|
||||
if "GOOGLE_API_KEY" in env and "GEMINI_API_KEY" in env:
|
||||
env.pop("GOOGLE_API_KEY", None)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, env=env, timeout=SNIPPET_TIMEOUT
|
||||
)
|
||||
return {
|
||||
"exit_code": result.returncode,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr,
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"exit_code": EXIT_RUN_FAILURE,
|
||||
"stdout": (
|
||||
"❌ Run Failure: Snippet execution timed out after"
|
||||
f" {SNIPPET_TIMEOUT} seconds."
|
||||
),
|
||||
"stderr": (
|
||||
"TimeoutExpired: The snippet process did not complete within the"
|
||||
f" {SNIPPET_TIMEOUT}-second limit."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def extract_error_detail(stdout: str, stderr: str) -> str:
|
||||
"""Extracts the most relevant error line from run.py's output.
|
||||
|
||||
Searches in order:
|
||||
1. Last line in stderr that looks like a Python exception (``<Name>Error:``
|
||||
or ``<Name>Exception:``). Scoping to stderr avoids matching runner prose
|
||||
in stdout (e.g. "❌ Run Failure: ...") which contains words like "Failure"
|
||||
but is not an exception line.
|
||||
2. Last line in stdout with the same pattern, as a fallback for runtimes that
|
||||
write tracebacks to stdout instead of stderr.
|
||||
3. Last line in stderr matching the generic ``<ClassName>: <detail>`` format
|
||||
(custom exception classes that don't end in Error/Exception).
|
||||
4. Fallback string if nothing matches.
|
||||
"""
|
||||
# Matches standard Python exception class names: ends in 'Error' or 'Exception',
|
||||
# followed by a colon and detail text. Anchored to the start of the stripped line
|
||||
# so runner prose ("❌ Run Failure: ...") is not matched.
|
||||
_exception_re = re.compile(r"^[A-Za-z]\w*(?:Error|Exception|Warning):\s*.+")
|
||||
|
||||
for source in (stderr, stdout):
|
||||
for line in reversed(source.splitlines()):
|
||||
if _exception_re.match(line.strip()):
|
||||
return f"`{line.strip()}`"
|
||||
|
||||
# Pass 3: generic '<ClassName>: <detail>' in stderr only
|
||||
for line in reversed(stderr.splitlines()):
|
||||
if re.match(r"^[A-Za-z]\w*:.+", line.strip()):
|
||||
return f"`{line.strip()}`"
|
||||
|
||||
return "Failed to compile/load."
|
||||
|
||||
|
||||
def clean_name(name: str):
|
||||
"""Sanitizes a string to be a safe filename."""
|
||||
name = name.lower().replace(" ", "_")
|
||||
return re.sub(r"[^a-z0-9_]", "", name)
|
||||
|
||||
|
||||
def md_cell(value: str) -> str:
|
||||
"""Escapes pipe characters so the value is safe inside a Markdown table cell."""
|
||||
return value.replace("|", r"\|")
|
||||
|
||||
|
||||
def safe_fence(content: str, language: str = "") -> str:
|
||||
"""Returns a Markdown fenced code block that safely wraps *content*.
|
||||
|
||||
Picks the shortest fence (minimum three backticks) that is strictly longer
|
||||
than any contiguous run of backticks found inside *content*, so the fence
|
||||
cannot be prematurely closed by content that itself contains backtick runs.
|
||||
This is the approach recommended by the CommonMark spec.
|
||||
|
||||
Example::
|
||||
|
||||
safe_fence("x = ```foo```", "python")
|
||||
# returns:
|
||||
# ````python
|
||||
# x = ```foo```
|
||||
# ````
|
||||
"""
|
||||
# Find the longest run of backticks inside the content
|
||||
max_run = max(
|
||||
(len(m.group()) for m in re.finditer(r"`+", content)), default=0
|
||||
)
|
||||
# The outer fence must be strictly longer, and at least 3 characters
|
||||
fence_len = max(3, max_run + 1)
|
||||
fence = "`" * fence_len
|
||||
tag = f"{fence}{language}\n" if language else f"{fence}\n"
|
||||
return f"{tag}{content}\n{fence}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Markdown Snippet Verifier")
|
||||
parser.add_argument(
|
||||
"file", type=str, help="Path to the markdown file to verify"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
md_path = Path(args.file).resolve()
|
||||
if not md_path.exists():
|
||||
print(f"❌ Error: Markdown file '{md_path}' does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
# Locate run.py bundled inside the same scripts folder as verify_md.py (portable mode!)
|
||||
run_py_path = Path(__file__).parent / "run.py"
|
||||
if not run_py_path.exists():
|
||||
print(f"❌ Error: Bundled runner 'run.py' not found at '{run_py_path}'.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🔬 Analyzing Markdown: {md_path.name}")
|
||||
|
||||
# 1. Extract snippets
|
||||
snippets = extract_snippets(md_path)
|
||||
if not snippets:
|
||||
print(f"⚠️ No python code blocks found in '{md_path.name}'.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"📋 Found {len(snippets)} python code snippets to verify.")
|
||||
|
||||
# Create a unique temp directory to avoid collisions with concurrent runs
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="verify_snippets_"))
|
||||
|
||||
results = []
|
||||
|
||||
# 2. Execute each snippet, then write the report — both inside the try so
|
||||
# the finally cleanup only runs after the report is fully written.
|
||||
try:
|
||||
for i, snippet in enumerate(snippets, start=1):
|
||||
heading = snippet["heading"]
|
||||
code = snippet["code"]
|
||||
is_skipped = snippet.get("skip", False)
|
||||
|
||||
# Create a unique, sanitized filename for the snippet
|
||||
safe_heading = clean_name(heading)
|
||||
temp_file_name = f"snippet_{i}_{safe_heading}.py"
|
||||
temp_file_path = temp_dir / temp_file_name
|
||||
|
||||
if is_skipped:
|
||||
print(
|
||||
f"⏭️ Skipping Snippet {i}/{len(snippets)} under heading"
|
||||
f" '{heading}' (marked ignore)."
|
||||
)
|
||||
results.append({
|
||||
"index": i,
|
||||
"heading": heading,
|
||||
"code": code,
|
||||
"temp_file": temp_file_name,
|
||||
"exit_code": 0,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"skipped": True,
|
||||
})
|
||||
continue
|
||||
|
||||
# Write snippet to file
|
||||
with open(temp_file_path, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
print(
|
||||
f"🧪 Testing Snippet {i}/{len(snippets)} under heading '{heading}'..."
|
||||
)
|
||||
|
||||
# Run the snippet
|
||||
run_res = run_snippet(run_py_path, temp_file_path)
|
||||
|
||||
results.append({
|
||||
"index": i,
|
||||
"heading": heading,
|
||||
"code": code,
|
||||
"temp_file": temp_file_name,
|
||||
"exit_code": run_res["exit_code"],
|
||||
"stdout": run_res["stdout"],
|
||||
"stderr": run_res["stderr"],
|
||||
"skipped": False,
|
||||
})
|
||||
|
||||
# 3. Generate Markdown Report — inside the try so finally runs after this completes.
|
||||
# Use clean_name on the stem so the report path is safe on all filesystems.
|
||||
# If clean_name strips everything (e.g. a fully non-ASCII filename), fall
|
||||
# back to a hash of the original stem so two such files in the same
|
||||
# directory never produce the same report path.
|
||||
safe_stem = clean_name(md_path.stem) or f"report_{abs(hash(md_path.stem))}"
|
||||
report_path = md_path.parent / f"{safe_stem}_REPORT.md"
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
with open(report_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"# 🔬 ADK Markdown Snippet Verification Report\n\n")
|
||||
f.write(f"* **Source File**: [{md_path.name}](file://{md_path})\n")
|
||||
f.write(f"* **Verified On**: `{timestamp}`\n\n")
|
||||
|
||||
# Write summary table
|
||||
f.write("## 📈 Executive Summary\n\n")
|
||||
f.write(
|
||||
"| Snippet | Preceding Heading | Load Phase | Run Phase | Coverage |"
|
||||
" Details |\n"
|
||||
)
|
||||
f.write("| :--- | :--- | :---: | :---: | :---: | :--- |\n")
|
||||
|
||||
for r in results:
|
||||
# Handle explicitly skipped snippets
|
||||
if r.get("skipped"):
|
||||
f.write(
|
||||
f"| **Snippet {r['index']}** | `{md_cell(r['heading'])}` | ⏭️"
|
||||
f" **SKIPPED** | ⏭️ **SKIPPED** | — | Marked `{SKIP_ANNOTATION}`"
|
||||
" — intentionally ignored. |\n"
|
||||
)
|
||||
continue
|
||||
|
||||
# Determine Phase 1 (Load) and Phase 3 (Run) statuses from the
|
||||
# structured exit code emitted by run.py — no emoji/string matching.
|
||||
exit_code = r["exit_code"]
|
||||
load_status = "✅ **PASS**"
|
||||
run_status = "✅ **PASS**"
|
||||
coverage_pct = "—"
|
||||
|
||||
stdout_and_stderr = r["stdout"] + "\n" + r["stderr"]
|
||||
|
||||
if exit_code == EXIT_LOAD_FAILURE:
|
||||
load_status = "❌ **FAIL**"
|
||||
run_status = "➖ **SKIPPED**"
|
||||
elif exit_code == EXIT_NO_COMPONENT:
|
||||
run_status = "➖ **NO ADK COMPONENT**"
|
||||
elif exit_code == EXIT_RUN_FAILURE:
|
||||
run_status = "❌ **FAIL**"
|
||||
# EXIT_SUCCESS (0): both statuses remain ✅ **PASS**
|
||||
|
||||
# 3. Parse Coverage — anchor to line start to avoid matching prose.
|
||||
# Handles both branch (5 numeric cols) and non-branch (3 cols) formats.
|
||||
total_match = re.search(
|
||||
r"^TOTAL(?:\s+\d+)+\s+(\d+)%", r["stdout"], re.MULTILINE
|
||||
)
|
||||
if total_match and load_status != "❌ **FAIL**":
|
||||
coverage_pct = f"`{total_match.group(1)}`"
|
||||
|
||||
# 4. Formulate details and handle transient 503s
|
||||
details = "All checks passed successfully."
|
||||
if load_status == "❌ **FAIL**":
|
||||
details = extract_error_detail(r["stdout"], r["stderr"])
|
||||
elif run_status == "➖ **NO ADK COMPONENT**":
|
||||
details = (
|
||||
"No module-level `Workflow`, `Agent`, or `App` instance found."
|
||||
" Assign one to a top-level variable to enable runnability"
|
||||
" testing."
|
||||
)
|
||||
elif run_status == "❌ **FAIL**":
|
||||
if "503" in stdout_and_stderr and "UNAVAILABLE" in stdout_and_stderr:
|
||||
details = (
|
||||
"⚠️ **Transient 503 from Gemini API (overloaded)**. Code"
|
||||
" structure is correct."
|
||||
)
|
||||
else:
|
||||
details = extract_error_detail(r["stdout"], r["stderr"])
|
||||
|
||||
# Store statuses for reuse in the detailed section
|
||||
r["load_status"] = load_status
|
||||
r["run_status"] = run_status
|
||||
|
||||
f.write(
|
||||
f"| **Snippet {r['index']}** | `{md_cell(r['heading'])}` |"
|
||||
f" {load_status} | {run_status} | {coverage_pct} |"
|
||||
f" {md_cell(details)} |\n"
|
||||
)
|
||||
|
||||
f.write("\n---\n\n## 🔍 Detailed Snippet Reports\n\n")
|
||||
|
||||
for r in results:
|
||||
if r.get("skipped"):
|
||||
f.write(
|
||||
f"### ⏭️ Snippet {r['index']}: `{r['heading']}` *(ignored)*\n\n"
|
||||
)
|
||||
f.write("#### 📝 Code Block\n")
|
||||
f.write(safe_fence(r["code"], "python"))
|
||||
f.write("\n\n")
|
||||
f.write(
|
||||
"> This snippet was skipped because it is annotated with"
|
||||
f" `{SKIP_ANNOTATION}`.\n\n"
|
||||
)
|
||||
f.write("---\n\n")
|
||||
continue
|
||||
|
||||
l_stat = r.get("load_status", "✅ **PASS**")
|
||||
r_stat = r.get("run_status", "✅ **PASS**")
|
||||
if l_stat == "❌ **FAIL**" or r_stat == "❌ **FAIL**":
|
||||
status_icon = "❌"
|
||||
elif r_stat == "➖ **NO ADK COMPONENT**":
|
||||
status_icon = "➖"
|
||||
else:
|
||||
status_icon = "✅"
|
||||
f.write(f"### {status_icon} Snippet {r['index']}: `{r['heading']}`\n\n")
|
||||
|
||||
f.write("#### 📝 Code Block\n")
|
||||
f.write(safe_fence(r["code"], "python"))
|
||||
f.write("\n\n")
|
||||
|
||||
# Write stdout / stderr logs
|
||||
# Split run.py stdout into main log and coverage section using the
|
||||
# shared COV_SECTION_HEADER constant (kept in sync with run.py).
|
||||
stdout_clean = r["stdout"]
|
||||
cov_section_match = re.search(
|
||||
rf"({re.escape(COV_SECTION_HEADER)}.*)", r["stdout"], re.DOTALL
|
||||
)
|
||||
cov_text = cov_section_match.group(1) if cov_section_match else None
|
||||
|
||||
if cov_text:
|
||||
stdout_clean = r["stdout"].replace(cov_text, "").strip()
|
||||
|
||||
log_content = stdout_clean
|
||||
if r["stderr"]:
|
||||
log_content += "\n\n=== STDERR/TRACEBACK ===\n" + r["stderr"].strip()
|
||||
|
||||
f.write("#### 🖥️ Loadability & Runnability Logs\n")
|
||||
f.write(safe_fence(log_content))
|
||||
f.write("\n\n")
|
||||
|
||||
# Write coverage report if available
|
||||
if cov_text:
|
||||
f.write("#### 📊 Coverage Report\n")
|
||||
f.write(safe_fence(cov_text))
|
||||
f.write("\n\n")
|
||||
|
||||
f.write("---\n\n")
|
||||
|
||||
print(f"🎉 Verification complete! Report generated at: {report_path}")
|
||||
|
||||
finally:
|
||||
# Always clean up the temp directory, even on Ctrl+C or unexpected errors.
|
||||
# This runs after report generation completes (or if it raises), ensuring
|
||||
# temp files are never left behind.
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"contextFileName": "AGENTS.md"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "2.0.0-alpha.1"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "2.4.0"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Required Information
|
||||
*Please ensure all items in this section are completed to allow for efficient
|
||||
triaging. Requests without complete information may be rejected / deprioritized.
|
||||
If an item is not applicable to you - please mark it as N/A*
|
||||
|
||||
**Describe the Bug:**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to Reproduce:**
|
||||
Please provide a numbered list of steps to reproduce the behavior:
|
||||
1. Install '...'
|
||||
2. Run '....'
|
||||
3. Open '....'
|
||||
4. Provide error or stacktrace
|
||||
|
||||
**Expected Behavior:**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Observed Behavior:**
|
||||
What actually happened? Include error messages or crash stack traces here.
|
||||
|
||||
**Environment Details:**
|
||||
|
||||
- ADK Library Version (pip show google-adk):
|
||||
- Desktop OS:** [e.g., macOS, Linux, Windows]
|
||||
- Python Version (python -V):
|
||||
|
||||
**Model Information:**
|
||||
|
||||
- Are you using LiteLLM: Yes/No
|
||||
- Which model is being used: (e.g., gemini-2.5-pro)
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Optional Information
|
||||
*Providing this information greatly speeds up the resolution process.*
|
||||
|
||||
**Regression:**
|
||||
Did this work in a previous version of ADK? If so, which one?
|
||||
|
||||
**Logs:**
|
||||
Please attach relevant logs. Wrap them in code blocks (```) or attach a
|
||||
text file.
|
||||
```text
|
||||
// Paste logs here
|
||||
```
|
||||
|
||||
**Screenshots / Video:**
|
||||
If applicable, add screenshots or screen recordings to help explain
|
||||
your problem.
|
||||
|
||||
**Additional Context:**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Minimal Reproduction Code:**
|
||||
Please provide a code snippet or a link to a Gist/repo that isolates the issue.
|
||||
```python
|
||||
// Code snippet here
|
||||
```
|
||||
|
||||
**How often has this issue occurred?:**
|
||||
|
||||
- Always (100%)
|
||||
- Often (50%+)
|
||||
- Intermittently (<50%)
|
||||
- Once / Rare
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
** Please make sure you read the contribution guide and file the issues in the right place. **
|
||||
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)
|
||||
|
||||
## 🔴 Required Information
|
||||
*Please ensure all items in this section are completed to allow for efficient
|
||||
triaging. Requests without complete information may be rejected / deprioritized.
|
||||
If an item is not applicable to you - please mark it as N/A*
|
||||
|
||||
### Is your feature request related to a specific problem?
|
||||
Please describe the problem you are trying to solve. (Ex: "I'm always frustrated
|
||||
when I have to manually handle X...")
|
||||
|
||||
### Describe the Solution You'd Like
|
||||
A clear and concise description of the feature or API change you want.
|
||||
Be specific about input/outputs if this involves an API change.
|
||||
|
||||
### Impact on your work
|
||||
How does this feature impact your work and what are you trying to achieve?
|
||||
If this is critical for you, tell us if there is a timeline by when you need
|
||||
this feature.
|
||||
|
||||
### Willingness to contribute
|
||||
Are you interested in implementing this feature yourself or submitting a PR?
|
||||
(Yes/No)
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Recommended Information
|
||||
|
||||
### Describe Alternatives You've Considered
|
||||
A clear and concise description of any alternative solutions or workarounds
|
||||
you've considered and why they didn't work for you.
|
||||
|
||||
### Proposed API / Implementation
|
||||
If you have ideas on how this should look in code, please share a
|
||||
pseudo-code example.
|
||||
|
||||
### Additional Context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
allowedCopyrightHolders:
|
||||
- 'Google LLC'
|
||||
allowedLicenses:
|
||||
- 'Apache-2.0'
|
||||
- 'MIT'
|
||||
- 'BSD-3'
|
||||
sourceFileExtensions:
|
||||
- 'ts'
|
||||
- 'js'
|
||||
- 'java'
|
||||
- 'py'
|
||||
- 'yaml'
|
||||
- 'yml'
|
||||
ignoreFiles:
|
||||
- 'src/google/adk/cli/browser/**'
|
||||
@@ -0,0 +1,52 @@
|
||||
**Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.**
|
||||
|
||||
### Link to Issue or Description of Change
|
||||
|
||||
**1. Link to an existing issue (if applicable):**
|
||||
|
||||
- Closes: #_issue_number_
|
||||
- Related: #_issue_number_
|
||||
|
||||
**2. Or, if no issue exists, describe the change:**
|
||||
|
||||
_If applicable, please follow the issue templates to provide as much detail as
|
||||
possible._
|
||||
|
||||
**Problem:**
|
||||
_A clear and concise description of what the problem is._
|
||||
|
||||
**Solution:**
|
||||
_A clear and concise description of what you want to happen and why you choose
|
||||
this solution._
|
||||
|
||||
### Testing Plan
|
||||
|
||||
_Please describe the tests that you ran to verify your changes. This is required
|
||||
for all PRs that are not small documentation or typo fixes._
|
||||
|
||||
**Unit Tests:**
|
||||
|
||||
- [ ] I have added or updated unit tests for my change.
|
||||
- [ ] All unit tests pass locally.
|
||||
|
||||
_Please include a summary of passed `pytest` results._
|
||||
|
||||
**Manual End-to-End (E2E) Tests:**
|
||||
|
||||
_Please provide instructions on how to manually test your changes, including any
|
||||
necessary setup or configuration. Please provide logs or screenshots to help
|
||||
reviewers better understand the fix._
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document.
|
||||
- [ ] I have performed a self-review of my own code.
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas.
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works.
|
||||
- [ ] New and existing unit tests pass locally with my changes.
|
||||
- [ ] I have manually tested my changes end-to-end.
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules.
|
||||
|
||||
### Additional context
|
||||
|
||||
_Add any other context or screenshots about the feature request here._
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "python",
|
||||
"versioning": "prerelease",
|
||||
"prerelease": true,
|
||||
"prerelease-type": "alpha",
|
||||
"package-name": "google-adk",
|
||||
"include-component-in-tag": false,
|
||||
"skip-github-release": true,
|
||||
"changelog-path": "CHANGELOG-v2.md",
|
||||
"changelog-sections": [
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "Performance Improvements"
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"section": "Code Refactoring",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"section": "Documentation"
|
||||
},
|
||||
{
|
||||
"type": "test",
|
||||
"section": "Tests",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"section": "Build System",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "ci",
|
||||
"section": "CI/CD",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "style",
|
||||
"section": "Styles",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "Miscellaneous Chores",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"last-release-sha": "4af7cbb5c8319208337e18b0a6bc55288b51b0b1"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "python",
|
||||
"package-name": "google-adk",
|
||||
"include-component-in-tag": false,
|
||||
"skip-github-release": true,
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"changelog-sections": [
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "Performance Improvements"
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"section": "Code Refactoring",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"section": "Documentation"
|
||||
},
|
||||
{
|
||||
"type": "test",
|
||||
"section": "Tests",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"section": "Build System",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "ci",
|
||||
"section": "CI/CD",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "style",
|
||||
"section": "Styles",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "Miscellaneous Chores",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"last-release-sha": "44d747ed5eaf543b5b8d22e0088f8a7c7eeee846"
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# 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.
|
||||
|
||||
name: Analyze New Release for ADK Docs Updates
|
||||
|
||||
on:
|
||||
# Runs on every new release.
|
||||
release:
|
||||
types: [published]
|
||||
# Manual trigger for testing and retrying.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume:
|
||||
description: 'Resume from the last failed/interrupted run'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
start_tag:
|
||||
description: 'Older release tag (base), e.g. v1.26.0'
|
||||
required: false
|
||||
type: string
|
||||
end_tag:
|
||||
description: 'Newer release tag (head), e.g. v1.27.0'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
analyze-new-release-for-adk-docs-updates:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Load adk-bot SSH Private Key
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.ADK_BOT_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests "google-adk[db]"
|
||||
|
||||
- name: Restore session DB from cache
|
||||
if: ${{ github.event.inputs.resume == 'true' }}
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: contributing/samples/adk_team/adk_documentation/adk_release_analyzer/sessions.db
|
||||
key: analyzer-session-db-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
analyzer-session-db-
|
||||
|
||||
- name: Run Analyzing Script
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY_FOR_DOCS_AGENTS }}
|
||||
GOOGLE_GENAI_USE_VERTEXAI: 0
|
||||
DOC_OWNER: 'google'
|
||||
CODE_OWNER: 'google'
|
||||
DOC_REPO: 'adk-docs'
|
||||
CODE_REPO: 'adk-python'
|
||||
INTERACTIVE: 0
|
||||
PYTHONPATH: contributing/samples/adk_team
|
||||
ANALYZER_RESUME: ${{ github.event.inputs.resume }}
|
||||
ANALYZER_START_TAG: ${{ github.event.inputs.start_tag }}
|
||||
ANALYZER_END_TAG: ${{ github.event.inputs.end_tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=()
|
||||
if [[ "${ANALYZER_RESUME:-false}" == "true" ]]; then
|
||||
args+=(--resume)
|
||||
fi
|
||||
if [[ -n "${ANALYZER_START_TAG:-}" ]]; then
|
||||
args+=(--start-tag "$ANALYZER_START_TAG")
|
||||
fi
|
||||
if [[ -n "${ANALYZER_END_TAG:-}" ]]; then
|
||||
args+=(--end-tag "$ANALYZER_END_TAG")
|
||||
fi
|
||||
python -m adk_documentation.adk_release_analyzer.main "${args[@]}"
|
||||
|
||||
- name: Save session DB to cache
|
||||
if: always()
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: contributing/samples/adk_team/adk_documentation/adk_release_analyzer/sessions.db
|
||||
key: analyzer-session-db-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
name: Do Not Merge on GitHub
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
block-merge:
|
||||
if: github.repository == 'google/adk-python'
|
||||
name: maintainers will submit via Copybara
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Explain why merging is blocked
|
||||
run: |
|
||||
echo "::error title=GitHub merge is disabled::Do NOT merge this pull request on GitHub. A maintainer will land the change internally, and Copybara will sync it back to this repository automatically."
|
||||
exit 1
|
||||
@@ -0,0 +1,194 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, v1]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '.pre-commit-config.yaml'
|
||||
- 'pyproject.toml'
|
||||
- 'tests/**'
|
||||
pull_request:
|
||||
branches: [main, v1]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '.pre-commit-config.yaml'
|
||||
- 'pyproject.toml'
|
||||
- 'tests/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# 1. Code format and linting (Linter)
|
||||
lint:
|
||||
name: Pre-commit Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run pre-commit checks
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
# 2. Static type analysis (Mypy Check with Matrix)
|
||||
# Compares new changes against the target base branch dynamically to support v1.
|
||||
type-check:
|
||||
name: Mypy Check (Python ${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Generate Baseline
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
|
||||
run: |
|
||||
# Switch to target base branch to generate baseline
|
||||
git checkout origin/$TARGET_BRANCH
|
||||
|
||||
git checkout ${{ github.sha }} -- pyproject.toml
|
||||
|
||||
# Install dependencies for target branch
|
||||
uv venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync --all-extras
|
||||
|
||||
# Run mypy, filter for errors only, remove line numbers, and sort
|
||||
# We ignore exit code (|| true) because we expect errors on baseline
|
||||
uv run mypy . | grep "error:" | sed 's/:\([0-9]\+\):/::/g' | sort > baseline_errors.txt || true
|
||||
echo "Found $(wc -l < baseline_errors.txt) errors on $TARGET_BRANCH."
|
||||
|
||||
- name: Check PR Branch
|
||||
run: |
|
||||
# Switch back to the PR commit
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
# Re-sync dependencies in case the PR changed them
|
||||
source .venv/bin/activate
|
||||
uv sync --all-extras
|
||||
|
||||
# Run mypy on PR code, apply same processing
|
||||
uv run mypy . | grep "error:" | sed 's/:\([0-9]\+\):/::/g' | sort > pr_errors.txt || true
|
||||
echo "Found $(wc -l < pr_errors.txt) errors on PR branch."
|
||||
|
||||
- name: Compare and Fail on New Errors
|
||||
run: |
|
||||
# 'comm -13' suppresses unique lines in file1 (baseline) and common lines,
|
||||
# leaving only lines unique to file2 (PR) -> The new errors.
|
||||
comm -13 baseline_errors.txt pr_errors.txt > new_errors.txt
|
||||
|
||||
if [ -s new_errors.txt ]; then
|
||||
echo "::error::The following NEW mypy errors were introduced:"
|
||||
cat new_errors.txt
|
||||
exit 1
|
||||
else
|
||||
echo "Great job! No new mypy errors introduced."
|
||||
fi
|
||||
|
||||
# 3a. Unit testing (Unit Tests with Matrix)
|
||||
unit-test:
|
||||
name: Unit Tests (Python ${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync --extra test
|
||||
|
||||
- name: Run unit tests with pytest
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pytest tests/unittests \
|
||||
-n auto \
|
||||
--ignore=tests/unittests/artifacts/test_artifact_service.py \
|
||||
--ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
|
||||
|
||||
# 3b. Dual-version A2A coverage: This job pins a2a-sdk to 0.3.x and
|
||||
# runs only the A2A-related tests, so the a2a/_compat.py shim stays
|
||||
# verified against both SDK majors.
|
||||
# TODO: Remove this 0.3.x re-run once a2a-sdk 0.3.x support is dropped.
|
||||
unit-test-a2a-v0-3:
|
||||
name: A2A v0.3 Tests (Python ${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync --extra test
|
||||
|
||||
- name: Run A2A tests against a2a-sdk v0.3
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
uv pip install --reinstall-package a2a-sdk 'a2a-sdk>=0.3.4,<0.4'
|
||||
pytest tests/unittests/a2a \
|
||||
tests/unittests/agents/test_remote_a2a_agent.py \
|
||||
tests/unittests/integrations/agent_registry/test_agent_registry.py
|
||||
@@ -0,0 +1,163 @@
|
||||
# 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.
|
||||
|
||||
name: Copybara PR Handler
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to close (for testing)'
|
||||
required: true
|
||||
type: string
|
||||
commit_sha:
|
||||
description: 'Commit SHA reference (optional, for testing)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
close-imported-pr:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check for Copybara commits and close PRs
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
script: |
|
||||
// Check if this is a manual test run
|
||||
const isManualRun = context.eventName === 'workflow_dispatch';
|
||||
|
||||
let prsToClose = [];
|
||||
|
||||
if (isManualRun) {
|
||||
// Manual testing mode
|
||||
const prNumber = parseInt(context.payload.inputs.pr_number);
|
||||
const commitSha = context.payload.inputs.commit_sha || context.sha.substring(0, 7);
|
||||
|
||||
console.log('=== MANUAL TEST MODE ===');
|
||||
console.log(`Testing with PR #${prNumber}, commit ${commitSha}`);
|
||||
|
||||
prsToClose.push({ prNumber, commitSha });
|
||||
} else {
|
||||
// Normal mode: process commits from push event
|
||||
const commits = context.payload.commits || [];
|
||||
console.log(`Found ${commits.length} commit(s) in this push`);
|
||||
|
||||
// Process each commit
|
||||
for (const commit of commits) {
|
||||
const sha = commit.id;
|
||||
const committer = commit.committer.name;
|
||||
const message = commit.message;
|
||||
|
||||
console.log(`\n--- Processing commit ${sha.substring(0, 7)} ---`);
|
||||
console.log(`Committer: ${committer}`);
|
||||
|
||||
// Check if this is a Copybara commit or has a pull request reference
|
||||
const prRegex = /Merges?:?\s+(?:https:\/\/github\.com\/google\/adk-python\/pull\/|#)(\d+)/i;
|
||||
const isCopybara = committer === 'Copybara-Service' ||
|
||||
commit.author?.email === 'genai-sdk-bot@google.com' ||
|
||||
message.includes('GitOrigin-RevId:') ||
|
||||
message.includes('PiperOrigin-RevId:') ||
|
||||
prRegex.test(message);
|
||||
|
||||
if (!isCopybara) {
|
||||
console.log('Not a Copybara commit, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract PR number from commit message
|
||||
const prMatch = message.match(prRegex);
|
||||
|
||||
if (!prMatch) {
|
||||
console.log('No PR number found in Copybara commit message');
|
||||
continue;
|
||||
}
|
||||
|
||||
const prNumber = parseInt(prMatch[1]);
|
||||
const commitSha = sha.substring(0, 7);
|
||||
|
||||
prsToClose.push({ prNumber, commitSha });
|
||||
}
|
||||
}
|
||||
|
||||
// Process PRs to close
|
||||
for (const { prNumber, commitSha } of prsToClose) {
|
||||
console.log(`\n--- Processing PR #${prNumber} ---`);
|
||||
|
||||
// Get PR details to check if it's open
|
||||
let pr;
|
||||
try {
|
||||
pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`PR #${prNumber} not found or inaccessible:`, error.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only close if PR is still open
|
||||
if (pr.data.state !== 'open') {
|
||||
console.log(`PR #${prNumber} is already ${pr.data.state}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const author = pr.data.user.login;
|
||||
|
||||
try {
|
||||
// Add comment with commit reference
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: `Thank you @${author} for your contribution! 🎉\n\nYour changes have been successfully imported and merged via Copybara in commit ${commitSha}.\n\nClosing this PR as the changes are now in the main branch.`
|
||||
});
|
||||
|
||||
// Add 'merged' label to the PR
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
labels: ['merged']
|
||||
});
|
||||
|
||||
// Close the PR
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
console.log(`Successfully closed PR #${prNumber}`);
|
||||
} catch (error) {
|
||||
console.log(`Error closing PR #${prNumber}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (isManualRun) {
|
||||
console.log('\n=== TEST COMPLETED ===');
|
||||
} else {
|
||||
console.log('\n--- Finished processing all commits ---');
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
name: ADK Answering Agent for Discussions
|
||||
|
||||
on:
|
||||
discussion:
|
||||
types: [created]
|
||||
discussion_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
agent-answer-questions:
|
||||
if: >-
|
||||
github.repository == 'google/adk-python' && (
|
||||
(github.event_name == 'discussion' && github.event.discussion.category.name == 'Q&A') ||
|
||||
(github.event_name == 'discussion_comment' && contains(github.event.comment.body, '@adk-bot') && github.event.sender.login != 'adk-bot')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
id: auth
|
||||
uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
credentials_json: '${{ secrets.ADK_GCP_SA_KEY }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install google-adk google-cloud-discoveryengine
|
||||
|
||||
- name: Run Answering Script
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
ADK_GCP_SA_KEY: ${{ secrets.ADK_GCP_SA_KEY }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }}
|
||||
VERTEXAI_DATASTORE_ID: ${{ secrets.VERTEXAI_DATASTORE_ID }}
|
||||
GEMINI_API_DATASTORE_ID: ${{ secrets.GEMINI_API_DATASTORE_ID }}
|
||||
GOOGLE_GENAI_USE_VERTEXAI: 1
|
||||
OWNER: 'google'
|
||||
REPO: 'adk-python'
|
||||
INTERACTIVE: 0
|
||||
PYTHONPATH: contributing/samples/adk_team
|
||||
DISCUSSION_JSON: ${{ toJson(github.event.discussion) }}
|
||||
run: |
|
||||
python -c "import os; open('/tmp/discussion.json', 'w').write(os.environ['DISCUSSION_JSON'])"
|
||||
python -m adk_answering_agent.main --discussion-file /tmp/discussion.json
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
name: "ADK Issue Tracker Maintenance"
|
||||
|
||||
on:
|
||||
# Daily background cleanup at 6:00 AM UTC (10 PM PST)
|
||||
schedule:
|
||||
- cron: '0 6 * * *'
|
||||
|
||||
# Allows manual triggering from the Actions console
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_spam_monitor:
|
||||
description: 'Run Issue Monitoring Agent (Spam Sweep)'
|
||||
type: boolean
|
||||
default: true
|
||||
full_scan:
|
||||
description: 'For Issue Monitoring: Run an Initial Full Scan of ALL open issues'
|
||||
type: boolean
|
||||
default: false
|
||||
run_stale_auditor:
|
||||
description: 'Run Stale Issue Auditor Agent'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
REPO: adk-python
|
||||
CONCURRENCY_LIMIT: 3
|
||||
LLM_MODEL_NAME: "gemini-3.5-flash"
|
||||
PYTHONPATH: contributing/samples/adk_team
|
||||
|
||||
jobs:
|
||||
# 1. Sweep for spam, advertising, and invalid issues
|
||||
sweep-spam:
|
||||
if: |
|
||||
github.repository == 'google/adk-python' &&
|
||||
(github.event_name == 'schedule' || github.event.inputs.run_spam_monitor == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests google-adk python-dotenv
|
||||
|
||||
- name: Run Issue Monitoring Agent (Spam Sweep)
|
||||
env:
|
||||
INITIAL_FULL_SCAN: ${{ github.event.inputs.full_scan == 'true' }}
|
||||
run: python -m adk_issue_monitoring_agent.main
|
||||
|
||||
# 2. Audit inactive issues and nudge/close them
|
||||
audit-stale:
|
||||
if: |
|
||||
github.repository == 'google/adk-python' &&
|
||||
(github.event_name == 'schedule' || github.event.inputs.run_stale_auditor == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests google-adk python-dateutil
|
||||
|
||||
- name: Run Stale Auditor Agent
|
||||
run: python -m adk_stale_agent.main
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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.
|
||||
|
||||
name: ADK Pull Request Triaging Agent
|
||||
|
||||
on:
|
||||
# React within seconds of a PR opening/updating so the owner is assigned
|
||||
# promptly. pull_request_target (not pull_request) is required so the run has
|
||||
# the base-repo token needed to assign on community fork PRs; this workflow
|
||||
# only reads PR metadata via the API and never checks out untrusted PR code.
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
schedule:
|
||||
# Backfill every 6 hours in case an event was missed.
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'The Pull Request number to triage (leave empty for batch mode)'
|
||||
required: false
|
||||
type: 'string'
|
||||
pr_count:
|
||||
description: 'Number of PRs to process in batch mode (default: 10)'
|
||||
required: false
|
||||
default: '10'
|
||||
type: 'string'
|
||||
|
||||
# Never let two runs triage the same PR at once (e.g. rapid pushes); the
|
||||
# scheduled backfill still runs under its own group.
|
||||
concurrency:
|
||||
group: pr-triage-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
agent-triage-pull-request:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests google-adk
|
||||
|
||||
- name: Run Triaging Script
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GOOGLE_GENAI_USE_VERTEXAI: 0
|
||||
OWNER: 'google'
|
||||
REPO: 'adk-python'
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
|
||||
PR_COUNT_TO_PROCESS: ${{ github.event.inputs.pr_count || '10' }}
|
||||
INTERACTIVE: ${{ vars.PR_TRIAGE_INTERACTIVE }}
|
||||
PYTHONPATH: contributing/samples/adk_team
|
||||
run: python -m adk_pr_triaging_agent.main
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
# Step 3 (optional): Cherry-picks a commit from a base branch (main or v1) into the active release candidate branch.
|
||||
# Use this between step 1 and step 4 to include hotfixes in an in-progress release.
|
||||
# Note: Does NOT auto-trigger release-please to preserve manual changelog edits.
|
||||
name: "Release: Cherry-pick"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch line of the release candidate (main or v1)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: choice
|
||||
options:
|
||||
- main
|
||||
- v1
|
||||
commit_sha:
|
||||
description: 'Commit SHA to cherry-pick'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine Branch Configurations
|
||||
id: config
|
||||
run: |
|
||||
BRANCH="${{ inputs.branch }}"
|
||||
if [ "$BRANCH" = "v1" ]; then
|
||||
echo "candidate_branch=release/v1-candidate" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "candidate_branch=release/candidate" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.config.outputs.candidate_branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Cherry-pick commit
|
||||
run: |
|
||||
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
|
||||
echo "Cherry-picking ${INPUTS_COMMIT_SHA} to $CANDIDATE_BRANCH"
|
||||
git cherry-pick ${INPUTS_COMMIT_SHA}
|
||||
env:
|
||||
INPUTS_COMMIT_SHA: ${{ inputs.commit_sha }}
|
||||
|
||||
- name: Push changes
|
||||
run: |
|
||||
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
|
||||
git push origin "$CANDIDATE_BRANCH"
|
||||
echo "Successfully cherry-picked commit to $CANDIDATE_BRANCH"
|
||||
echo "If you want to regenerate the changelog PR, run the 'Release: Cut' workflow manually"
|
||||
echo "with action='regenerate' and branch='${{ inputs.branch }}'."
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
# Unified release manager. Supports:
|
||||
# 1. Cutting a new release candidate branch from main or v1.
|
||||
# 2. Regenerating/updating the changelog PR on an existing candidate branch.
|
||||
name: "Release: Cut"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: true
|
||||
default: 'cut'
|
||||
type: choice
|
||||
options:
|
||||
- cut
|
||||
- regenerate
|
||||
branch:
|
||||
description: 'Branch to release from (main or v1)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: choice
|
||||
options:
|
||||
- main
|
||||
- v1
|
||||
commit_sha:
|
||||
description: 'Optional Commit SHA (only used for "cut" action; overrides branch latest)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
cut-or-regenerate:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine Branch Configurations
|
||||
id: config
|
||||
run: |
|
||||
BRANCH="${{ inputs.branch }}"
|
||||
if [ "$BRANCH" = "v1" ]; then
|
||||
echo "base_ref=v1" >> $GITHUB_OUTPUT
|
||||
echo "candidate_branch=release/v1-candidate" >> $GITHUB_OUTPUT
|
||||
echo "config_file=.github/release-please-config-v1.json" >> $GITHUB_OUTPUT
|
||||
echo "manifest_file=.github/.release-please-manifest-v1.json" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "base_ref=main" >> $GITHUB_OUTPUT
|
||||
echo "candidate_branch=release/candidate" >> $GITHUB_OUTPUT
|
||||
echo "config_file=.github/release-please-config.json" >> $GITHUB_OUTPUT
|
||||
echo "manifest_file=.github/.release-please-manifest.json" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Action: CUT NEW RELEASE
|
||||
- name: Checkout base ref (Cut)
|
||||
if: inputs.action == 'cut'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || steps.config.outputs.base_ref }}
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
|
||||
- name: Check for existing candidate branch (Cut)
|
||||
if: inputs.action == 'cut'
|
||||
run: |
|
||||
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
|
||||
if git ls-remote --exit-code --heads origin "$CANDIDATE_BRANCH" &>/dev/null; then
|
||||
echo "Error: Branch $CANDIDATE_BRANCH already exists."
|
||||
echo "Please finalize or delete the existing release candidate before starting a new one."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create and push candidate branch (Cut)
|
||||
if: inputs.action == 'cut'
|
||||
run: |
|
||||
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
|
||||
git checkout -b "$CANDIDATE_BRANCH"
|
||||
git push origin "$CANDIDATE_BRANCH"
|
||||
echo "Created and pushed branch: $CANDIDATE_BRANCH"
|
||||
|
||||
# Action: REGENERATE EXISTING PR
|
||||
- name: Checkout existing candidate branch (Regenerate)
|
||||
if: inputs.action == 'regenerate'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.config.outputs.candidate_branch }}
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
|
||||
# Run Release Please
|
||||
- name: Run Release Please
|
||||
id: release_please
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
config-file: ${{ steps.config.outputs.config_file }}
|
||||
manifest-file: ${{ steps.config.outputs.manifest_file }}
|
||||
target-branch: ${{ steps.config.outputs.candidate_branch }}
|
||||
|
||||
# Curate the changelog: clean up and (for large releases) fold the
|
||||
# release-please output, draft a Highlights section on top, commit it back
|
||||
# to the release PR branch, and sync the PR description to match. The
|
||||
# script falls back to an empty Highlights template if drafting fails, so
|
||||
# this step never blocks the release. Guarded on `pr` (not `prs_created`)
|
||||
# so it also runs when release-please updates an existing PR (regenerate).
|
||||
- name: Set up Python
|
||||
if: steps.release_please.outputs.pr != ''
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install changelog curation dependencies
|
||||
if: steps.release_please.outputs.pr != ''
|
||||
run: pip install --upgrade google-genai
|
||||
|
||||
- name: Curate changelog Highlights
|
||||
if: steps.release_please.outputs.pr != ''
|
||||
# Curation is a nice-to-have layered on top of the release PR; never let
|
||||
# it turn the release run red (e.g. a push race or a transient error).
|
||||
continue-on-error: true
|
||||
env:
|
||||
RELEASE_PR: ${{ steps.release_please.outputs.pr }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GOOGLE_GENAI_USE_VERTEXAI: '0'
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PR_BRANCH=$(echo "$RELEASE_PR" | jq -r '.headBranchName')
|
||||
PR_NUMBER=$(echo "$RELEASE_PR" | jq -r '.number')
|
||||
echo "Curating changelog on release PR #$PR_NUMBER (branch: $PR_BRANCH)"
|
||||
git fetch origin "$PR_BRANCH"
|
||||
git checkout -B "$PR_BRANCH" FETCH_HEAD
|
||||
python scripts/curate_changelog.py --changelog CHANGELOG.md \
|
||||
--section-out /tmp/pr_body.md
|
||||
# Mirror the curated notes into the PR description so reviewers read
|
||||
# the same thing that ships in CHANGELOG.md. Done regardless of whether
|
||||
# the file changed, since the body is regenerated by release-please.
|
||||
if [ -s /tmp/pr_body.md ]; then
|
||||
gh pr edit "$PR_NUMBER" --body-file /tmp/pr_body.md
|
||||
fi
|
||||
if git diff --quiet -- CHANGELOG.md; then
|
||||
echo "No changelog file changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
USER_JSON=$(gh api user)
|
||||
git config user.name "$(echo "$USER_JSON" | jq -r '.login')"
|
||||
git config user.email "$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "chore: add curated highlights to changelog"
|
||||
# Rebase onto any concurrent PR-branch updates so the push doesn't fail on a stale ref.
|
||||
git pull --rebase origin "$PR_BRANCH"
|
||||
git push origin "$PR_BRANCH"
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
# Triggers automatically when the changelog PR is merged to a candidate branch.
|
||||
# Records the last-release-sha for Release Please and renames the branch to release/v{version}.
|
||||
name: "Release: Finalize"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- release/candidate
|
||||
- release/v1-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
finalize:
|
||||
if: github.event.pull_request.merged == true && github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for release-please PR
|
||||
id: check
|
||||
env:
|
||||
LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
|
||||
run: |
|
||||
if echo "$LABELS" | grep -q "autorelease: pending"; then
|
||||
echo "is_release_pr=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Not a release-please PR, skipping"
|
||||
echo "is_release_pr=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Determine Branch Configurations
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
id: config
|
||||
run: |
|
||||
CANDIDATE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [ "$CANDIDATE_BRANCH" = "release/v1-candidate" ]; then
|
||||
echo "base_branch=v1" >> $GITHUB_OUTPUT
|
||||
echo "config_file=.github/release-please-config-v1.json" >> $GITHUB_OUTPUT
|
||||
echo "manifest_file=.github/.release-please-manifest-v1.json" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "base_branch=main" >> $GITHUB_OUTPUT
|
||||
echo "config_file=.github/release-please-config.json" >> $GITHUB_OUTPUT
|
||||
echo "manifest_file=.github/.release-please-manifest.json" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version from manifest
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(jq -r '.["."]' "${{ steps.config.outputs.manifest_file }}")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Extracted version: $VERSION"
|
||||
|
||||
- name: Configure git identity from RELEASE_PAT
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
run: |
|
||||
USER_JSON=$(gh api user)
|
||||
git config user.name "$(echo "$USER_JSON" | jq -r '.login')"
|
||||
git config user.email "$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com"
|
||||
|
||||
- name: Record last-release-sha for release-please
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
run: |
|
||||
BASE_BRANCH="${{ steps.config.outputs.base_branch }}"
|
||||
CONFIG_FILE="${{ steps.config.outputs.config_file }}"
|
||||
CANDIDATE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
|
||||
git fetch origin "$BASE_BRANCH"
|
||||
CUT_SHA=$(git merge-base "origin/$BASE_BRANCH" HEAD)
|
||||
echo "Release was cut from $BASE_BRANCH at: $CUT_SHA"
|
||||
|
||||
jq --arg sha "$CUT_SHA" '. + {"last-release-sha": $sha}' \
|
||||
"$CONFIG_FILE" > tmp.json && mv tmp.json "$CONFIG_FILE"
|
||||
|
||||
git add "$CONFIG_FILE"
|
||||
git commit -m "chore: update last-release-sha for next $BASE_BRANCH release"
|
||||
git push origin "$CANDIDATE_BRANCH"
|
||||
|
||||
- name: Rename candidate to release/v{version}
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
run: |
|
||||
VERSION="v${STEPS_VERSION_OUTPUTS_VERSION}"
|
||||
CANDIDATE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
git push origin "$CANDIDATE_BRANCH:refs/heads/release/$VERSION" ":$CANDIDATE_BRANCH"
|
||||
echo "Renamed $CANDIDATE_BRANCH to release/$VERSION"
|
||||
env:
|
||||
STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Update PR label to tagged
|
||||
if: steps.check.outputs.is_release_pr == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--remove-label "autorelease: pending" \
|
||||
--add-label "autorelease: tagged"
|
||||
echo "Updated PR label to autorelease: tagged"
|
||||
@@ -0,0 +1,108 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
# Builds and publishes the package to PyPI from a release/v* branch.
|
||||
# Supports both main (v2+) stable releases and v1 pre-releases (with auto PEP 440 version mapping).
|
||||
# Creates a merge-back PR to sync changes back to the base branch (main or v1).
|
||||
name: "Release: Publish to PyPi"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate branch
|
||||
run: |
|
||||
if [[ ! "${GITHUB_REF_NAME}" =~ ^release/v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "Error: Must run from a release/v* branch (e.g., release/v0.3.0 or release/v1.35.0-alpha.1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Determine Release Type and Extract Version
|
||||
id: version
|
||||
run: |
|
||||
BRANCH_NAME="${GITHUB_REF_NAME}"
|
||||
VERSION="${BRANCH_NAME#release/v}"
|
||||
|
||||
# Check if this version matches the one in the v1 manifest to determine if it's a v1 release
|
||||
if [ -f .github/.release-please-manifest-v1.json ] && jq -e --arg v "$VERSION" '.["."] == $v' .github/.release-please-manifest-v1.json &>/dev/null; then
|
||||
echo "is_v1=true" >> $GITHUB_OUTPUT
|
||||
echo "base_branch=v1" >> $GITHUB_OUTPUT
|
||||
|
||||
SEMVER="$VERSION"
|
||||
echo "semver=$SEMVER" >> $GITHUB_OUTPUT
|
||||
echo "Semver version (v1): $SEMVER"
|
||||
|
||||
# PEP 440 Conversion (e.g., 2.0.0-alpha.1 -> 2.0.0a1)
|
||||
PEP440=$(echo "$SEMVER" | sed -E 's/-alpha\./a/; s/-beta\./b/; s/-rc\./rc/')
|
||||
echo "pep440=$PEP440" >> $GITHUB_OUTPUT
|
||||
echo "PEP 440 version (v1): $PEP440"
|
||||
else
|
||||
echo "is_v1=false" >> $GITHUB_OUTPUT
|
||||
echo "base_branch=main" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "semver=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "pep440=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Semver version (main): $VERSION"
|
||||
fi
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Update version.py with PEP 440 version (v1 only)
|
||||
if: steps.version.outputs.is_v1 == 'true'
|
||||
env:
|
||||
PEP440_VERSION: ${{ steps.version.outputs.pep440 }}
|
||||
run: |
|
||||
sed -i "s/^__version__ = .*/__version__ = \"${PEP440_VERSION}\"/" src/google/adk/version.py
|
||||
echo "Updated version.py to ${PEP440_VERSION}"
|
||||
grep __version__ src/google/adk/version.py
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
run: uv publish
|
||||
|
||||
- name: Create merge-back PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
SEMVER_VERSION: ${{ steps.version.outputs.semver }}
|
||||
PEP440_VERSION: ${{ steps.version.outputs.pep440 }}
|
||||
BASE_BRANCH: ${{ steps.version.outputs.base_branch }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "${GITHUB_REF_NAME}" \
|
||||
--title "chore: merge release v${PEP440_VERSION} to $BASE_BRANCH" \
|
||||
--body "Syncs version bump and CHANGELOG from release v${SEMVER_VERSION} to $BASE_BRANCH."
|
||||
@@ -0,0 +1,92 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
name: "Release: Update ADk Web"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
adk_web_repo:
|
||||
description: 'Source adk-web repository'
|
||||
required: true
|
||||
default: 'google/adk-web' # Default source repo
|
||||
adk_web_tag:
|
||||
description: 'Tag of the release to download (e.g. v1.0.0).'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
update-frontend:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Fetch and unzip frontend assets
|
||||
run: |
|
||||
TARGET_DIR="src/google/adk/cli/browser"
|
||||
REPO="${{ github.event.inputs.adk_web_repo }}"
|
||||
TAG="${{ github.event.inputs.adk_web_tag }}"
|
||||
# Clean target directory
|
||||
rm -rf "$TARGET_DIR"/*
|
||||
mkdir -p "$TARGET_DIR"
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "Fetching latest release metadata for $REPO..."
|
||||
RELEASE_JSON=$(curl -s "https://api.github.com/repos/$REPO/releases/latest")
|
||||
else
|
||||
echo "Fetching release metadata for $REPO tag $TAG..."
|
||||
RELEASE_JSON=$(curl -s "https://api.github.com/repos/$REPO/releases/tags/$TAG")
|
||||
fi
|
||||
# Extract download URL for adk-web-browser.zip
|
||||
DOWNLOAD_URL=$(echo "$RELEASE_JSON" | grep -o -E '"browser_download_url": "[^"]+"' | grep -o -E 'https://[^"]+' | grep 'adk-web-browser.zip' | head -n 1)
|
||||
if [ -z "$DOWNLOAD_URL" ]; then
|
||||
echo "Error: Could not find adk-web-browser.zip asset in the release."
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloading assets from: $DOWNLOAD_URL"
|
||||
curl -L -o frontend.zip "$DOWNLOAD_URL"
|
||||
echo "Extracting assets to $TARGET_DIR..."
|
||||
unzip -o frontend.zip -d "$TARGET_DIR"
|
||||
rm frontend.zip
|
||||
echo "Assets extracted successfully."
|
||||
|
||||
- name: Extract Bot Identity
|
||||
id: bot-identity
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
run: |
|
||||
USER_JSON=$(gh api user)
|
||||
echo "name=$(echo "$USER_JSON" | jq -r '.login')" >> $GITHUB_OUTPUT
|
||||
echo "email=$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
commit-message: "Update compiled adk web files from ${{ github.event.inputs.adk_web_repo }}@${{ github.event.inputs.adk_web_tag || 'latest' }}"
|
||||
branch: update-frontend-assets
|
||||
delete-branch: true
|
||||
title: "chore: update compiled adk web assets"
|
||||
committer: "${{ steps.bot-identity.outputs.name }} <${{ steps.bot-identity.outputs.email }}>"
|
||||
author: "${{ steps.bot-identity.outputs.name }} <${{ steps.bot-identity.outputs.email }}>"
|
||||
body: |
|
||||
This PR automatically updates the compiled adk web files in `src/google/adk/cli/browser/` using the assets from `${{ github.event.inputs.adk_web_repo }}@${{ github.event.inputs.adk_web_tag || 'latest' }}`.
|
||||
Please review the diff before merging.
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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.
|
||||
|
||||
name: Upload ADK Docs to Vertex AI Search
|
||||
|
||||
on:
|
||||
# Runs once per day at 16:00 UTC
|
||||
schedule:
|
||||
- cron: '00 16 * * *'
|
||||
# Manual trigger for testing and fixing
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload-adk-docs-to-vertex-ai-search:
|
||||
if: github.repository == 'google/adk-python'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Clone adk-docs repository
|
||||
run: git clone https://github.com/google/adk-docs.git /tmp/adk-docs
|
||||
|
||||
- name: Clone adk-python repository
|
||||
run: git clone https://github.com/google/adk-python.git /tmp/adk-python
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
id: auth
|
||||
uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
credentials_json: '${{ secrets.ADK_GCP_SA_KEY }}'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install google-adk markdown google-cloud-storage google-cloud-discoveryengine
|
||||
|
||||
- name: Run Answering Script
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }}
|
||||
VERTEXAI_DATASTORE_ID: ${{ secrets.VERTEXAI_DATASTORE_ID }}
|
||||
GOOGLE_GENAI_USE_VERTEXAI: 1
|
||||
GCS_BUCKET_NAME: ${{ secrets.GCS_BUCKET_NAME }}
|
||||
ADK_DOCS_ROOT_PATH: /tmp/adk-docs
|
||||
ADK_PYTHON_ROOT_PATH: /tmp/adk-python
|
||||
PYTHONPATH: ${{ github.workspace }}/contributing/samples/adk_team
|
||||
run: python -m adk_answering_agent.upload_docs_to_vertex_ai_search
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.env
|
||||
.venv
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.pytest_cache/
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# Local development settings
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
uv.lock
|
||||
|
||||
# Google Cloud specific
|
||||
.gcloudignore
|
||||
.gcloudignore.local
|
||||
|
||||
# Documentation
|
||||
docs/_build/
|
||||
site/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.bak
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# AI Coding Tools - Project-specific configs
|
||||
# Developers should symlink or copy AGENTS.md and add their own overrides locally
|
||||
.adk/
|
||||
.claude/
|
||||
.jetski*
|
||||
.antigravity*
|
||||
CLAUDE.md
|
||||
.cursor/
|
||||
.cursorrules
|
||||
.cursorignore
|
||||
.windsurfrules
|
||||
.aider*
|
||||
.continue/
|
||||
.codeium/
|
||||
.githubnext/
|
||||
.roo/
|
||||
.rooignore
|
||||
.bolt/
|
||||
.v0/
|
||||
|
||||
# Conformance test outputs (timestamped folders from --test mode)
|
||||
**/conformance/20*-*-*_*-*-*/
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
exclude: ^(src/google/adk/cli/browser/|src/google/adk/v1/|v1_tests/)
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.24.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.17
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
files: ^src/
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 8.0.1
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/google/pyink
|
||||
rev: 25.12.0
|
||||
hooks:
|
||||
- id: pyink
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: addlicense
|
||||
name: addlicense
|
||||
entry: >
|
||||
bash -c 'if command -v addlicense >/dev/null 2>&1;
|
||||
then addlicense -c "Google LLC" -l apache "$@";
|
||||
else echo "Warning: addlicense not installed, skipping"; fi' --
|
||||
language: system
|
||||
files: \.(py|sh)$
|
||||
- id: check-new-py-prefix
|
||||
name: Check new Python files have _ prefix
|
||||
description: Enforces private-by-default policy for new Python files (see .agents/skills/adk-style/references/visibility.md).
|
||||
entry: scripts/check_new_py_files.sh
|
||||
language: script
|
||||
files: ^src/google/adk/.*\.py$
|
||||
pass_filenames: false
|
||||
- id: compliance-checks
|
||||
name: ADK Compliance Checks
|
||||
entry: scripts/compliance_checks.py
|
||||
language: script
|
||||
files: \.py$
|
||||
# This script documents and matches the very patterns it forbids, so
|
||||
# it must not scan itself or other dev-only tooling.
|
||||
exclude: ^scripts/
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.22
|
||||
hooks:
|
||||
- id: mdformat
|
||||
files: ^(README\.md|CONTRIBUTING\.md|contributing/.*\.md)$
|
||||
exclude: (?i)SKILL\.md$
|
||||
additional_dependencies:
|
||||
- mdformat-gfm
|
||||
@@ -0,0 +1,28 @@
|
||||
## Project Overview
|
||||
|
||||
The Agent Development Kit (ADK) is an open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents.
|
||||
|
||||
### Key Components
|
||||
|
||||
- **Agent**: Blueprint defining identity, instructions, and tools.
|
||||
- **Runner**: Stateless execution engine that orchestrates agent execution.
|
||||
- **Tool**: Functions/capabilities agents can call.
|
||||
- **Session**: Conversation state management.
|
||||
- **Memory**: Long-term recall across sessions.
|
||||
- **Workflow** (ADK 2.0): Graph-based orchestration of complex, multi-step agent interactions.
|
||||
- **BaseNode** (ADK 2.0): Contract for all nodes, supporting output streaming and human-in-the-loop steps.
|
||||
- **Context** (ADK 2.0): Holds execution state and telemetry context mapped 1:1 to nodes.
|
||||
|
||||
For details on how the Runner works and the invocation lifecycle, please refer to the `adk-architecture` skill and the referenced documentation therein.
|
||||
|
||||
## ADK Knowledge, Architecture, and Style
|
||||
|
||||
Skills related to ADK development are in `.agents/skills/`.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
For detailed architecture patterns, component descriptions, and core interfaces, please refer to the **`adk-architecture`** skill at `.agents/skills/adk-architecture/SKILL.md`.
|
||||
|
||||
## Development Setup
|
||||
|
||||
The project uses `uv` for package management and Python 3.10+. Please refer to the **`adk-setup`** skill at `.agents/skills/adk-setup/SKILL.md` for detailed instructions.
|
||||
+2758
File diff suppressed because it is too large
Load Diff
+271
@@ -0,0 +1,271 @@
|
||||
# How to contribute
|
||||
|
||||
We'd love to accept your patches and contributions to this project.
|
||||
|
||||
- [How to contribute](#how-to-contribute)
|
||||
- [Before you begin](#before-you-begin)
|
||||
- [Sign our Contributor License Agreement](#sign-our-contributor-license-agreement)
|
||||
- [Review our community guidelines](#review-our-community-guidelines)
|
||||
- [Contribution workflow](#contribution-workflow)
|
||||
- [Finding Issues to Work On](#finding-issues-to-work-on)
|
||||
- [Requirement for PRs](#requirement-for-prs)
|
||||
- [Large or Complex Changes](#large-or-complex-changes)
|
||||
- [Testing Requirements](#testing-requirements)
|
||||
- [Unit Tests](#unit-tests)
|
||||
- [Manual End-to-End (E2E) Tests](#manual-end-to-end-e2e-tests)
|
||||
- [Documentation](#documentation)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Code reviews](#code-reviews)
|
||||
|
||||
## Before you begin
|
||||
|
||||
### Sign our Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a
|
||||
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
|
||||
You (or your employer) retain the copyright to your contribution; this simply
|
||||
gives us permission to use and redistribute your contributions as part of the
|
||||
project.
|
||||
|
||||
If you or your current employer have already signed the Google CLA (even if it
|
||||
was for a different project), you probably don't need to do it again.
|
||||
|
||||
Visit <https://cla.developers.google.com/> to see your current agreements or to
|
||||
sign a new one.
|
||||
|
||||
### Review our community guidelines
|
||||
|
||||
This project follows
|
||||
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
|
||||
|
||||
### Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
|
||||
## Contribution workflow
|
||||
|
||||
### Finding Issues to Work On
|
||||
|
||||
- Browse issues labeled **`good first issue`** (newcomer-friendly) or **`help wanted`** (general contributions).
|
||||
- For other issues, please kindly ask before contributing to avoid
|
||||
duplication.
|
||||
|
||||
### Requirement for PRs
|
||||
|
||||
- All PRs, other than small documentation or typo fixes, should have an Issue
|
||||
associated. If a relevant issue doesn't exist, please create one first or
|
||||
you may instead describe the bug or feature directly within the PR
|
||||
description, following the structure of our issue templates.
|
||||
- Small, focused PRs. Keep changes minimal—one concern per PR.
|
||||
- For bug fixes or features, please provide logs or screenshot after the fix
|
||||
is applied to help reviewers better understand the fix.
|
||||
- Please include a `testing plan` section in your PR to describe how you
|
||||
will test. This will save time for PR review. See `Testing Requirements`
|
||||
section for more details.
|
||||
|
||||
### Large or Complex Changes
|
||||
|
||||
For substantial features or architectural revisions:
|
||||
|
||||
- Open an Issue First: Outline your proposal, including design considerations
|
||||
and impact.
|
||||
- Gather Feedback: Discuss with maintainers and the community to ensure
|
||||
alignment and avoid duplicate work
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
To maintain code quality and prevent regressions, all code changes must include
|
||||
comprehensive tests and verifiable end-to-end (E2E) evidence.
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
Please add or update unit tests for your change. Please include a summary of
|
||||
passed `pytest` results.
|
||||
|
||||
Requirements for unit tests:
|
||||
|
||||
- **Coverage:** Cover new features, edge cases, error conditions, and typical
|
||||
use cases.
|
||||
- **Location:** Add or update tests under `tests/unittests/`, following
|
||||
existing naming conventions (e.g., `test_<module>_<feature>.py`).
|
||||
- **Framework:** Use `pytest`. Tests should be:
|
||||
- Fast and isolated.
|
||||
- Written clearly with descriptive names.
|
||||
- Free of external dependencies (use mocks or fixtures as needed).
|
||||
- **Quality:** Aim for high readability and maintainability; include
|
||||
docstrings or comments for complex scenarios.
|
||||
|
||||
#### Manual End-to-End (E2E) Tests
|
||||
|
||||
Manual E2E tests ensure integrated flows work as intended. Your tests should
|
||||
cover all scenarios. Sometimes, it's also good to ensure relevant functionality
|
||||
is not impacted.
|
||||
|
||||
Depending on your change:
|
||||
|
||||
- **ADK Web:**
|
||||
|
||||
- Use the `adk web` to verify functionality.
|
||||
- Capture and attach relevant screenshots demonstrating the UI/UX changes
|
||||
or outputs.
|
||||
- Label screenshots clearly in your PR description.
|
||||
|
||||
- **Runner:**
|
||||
|
||||
- Provide the testing setup. For example, the agent definition, and the
|
||||
runner setup.
|
||||
- Execute the `runner` tool to reproduce workflows.
|
||||
- Include the command used and console output showing test results.
|
||||
- Highlight sections of the log that directly relate to your change.
|
||||
|
||||
### Documentation
|
||||
|
||||
For any changes that impact user-facing documentation (guides, API reference,
|
||||
tutorials), please open a PR in the
|
||||
[adk-docs](https://github.com/google/adk-docs) repository to update the relevant
|
||||
part before or alongside your code PR.
|
||||
|
||||
## Development Setup
|
||||
|
||||
1. **Clone the repository:**
|
||||
|
||||
```shell
|
||||
gh repo clone google/adk-python
|
||||
cd adk-python
|
||||
```
|
||||
|
||||
1. **Install uv:**
|
||||
|
||||
Check out
|
||||
[uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
1. **Setup Development Tools:**
|
||||
|
||||
We use `pre-commit` for code formatting and license enforcement,
|
||||
`tox` with `tox-uv` for isolated multi-version testing, and
|
||||
`addlicense` for Apache 2.0 license headers.
|
||||
|
||||
```shell
|
||||
uv tool install pre-commit
|
||||
uv tool install tox --with tox-uv
|
||||
```
|
||||
|
||||
Optionally, install Google's `addlicense` tool for license header
|
||||
checks (requires Go):
|
||||
|
||||
```shell
|
||||
go install github.com/google/addlicense@latest
|
||||
```
|
||||
|
||||
If `addlicense` is not installed, the pre-commit hook will be
|
||||
skipped and CI will catch missing headers.
|
||||
|
||||
Install the git hooks to automatically format and check your code
|
||||
before committing:
|
||||
|
||||
```shell
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
The pre-commit hooks run `isort`, `pyink`, `addlicense`, and
|
||||
`mdformat` automatically on each commit.
|
||||
|
||||
1. **Create virtual environment and install dependencies:**
|
||||
|
||||
```shell
|
||||
uv venv --python "python3.11" ".venv"
|
||||
source .venv/bin/activate
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
1. **Run unit tests locally (Fast):**
|
||||
|
||||
If you just want to run tests quickly while developing, run `pytest`:
|
||||
|
||||
```shell
|
||||
pytest ./tests/unittests
|
||||
```
|
||||
|
||||
1. **Run multi-version unit tests (Required before PR):**
|
||||
|
||||
ADK guarantees compatibility across Python versions. You must run the full test suite across all supported versions using `tox`. This will execute tests in pristine, isolated environments.
|
||||
|
||||
```shell
|
||||
tox
|
||||
```
|
||||
|
||||
_(Note: `uv` will automatically download any Python interpreters you are missing!)_
|
||||
|
||||
1. **Auto-format the code:**
|
||||
|
||||
If you installed the git hooks in Step 3, this happens automatically on commit. To run it manually across all files:
|
||||
|
||||
```shell
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
1. **Build the wheel file:**
|
||||
|
||||
```shell
|
||||
uv build
|
||||
```
|
||||
|
||||
1. **Test the locally built wheel file:** Have a simple testing folder setup as
|
||||
mentioned in the
|
||||
[quickstart](https://google.github.io/adk-docs/get-started/quickstart/).
|
||||
|
||||
Then following below steps to test your changes:
|
||||
|
||||
Create a clean venv and activate it:
|
||||
|
||||
```shell
|
||||
VENV_PATH=~/venvs/adk-quickstart
|
||||
```
|
||||
|
||||
```shell
|
||||
command -v deactivate >/dev/null 2>&1 && deactivate
|
||||
```
|
||||
|
||||
```shell
|
||||
rm -rf $VENV_PATH \
|
||||
&& python3 -m venv $VENV_PATH \
|
||||
&& source $VENV_PATH/bin/activate
|
||||
```
|
||||
|
||||
Install the locally built wheel file:
|
||||
|
||||
```shell
|
||||
pip install dist/google_adk-<version>-py3-none-any.whl
|
||||
```
|
||||
|
||||
## Contributing Resources
|
||||
|
||||
[Contributing folder](https://github.com/google/adk-python/tree/main/contributing)
|
||||
has resources that are helpful for contributors.
|
||||
|
||||
## AI-Assisted Development
|
||||
|
||||
This repo includes built-in skills for AI coding agents
|
||||
(Antigravity, Gemini CLI, and others) to help with ADK development:
|
||||
|
||||
- **`setup-dev-env`** — Set up the local development environment:
|
||||
install dependencies, configure pre-commit hooks, and verify
|
||||
the setup.
|
||||
|
||||
- **`adk-debug`** — Debug ADK agents: inspect sessions, trace event
|
||||
flows, check LLM requests/responses, diagnose tool call issues.
|
||||
Supports both `adk web` (browser UI) and `adk run` (CLI) workflows.
|
||||
|
||||
- **`adk-workflow`** — Build graph-based workflow agents: function
|
||||
nodes, LLM agent nodes, edge patterns, routing, parallel processing
|
||||
(fan-out and ParallelWorker), human-in-the-loop, state management,
|
||||
and best practices. Includes reference docs and tested samples.
|
||||
|
||||
These skills are in `.agents/skills/` and are automatically available
|
||||
when using compatible AI coding tools in this repo.
|
||||
|
||||
The `AGENTS.md` file provides additional project context that can
|
||||
be used as LLM input.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Agent Development Kit (ADK) 2.0
|
||||
|
||||
[](LICENSE)
|
||||
[](https://pypi.org/project/google-adk/)
|
||||
[](https://pypi.org/project/google-adk/)
|
||||
[](https://pepy.tech/project/google-adk)
|
||||
[](https://github.com/google/adk-python/actions/workflows/python-unit-tests.yml)
|
||||
[](https://google.github.io/adk-docs/)
|
||||
|
||||
<h2 align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/adk-python/main/assets/agent-development-kit.png" width="256"/>
|
||||
</h2>
|
||||
<h3 align="center">
|
||||
An open-source, code-first Python framework for building, evaluating, and deploying sophisticated AI agents with flexibility and control.
|
||||
</h3>
|
||||
<h3 align="center">
|
||||
Important Links:
|
||||
<a href="https://google.github.io/adk-docs/">Docs</a>,
|
||||
<a href="https://github.com/google/adk-samples">Samples</a> &
|
||||
<a href="https://github.com/google/adk-web">ADK Web</a>.
|
||||
</h3>
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
> **⚠️ BREAKING CHANGES FROM 1.x**
|
||||
>
|
||||
> This release includes breaking changes to the agent API, event model, and
|
||||
> session schema. **Sessions generated by ADK 2.0 are readable by ADK 1.28+
|
||||
> (extra fields will be ignored), but are incompatible with older 1.x versions.**
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 🔥 What's New in 2.0
|
||||
|
||||
- **Workflow Runtime**: A graph-based execution engine for composing
|
||||
deterministic execution flows for agentic apps, with support for routing,
|
||||
fan-out/fan-in, loops, retry, state management, dynamic nodes,
|
||||
human-in-the-loop, and nested workflows.
|
||||
|
||||
- **Task API**: Structured agent-to-agent delegation with multi-turn task
|
||||
mode, single-turn controlled output, mixed delegation patterns,
|
||||
human-in-the-loop, and task agents as workflow nodes.
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
```bash
|
||||
pip install google-adk
|
||||
```
|
||||
|
||||
**Requirements:** Python 3.10+.
|
||||
|
||||
To install optional integrations, you can use the following command:
|
||||
|
||||
```bash
|
||||
pip install "google-adk[extensions]"
|
||||
```
|
||||
|
||||
The release cadence is roughly bi-weekly.
|
||||
|
||||
## Quick Start
|
||||
|
||||
> **Beginner Note:** ADK applications are built using two main classes:
|
||||
> **`Agent`** (defines an AI's instructions, tools, and behavior) and
|
||||
> **`Workflow`** (orchestrates agents and tasks in a graph-based flow).
|
||||
|
||||
### Agent
|
||||
|
||||
```python
|
||||
from google.adk import Agent
|
||||
|
||||
root_agent = Agent(
|
||||
name="greeting_agent",
|
||||
model="gemini-2.5-flash",
|
||||
instruction="You are a helpful assistant. Greet the user warmly.",
|
||||
)
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
```python
|
||||
from google.adk import Agent, Workflow
|
||||
|
||||
generate_fruit_agent = Agent(
|
||||
name="generate_fruit_agent",
|
||||
instruction="Return the name of a random fruit. Return only the name.",
|
||||
)
|
||||
|
||||
generate_benefit_agent = Agent(
|
||||
name="generate_benefit_agent",
|
||||
instruction="Tell me a health benefit about the specified fruit.",
|
||||
)
|
||||
|
||||
root_agent = Workflow(
|
||||
name="root_agent",
|
||||
edges=[("START", generate_fruit_agent, generate_benefit_agent)],
|
||||
)
|
||||
```
|
||||
|
||||
### Run Locally
|
||||
|
||||
```bash
|
||||
# Interactive CLI
|
||||
adk run path/to/my_agent
|
||||
|
||||
# Web UI (supports multi-agent directories or pointing directly to a single agent folder)
|
||||
adk web path/to/agents_dir
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **Getting Started**: https://google.github.io/adk-docs/
|
||||
- **Samples**: See `contributing/workflow_samples/` and
|
||||
`contributing/task_samples/` for workflow and task API examples.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache 2.0 License — see the
|
||||
[LICENSE](LICENSE) file for details.
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`google/adk-python`
|
||||
- 原始仓库:https://github.com/google/adk-python
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 413 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,16 @@
|
||||
# Contributing Resources
|
||||
|
||||
This folder hosts resources for ADK contributors, for example, testing samples etc.
|
||||
|
||||
## Samples
|
||||
|
||||
Samples folder host samples to test different features. The samples are usually minimal and simplistic to test one or a few scenarios.
|
||||
|
||||
**Note**: This is different from the [google/adk-samples](https://github.com/google/adk-samples) repo, which hosts more complex e2e samples for customers to use or modify directly.
|
||||
|
||||
## ADK project and architecture overview
|
||||
|
||||
The [adk_project_overview_and_architecture.md](adk_project_overview_and_architecture.md) describes the ADK project overview and its technical architecture from high-level.
|
||||
|
||||
This is helpful for contributors to understand the project and design philosophy.
|
||||
It can also be fed into LLMs for vibe-coding.
|
||||
@@ -0,0 +1,129 @@
|
||||
# ADK Project Overview and Architecture
|
||||
|
||||
Google Agent Development Kit (ADK) for Python
|
||||
|
||||
## Core Philosophy & Architecture
|
||||
|
||||
- Code-First: Everything is defined in Python code for versioning, testing, and IDE support. Avoid GUI-based logic.
|
||||
|
||||
- Modularity & Composition: We build complex multi-agent systems by composing multiple, smaller, specialized agents.
|
||||
|
||||
- Deployment-Agnostic: The agent's core logic is separate from its deployment environment. The same agent.py can be run locally for testing, served via an API, or deployed to the cloud.
|
||||
|
||||
## Foundational Abstractions (Our Vocabulary)
|
||||
|
||||
- Agent: The blueprint. It defines an agent's identity, instructions, and tools. It's a declarative configuration object.
|
||||
|
||||
- Tool: A capability. A Python function an agent can call to interact with the world (e.g., search, API call).
|
||||
|
||||
- Runner: The engine. It orchestrates the "Reason-Act" loop, manages LLM calls, and executes tools.
|
||||
|
||||
- Session: The conversation state. It holds the history for a single, continuous dialogue.
|
||||
|
||||
- Memory: Long-term recall across different sessions.
|
||||
|
||||
- Artifact Service: Manages non-textual data like files.
|
||||
|
||||
## Canonical Project Structure
|
||||
|
||||
Adhere to this structure for compatibility with ADK tooling.
|
||||
|
||||
```
|
||||
my_adk_project/
|
||||
└── src/
|
||||
└── my_app/
|
||||
├── agents/
|
||||
│ ├── my_agent/
|
||||
│ │ ├── __init__.py # Must contain: from . import agent \
|
||||
│ │ └── agent.py # Must contain: root_agent = Agent(...) \
|
||||
│ └── another_agent/
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py\
|
||||
```
|
||||
|
||||
agent.py: Must define the agent and assign it to a variable named root_agent. This is how ADK's tools find it.
|
||||
|
||||
`__init__.py`: In each agent directory, it must contain `from . import agent` to make the agent discoverable.
|
||||
|
||||
### Nested Agent Directories (Dev Mode / `adk web`)
|
||||
|
||||
In the local development server (`adk web` / `dev_server`), ADK supports deeply nested agent directories (e.g., sub-packages or structured folders).
|
||||
|
||||
- **Recursive Discovery**: The loader recursively walks directories to discover all valid agent applications containing an `agent.py`, `root_agent.yaml`, or `__init__.py` file.
|
||||
- **Dot Naming Convention**: Nested agents are represented in the system and referenced inside the Web UI using a standard dot-separated namespace notation (e.g., `agent_samples.empty_agent` or `workflow_samples.fan_out_fan_in`).
|
||||
- **Isolation**: Production environments (`adk api_server`) only support flat single-level agent directories for maximum security and isolation.
|
||||
|
||||
## Local Development & Debugging
|
||||
|
||||
Interactive UI (adk web): This is our primary debugging tool. It's a decoupled system:
|
||||
|
||||
Backend: A FastAPI server started with adk api_server.
|
||||
|
||||
Frontend: An Angular app that connects to the backend.
|
||||
|
||||
Use the "Events" tab to inspect the full execution trace (prompts, tool calls, responses).
|
||||
|
||||
CLI (adk run): For quick, stateless functional checks in the terminal.
|
||||
|
||||
Programmatic (pytest): For writing automated unit and integration tests.
|
||||
|
||||
## The API Layer (FastAPI)
|
||||
|
||||
We expose agents as production APIs using FastAPI.
|
||||
|
||||
- get_fast_api_app: This is the key helper function from google.adk.cli.fast_api that creates a FastAPI app from our agent directory.
|
||||
|
||||
- Standard Endpoints: The generated app includes standard routes like /list-apps and /run_sse for streaming responses. The wire format is camelCase.
|
||||
|
||||
- Custom Endpoints: We can add our own routes (e.g., /health) to the app object returned by the helper.
|
||||
|
||||
```Python
|
||||
|
||||
from google.adk.cli.fast_api import get_fast_api_app
|
||||
app = get_fast_api_app(agent_dir="./agents")
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
```
|
||||
|
||||
### Default Application Resolution (`ADK_DEFAULT_APP_NAME`)
|
||||
|
||||
By default, the ADK API server expects an explicit application context in all requests (e.g., via the `/apps/{app_name}/...` path or in the payload body).
|
||||
|
||||
However, if the environment variable `ADK_DEFAULT_APP_NAME` is set, or if the server is running in **single agent mode** (when pointing directly to a directory containing an agent instead of a directory of agents), the server will automatically resolve and fall back to that agent as the default application whenever a request lacks an explicit app name. In single agent mode, the local agent takes precedence over the `ADK_DEFAULT_APP_NAME` environment variable.
|
||||
|
||||
- **URL Path-Rewriting (Production Endpoints)**: Requests to production endpoints that omit the `/apps/{app_name}` prefix (such as `/users/{user_id}/sessions` or `/app-info`) are automatically rewritten by an internal ASGI middleware to target the default application. (Note: `/dev` and `/builder` endpoints are excluded from rewriting).
|
||||
- **Agent Execution & Streaming**: Requests to `/run`, `/run_sse`, or `/run_live` that omit the `app_name` parameter in their payload body or query string will automatically resolve to the default application.
|
||||
|
||||
## Deployment to Production
|
||||
|
||||
The adk cli provides the "adk deploy" command to deploy to Google Vertex Agent Engine, Google CloudRun, Google GKE.
|
||||
|
||||
## Testing & Evaluation Strategy
|
||||
|
||||
Testing is layered, like a pyramid.
|
||||
|
||||
### Layer 1: Unit Tests (Base)
|
||||
|
||||
What: Test individual Tool functions in isolation.
|
||||
|
||||
How: Use pytest in tests/test_tools.py. Verify deterministic logic.
|
||||
|
||||
### Layer 2: Integration Tests (Middle)
|
||||
|
||||
What: Test the agent's internal logic and interaction with tools.
|
||||
|
||||
How: Use pytest in tests/test_agent.py, often with mocked LLMs or services.
|
||||
|
||||
### Layer 3: Evaluation Tests (Top)
|
||||
|
||||
What: Assess end-to-end performance with a live LLM. This is about quality, not just pass/fail.
|
||||
|
||||
How: Use the ADK Evaluation Framework.
|
||||
|
||||
Test Cases: Create JSON files with input and a reference (expected tool calls and final response).
|
||||
|
||||
Metrics: tool_trajectory_avg_score (does it use tools correctly?) and response_match_score (is the final answer good?).
|
||||
|
||||
Run via: adk web (UI), pytest (for CI/CD), or adk eval (CLI).
|
||||
@@ -0,0 +1,234 @@
|
||||
# A2A OAuth Authentication Sample Agent
|
||||
|
||||
This sample demonstrates the **Agent-to-Agent (A2A)** architecture with **OAuth Authentication** workflows in the Agent Development Kit (ADK). The sample implements a multi-agent system where a remote agent can surface OAuth authentication requests to the local agent, which then guides the end user through the OAuth flow before returning the authentication credentials to the remote agent for API access.
|
||||
|
||||
## Overview
|
||||
|
||||
The A2A OAuth Authentication sample consists of:
|
||||
|
||||
- **Root Agent** (`root_agent`): The main orchestrator that handles user requests and delegates tasks to specialized agents
|
||||
- **YouTube Search Agent** (`youtube_search_agent`): A local agent that handles YouTube video searches using LangChain tools
|
||||
- **BigQuery Agent** (`bigquery_agent`): A remote A2A agent that manages BigQuery operations and requires OAuth authentication for Google Cloud access
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────┐
|
||||
│ End User │───▶│ Root Agent │───▶│ BigQuery Agent │
|
||||
│ (OAuth Flow) │ │ (Local) │ │ (Remote A2A) │
|
||||
│ │ │ │ │ (localhost:8001) │
|
||||
│ OAuth UI │◀───│ │◀───│ OAuth Request │
|
||||
└─────────────────┘ └────────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Multi-Agent Architecture**
|
||||
|
||||
- Root agent coordinates between local YouTube search and remote BigQuery operations
|
||||
- Demonstrates hybrid local/remote agent workflows
|
||||
- Seamless task delegation based on user request types
|
||||
|
||||
### 2. **OAuth Authentication Workflow**
|
||||
|
||||
- Remote BigQuery agent surfaces OAuth authentication requests to the root agent
|
||||
- Root agent guides end users through Google OAuth flow for BigQuery access
|
||||
- Secure token exchange between agents for authenticated API calls
|
||||
|
||||
### 3. **Google Cloud Integration**
|
||||
|
||||
- BigQuery toolset with comprehensive dataset and table management capabilities
|
||||
- OAuth-protected access to user's Google Cloud BigQuery resources
|
||||
- Support for listing, creating, and managing datasets and tables
|
||||
|
||||
### 4. **LangChain Tool Integration**
|
||||
|
||||
- YouTube search functionality using LangChain community tools
|
||||
- Demonstrates integration of third-party tools in agent workflows
|
||||
|
||||
## Setup and Usage
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Set up OAuth Credentials**:
|
||||
|
||||
```bash
|
||||
export OAUTH_CLIENT_ID=your_google_oauth_client_id
|
||||
export OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
|
||||
```
|
||||
|
||||
1. **Start the Remote BigQuery Agent server**:
|
||||
|
||||
```bash
|
||||
# Start the remote a2a server that serves the BigQuery agent on port 8001
|
||||
adk api_server --a2a --port 8001 contributing/samples/a2a_auth/remote_a2a
|
||||
```
|
||||
|
||||
1. **Run the Main Agent**:
|
||||
|
||||
```bash
|
||||
# In a separate terminal, run the adk web server
|
||||
adk web contributing/samples/
|
||||
```
|
||||
|
||||
### Example Interactions
|
||||
|
||||
Once both services are running, you can interact with the root agent:
|
||||
|
||||
**YouTube Search (No Authentication Required):**
|
||||
|
||||
```
|
||||
User: Search for 3 Taylor Swift music videos
|
||||
Agent: I'll help you search for Taylor Swift music videos on YouTube.
|
||||
[Agent delegates to YouTube Search Agent]
|
||||
Agent: I found 3 Taylor Swift music videos:
|
||||
1. "Anti-Hero" - Official Music Video
|
||||
2. "Shake It Off" - Official Music Video
|
||||
3. "Blank Space" - Official Music Video
|
||||
```
|
||||
|
||||
**BigQuery Operations (OAuth Required):**
|
||||
|
||||
```
|
||||
User: List my BigQuery datasets
|
||||
Agent: I'll help you access your BigQuery datasets. This requires authentication with your Google account.
|
||||
[Agent delegates to BigQuery Agent]
|
||||
Agent: To access your BigQuery data, please complete the OAuth authentication.
|
||||
[OAuth flow initiated - user redirected to Google authentication]
|
||||
User: [Completes OAuth flow in browser]
|
||||
Agent: Authentication successful! Here are your BigQuery datasets:
|
||||
- dataset_1: Customer Analytics
|
||||
- dataset_2: Sales Data
|
||||
- dataset_3: Marketing Metrics
|
||||
```
|
||||
|
||||
**Dataset Management:**
|
||||
|
||||
```
|
||||
User: Show me details for my Customer Analytics dataset
|
||||
Agent: I'll get the details for your Customer Analytics dataset.
|
||||
[Using existing OAuth token]
|
||||
Agent: Customer Analytics Dataset Details:
|
||||
- Created: 2024-01-15
|
||||
- Location: US
|
||||
- Tables: 5
|
||||
- Description: Customer behavior and analytics data
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Main Agent (`agent.py`)
|
||||
|
||||
- **`youtube_search_agent`**: Local agent with LangChain YouTube search tool
|
||||
- **`bigquery_agent`**: Remote A2A agent configuration for BigQuery operations
|
||||
- **`root_agent`**: Main orchestrator with task delegation logic
|
||||
|
||||
### Remote BigQuery Agent (`remote_a2a/bigquery_agent/`)
|
||||
|
||||
- **`agent.py`**: Implementation of the BigQuery agent with OAuth toolset
|
||||
- **`agent.json`**: Agent card of the A2A agent
|
||||
- **`BigQueryToolset`**: OAuth-enabled tools for BigQuery dataset and table management
|
||||
|
||||
## OAuth Authentication Workflow
|
||||
|
||||
The OAuth authentication process follows this pattern:
|
||||
|
||||
1. **Initial Request**: User requests BigQuery operation through root agent
|
||||
1. **Delegation**: Root agent delegates to remote BigQuery agent
|
||||
1. **Auth Check**: BigQuery agent checks for valid OAuth token
|
||||
1. **Auth Request**: If no token, agent surfaces OAuth request to root agent
|
||||
1. **User OAuth**: Root agent guides user through Google OAuth flow
|
||||
1. **Token Exchange**: Root agent sends OAuth token to BigQuery agent
|
||||
1. **API Call**: BigQuery agent uses token to make authenticated API calls
|
||||
1. **Result Return**: BigQuery agent returns results through root agent to user
|
||||
|
||||
## Supported BigQuery Operations
|
||||
|
||||
The BigQuery agent supports the following operations:
|
||||
|
||||
### Dataset Operations:
|
||||
|
||||
- **List Datasets**: `bigquery_datasets_list` - Get all user's datasets
|
||||
- **Get Dataset**: `bigquery_datasets_get` - Get specific dataset details
|
||||
- **Create Dataset**: `bigquery_datasets_insert` - Create new dataset
|
||||
|
||||
### Table Operations:
|
||||
|
||||
- **List Tables**: `bigquery_tables_list` - Get tables in a dataset
|
||||
- **Get Table**: `bigquery_tables_get` - Get specific table details
|
||||
- **Create Table**: `bigquery_tables_insert` - Create new table in dataset
|
||||
|
||||
## Extending the Sample
|
||||
|
||||
You can extend this sample by:
|
||||
|
||||
- Adding more Google Cloud services (Cloud Storage, Compute Engine, etc.)
|
||||
- Implementing token refresh and expiration handling
|
||||
- Adding role-based access control for different BigQuery operations
|
||||
- Creating OAuth flows for other providers (Microsoft, Facebook, etc.)
|
||||
- Adding audit logging for authentication events
|
||||
- Implementing multi-tenant OAuth token management
|
||||
|
||||
## Deployment to Other Environments
|
||||
|
||||
When deploying the remote BigQuery A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
|
||||
|
||||
### Local Development
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:8001/a2a/bigquery_agent",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Cloud Run Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-bigquery-service-abc123-uc.a.run.app/a2a/bigquery_agent",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Host/Port Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-domain.com:9000/a2a/bigquery_agent",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The `url` field in `remote_a2a/bigquery_agent/agent.json` must point to the actual RPC endpoint where your remote BigQuery A2A agent is deployed and accessible.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection Issues:**
|
||||
|
||||
- Ensure the local ADK web server is running on port 8000
|
||||
- Ensure the remote A2A server is running on port 8001
|
||||
- Check that no firewall is blocking localhost connections
|
||||
- **Verify the `url` field in `remote_a2a/bigquery_agent/agent.json` matches the actual deployed location of your remote A2A server**
|
||||
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
|
||||
|
||||
**OAuth Issues:**
|
||||
|
||||
- Verify OAuth client ID and secret are correctly set in .env file
|
||||
- Ensure OAuth redirect URIs are properly configured in Google Cloud Console
|
||||
- Check that the OAuth scopes include BigQuery access permissions
|
||||
- Verify the user has access to the BigQuery projects/datasets
|
||||
|
||||
**BigQuery Access Issues:**
|
||||
|
||||
- Ensure the authenticated user has BigQuery permissions
|
||||
- Check that the Google Cloud project has BigQuery API enabled
|
||||
- Verify dataset and table names are correct and accessible
|
||||
- Check for quota limits on BigQuery API calls
|
||||
|
||||
**Agent Communication Issues:**
|
||||
|
||||
- Check the logs for both the local ADK web server and remote A2A server
|
||||
- Verify OAuth tokens are properly passed between agents
|
||||
- Ensure agent instructions are clear about authentication requirements
|
||||
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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,61 @@
|
||||
# Copyright 2026 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 Agent
|
||||
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
|
||||
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
|
||||
from google.adk.tools.langchain_tool import LangchainTool
|
||||
from langchain_community.tools.youtube.search import YouTubeSearchTool
|
||||
|
||||
# Instantiate the tool
|
||||
langchain_yt_tool = YouTubeSearchTool()
|
||||
|
||||
# Wrap the tool in the LangchainTool class from ADK
|
||||
adk_yt_tool = LangchainTool(
|
||||
tool=langchain_yt_tool,
|
||||
)
|
||||
|
||||
youtube_search_agent = Agent(
|
||||
name="youtube_search_agent",
|
||||
instruction="""
|
||||
Ask customer to provide singer name, and the number of videos to search.
|
||||
""",
|
||||
description="Help customer to search for a video on Youtube.",
|
||||
tools=[adk_yt_tool],
|
||||
output_key="youtube_search_output",
|
||||
)
|
||||
|
||||
bigquery_agent = RemoteA2aAgent(
|
||||
name="bigquery_agent",
|
||||
description="Help customer to manage notion workspace.",
|
||||
agent_card=(
|
||||
f"http://localhost:8001/a2a/bigquery_agent{AGENT_CARD_WELL_KNOWN_PATH}"
|
||||
),
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name="root_agent",
|
||||
instruction="""
|
||||
You are a helpful assistant that can help search youtube videos, look up BigQuery datasets and tables.
|
||||
You delegate youtube search tasks to the youtube_search_agent.
|
||||
You delegate BigQuery tasks to the bigquery_agent.
|
||||
Always clarify the results before proceeding.
|
||||
""",
|
||||
global_instruction=(
|
||||
"You are a helpful assistant that can help search youtube videos, look"
|
||||
" up BigQuery datasets and tables."
|
||||
),
|
||||
sub_agents=[youtube_search_agent, bigquery_agent],
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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,45 @@
|
||||
{
|
||||
"capabilities": {},
|
||||
"defaultInputModes": [
|
||||
"text/plain"
|
||||
],
|
||||
"defaultOutputModes": [
|
||||
"application/json"
|
||||
],
|
||||
"description": "A Google BigQuery agent that helps manage users' data on Google BigQuery. Can list, get, and create datasets, as well as manage tables within datasets. Supports OAuth authentication for secure access to BigQuery resources.",
|
||||
"name": "bigquery_agent",
|
||||
"skills": [
|
||||
{
|
||||
"id": "dataset_management",
|
||||
"name": "Dataset Management",
|
||||
"description": "List, get details, and create BigQuery datasets",
|
||||
"tags": [
|
||||
"bigquery",
|
||||
"datasets",
|
||||
"google-cloud"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "table_management",
|
||||
"name": "Table Management",
|
||||
"description": "List, get details, and create BigQuery tables within datasets",
|
||||
"tags": [
|
||||
"bigquery",
|
||||
"tables",
|
||||
"google-cloud"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "oauth_authentication",
|
||||
"name": "OAuth Authentication",
|
||||
"description": "Secure authentication with Google BigQuery using OAuth",
|
||||
"tags": [
|
||||
"authentication",
|
||||
"oauth",
|
||||
"security"
|
||||
]
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:8001/a2a/bigquery_agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright 2026 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 os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.google_api_tool import BigQueryToolset
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Access the variable
|
||||
oauth_client_id = os.getenv("OAUTH_CLIENT_ID")
|
||||
oauth_client_secret = os.getenv("OAUTH_CLIENT_SECRET")
|
||||
tools_to_expose = [
|
||||
"bigquery_datasets_list",
|
||||
"bigquery_datasets_get",
|
||||
"bigquery_datasets_insert",
|
||||
"bigquery_tables_list",
|
||||
"bigquery_tables_get",
|
||||
"bigquery_tables_insert",
|
||||
]
|
||||
bigquery_toolset = BigQueryToolset(
|
||||
client_id=oauth_client_id,
|
||||
client_secret=oauth_client_secret,
|
||||
tool_filter=tools_to_expose,
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name="bigquery_agent",
|
||||
instruction="""
|
||||
You are a helpful Google BigQuery agent that help to manage users' data on Google BigQuery.
|
||||
Use the provided tools to conduct various operations on users' data in Google BigQuery.
|
||||
|
||||
Scenario 1:
|
||||
The user wants to query their bigquery datasets
|
||||
Use bigquery_datasets_list to query user's datasets
|
||||
|
||||
Scenario 2:
|
||||
The user wants to query the details of a specific dataset
|
||||
Use bigquery_datasets_get to get a dataset's details
|
||||
|
||||
Scenario 3:
|
||||
The user wants to create a new dataset
|
||||
Use bigquery_datasets_insert to create a new dataset
|
||||
|
||||
Scenario 4:
|
||||
The user wants to query their tables in a specific dataset
|
||||
Use bigquery_tables_list to list all tables in a dataset
|
||||
|
||||
Scenario 5:
|
||||
The user wants to query the details of a specific table
|
||||
Use bigquery_tables_get to get a table's details
|
||||
|
||||
Scenario 6:
|
||||
The user wants to insert a new table into a dataset
|
||||
Use bigquery_tables_insert to insert a new table into a dataset
|
||||
|
||||
Current user:
|
||||
<User>
|
||||
{userInfo?}
|
||||
</User>
|
||||
""",
|
||||
tools=[bigquery_toolset],
|
||||
)
|
||||
@@ -0,0 +1,165 @@
|
||||
# A2A Basic Sample Agent
|
||||
|
||||
This sample demonstrates the **Agent-to-Agent (A2A)** architecture in the Agent Development Kit (ADK), showcasing how multiple agents can work together to handle complex tasks. The sample implements an agent that can roll dice and check if numbers are prime.
|
||||
|
||||
## Overview
|
||||
|
||||
The A2A Basic sample consists of:
|
||||
|
||||
- **Root Agent** (`root_agent`): The main orchestrator that delegates tasks to specialized sub-agents
|
||||
- **Roll Agent** (`roll_agent`): A local sub-agent that handles dice rolling operations
|
||||
- **Prime Agent** (`prime_agent`): A remote A2A agent that checks if numbers are prime, this agent is running on a separate A2A server
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌────────────────────┐
|
||||
│ Root Agent │───▶│ Roll Agent │ │ Remote Prime │
|
||||
│ (Local) │ │ (Local) │ │ Agent │
|
||||
│ │ │ │ │ (localhost:8001) │
|
||||
│ │───▶│ │◀───│ │
|
||||
└─────────────────┘ └──────────────────┘ └────────────────────┘
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Local Sub-Agent Integration**
|
||||
|
||||
- The `roll_agent` demonstrates how to create and integrate local sub-agents
|
||||
- Handles dice rolling with configurable number of sides
|
||||
- Uses a simple function tool (`roll_die`) for random number generation
|
||||
|
||||
### 2. **Remote A2A Agent Integration**
|
||||
|
||||
- The `prime_agent` shows how to connect to remote agent services
|
||||
- Communicates with a separate service via HTTP at `http://localhost:8001/a2a/check_prime_agent`
|
||||
- Demonstrates cross-service agent communication
|
||||
|
||||
### 3. **Agent Orchestration**
|
||||
|
||||
- The root agent intelligently delegates tasks based on user requests
|
||||
- Can chain operations (e.g., "roll a die and check if it's prime")
|
||||
- Provides clear workflow coordination between multiple agents
|
||||
|
||||
### 4. **Example Tool Integration**
|
||||
|
||||
- Includes an `ExampleTool` with sample interactions for context
|
||||
- Helps the agent understand expected behavior patterns
|
||||
|
||||
## Setup and Usage
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Start the Remote Prime Agent server**:
|
||||
|
||||
```bash
|
||||
# Start the remote a2a server that serves the check prime agent on port 8001
|
||||
adk api_server --a2a --port 8001 contributing/samples/a2a_basic/remote_a2a
|
||||
```
|
||||
|
||||
1. **Run the Main Agent**:
|
||||
|
||||
```bash
|
||||
# In a separate terminal, run the adk web server
|
||||
adk web contributing/samples/
|
||||
```
|
||||
|
||||
### Example Interactions
|
||||
|
||||
Once both services are running, you can interact with the root agent:
|
||||
|
||||
**Simple Dice Rolling:**
|
||||
|
||||
```
|
||||
User: Roll a 6-sided die
|
||||
Bot: I rolled a 4 for you.
|
||||
```
|
||||
|
||||
**Prime Number Checking:**
|
||||
|
||||
```
|
||||
User: Is 7 a prime number?
|
||||
Bot: Yes, 7 is a prime number.
|
||||
```
|
||||
|
||||
**Combined Operations:**
|
||||
|
||||
```
|
||||
User: Roll a 10-sided die and check if it's prime
|
||||
Bot: I rolled an 8 for you.
|
||||
Bot: 8 is not a prime number.
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Main Agent (`agent.py`)
|
||||
|
||||
- **`roll_die(sides: int)`**: Function tool for rolling dice
|
||||
- **`roll_agent`**: Local agent specialized in dice rolling
|
||||
- **`prime_agent`**: Remote A2A agent configuration
|
||||
- **`root_agent`**: Main orchestrator with delegation logic
|
||||
|
||||
### Remote Prime Agent (`remote_a2a/check_prime_agent/`)
|
||||
|
||||
- **`agent.py`**: Implementation of the prime checking service
|
||||
- **`agent.json`**: Agent card of the A2A agent
|
||||
- **`check_prime(nums: list[int])`**: Prime number checking algorithm
|
||||
|
||||
## Extending the Sample
|
||||
|
||||
You can extend this sample by:
|
||||
|
||||
- Adding more mathematical operations (factorization, square roots, etc.)
|
||||
- Creating additional remote agent
|
||||
- Implementing more complex delegation logic
|
||||
- Adding persistent state management
|
||||
- Integrating with external APIs or databases
|
||||
|
||||
## Deployment to Other Environments
|
||||
|
||||
When deploying the remote A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
|
||||
|
||||
### Local Development
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:8001/a2a/check_prime_agent",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Cloud Run Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-service-abc123-uc.a.run.app/a2a/check_prime_agent",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Host/Port Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-domain.com:9000/a2a/check_prime_agent",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The `url` field in `remote_a2a/check_prime_agent/agent.json` must point to the actual RPC endpoint where your remote A2A agent is deployed and accessible.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection Issues:**
|
||||
|
||||
- Ensure the local ADK web server is running on port 8000
|
||||
- Ensure the remote A2A server is running on port 8001
|
||||
- Check that no firewall is blocking localhost connections
|
||||
- **Verify the `url` field in `remote_a2a/check_prime_agent/agent.json` matches the actual deployed location of your remote A2A server**
|
||||
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
|
||||
|
||||
**Agent Not Responding:**
|
||||
|
||||
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
|
||||
- Verify the agent instructions are clear and unambiguous
|
||||
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
# Copyright 2026 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.llm_agent import Agent
|
||||
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
|
||||
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
|
||||
from google.adk.tools.example_tool import ExampleTool
|
||||
from google.genai import types
|
||||
|
||||
|
||||
# --- Roll Die Sub-Agent ---
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result."""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
roll_agent = Agent(
|
||||
name="roll_agent",
|
||||
description="Handles rolling dice of different sizes.",
|
||||
instruction="""
|
||||
You are responsible for rolling dice based on the user's request.
|
||||
When asked to roll a die, you must call the roll_die tool with the number of sides as an integer.
|
||||
""",
|
||||
tools=[roll_die],
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
safety_settings=[
|
||||
types.SafetySetting( # avoid false alarm about rolling dice.
|
||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=types.HarmBlockThreshold.OFF,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
example_tool = ExampleTool([
|
||||
{
|
||||
"input": {
|
||||
"role": "user",
|
||||
"parts": [{"text": "Roll a 6-sided die."}],
|
||||
},
|
||||
"output": [
|
||||
{"role": "model", "parts": [{"text": "I rolled a 4 for you."}]}
|
||||
],
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"role": "user",
|
||||
"parts": [{"text": "Is 7 a prime number?"}],
|
||||
},
|
||||
"output": [{
|
||||
"role": "model",
|
||||
"parts": [{"text": "Yes, 7 is a prime number."}],
|
||||
}],
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"role": "user",
|
||||
"parts": [{"text": "Roll a 10-sided die and check if it's prime."}],
|
||||
},
|
||||
"output": [
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [{"text": "I rolled an 8 for you."}],
|
||||
},
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [{"text": "8 is not a prime number."}],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
prime_agent = RemoteA2aAgent(
|
||||
name="prime_agent",
|
||||
description="Agent that handles checking if numbers are prime.",
|
||||
agent_card=(
|
||||
f"http://localhost:8001/a2a/check_prime_agent{AGENT_CARD_WELL_KNOWN_PATH}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name="root_agent",
|
||||
instruction="""
|
||||
You are a helpful assistant that can roll dice and check if numbers are prime.
|
||||
You delegate rolling dice tasks to the roll_agent and prime checking tasks to the prime_agent.
|
||||
Follow these steps:
|
||||
1. If the user asks to roll a die, delegate to the roll_agent.
|
||||
2. If the user asks to check primes, delegate to the prime_agent.
|
||||
3. If the user asks to roll a die and then check if the result is prime, call roll_agent first, then pass the result to prime_agent.
|
||||
Always clarify the results before proceeding.
|
||||
""",
|
||||
global_instruction=(
|
||||
"You are DicePrimeBot, ready to roll dice and check prime numbers."
|
||||
),
|
||||
sub_agents=[roll_agent, prime_agent],
|
||||
tools=[example_tool],
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
safety_settings=[
|
||||
types.SafetySetting( # avoid false alarm about rolling dice.
|
||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=types.HarmBlockThreshold.OFF,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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,26 @@
|
||||
{
|
||||
"capabilities": {},
|
||||
"defaultInputModes": [
|
||||
"text/plain"
|
||||
],
|
||||
"defaultOutputModes": [
|
||||
"application/json"
|
||||
],
|
||||
"description": "An agent specialized in checking whether numbers are prime. It can efficiently determine the primality of individual numbers or lists of numbers.",
|
||||
"name": "check_prime_agent",
|
||||
"skills": [
|
||||
{
|
||||
"id": "prime_checking",
|
||||
"name": "Prime Number Checking",
|
||||
"description": "Check if numbers in a list are prime using efficient mathematical algorithms",
|
||||
"tags": [
|
||||
"mathematical",
|
||||
"computation",
|
||||
"prime",
|
||||
"numbers"
|
||||
]
|
||||
}
|
||||
],
|
||||
"url": "http://localhost:8001/a2a/check_prime_agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright 2026 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 import Agent
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
'No prime numbers found.'
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name='check_prime_agent',
|
||||
description='check prime agent that can check whether numbers are prime.',
|
||||
instruction="""
|
||||
You check whether numbers are prime.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
check_prime,
|
||||
],
|
||||
# planner=BuiltInPlanner(
|
||||
# thinking_config=types.ThinkingConfig(
|
||||
# include_thoughts=True,
|
||||
# ),
|
||||
# ),
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
safety_settings=[
|
||||
types.SafetySetting( # avoid false alarm about rolling dice.
|
||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=types.HarmBlockThreshold.OFF,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
# A2A Human-in-the-Loop Sample Agent
|
||||
|
||||
This sample demonstrates the **Agent-to-Agent (A2A)** architecture with **Human-in-the-Loop** workflows in the Agent Development Kit (ADK). The sample implements a reimbursement processing agent that automatically handles small expenses while requiring remote agent to process for larger amounts. The remote agent will require a human approval for large amounts, thus surface this request to local agent and human interacting with local agent can approve the request.
|
||||
|
||||
## Overview
|
||||
|
||||
The A2A Human-in-the-Loop sample consists of:
|
||||
|
||||
- **Root Agent** (`root_agent`): The main reimbursement agent that handles expense requests and delegates approval to remote Approval Agent for large amounts
|
||||
- **Approval Agent** (`approval_agent`): A remote A2A agent that handles the human approval process via long-running tools (which implements asynchronous approval workflows that can pause execution and wait for human input), this agent is running on a separate A2A server
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────┐
|
||||
│ Human Manager │───▶│ Root Agent │───▶│ Approval Agent │
|
||||
│ (External) │ │ (Local) │ │ (Remote A2A) │
|
||||
│ │ │ │ │ (localhost:8001) │
|
||||
│ Approval UI │◀───│ │◀───│ │
|
||||
└─────────────────┘ └────────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Automated Decision Making**
|
||||
|
||||
- Automatically approves reimbursements under $100
|
||||
- Uses business logic to determine when human intervention is required
|
||||
- Provides immediate responses for simple cases
|
||||
|
||||
### 2. **Human-in-the-Loop Workflow**
|
||||
|
||||
- Seamlessly escalates high-value requests (>$100) to remote approval agent
|
||||
- Remote approval agent uses long-running tools to surface approval requests back to the root agent
|
||||
- Human managers interact directly with the root agent to approve/reject requests
|
||||
|
||||
### 3. **Long-Running Tool Integration**
|
||||
|
||||
- Demonstrates `LongRunningFunctionTool` for asynchronous operations
|
||||
- Shows how to handle pending states and external updates
|
||||
- Implements proper tool response handling for delayed approvals
|
||||
|
||||
### 4. **Remote A2A Agent Communication**
|
||||
|
||||
- The approval agent runs as a separate service that processes approval workflows
|
||||
- Communicates via HTTP at `http://localhost:8001/a2a/human_in_loop`
|
||||
- Surfaces approval requests back to the root agent for human interaction
|
||||
|
||||
## Setup and Usage
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Start the Remote Approval Agent server**:
|
||||
|
||||
```bash
|
||||
# Start the remote a2a server that serves the human-in-the-loop approval agent on port 8001
|
||||
adk api_server --a2a --port 8001 contributing/samples/a2a/a2a_human_in_loop/remote_a2a
|
||||
```
|
||||
|
||||
1. **Run the Main Agent**:
|
||||
|
||||
```bash
|
||||
# In a separate terminal, run the adk web server
|
||||
adk web contributing/samples/a2a
|
||||
```
|
||||
|
||||
### Example Interactions
|
||||
|
||||
Once both services are running, you can interact with the root agent through the approval workflow:
|
||||
|
||||
**Automatic Approval (Under $100):**
|
||||
|
||||
```
|
||||
User: Please reimburse $50 for meals
|
||||
Agent: I'll process your reimbursement request for $50 for meals. Since this amount is under $100, I can approve it automatically.
|
||||
Agent: ✅ Reimbursement approved and processed: $50 for meals
|
||||
```
|
||||
|
||||
**Human Approval Required (Over $100):**
|
||||
|
||||
```
|
||||
User: Please reimburse $200 for conference travel
|
||||
Agent: I'll process your reimbursement request for $200 for conference travel. Since this amount exceeds $100, I need to get manager approval.
|
||||
Agent: 🔄 Request submitted for approval (Ticket: reimbursement-ticket-001). Please wait for manager review.
|
||||
[Human manager approves the pending request from the ADK Web UI]
|
||||
Agent: ✅ Great news! Your reimbursement has been approved by the manager. Processing $200 for conference travel.
|
||||
```
|
||||
|
||||
> **Approving from the ADK Web UI:** The approval is a *long-running tool* call
|
||||
> that runs on the remote approval agent. The pending call is surfaced in the
|
||||
> Web UI as a function call awaiting a response. To approve (or reject), hover
|
||||
> over the pending `ask_for_approval` function response in the UI and use
|
||||
> **"Send another response"** to send back an updated response such as
|
||||
> `{"status": "approved", "ticketId": "reimbursement-ticket-001"}`. Simply
|
||||
> typing "I approve" as a chat message will **not** resume the pending request,
|
||||
> because the framework needs a `FunctionResponse` that carries the same call
|
||||
> `id` to resume the long-running tool.
|
||||
>
|
||||
> For this resume to be routed back to the remote approval agent (rather than
|
||||
> restarting at the root agent), the sample is exposed as an `App` with
|
||||
> `ResumabilityConfig(is_resumable=True)` in `agent.py`.
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Main Agent (`agent.py`)
|
||||
|
||||
- **`reimburse(purpose: str, amount: float)`**: Function tool for processing reimbursements
|
||||
- **`approval_agent`**: Remote A2A agent configuration for human approval workflows
|
||||
- **`root_agent`**: Main reimbursement agent with automatic/manual approval logic
|
||||
|
||||
### Remote Approval Agent (`remote_a2a/human_in_loop/`)
|
||||
|
||||
- **`agent.py`**: Implementation of the approval agent with long-running tools
|
||||
|
||||
- **`agent.json`**: Agent card of the A2A agent
|
||||
|
||||
- **`ask_for_approval()`**: Long-running tool that handles approval requests
|
||||
|
||||
## Long-Running Tool Workflow
|
||||
|
||||
The human-in-the-loop process follows this pattern:
|
||||
|
||||
1. **Initial Call**: Root agent delegates approval request to remote approval agent for amounts >$100
|
||||
1. **Pending Response**: Remote approval agent returns immediate response with `status: "pending"` and ticket ID and surface the approval request to root agent
|
||||
1. **Agent Acknowledgment**: Root agent informs user about pending approval status
|
||||
1. **Human Interaction**: Human manager interacts with root agent to review and approve/reject the request
|
||||
1. **Updated Response**: Root agent receives updated tool response with approval decision and send it to remote agent
|
||||
1. **Final Action**: Remote agent processes the approval and completes the reimbursement and send the result to root_agent
|
||||
|
||||
## Extending the Sample
|
||||
|
||||
You can extend this sample by:
|
||||
|
||||
- Adding more complex approval hierarchies (multiple approval levels)
|
||||
- Implementing different approval rules based on expense categories
|
||||
- Creating additional remote agent for budget checking or policy validation
|
||||
- Adding notification systems for approval status updates
|
||||
- Integrating with external approval systems or databases
|
||||
- Implementing approval timeouts and escalation procedures
|
||||
|
||||
## Deployment to Other Environments
|
||||
|
||||
When deploying the remote approval A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
|
||||
|
||||
### Local Development
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:8001/a2a/human_in_loop",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Cloud Run Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-approval-service-abc123-uc.a.run.app/a2a/human_in_loop",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Host/Port Example
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-domain.com:9000/a2a/human_in_loop",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The `url` field in `remote_a2a/human_in_loop/agent.json` must point to the actual RPC endpoint where your remote approval A2A agent is deployed and accessible.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection Issues:**
|
||||
|
||||
- Ensure the local ADK web server is running on port 8000
|
||||
- Ensure the remote A2A server is running on port 8001
|
||||
- Check that no firewall is blocking localhost connections
|
||||
- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server**
|
||||
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
|
||||
|
||||
**Agent Not Responding:**
|
||||
|
||||
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
|
||||
- Verify the agent instructions are clear and unambiguous
|
||||
- Ensure long-running tool responses are properly formatted with matching IDs
|
||||
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
|
||||
|
||||
**Approval Workflow Issues:**
|
||||
|
||||
- Verify that updated tool responses use the same `id` and `name` as the original function call
|
||||
- Check that the approval status is correctly updated in the tool response
|
||||
- Ensure the human approval process is properly simulated or integrated
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user