From 509dfd1f21ce6c6cc8a144a9c068959eab911522 Mon Sep 17 00:00:00 2001 From: Sydney Runkle Date: Mon, 28 Jul 2025 15:05:57 -0400 Subject: [PATCH 1/3] better hitl multi interrupt resume docs --- docs/docs/concepts/human_in_the_loop.md | 2 +- .../how-tos/assets/human_in_loop_parallel.png | Bin 0 -> 9381 bytes .../add-human-in-the-loop.md | 96 ++++++++++++++++-- 3 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 docs/docs/how-tos/assets/human_in_loop_parallel.png diff --git a/docs/docs/concepts/human_in_the_loop.md b/docs/docs/concepts/human_in_the_loop.md index 442a98b35..aa26f44a5 100644 --- a/docs/docs/concepts/human_in_the_loop.md +++ b/docs/docs/concepts/human_in_the_loop.md @@ -28,7 +28,7 @@ To review, edit, and approve tool calls in an agent or workflow, [use LangGraph' There are two ways to pause a graph: - [Dynamic interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#pause-using-interrupt): Use `interrupt` to pause a graph from inside a specific node, based on the current state of the graph. - - [Static interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#debug-with-interrupts): Use `interrupt_before` and `interrupt_after` to pause the graph at defined points, either before or after a node executes. + - [Static interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#debug-with-interrupts): Use `interrupt_before` and `interrupt_after` to pause the graph at pre-defined points, either before or after a node executes.
![image](./img/breakpoints.png){: style="max-height:400px"} diff --git a/docs/docs/how-tos/assets/human_in_loop_parallel.png b/docs/docs/how-tos/assets/human_in_loop_parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdb9da95147ccec18457e71ea3beb385cb26fbc GIT binary patch literal 9381 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 literal 0 HcmV?d00001 diff --git a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md index e9d18ea4a..45c5b3ff7 100644 --- a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md +++ b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md @@ -128,7 +128,7 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! !!! tip "New in 0.4.0" - `__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` and `ainvoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream` or `astream`. You can also use `graph.get_state(thread_id)` to get the interrupt value. + `__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` and `ainvoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream` or `astream`. You can also use `graph.get_state(thread_id)` to get the interrupt value(s). !!! warning @@ -145,21 +145,99 @@ To resume execution, use the [`Command`][langgraph.types.Command] primitive, whi graph.invoke(Command(resume={"age": "25"}), thread_config) ``` -### Resume multiple interrupts with one invocation +### Multiple interrupts -If you have multiple interrupts in the task queue, you can use `Command.resume` with a dictionary mapping of interrupt ids to resume with a single `invoke` / `stream` call. +When nodes with interrupt conditions are run in parallel, it's possible to have multiple interrupts in the task queue. +For example, the following graph has two nodes run in parallel that require human input: -For example, once your graph has been interrupted (multiple times, theoretically) and is stalled: +
+![image](../assets/human_in_loop_parallel.png){: style="max-height:400px"} +
+ +Once your graph has been interrupted and is stalled, you can resume all the interrupts at once with `Command.resume`, passing a dictionary mapping of interrupt ids to resume values. ```python -resume_map = { - i.id: f"human input for prompt {i.value}" - for i in parent.get_state(thread_config).interrupts -} +# Run the graph, resulting in two interrupts +result = graph.invoke( + { + 'text_1': 'original text 1', + 'text_2': 'original text 2' + }, + config=thread_config +) -parent_graph.invoke(Command(resume=resume_map), config=thread_config) +print(result["__interrupt__"]) +""" +[ + Interrupt(value={'text_to_revise': 'original text 1'}, id='bba46aa8d060d6e1edbc6cd913a96494'), + Interrupt(value={'text_to_revise': 'original text 2'}, id='22632249d75d67e0e718c1d18596c78a') +] +""" + +# Create a mapping of interrupt ids to corresponding resume values +resume_map = { + i.id: f"edited text for {i.value['text_to_revise']}" + for i in result["__interrupt__"] +} +graph.invoke(Command(resume=resume_map), config=thread_config) ``` +!!! example "Extended example: resume multiple interrupts" + + ```python + from typing import TypedDict + import uuid + from langchain_core.runnables import RunnableConfig + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.constants import START + from langgraph.graph import StateGraph + from langgraph.types import interrupt, Command + + + class State(TypedDict): + text_1: str + text_2: str + + + def human_node_1(state: State): + value = interrupt({"text_to_revise": state["text_1"]}) + return {"text_1": value} + + + def human_node_2(state: State): + value = interrupt({"text_to_revise": state["text_2"]}) + return {"text_2": value} + + + # Build the graph + graph_builder = StateGraph(State) + graph_builder.add_node("human_node_1", human_node_1) + graph_builder.add_node("human_node_2", human_node_2) + + # Add both nodes in parallel from START + graph_builder.add_edge(START, "human_node_1") + graph_builder.add_edge(START, "human_node_2") + + checkpointer = InMemorySaver() + graph = graph_builder.compile(checkpointer=checkpointer) + + # Pass a thread ID to the graph to run it + thread_id = str(uuid.uuid4()) + config: RunnableConfig = {"configurable": {"thread_id": thread_id}} + + # Run the graph until both interrupts are hit + result = graph.invoke( + {"text_1": "original text 1", "text_2": "original text 2"}, config=config + ) + + interrupts = result["__interrupt__"] + resume_map = {i.id: f"edited text for {i.value['text_to_revise']}" for i in interrupts} + + # Resume with mapping of interrupt IDs to values + print(graph.invoke(Command(resume=resume_map), config=config)) + # > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'} + ``` + ## Common patterns Below we show different design patterns that can be implemented using `interrupt` and `Command`. From 03726f9bc6121cebe05107ea8b14d374e1880e7d Mon Sep 17 00:00:00 2001 From: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:14:37 -0400 Subject: [PATCH 2/3] Update docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md Co-authored-by: Eugene Yurtsev --- docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md index 45c5b3ff7..ab5aaf0b9 100644 --- a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md +++ b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md @@ -182,7 +182,7 @@ resume_map = { graph.invoke(Command(resume=resume_map), config=thread_config) ``` -!!! example "Extended example: resume multiple interrupts" +??? example "Extended example: resume multiple interrupts" ```python from typing import TypedDict From f4633a0015cd41807560e9a6ad7c74cead3bcca4 Mon Sep 17 00:00:00 2001 From: Sydney Runkle Date: Mon, 28 Jul 2025 15:20:32 -0400 Subject: [PATCH 3/3] single example --- .../add-human-in-the-loop.md | 118 +++++++----------- 1 file changed, 44 insertions(+), 74 deletions(-) diff --git a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md index 45c5b3ff7..3445f710b 100644 --- a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md +++ b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md @@ -145,7 +145,7 @@ To resume execution, use the [`Command`][langgraph.types.Command] primitive, whi graph.invoke(Command(resume={"age": "25"}), thread_config) ``` -### Multiple interrupts +## Resuming Multiple interrupts When nodes with interrupt conditions are run in parallel, it's possible to have multiple interrupts in the task queue. For example, the following graph has two nodes run in parallel that require human input: @@ -156,88 +156,58 @@ For example, the following graph has two nodes run in parallel that require huma Once your graph has been interrupted and is stalled, you can resume all the interrupts at once with `Command.resume`, passing a dictionary mapping of interrupt ids to resume values. + ```python -# Run the graph, resulting in two interrupts +from typing import TypedDict +import uuid +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.constants import START +from langgraph.graph import StateGraph +from langgraph.types import interrupt, Command + + +class State(TypedDict): + text_1: str + text_2: str + + +def human_node_1(state: State): + value = interrupt({"text_to_revise": state["text_1"]}) + return {"text_1": value} + + +def human_node_2(state: State): + value = interrupt({"text_to_revise": state["text_2"]}) + return {"text_2": value} + + +graph_builder = StateGraph(State) +graph_builder.add_node("human_node_1", human_node_1) +graph_builder.add_node("human_node_2", human_node_2) + +# Add both nodes in parallel from START +graph_builder.add_edge(START, "human_node_1") +graph_builder.add_edge(START, "human_node_2") + +checkpointer = InMemorySaver() +graph = graph_builder.compile(checkpointer=checkpointer) + +thread_id = str(uuid.uuid4()) +config: RunnableConfig = {"configurable": {"thread_id": thread_id}} result = graph.invoke( - { - 'text_1': 'original text 1', - 'text_2': 'original text 2' - }, - config=thread_config + {"text_1": "original text 1", "text_2": "original text 2"}, config=config ) -print(result["__interrupt__"]) -""" -[ - Interrupt(value={'text_to_revise': 'original text 1'}, id='bba46aa8d060d6e1edbc6cd913a96494'), - Interrupt(value={'text_to_revise': 'original text 2'}, id='22632249d75d67e0e718c1d18596c78a') -] -""" - -# Create a mapping of interrupt ids to corresponding resume values +# Resume with mapping of interrupt IDs to values resume_map = { i.id: f"edited text for {i.value['text_to_revise']}" for i in result["__interrupt__"] } -graph.invoke(Command(resume=resume_map), config=thread_config) +print(graph.invoke(Command(resume=resume_map), config=config)) +# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'} ``` -!!! example "Extended example: resume multiple interrupts" - - ```python - from typing import TypedDict - import uuid - from langchain_core.runnables import RunnableConfig - from langgraph.checkpoint.memory import InMemorySaver - from langgraph.constants import START - from langgraph.graph import StateGraph - from langgraph.types import interrupt, Command - - - class State(TypedDict): - text_1: str - text_2: str - - - def human_node_1(state: State): - value = interrupt({"text_to_revise": state["text_1"]}) - return {"text_1": value} - - - def human_node_2(state: State): - value = interrupt({"text_to_revise": state["text_2"]}) - return {"text_2": value} - - - # Build the graph - graph_builder = StateGraph(State) - graph_builder.add_node("human_node_1", human_node_1) - graph_builder.add_node("human_node_2", human_node_2) - - # Add both nodes in parallel from START - graph_builder.add_edge(START, "human_node_1") - graph_builder.add_edge(START, "human_node_2") - - checkpointer = InMemorySaver() - graph = graph_builder.compile(checkpointer=checkpointer) - - # Pass a thread ID to the graph to run it - thread_id = str(uuid.uuid4()) - config: RunnableConfig = {"configurable": {"thread_id": thread_id}} - - # Run the graph until both interrupts are hit - result = graph.invoke( - {"text_1": "original text 1", "text_2": "original text 2"}, config=config - ) - - interrupts = result["__interrupt__"] - resume_map = {i.id: f"edited text for {i.value['text_to_revise']}" for i in interrupts} - - # Resume with mapping of interrupt IDs to values - print(graph.invoke(Command(resume=resume_map), config=config)) - # > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'} - ``` - ## Common patterns Below we show different design patterns that can be implemented using `interrupt` and `Command`. @@ -1105,7 +1075,7 @@ def node_in_parent_graph(state: State): {'parent_node': {'state_counter': 1}} ``` -### Using multiple interrupts +### Using multiple interrupts in a single node Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](#validate-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully.