commit 3c81f16ab4d8f130f8101fba7552fb239b275075 Author: kuzakhmetovartur Date: Wed Jun 24 14:57:58 2026 +0300 feat: solution for 'Human-in-the-loop (interrupt / resume)' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b16538b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dist/ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..7477c5e --- /dev/null +++ b/README.md @@ -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! \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..402c1f7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +langgraph +questionary \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..4f69aa9 --- /dev/null +++ b/src/main.py @@ -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() \ No newline at end of file