From 62ff2eb32dc0b3d5fe4ddc6a11586d9004351376 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Thu, 5 Dec 2024 17:16:34 -0500 Subject: [PATCH] x --- docs/docs/concepts/human_in_the_loop.md | 134 +++++++++++++++++++++--- docs/docs/concepts/low_level.md | 93 +--------------- libs/langgraph/langgraph/types.py | 96 +++++++++-------- 3 files changed, 172 insertions(+), 151 deletions(-) diff --git a/docs/docs/concepts/human_in_the_loop.md b/docs/docs/concepts/human_in_the_loop.md index 77a8114dd..32cf7582b 100644 --- a/docs/docs/concepts/human_in_the_loop.md +++ b/docs/docs/concepts/human_in_the_loop.md @@ -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 diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index 266ced5be..6080c7d58 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -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 diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index 26d27acab..8853a501d 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -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