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:
committed by
Copybara-Service
parent
4ccc6be6d4
commit
a308848969
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user