This commit is contained in:
Eugene Yurtsev
2024-12-05 17:16:34 -05:00
parent 4fd261765a
commit 62ff2eb32d
3 changed files with 172 additions and 151 deletions
+122 -12
View File
@@ -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
+3 -90
View File
@@ -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
+47 -49
View File
@@ -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