mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-30 11:49:38 +02:00
x
This commit is contained in:
@@ -1,25 +1,51 @@
|
||||
# Human-in-the-loop
|
||||
|
||||
Human-in-the-loop (or "on-the-loop") workflows enhance agent capabilities through several common user interaction patterns.
|
||||
**Human-in-the-loop** (or "on-the-loop") workflows enhance agent capabilities by incorporating human interactions at key points. Common interaction patterns include:
|
||||
|
||||
Common interaction patterns include:
|
||||
|
||||
1. **Approval**: Pause the agent, present its current state to the user, and allow the user to approve or reject a proposed action.
|
||||
2. **Editing**: Pause the agent, present its current state to the user, and allow the user to make modifications to the agent's state.
|
||||
3. **Input**: Introduce a dedicated graph node to explicitly collect user input, which is then integrated into the agent's state.
|
||||
1. ✅ **Approval**: Pause the agent, present its current state to the user, and allow the user to approve or reject a proposed action.
|
||||
2. 📝 **Editing**: Pause the agent, present its current state to the user, and allow the user to make modifications to the agent's state.
|
||||
3. 💬 **Input**: Introduce a dedicated graph node to explicitly collect user input, which is then integrated into the agent's state.
|
||||
|
||||
Use-cases for these interaction patterns include:
|
||||
|
||||
1. `Reviewing tool calls` - We can interrupt an agent to review and edit the results of tool calls.
|
||||
2. `Time travel` - We can manually re-play and / or fork past actions of an agent.
|
||||
1. [**Reviewing tool calls**](#reviewing-tool-calls): Pause the agent to review and edit the results of tool executions.
|
||||
2. [**Time travel**](#time-travel): Replay or fork the agent's past actions for further exploration.
|
||||
|
||||
## Persistence
|
||||
|
||||
All of these interaction patterns are enabled by LangGraph's built-in [persistence](./persistence.md) layer, which will write a checkpoint of the graph state at each step. Persistence allows the graph to stop so that a human can review and / or edit the current state of the graph and then resume with the human's input.
|
||||
Human-in-the-loop patterns are enabled by LangGraph's built-in [persistence](./persistence.md) layer, which writes a checkpoint of the graph state at each step.
|
||||
Persistence allows pausing graph execution so that a human can review and / or edit the current state of the graph and then resume with the human's input.
|
||||
|
||||
### Breakpoints
|
||||
## Breakpoints
|
||||
|
||||
Breakpoints allow **pausing** graph execution to allow for human review before **resuming** execution. This functionality is enabled by LangGraph's built-in [checkpointer](./persistence.md#checkpointer), which writes a checkpoint of the graph state at each step.
|
||||
|
||||
There are two types of breakpoints:
|
||||
|
||||
1. [**Static breakpoints**](#static-breakpoints): Pause the graph **before** or **after** a node executes.
|
||||
2. [**Dynamic breakpoints**](#dynamic-breakpoints): Pause the graph from **inside** a node often based on some condition.
|
||||
|
||||
!!! important "Checkpointer Required"
|
||||
|
||||
You must compile your graph with a checkpointer to use breakpoints.
|
||||
|
||||
### Static Breakpoints
|
||||
|
||||
Use static breakpoints if you want to **ALWAYS** pause the graph either **before** or **after** one or more nodes execute.
|
||||
|
||||
To set static breakpoints, specify the `interrupt_before` and/or `interrupt_after` key when [compiling your graph](#compiling-your-graph).
|
||||
|
||||
```python
|
||||
# Compile our graph with a checkpointer and a breakpoint before "node_a" and after "node_b" and "node_c"
|
||||
graph = graph_builder.compile(
|
||||
interrupt_before=["node_a"],
|
||||
interrupt_after=["node_b", "node_c"],
|
||||
checkpointer=checkpointer, # Required
|
||||
)
|
||||
```
|
||||
|
||||
When using sub-graphs, specify the `interrupt_before` and `interrupt_after` values when compiling the subgraph.
|
||||
|
||||
Adding a [breakpoint](./low_level.md#breakpoints) a specific location in the graph flow is one way to enable human-in-the-loop. In this case, the developer knows *where* in the workflow human input is needed and simply places a breakpoint prior to or following that particular graph node.
|
||||
|
||||
Here, we compile our graph with a checkpointer and a breakpoint at the node we want to interrupt before, `step_for_human_in_the_loop`. We then perform one of the above interaction patterns, which will create a new checkpoint if a human edits the graph state. The new checkpoint is saved to the `thread` and we can resume the graph execution from there by passing in `None` as the input.
|
||||
|
||||
@@ -41,6 +67,15 @@ for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
|
||||
### Dynamic Breakpoints
|
||||
|
||||
Alternatively, you may want to raise a breakpoint from inside a node, potentially based on some condition that is not known until runtime. This is called a dynamic breakpoint.
|
||||
|
||||
This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
|
||||
There are two ways to interrupt the graph dynamically:
|
||||
|
||||
1. `interrupt` **function (recommended)**: Interrupts the graph within a node and surfaces a value to the client as part of the interrupt information.
|
||||
2. `NodeInterrupt` exception: An older, less flexible method for interrupting.
|
||||
|
||||
Alternatively, the developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
|
||||
```python
|
||||
@@ -50,7 +85,7 @@ def my_node(state: State) -> State:
|
||||
return state
|
||||
```
|
||||
|
||||
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
|
||||
Let's assume we run the graph with an input that triggers the dynamic breakpoint and then attempt to resume the graph execution simply by passing in `None` for the input.
|
||||
|
||||
```python
|
||||
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
|
||||
@@ -78,6 +113,81 @@ for event in graph.stream(None, thread_config, stream_mode="values"):
|
||||
|
||||
See [our guide](../how-tos/human_in_the_loop/dynamic_breakpoints.ipynb) for a detailed how-to on doing this!
|
||||
|
||||
### Dynamic Breakpoints
|
||||
|
||||
There are two ways to interrupt the graph dynamically:
|
||||
|
||||
1. `interrupt` **function (recommended)**: Interrupts the graph within a node and surfaces a value to the client as part of the interrupt information.
|
||||
2. `NodeInterrupt` exception: An older, less flexible method for interrupting.
|
||||
|
||||
#### `interrupt`
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def node(state: State):
|
||||
...
|
||||
client_value = interrupt(
|
||||
# This value will be sent to the client.
|
||||
# It can be any JSON serializable value.
|
||||
{"key": "value"}
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
#### `NodeInterrupt`
|
||||
|
||||
Throw a `NodeInterrupt` exception to interrupt the graph.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
if len(state['input']) > 5:
|
||||
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
|
||||
|
||||
return state
|
||||
```
|
||||
|
||||
### Resuming
|
||||
|
||||
When a breakpoint is hit, graph execution will pause.
|
||||
|
||||
=== "Command"
|
||||
|
||||
Resume execution using the new `Command` primitive.
|
||||
|
||||
```python
|
||||
graph.invoke(inputs, config=config) # This will pause at the breakpoint
|
||||
...
|
||||
# Do something (e.g., get human input)
|
||||
...
|
||||
graph.invoke(
|
||||
Command(
|
||||
# Use `resume` to pass a value to the `interrupt`.
|
||||
resume=resume,
|
||||
# For other kinds of breakpoints, use `update` to update the state.
|
||||
update=update,
|
||||
),
|
||||
config=config
|
||||
)
|
||||
```
|
||||
|
||||
=== "Without the Command Primitive"
|
||||
|
||||
Resume execution without the `Command` primitive (older versions of LangGraph).
|
||||
|
||||
```python
|
||||
graph.invoke(inputs, config=config) # This will pause at the breakpoint
|
||||
...
|
||||
# Do something (e.g., get human input)
|
||||
...
|
||||
|
||||
graph.update_state(update, config=config)
|
||||
graph.invoke(None, config=config)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a full walkthrough of how to add breakpoints.
|
||||
|
||||
|
||||
## Interaction Patterns
|
||||
|
||||
### Approval
|
||||
|
||||
@@ -453,100 +453,13 @@ Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-li
|
||||
|
||||
Breakpoints enable **human-in-the-loop** workflows by **pausing** graph execution to allow for human review before continuing.
|
||||
|
||||
You **MUST** use a [checkpointer](./persistence.md) when using breakpoints as breakpoints require the ability to save the state of the graph at the time of pausing.
|
||||
|
||||
There are two types of breakpoints:
|
||||
|
||||
1. **Static breakpoints**: Pause the graph **before** or **after** a node executes.
|
||||
2. **Dynamic breakpoints**: Pause the graph from **inside** a node.
|
||||
1. **Static breakpoints**: Pause the graph **before** or **after** a node executes. This is achieved by specifying the `interrupt_before` and `interrupt_after` keys when [compiling your graph](#compiling-your-graph).
|
||||
2. **Dynamic breakpoints**: Pause the graph from **inside** a node. This is achieved by using the `interrupt` function or raising a `NodeInterrupt` exception.
|
||||
|
||||
### Static Breakpoints
|
||||
Please see the [Human-in-the-Loop guide](../human_in_the_loop) for conceptual information about breakpoints.
|
||||
|
||||
To set static breakpoints, specify the `interrupt_before` and/or `interrupt_after` key when [compiling your graph](#compiling-your-graph).
|
||||
|
||||
```python
|
||||
graph = graph_builder.compile(
|
||||
interrupt_before=["node_a"],
|
||||
interrupt_after=["node_b", "node_c"],
|
||||
checkpointer=..., # Required
|
||||
)
|
||||
```
|
||||
|
||||
When using sub-graphs, specify the `interrupt_before` and `interrupt_after` values when compiling the subgraph.
|
||||
|
||||
### Dynamic Breakpoints
|
||||
|
||||
There are two ways to interrupt the graph dynamically:
|
||||
|
||||
1. `interrupt` **function (recommended)**: Interrupts the graph within a node and surfaces a value to the client as part of the interrupt information.
|
||||
2. `NodeInterrupt` exception: An older, less flexible method for interrupting.
|
||||
|
||||
#### `interrupt`
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
|
||||
def node(state: State):
|
||||
...
|
||||
client_value = interrupt(
|
||||
# This value will be sent to the client.
|
||||
# It can be any JSON serializable value.
|
||||
{"key": "value"}
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
#### `NodeInterrupt`
|
||||
|
||||
Throw a `NodeInterrupt` exception to interrupt the graph.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
if len(state['input']) > 5:
|
||||
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
|
||||
|
||||
return state
|
||||
```
|
||||
|
||||
### Resuming
|
||||
|
||||
When a breakpoint is hit, graph execution will pause.
|
||||
|
||||
=== "Command"
|
||||
|
||||
Resume execution using the new `Command` primitive.
|
||||
|
||||
```python
|
||||
graph.invoke(inputs, config=config) # This will pause at the breakpoint
|
||||
...
|
||||
# Do something (e.g., get human input)
|
||||
...
|
||||
graph.invoke(
|
||||
Command(
|
||||
# Use `resume` to pass a value to the `interrupt`.
|
||||
resume=resume,
|
||||
# For other kinds of breakpoints, use `update` to update the state.
|
||||
update=update,
|
||||
),
|
||||
config=config
|
||||
)
|
||||
```
|
||||
|
||||
=== "Without the Command Primitive"
|
||||
|
||||
Resume execution without the `Command` primitive (older versions of LangGraph).
|
||||
|
||||
```python
|
||||
graph.invoke(inputs, config=config) # This will pause at the breakpoint
|
||||
...
|
||||
# Do something (e.g., get human input)
|
||||
...
|
||||
|
||||
graph.update_state(update, config=config)
|
||||
graph.invoke(None, config=config)
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a full walkthrough of how to add breakpoints.
|
||||
|
||||
## Subgraphs
|
||||
|
||||
|
||||
@@ -349,77 +349,75 @@ def interrupt(value: Any) -> Any:
|
||||
To use an `interrupt`, you must enable a checkpointer, as the feature relies
|
||||
on persisting the graph state.
|
||||
|
||||
Example: Basic interrupt and resume
|
||||
Example:
|
||||
```python
|
||||
import uuid
|
||||
from typing import TypedDict, Optional
|
||||
|
||||
```python
|
||||
import uuid
|
||||
from typing import TypedDict, Optional
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
\"\"\"The graph state.\"\"\"
|
||||
class State(TypedDict):
|
||||
\"\"\"The graph state.\"\"\"
|
||||
|
||||
foo: str
|
||||
human_value: Optional[str]
|
||||
\"\"\"Human value will be updated using an interrupt.\"\"\"
|
||||
foo: str
|
||||
human_value: Optional[str]
|
||||
\"\"\"Human value will be updated using an interrupt.\"\"\"
|
||||
|
||||
|
||||
def node(state: State):
|
||||
answer = interrupt(
|
||||
# This value will be sent to the client
|
||||
# as part of the interrupt information.
|
||||
\"what is your age?\"
|
||||
)
|
||||
print(f\"> Received an input from the interrupt: {answer}\")
|
||||
return {\"human_value\": answer}
|
||||
def node(state: State):
|
||||
answer = interrupt(
|
||||
# This value will be sent to the client
|
||||
# as part of the interrupt information.
|
||||
\"what is your age?\"
|
||||
)
|
||||
print(f\"> Received an input from the interrupt: {answer}\")
|
||||
return {\"human_value\": answer}
|
||||
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node(\"node\", node)
|
||||
builder.add_edge(START, \"node\")
|
||||
builder = StateGraph(State)
|
||||
builder.add_node(\"node\", node)
|
||||
builder.add_edge(START, \"node\")
|
||||
|
||||
# A checkpointer must be enabled for interrupts to work!
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
# A checkpointer must be enabled for interrupts to work!
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {
|
||||
\"configurable\": {
|
||||
\"thread_id\": uuid.uuid4(),
|
||||
config = {
|
||||
\"configurable\": {
|
||||
\"thread_id\": uuid.uuid4(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in graph.stream({\"foo\": \"abc\"}, config):
|
||||
print(chunk)
|
||||
```
|
||||
for chunk in graph.stream({\"foo\": \"abc\"}, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'__interrupt__': (Interrupt(value='what is your age?', resumable=True, ns=['node:62e598fa-8653-9d6d-2046-a70203020e37'], when='during'),)}
|
||||
```
|
||||
```pycon
|
||||
{'__interrupt__': (Interrupt(value='what is your age?', resumable=True, ns=['node:62e598fa-8653-9d6d-2046-a70203020e37'], when='during'),)}
|
||||
```
|
||||
|
||||
```python
|
||||
command = Command(resume=\"some input from a human!!!\")
|
||||
```python
|
||||
command = Command(resume=\"some input from a human!!!\")
|
||||
|
||||
for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config):
|
||||
print(chunk)
|
||||
```
|
||||
for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
Received an input from the interrupt: some input from a human!!!
|
||||
{'node': {'human_value': 'some input from a human!!!'}}
|
||||
```
|
||||
```pycon
|
||||
Received an input from the interrupt: some input from a human!!!
|
||||
{'node': {'human_value': 'some input from a human!!!'}}
|
||||
```
|
||||
|
||||
|
||||
Args:
|
||||
value: The value to surface to the client when the graph is interrupted.
|
||||
|
||||
Returns:
|
||||
On subsequent invocations within the same node (same task to be precise),
|
||||
returns the value provided during the first invocation
|
||||
Any: On subsequent invocations within the same node (same task to be precise), returns the value provided during the first invocation
|
||||
|
||||
Raises:
|
||||
GraphInterrupt: On the first invocation within the node, halts execution
|
||||
|
||||
Reference in New Issue
Block a user