Skip to content

Commit 8bbeedd

Browse files
committed
feat: Add InterruptService for human-in-the-loop graph workflows
1 parent fd2ba4d commit 8bbeedd

File tree

36 files changed

+9015
-122
lines changed

36 files changed

+9015
-122
lines changed

contributing/samples/graph_agent_dynamic_queue/agent.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def dynamic_task_dispatcher(
166166
result = ""
167167
async for event in agent.run_async(agent_ctx):
168168
if event.content and event.content.parts:
169-
result = event.content.parts[0].text or ""
169+
result += event.content.parts[0].text or ""
170170

171171
print(f" Result: {result[:100]}...")
172172

@@ -203,7 +203,7 @@ def build_dynamic_task_queue_graph() -> GraphAgent:
203203
graph.add_node("task_dispatcher", function=dynamic_task_dispatcher)
204204

205205
# Loop back to dispatcher while tasks remain.
206-
# Check task_queue directly (mutated in-place by the function node).
206+
# Check task_queue directly (updated via output_mapper return value).
207207
# The return dict {"tasks_remaining": N} is stored under state.data["task_dispatcher"]
208208
# by the output mapper, so state.data.get("tasks_remaining") would always be 0.
209209
graph.add_edge(
@@ -283,7 +283,12 @@ async def main():
283283
fresh_session = await session_service.get_session(
284284
app_name="dynamic_queue_demo", user_id="demo_user", session_id=session.id
285285
)
286-
final_session = fresh_session or session
286+
if fresh_session is None:
287+
print(
288+
"WARNING: session_service.get_session returned None, using stale copy"
289+
)
290+
fresh_session = session
291+
final_session = fresh_session
287292
final_data = final_session.state.get("graph_data", {})
288293
final_state = GraphState(data=final_data) if final_data else GraphState()
289294

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Composable HITL Orchestrated Pipeline
2+
3+
Demonstrates how to compose HITL review loops as reusable `NestedGraphNode` building blocks in a larger orchestrated pipeline.
4+
5+
## Graph Structure
6+
7+
**Outer graph** (`document_pipeline`):
8+
```
9+
[classify] --> [process] --> [aggregate]
10+
```
11+
12+
**Inner graph** (`review_stage`, wrapped as `NestedGraphNode`):
13+
```
14+
[execute] --> [review_gate] --> approved? --> [done]
15+
|
16+
v rejected
17+
[revise] --> [review_gate] (loop)
18+
```
19+
20+
## Key Concepts
21+
22+
- **Reusable HITL block**: The inner review graph is built once and wrapped in `NestedGraphNode`
23+
- **Clean abstraction**: Outer orchestrator doesn't know about inner HITL details
24+
- **Independent review cycles**: Each inner graph has its own interrupt timing
25+
- **Observability**: `_debug_process_output` tracks nested graph output (via Part B observability)
26+
- **Rule-based classification**: `classify` node determines stages without LLM
27+
28+
## Running
29+
30+
```bash
31+
# Without LLM (deterministic fallback):
32+
python -m contributing.samples.graph_agent_hitl_orchestrated.agent
33+
34+
# With LLM:
35+
export GOOGLE_API_KEY="your-key"
36+
python -m contributing.samples.graph_agent_hitl_orchestrated.agent
37+
```
38+
39+
## How It Works
40+
41+
1. `classify` reads input document, determines processing stages (extract/summarize/translate)
42+
2. `process` (NestedGraphNode) runs the inner review graph with HITL loop
43+
3. Inner `execute` performs the stage task, `review_gate` pauses for human approval
44+
4. If rejected: inner `revise` incorporates feedback, loops back
45+
5. If approved: inner `done` returns output to outer graph
46+
6. `aggregate` combines results into final output
47+
48+
## Differences from `graph_agent_hitl_review`
49+
50+
- `graph_agent_hitl_review`: Standalone HITL review loop
51+
- `graph_agent_hitl_orchestrated`: Wraps the review pattern as a NestedGraphNode in a larger pipeline, showing composability

contributing/samples/graph_agent_hitl_orchestrated/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)