Overview
Human-in-the-Loop (HITL) patterns let you pause execution for human review, approval, or input. This is essential for:
- ✅ Content Moderation - Review AI-generated content before publishing
- 🎯 Decision Approval - Require human sign-off on critical actions
- ✏️ Content Editing - Let humans refine AI outputs
- 🔍 Quality Control - Inspect intermediate results
The Interrupt Primitive
The interrupt() function pauses execution and returns control to the caller:
from upsonic.graphv2 import interrupt
def review_node(state: MyState) -> dict:
# Pause and show data to human
human_response = interrupt({
"action": "review",
"data": state["draft"],
"options": ["approve", "edit", "reject"]
})
# Execution resumes here when graph.invoke() is called with Command(resume=...)
if human_response.get("action") == "approve":
return {"approved": True}
else:
return {"approved": False}
How Interrupts Work
- Node calls interrupt() - Execution pauses
- Graph returns with
__interrupt__ key - Contains interrupt data
- Application shows data to human - Display UI, wait for input
- Resume with Command(resume=…) - Continue execution with human response
Basic Interrupt Example
from typing_extensions import TypedDict
from upsonic.graphv2 import StateGraph, START, END, MemorySaver, Command, interrupt
class ApprovalState(TypedDict):
content: str
approved: bool
feedback: str
def generate_content(state: ApprovalState) -> dict:
draft = "This is AI-generated content about " + state["content"]
return {"content": draft}
def review_node(state: ApprovalState) -> dict:
response = interrupt({
"action": "review_content",
"content": state["content"],
"instruction": "Please review and approve or provide feedback"
})
if response.get("action") == "approve":
return {"approved": True, "feedback": ""}
else:
return {"approved": False, "feedback": response.get("feedback", "")}
builder = StateGraph(ApprovalState)
builder.add_node("generate", generate_content)
builder.add_node("review", review_node)
builder.add_edge(START, "generate")
builder.add_edge("generate", "review")
builder.add_edge("review", END)
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "approval-1"}}
result = graph.invoke(
{"content": "Python programming", "approved": False, "feedback": ""},
config=config
)
# Check if interrupted
if "__interrupt__" in result:
print("⏸️ Execution paused for review")
interrupt_data = result["__interrupt__"][0]["value"]
print(f"Content to review: {interrupt_data['content']}")
# Simulate human approval
human_decision = {"action": "approve"}
# Resume execution
final_result = graph.invoke(
Command(resume=human_decision),
config=config
)
print(f"Approved: {final_result['approved']}")
Interrupts require checkpointers because execution state must be persisted between the interrupt and resume calls.
Interrupt Patterns
Pattern 1: Approve/Reject
def approval_node(state: State) -> dict:
approved = interrupt({
"action": "approve_action",
"data": state["action_details"]
})
return {"status": "approved" if approved else "rejected"}
# Resume with boolean
graph.invoke(Command(resume=True), config=config) # Approve
Pattern 2: Edit Content
def edit_node(state: State) -> dict:
edited_text = interrupt({
"action": "edit_content",
"original": state["draft"]
})
return {"final_content": edited_text}
# Resume with edited text
graph.invoke(
Command(resume="Human-edited content here"),
config=config
)
Pattern 3: Iterative Refinement
from upsonic.graphv2 import Command, END
def generate_and_review(state: State) -> Command:
draft = generate_content(state)
feedback = interrupt({
"action": "review_and_refine",
"draft": draft
})
if feedback["action"] == "approve":
return Command(
update={"final": draft, "approved": True},
goto=END
)
else:
return Command(
update={"feedback": feedback["comments"]},
goto="generate_and_review" # Loop back
)
Interrupt Configuration
Configure where interrupts can happen:
# Interrupt before specific nodes
graph = builder.compile(
checkpointer=checkpointer,
interrupt_before=["critical_action"]
)
# Interrupt after specific nodes
graph = builder.compile(
checkpointer=checkpointer,
interrupt_after=["data_processing"]
)
Best Practices
1. Provide Clear Context
# ✅ Good - rich context
interrupt({
"action": "review",
"content": state["draft"],
"metadata": {
"author": "AI Assistant",
"word_count": len(state["draft"].split())
},
"instructions": "Review for accuracy and tone"
})
2. Handle Resume Values Safely
def review_node(state: State) -> dict:
response = interrupt({"action": "review"})
# ✅ Good - validate response
if not isinstance(response, dict):
response = {"action": "approve"} # Default
action = response.get("action", "approve")
return {"status": action}
Next Steps