Initial commit
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
# Human-in-the-Loop (Approval Flow)
|
||||
|
||||
A feature to pause graph execution and request human intervention.
|
||||
|
||||
## Overview
|
||||
|
||||
Human-in-the-Loop is a feature that requests **human approval or input** before important decisions or actions.
|
||||
|
||||
## Dynamic Interrupt (Recommended)
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def approval_node(state: State):
|
||||
"""Request approval"""
|
||||
approved = interrupt("Do you approve this action?")
|
||||
|
||||
if approved:
|
||||
return {"status": "approved"}
|
||||
else:
|
||||
return {"status": "rejected"}
|
||||
```
|
||||
|
||||
### Execution
|
||||
|
||||
```python
|
||||
# Initial execution (stops at interrupt)
|
||||
result = graph.invoke(input, config)
|
||||
|
||||
# Check interrupt information
|
||||
print(result["__interrupt__"]) # "Do you approve this action?"
|
||||
|
||||
# Approve and resume
|
||||
graph.invoke(None, config, resume=True)
|
||||
|
||||
# Or reject
|
||||
graph.invoke(None, config, resume=False)
|
||||
```
|
||||
|
||||
## Application Patterns
|
||||
|
||||
### Pattern 1: Approve or Reject
|
||||
|
||||
```python
|
||||
def action_approval(state: State):
|
||||
"""Approval before action execution"""
|
||||
action_details = prepare_action(state)
|
||||
|
||||
approved = interrupt({
|
||||
"question": "Approve this action?",
|
||||
"details": action_details
|
||||
})
|
||||
|
||||
if approved:
|
||||
result = execute_action(action_details)
|
||||
return {"result": result, "approved": True}
|
||||
else:
|
||||
return {"result": None, "approved": False}
|
||||
```
|
||||
|
||||
### Pattern 2: Editable Approval
|
||||
|
||||
```python
|
||||
def review_and_edit(state: State):
|
||||
"""Review and edit generated content"""
|
||||
generated = generate_content(state)
|
||||
|
||||
edited_content = interrupt({
|
||||
"instruction": "Review and edit this content",
|
||||
"content": generated
|
||||
})
|
||||
|
||||
return {"final_content": edited_content}
|
||||
|
||||
# Resume with edited version
|
||||
graph.invoke(None, config, resume=edited_version)
|
||||
```
|
||||
|
||||
### Pattern 3: Tool Execution Approval
|
||||
|
||||
```python
|
||||
@tool
|
||||
def send_email(to: str, subject: str, body: str):
|
||||
"""Send email (with approval)"""
|
||||
response = interrupt({
|
||||
"action": "send_email",
|
||||
"to": to,
|
||||
"subject": subject,
|
||||
"body": body,
|
||||
"message": "Approve sending this email?"
|
||||
})
|
||||
|
||||
if response.get("action") == "approve":
|
||||
# When approved, parameters can also be edited
|
||||
final_to = response.get("to", to)
|
||||
final_subject = response.get("subject", subject)
|
||||
final_body = response.get("body", body)
|
||||
|
||||
return actually_send_email(final_to, final_subject, final_body)
|
||||
else:
|
||||
return "Email cancelled by user"
|
||||
```
|
||||
|
||||
### Pattern 4: Input Validation Loop
|
||||
|
||||
```python
|
||||
def get_valid_input(state: State):
|
||||
"""Loop until valid input is obtained"""
|
||||
prompt = "Enter a positive number:"
|
||||
|
||||
while True:
|
||||
answer = interrupt(prompt)
|
||||
|
||||
if isinstance(answer, (int, float)) and answer > 0:
|
||||
break
|
||||
|
||||
prompt = f"'{answer}' is invalid. Enter a positive number:"
|
||||
|
||||
return {"value": answer}
|
||||
```
|
||||
|
||||
## Static Interrupt (For Debugging)
|
||||
|
||||
Set breakpoints at compile time:
|
||||
|
||||
```python
|
||||
graph = builder.compile(
|
||||
checkpointer=checkpointer,
|
||||
interrupt_before=["risky_node"], # Stop before node execution
|
||||
interrupt_after=["generate_content"] # Stop after node execution
|
||||
)
|
||||
|
||||
# Execute (stops before specified node)
|
||||
graph.invoke(input, config)
|
||||
|
||||
# Check state
|
||||
state = graph.get_state(config)
|
||||
|
||||
# Resume
|
||||
graph.invoke(None, config)
|
||||
```
|
||||
|
||||
## Practical Example: Multi-Stage Approval Workflow
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
class ApprovalState(TypedDict):
|
||||
request: str
|
||||
draft: str
|
||||
reviewed: str
|
||||
approved: bool
|
||||
|
||||
def draft_node(state: ApprovalState):
|
||||
"""Create draft"""
|
||||
draft = create_draft(state["request"])
|
||||
return {"draft": draft}
|
||||
|
||||
def review_node(state: ApprovalState):
|
||||
"""Review and edit"""
|
||||
reviewed = interrupt({
|
||||
"type": "review",
|
||||
"content": state["draft"],
|
||||
"instruction": "Review and improve the draft"
|
||||
})
|
||||
|
||||
return {"reviewed": reviewed}
|
||||
|
||||
def approval_node(state: ApprovalState):
|
||||
"""Final approval"""
|
||||
approved = interrupt({
|
||||
"type": "approval",
|
||||
"content": state["reviewed"],
|
||||
"question": "Approve for publication?"
|
||||
})
|
||||
|
||||
if approved:
|
||||
return Command(
|
||||
update={"approved": True},
|
||||
goto="publish"
|
||||
)
|
||||
else:
|
||||
return Command(
|
||||
update={"approved": False},
|
||||
goto="draft" # Return to draft
|
||||
)
|
||||
|
||||
def publish_node(state: ApprovalState):
|
||||
"""Publish"""
|
||||
publish(state["reviewed"])
|
||||
return {"status": "published"}
|
||||
|
||||
# Build graph
|
||||
builder.add_node("draft", draft_node)
|
||||
builder.add_node("review", review_node)
|
||||
builder.add_node("approval", approval_node)
|
||||
builder.add_node("publish", publish_node)
|
||||
|
||||
builder.add_edge(START, "draft")
|
||||
builder.add_edge("draft", "review")
|
||||
builder.add_edge("review", "approval")
|
||||
# approval node determines control flow with Command
|
||||
builder.add_edge("publish", END)
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
### ✅ Recommendations
|
||||
|
||||
- Pass values in JSON format
|
||||
- Keep `interrupt()` call order consistent
|
||||
- Make processing before `interrupt()` idempotent
|
||||
|
||||
### ❌ Prohibitions
|
||||
|
||||
- Don't catch `interrupt()` with `try-except`
|
||||
- Don't skip `interrupt()` conditionally
|
||||
- Don't pass non-serializable objects
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. High-Risk Operation Approval
|
||||
|
||||
```python
|
||||
def delete_data(state: State):
|
||||
"""Delete data (approval required)"""
|
||||
approved = interrupt({
|
||||
"action": "delete_data",
|
||||
"warning": "This cannot be undone!",
|
||||
"data_count": len(state["data_to_delete"])
|
||||
})
|
||||
|
||||
if approved:
|
||||
execute_delete(state["data_to_delete"])
|
||||
return {"deleted": True}
|
||||
return {"deleted": False}
|
||||
```
|
||||
|
||||
### 2. Creative Work Review
|
||||
|
||||
```python
|
||||
def creative_generation(state: State):
|
||||
"""Creative content generation and review"""
|
||||
versions = []
|
||||
|
||||
for _ in range(3):
|
||||
version = generate_creative(state["prompt"])
|
||||
versions.append(version)
|
||||
|
||||
selected = interrupt({
|
||||
"type": "select_version",
|
||||
"versions": versions,
|
||||
"instruction": "Select the best version or request regeneration"
|
||||
})
|
||||
|
||||
return {"final_version": selected}
|
||||
```
|
||||
|
||||
### 3. Incremental Data Input
|
||||
|
||||
```python
|
||||
def collect_user_info(state: State):
|
||||
"""Collect user information incrementally"""
|
||||
name = interrupt("What is your name?")
|
||||
|
||||
age = interrupt(f"Hello {name}, what is your age?")
|
||||
|
||||
city = interrupt("What city do you live in?")
|
||||
|
||||
return {
|
||||
"user_info": {
|
||||
"name": name,
|
||||
"age": age,
|
||||
"city": city
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Human-in-the-Loop is a feature for incorporating human judgment in important decisions. Dynamic interrupt is flexible and recommended.
|
||||
|
||||
## Related Pages
|
||||
|
||||
- [03_memory_management_persistence.md](03_memory_management_persistence.md) - Checkpointer is required
|
||||
- [02_graph_architecture_agent.md](02_graph_architecture_agent.md) - Combination with agents
|
||||
- [04_tool_integration_tool_node.md](04_tool_integration_tool_node.md) - Approval before tool execution
|
||||
Reference in New Issue
Block a user