feat: solution for 'Human-in-the-loop (interrupt / resume)'
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
@@ -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!
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
langgraph
|
||||||
|
questionary
|
||||||
+85
@@ -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()
|
||||||
Reference in New Issue
Block a user