Skip to content

Commit f362b1c

Browse files
authored
Merge branch 'main' into fix/compaction-interval-validation
2 parents dd65515 + ecbefd9 commit f362b1c

32 files changed

Lines changed: 1361 additions & 434 deletions

contributing/samples/workflows/node_as_tool/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44

55
Demonstrates wrapping both a regular ADK `Node` (using the `@node` decorator) and a `Workflow` as tools that can be automatically called by a parent `Agent`.
66

7+
It also demonstrates how a node-based tool can pause execution for Human-in-the-Loop (HITL) input using `RequestInput` and resume dynamically.
8+
79
In this sample:
810

911
1. The parent agent receives an inquiry about a customer's discount.
1012
1. It invokes `customer_lookup_workflow` (a `Workflow` wrapped as a tool) to retrieve customer status.
1113
1. It then invokes `calculate_discount` (a regular `Node` wrapped as a tool) using the retrieved status.
14+
1. If the customer is a VIP, `calculate_discount` yields a `RequestInput` event to ask for confirmation. The invocation pauses and is resumed once the user provides input.
1215

1316
## Sample Inputs
1417

1518
- `What discount does customer c123 get?`
1619

17-
*The parent agent first invokes `customer_lookup_workflow` to verify status, then invokes `calculate_discount` to determine the discount percentage, and summarizes the results.*
20+
*This is a multi-turn interaction demonstrating Human-in-the-Loop:*
21+
22+
- *Turn 1: The parent agent invokes `customer_lookup_workflow` to verify status, then invokes `calculate_discount`. `calculate_discount` yields a `RequestInput` asking "Apply VIP discount for tier 'Verified VIP Member'?", which pauses the run.*
23+
- *Turn 2: Respond with `yes` to resume the run. It calculates the "20% off" discount, and the parent agent summarizes the results.*
1824

1925
## Agent Topology Graph
2026

contributing/samples/workflows/node_as_tool/agent.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from google.adk import Agent
2020
from google.adk import Event
2121
from google.adk import Workflow
22+
from google.adk.apps._configs import ResumabilityConfig
23+
from google.adk.apps.app import App
2224
from google.adk.workflow import node
2325
from pydantic import BaseModel
2426
from pydantic import Field
@@ -29,18 +31,45 @@ class CustomerLookupArgs(BaseModel):
2931
user_id: str = Field(description="The customer's unique identifier.")
3032

3133

34+
from google.adk import Context
35+
from google.adk.events import RequestInput
36+
37+
3238
# 2. Define a regular Node using the @node decorator.
3339
# This Node is wrapped as a NodeTool automatically by the Agent.
3440
# As a NodeTool, it has the ability to yield intermediate Events during execution.
35-
@node
36-
def calculate_discount(tier: str, ctx) -> Generator[Event | str, None, None]:
41+
@node(rerun_on_resume=True)
42+
def calculate_discount(
43+
tier: str, ctx: Context
44+
) -> Generator[Event | RequestInput | str, None, None]:
3745
"""Calculates the discount percentage based on customer tier.
3846
3947
Args:
4048
tier: The customer's membership tier (e.g., VIP, Standard).
4149
"""
4250
yield Event(message=f"Checking discount rules for tier '{tier}'...")
43-
discount = "20% off" if "VIP" in tier else "5% off"
51+
52+
resume_input = ctx.resume_inputs.get("confirm_vip_discount")
53+
if "VIP" in tier:
54+
if not resume_input:
55+
yield RequestInput(
56+
interrupt_id="confirm_vip_discount",
57+
message=f"Apply VIP discount for tier '{tier}'?",
58+
)
59+
return
60+
61+
user_response = (
62+
resume_input.get("text")
63+
if isinstance(resume_input, dict)
64+
else resume_input
65+
)
66+
if str(user_response).lower() in ("yes", "y", "true"):
67+
discount = "20% off"
68+
else:
69+
discount = "5% off (VIP declined)"
70+
else:
71+
discount = "5% off"
72+
4473
yield discount
4574

4675

@@ -71,3 +100,13 @@ def lookup_customer_data(node_input: CustomerLookupArgs, ctx) -> dict[str, str]:
71100
""",
72101
tools=[customer_lookup_workflow, calculate_discount],
73102
)
103+
104+
105+
# Wrap the agent in an App and enable resumability. This is required because
106+
# the `calculate_discount` tool yields a RequestInput event which pauses
107+
# execution, and we need to resume the agent in a subsequent turn.
108+
app = App(
109+
name="node_as_tool",
110+
root_agent=root_agent,
111+
resumability_config=ResumabilityConfig(is_resumable=True),
112+
)
Lines changed: 111 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,158 @@
11
{
22
"appName": "node_as_tool",
3+
"id": "12345678-1234-1234-1234-123456789abc",
4+
"userId": "user",
35
"events": [
46
{
5-
"author": "user",
67
"content": {
78
"parts": [
89
{
9-
"text": "What discount does customer c123 get?"
10+
"functionCall": {
11+
"id": "fc-3",
12+
"args": {
13+
"interruptId": "fc-3",
14+
"payload": null,
15+
"message": "Apply VIP discount for tier 'Verified VIP Member'?",
16+
"response_schema": null
17+
},
18+
"name": "adk_request_input"
19+
}
20+
}
21+
]
22+
},
23+
"author": "calculate_discount",
24+
"nodeInfo": {
25+
"path": "calculate_discount@1"
26+
},
27+
"longRunningToolIds": [
28+
"fc-3"
29+
],
30+
"branch": "calculate_discount@fc-2"
31+
},
32+
{
33+
"content": {
34+
"parts": [
35+
{
36+
"text": "Checking discount rules for tier 'Verified VIP Member'..."
1037
}
1138
],
1239
"role": "user"
1340
},
14-
"id": "e-1",
15-
"invocationId": "i-1",
41+
"author": "calculate_discount",
1642
"nodeInfo": {
17-
"path": ""
18-
}
43+
"path": "calculate_discount@1"
44+
},
45+
"branch": "calculate_discount@fc-2"
1946
},
2047
{
21-
"author": "customer_service_agent",
2248
"content": {
2349
"parts": [
2450
{
25-
"functionCall": {
26-
"args": {
27-
"user_id": "c123"
28-
},
29-
"id": "fc-1",
30-
"name": "customer_lookup_workflow"
31-
}
51+
"text": "Checking discount rules for tier 'Verified VIP Member'..."
3252
}
3353
],
34-
"role": "model"
54+
"role": "user"
3555
},
36-
"id": "e-2",
37-
"invocationId": "i-1",
38-
"longRunningToolIds": [
39-
"fc-1"
40-
],
56+
"author": "calculate_discount",
4157
"nodeInfo": {
42-
"path": "customer_service_agent@1"
43-
}
58+
"path": "calculate_discount@1"
59+
},
60+
"branch": "calculate_discount@fc-2"
61+
},
62+
{
63+
"author": "calculate_discount",
64+
"output": "20% off",
65+
"nodeInfo": {
66+
"path": "calculate_discount@1",
67+
"outputFor": [
68+
"calculate_discount@1"
69+
]
70+
},
71+
"branch": "calculate_discount@fc-2"
4472
},
4573
{
4674
"author": "customer_lookup_workflow",
47-
"branch": "customer_lookup_workflow@fc-1",
48-
"id": "e-3",
49-
"invocationId": "i-1",
75+
"output": {
76+
"user_id": "c123",
77+
"tier": "Verified VIP Member"
78+
},
5079
"nodeInfo": {
80+
"path": "customer_lookup_workflow@1/lookup_customer_data@1",
5181
"outputFor": [
5282
"customer_lookup_workflow@1/lookup_customer_data@1",
5383
"customer_lookup_workflow@1"
54-
],
55-
"path": "customer_lookup_workflow@1/lookup_customer_data@1"
84+
]
5685
},
57-
"output": {
58-
"tier": "Verified VIP Member",
59-
"user_id": "c123"
60-
}
86+
"branch": "customer_lookup_workflow@fc-1"
6187
},
6288
{
63-
"author": "customer_service_agent",
6489
"content": {
6590
"parts": [
6691
{
67-
"functionResponse": {
68-
"id": "fc-1",
69-
"name": "customer_lookup_workflow",
70-
"response": {
71-
"tier": "Verified VIP Member",
72-
"user_id": "c123"
73-
}
92+
"functionCall": {
93+
"id": "fc-2",
94+
"args": {
95+
"tier": "Verified VIP Member"
96+
},
97+
"name": "calculate_discount"
7498
}
7599
}
76100
],
77-
"role": "user"
101+
"role": "model"
78102
},
79-
"id": "e-4",
80-
"invocationId": "i-1",
103+
"author": "customer_service_agent",
81104
"nodeInfo": {
82105
"path": "customer_service_agent@1"
83-
}
106+
},
107+
"longRunningToolIds": [
108+
"fc-2"
109+
]
84110
},
85111
{
86-
"author": "customer_service_agent",
87112
"content": {
88113
"parts": [
89114
{
90115
"functionCall": {
116+
"id": "fc-1",
91117
"args": {
92-
"tier": "Verified VIP Member"
118+
"user_id": "c123"
93119
},
94-
"id": "fc-2",
95-
"name": "calculate_discount"
120+
"name": "customer_lookup_workflow"
96121
}
97122
}
98123
],
99124
"role": "model"
100125
},
101-
"id": "e-5",
102-
"invocationId": "i-1",
103-
"longRunningToolIds": [
104-
"fc-2"
105-
],
126+
"author": "customer_service_agent",
106127
"nodeInfo": {
107128
"path": "customer_service_agent@1"
108-
}
129+
},
130+
"longRunningToolIds": [
131+
"fc-1"
132+
]
109133
},
110-
111134
{
112-
"author": "calculate_discount",
113-
"branch": "calculate_discount@fc-2",
114135
"content": {
115136
"parts": [
116137
{
117-
"text": "Checking discount rules for tier 'Verified VIP Member'..."
138+
"functionResponse": {
139+
"id": "fc-1",
140+
"name": "customer_lookup_workflow",
141+
"response": {
142+
"user_id": "c123",
143+
"tier": "Verified VIP Member"
144+
}
145+
}
118146
}
119147
],
120148
"role": "user"
121149
},
122-
"id": "e-6",
123-
"invocationId": "i-1",
150+
"author": "customer_service_agent",
124151
"nodeInfo": {
125-
"path": "calculate_discount@1"
152+
"path": "customer_service_agent@1"
126153
}
127154
},
128155
{
129-
"author": "calculate_discount",
130-
"branch": "calculate_discount@fc-2",
131-
"id": "e-7",
132-
"invocationId": "i-1",
133-
"nodeInfo": {
134-
"outputFor": [
135-
"calculate_discount@1"
136-
],
137-
"path": "calculate_discount@1"
138-
},
139-
"output": "20% off"
140-
},
141-
{
142-
"author": "customer_service_agent",
143156
"content": {
144157
"parts": [
145158
{
@@ -154,14 +167,12 @@
154167
],
155168
"role": "user"
156169
},
157-
"id": "e-8",
158-
"invocationId": "i-1",
170+
"author": "customer_service_agent",
159171
"nodeInfo": {
160172
"path": "customer_service_agent@1"
161173
}
162174
},
163175
{
164-
"author": "customer_service_agent",
165176
"content": {
166177
"parts": [
167178
{
@@ -170,13 +181,31 @@
170181
],
171182
"role": "model"
172183
},
173-
"id": "e-9",
174-
"invocationId": "i-1",
184+
"author": "customer_service_agent",
175185
"nodeInfo": {
176186
"path": "customer_service_agent@1"
177187
}
188+
},
189+
{
190+
"content": {
191+
"parts": [
192+
{
193+
"functionResponse": {
194+
"id": "fc-3",
195+
"name": "adk_request_input",
196+
"response": {
197+
"text": "yes"
198+
}
199+
}
200+
}
201+
],
202+
"role": "user"
203+
},
204+
"author": "user",
205+
"nodeInfo": {
206+
"path": ""
207+
},
208+
"branch": "calculate_discount@fc-2"
178209
}
179-
],
180-
"id": "12345678-1234-1234-1234-123456789abc",
181-
"userId": "user"
210+
]
182211
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ dependencies = [
4848
"python-multipart>=0.0.9,<1",
4949
"pyyaml>=6.0.2,<7",
5050
"requests>=2.32.4,<3",
51-
"starlette>=1.0.1,<2",
51+
"starlette>=1.3.1,<2",
5252
"tenacity>=9,<10",
5353
"typing-extensions>=4.5,<5",
5454
"tzlocal>=5.3,<6",

0 commit comments

Comments
 (0)