fix(samples): repair the workflow samples that produce the wrong output

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 962282749
This commit is contained in:
George Weale
2026-08-10 11:53:45 -07:00
committed by Copybara-Service
parent 4ccc6be6d4
commit a308848969
14 changed files with 61 additions and 24 deletions
@@ -30,7 +30,7 @@ graph TD
Key techniques demonstrated in this sample:
1. **Dynamic Scheduling**: Using a loop to create tasks via `ctx.run_node()`.
1. **Context Isolation**: Using `sub_branch` in `run_node` to isolate events for each parallel task, preventing context contamination.
1. **Context Isolation**: Using `use_sub_branch` in `run_node` to isolate events for each parallel task, preventing context contamination.
1. **`rerun_on_resume=True`**: Required on the orchestrator node to support resumption if any child node interrupts.
### Code Snippet
@@ -38,12 +38,12 @@ Key techniques demonstrated in this sample:
```python
# Fan-out: Schedule a dynamic node for each topic
tasks = []
for i, topic in enumerate(topics):
for topic in topics:
tasks.append(
ctx.run_node(
generator,
node_input=topic,
sub_branch=f"branch_{i}"
use_sub_branch=True,
)
)
@@ -15,6 +15,7 @@
from __future__ import annotations
import asyncio
from typing import AsyncGenerator
from google.adk import Agent
from google.adk import Context
@@ -33,7 +34,9 @@ generator = Agent(
@node(rerun_on_resume=True)
async def orchestrator(ctx: Context, node_input: str) -> str:
async def orchestrator(
ctx: Context, node_input: str
) -> AsyncGenerator[Event, None]:
"""Orchestrator node that performs dynamic fan-out and fan-in."""
# Split input comma-separated string into topics
topics = [t.strip() for t in node_input.split(",") if t.strip()]
@@ -41,7 +44,7 @@ async def orchestrator(ctx: Context, node_input: str) -> str:
# Fan-out: Schedule a dynamic node for each topic
tasks = []
for i, topic in enumerate(topics):
for topic in topics:
tasks.append(
ctx.run_node(
generator,
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import AsyncGenerator
from typing import Literal
from google.adk import Agent
@@ -59,7 +60,9 @@ evaluate_headline = Agent(
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
async def orchestrate(
ctx: Context, node_input: str
) -> AsyncGenerator[Event | str, None]:
yield Event(state={"topic": node_input})
while True:
@@ -7,6 +7,13 @@ YAML configuration file. It mirrors the
`contributing/samples/workflows/loop` sample, but uses YAML to define the
workflow structure instead of Python.
> **Status**: not runnable yet. The YAML agent loader resolves `agent_class`
> against `google.adk.agents` and requires a `BaseAgent` subclass with a config
> type, and no config field binds `edges`. `Workflow` satisfies neither, so
> `root_agent.yaml` below describes the intended syntax rather than syntax the
> loader accepts today. The individual agent files
> (`generate_headline.yaml`, `evaluate_headline.yaml`) do load.
## Sample Inputs
- `Python programming`
@@ -28,12 +35,12 @@ graph TD
This sample uses some special syntax in `root_agent.yaml` to support dynamic resolution and graph construction:
### 1. `_code` Suffix
### 1. Code References
Fields ending with `_code` (like `output_schema_code` in `evaluate_headline.yaml`) tell the ADK YAML mapper to resolve the value as a Python code reference rather than treating it as a plain string.
Fields that hold a Python object (like `output_schema` in `evaluate_headline.yaml`) take a `name` entry holding the fully qualified name of that object, which the loader imports.
- If it starts with `.`, it resolves relative to the current agent directory's Python package path.
- Example: `output_schema_code: .agent.Feedback` resolves to the `Feedback` Pydantic model in `agent.py` in the same directory.
- The name is resolved against `sys.path`, which includes the directory holding the agent folders.
- Example: `name: loop_config.agent.Feedback` resolves to the `Feedback` Pydantic model in `agent.py` in this directory.
### 2. Function References in Edges
@@ -16,5 +16,6 @@ agent_class: LlmAgent
name: evaluate_headline
instruction: |
Grade whether the headline is related to technology or software engineering.
output_schema_code: .agent.Feedback
output_schema:
name: loop_config.agent.Feedback
output_key: feedback
@@ -65,7 +65,7 @@ To send messages in an ADK node, yield an `Event` object with the `message` argu
```
1. **Stream a message in chunks**:
Provide the `partial=True` flag for intermediate chunks. This provides a better user experience by allowing the UI to show the response in a streaming fashion, thereby lowering the latency to see the first word. ADK automatically accumulates all partial messages and merges them into a final message for you for session storage.
Provide the `partial=True` flag for intermediate chunks. This provides a better user experience by allowing the UI to show the response in a streaming fashion, thereby lowering the latency to see the first word. Partial events are forwarded to the client but are not stored in the session, so finish by yielding the assembled message once without `partial=True`.
> **Note**: To stream multiple messages or tokens smoothly, your node function **must be an asynchronous generator** (`async def`). This allows ADK to yield messages to the client immediately without blocking.
@@ -78,4 +78,5 @@ To send messages in an ADK node, yield an `Event` object with the `message` argu
yield Event(message="may I", partial=True)
await asyncio.sleep(0.5)
yield Event(message=" help you?", partial=True)
yield Event(message="How may I help you?")
```
@@ -71,6 +71,8 @@ async def stream_sentence(node_input: Any = None):
"""
Demonstrates streaming by sending a sentence in chunks.
The `partial=True` flag tells the UI that this is part of an ongoing message.
Partial events are not written to the session, so the node ends by yielding
the assembled sentence once as a non-partial event.
"""
yield Event(message="#4 Starting to stream...")
sentence = """\
@@ -89,6 +91,8 @@ You can stream in markdown as well. For example, the table below:
yield Event(message=chunk, partial=True)
await sleep_if_not_pytest(0.2)
yield Event(message=sentence)
root_agent = Workflow(
name="message",
@@ -134,6 +134,22 @@
"nodeInfo": {
"path": "message@1/stream_sentence@1"
}
},
{
"author": "message",
"content": {
"parts": [
{
"text": "This is a streaming message sent in chunks.\n\nYou can stream in markdown as well. For example, the table below:\n\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n| Cell 3 | Cell 4 |\n"
}
],
"role": "user"
},
"id": "e-9",
"invocationId": "i-1",
"nodeInfo": {
"path": "message@1/stream_sentence@1"
}
}
],
"id": "9cfc0ef6-11d6-4260-84cf-be22731ab69e",
@@ -19,7 +19,7 @@ from typing import Generator
from google.adk import Agent
from google.adk import Event
from google.adk import Workflow
from google.adk.apps._configs import ResumabilityConfig
from google.adk.apps import ResumabilityConfig
from google.adk.apps.app import App
from google.adk.workflow import node
from pydantic import BaseModel
@@ -38,10 +38,10 @@ from google.adk.events import RequestInput
# 2. Define a regular Node using the @node decorator.
# This Node is wrapped as a NodeTool automatically by the Agent.
# As a NodeTool, it has the ability to yield intermediate Events during execution.
# Annotate the yield type with the data the tool returns, not the Event and
# RequestInput control-flow items, so the tool's response schema stays small.
@node(rerun_on_resume=True)
def calculate_discount(
tier: str, ctx: Context
) -> Generator[Event | RequestInput | str, None, None]:
def calculate_discount(tier: str, ctx: Context) -> Generator[str, None, None]:
"""Calculates the discount percentage based on customer tier.
Args:
@@ -20,7 +20,8 @@ This pattern is crucial for tasks where AI actions require human verification be
```mermaid
graph TD
START --> draft_email
START --> process_input
process_input --> draft_email
draft_email --> request_human_review
request_human_review --> handle_human_review
handle_human_review -->|revise| draft_email
@@ -59,7 +60,7 @@ graph TD
Workflow(
name="request_input",
edges=[
("START", ..., draft_email, request_human_review, handle_human_review),
("START", process_input, draft_email, request_human_review, handle_human_review),
(handle_human_review, {"revise": draft_email, "approved": send_email}),
],
)
@@ -54,16 +54,16 @@ graph TD
approved_days: Optional[int] = Field(None)
```
1. **Yield a RequestInput:** Pass the schema and optionally a `payload` for the client to display.
1. **Return a RequestInput:** Pass the schema and optionally a `payload` for the client to display.
```python
def evaluate_request(request: TimeOffRequest):
# ... logic to check if manager review is needed ...
yield RequestInput(
return RequestInput(
interrupt_id="manager_approval",
message="Please review this time off request.",
payload=request,
response_schema=TimeOffDecision.model_json_schema()
response_schema=TimeOffDecision,
)
```
@@ -25,7 +25,8 @@ This allows you to combine the requesting and handling of human input into a sin
```mermaid
graph TD
START --> draft_email
START --> process_input
process_input --> draft_email
draft_email --> human_review[human_review <br/>reruns on resume]
human_review -->|revise| draft_email
human_review -->|approved| send_email
@@ -8,7 +8,7 @@ The ADK framework allows you to easily handle these scenarios by wrapping the un
When a node raises an exception, the framework automatically emits an error event (with `error_code` and `error_message`) so the error is visible in the event stream. If the node has retry configured, it will be retried after the backoff delay.
This sample demonstrates a `get_weather` node that intentionally fails randomly (70% chance) by raising an `HTTPError` representing a 500 Internal Server error. The framework gracefully recovers and eventually succeeds, passing the result to `report_weather`.
This sample demonstrates a `get_weather` node that intentionally fails randomly (70% chance) by raising an `HTTPError` representing a 500 Internal Server error. The framework gracefully recovers and usually succeeds within the five configured attempts, passing the result to `report_weather`.
## Graph
@@ -15,8 +15,8 @@
from google.adk import Agent
from google.adk import Context
from google.adk.workflow import node
from google.adk.workflow import START
from google.adk.workflow import Workflow
from google.adk.workflow._base_node import START
summarizer = Agent(
name='summarizer',