Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

321 lines
10 KiB
Markdown

# 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 |