feat: solution for 'Human-in-the-loop (interrupt / resume)'

This commit is contained in:
2026-06-24 14:57:58 +03:00
commit 3c81f16ab4
4 changed files with 159 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.env
dist/
build/
*.log
+67
View File
@@ -0,0 +1,67 @@
# LangGraph Human-in-the-loop Demo
This project demonstrates how to create a LangGraph that pauses execution to ask the user for confirmation via a custom interrupt. The user is prompted in the console using `questionary`, and the graph resumes once the user provides an answer.
## Features
- **Custom interrupt node**: Triggers a pause and sends a structured payload (type, question, options).
- **Human-in-the-loop**: The graph waits for user input before continuing.
- **State persistence**: Uses an in-memory checkpoint to resume execution after the interrupt.
- **Simple console UI**: Uses `questionary` for interactive prompts.
## Requirements
- Python 3.10 or newer
- `langgraph`
- `questionary`
Install the dependencies with:
```bash
pip install -r requirements.txt
```
## Running the Demo
```bash
python src/main.py
```
You will see a prompt:
```
Уверены, что хотите продолжить?
approve
reject
```
Select an option. After you choose, the script will print the final state, which includes the user's answer.
## Project Structure
```
├── src/
│ └── main.py # Main script with graph definition and run loop
├── requirements.txt # Python dependencies
└── README.md # This file
```
## How It Works
1. **Graph Definition**
The graph has a single node `ask_user`.
- On first run, it triggers an interrupt with a payload containing a question and options.
- After the user responds, the node receives the answer via the `resume` parameter and stores it in the state.
2. **Interrupt Handling**
The main loop listens for chunks from `graph.stream()`.
- When an interrupt chunk is detected (`"__interrupt__"` key), it displays the question using `questionary.select`.
- The chosen answer is sent back to the graph via `Command(resume=answer)`.
3. **Resuming Execution**
The graph resumes from the same node, now with the answer available in the state.
The final state is printed to the console.
Feel free to extend the graph with more nodes or integrate it with other LangChain components.
Enjoy experimenting with LangGraph and human-in-the-loop workflows!
+2
View File
@@ -0,0 +1,2 @@
langgraph
questionary
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Demo of a LangGraph with a custom interrupt node that asks for user confirmation.
"""
from typing import TypedDict, Optional
import questionary
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START
from langgraph.types import interrupt, Command
class State(TypedDict):
"""Graph state schema."""
human_value: Optional[str]
foo: Optional[str]
def ask_user(state: State, resume: Optional[str] = None) -> State:
"""
Node that triggers an interrupt asking the user for confirmation.
After the user responds, the answer is stored in the state.
"""
# If we haven't asked yet, trigger interrupt
if state.get("human_value") is None:
payload = {
"type": "confirm",
"question": "Уверены, что хотите продолжить?",
"options": ["approve", "reject"],
}
return interrupt(payload)
# After resume, store the answer
if resume is not None:
state["human_value"] = resume
return state
def main() -> None:
# Build the graph
graph_builder = StateGraph(State)
graph_builder.add_node("ask_user", ask_user)
graph_builder.set_entry_point("ask_user")
graph_builder.set_finish_point("ask_user")
graph = graph_builder.compile(checkpointer=InMemorySaver())
# Initial state
init_state: State = {"human_value": None, "foo": "initial data"}
# Configuration for the thread
thread_id = "demo_thread"
config = {"thread_id": thread_id}
# Start streaming
stream = graph.stream(init_state, config=config)
while True:
try:
chunk = next(stream)
except StopIteration:
break
# Handle interrupt
if "__interrupt__" in chunk:
payload = chunk["__interrupt__"]
answer = questionary.select(
payload["question"],
choices=payload["options"],
).ask()
if answer is None:
print("No answer selected. Exiting.")
return
# Resume the graph with the user's answer
stream = graph.stream(Command(resume=answer), config=config)
continue
# Final state reached
print("\nFinal state:")
print(chunk)
break
if __name__ == "__main__":
main()