From 155e0c66d5644242ceb8a29370c5f43489cfdd27 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Fri, 23 Aug 2024 13:34:33 -0700 Subject: [PATCH] docs: add how-to for dynamic interrupts (#1446) --------- Co-authored-by: vbarda --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/how-tos/index.md | 1 + docs/mkdocs.yml | 1 + .../dynamic_breakpoints.ipynb | 417 ++++++++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 examples/human_in_the_loop/dynamic_breakpoints.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 2440185f9..caf2c6781 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -56,6 +56,7 @@ _MANUAL = { "create-react-agent-memory.ipynb", "create-react-agent-hitl.ipynb", "human_in_the_loop/breakpoints.ipynb", + "human_in_the_loop/dynamic_breakpoints.ipynb", "human_in_the_loop/time-travel.ipynb", "human_in_the_loop/edit-graph-state.ipynb", "human_in_the_loop/wait-user-input.ipynb", diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 09d5f2e8f..43b0c9265 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -35,6 +35,7 @@ One of LangGraph's main benefits is that it makes human-in-the-loop workflows ea These guides cover common examples of that. - [How to add breakpoints](human_in_the_loop/breakpoints.ipynb) +- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb) - [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb) - [How to wait for user input](human_in_the_loop/wait-user-input.ipynb) - [How to view and update past graph state](human_in_the_loop/time-travel.ipynb) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3bc3f3d24..1719b1cca 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -139,6 +139,7 @@ nav: - Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb - Human-in-the-loop: - Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb + - Add dynamic breakpoints: how-tos/human_in_the_loop/dynamic_breakpoints.ipynb - Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb - View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb - Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb diff --git a/examples/human_in_the_loop/dynamic_breakpoints.ipynb b/examples/human_in_the_loop/dynamic_breakpoints.ipynb new file mode 100644 index 000000000..f43049d28 --- /dev/null +++ b/examples/human_in_the_loop/dynamic_breakpoints.ipynb @@ -0,0 +1,417 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ee54cde3-7e4d-43f4-b921-e7141ea0f19e", + "metadata": {}, + "source": [ + "# How to add dynamic breakpoints" + ] + }, + { + "cell_type": "markdown", + "id": "607849c6-4b8c-4e06-ad9c-758bb5a08e86", + "metadata": {}, + "source": [ + "Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions).\n", + "\n", + "In LangGraph you can add breakpoints before / after a node is executed. But oftentimes it may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. When doing so, it may also be helpful to include information about **why** that interrupt was raised.\n", + "\n", + "This guide shows how you can dynamically interrupt the graph using `NodeInterupt` -- a special exception that can be raised from inside a node. Let's see it in action!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2013d058-c245-498e-ba05-5af99b9b8a1b", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U langgraph" + ] + }, + { + "cell_type": "markdown", + "id": "e9aa244f-1dd9-450e-9526-b1a28b30f84f", + "metadata": {}, + "source": [ + "### Define the graph" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9a14c8b2-5c25-4201-93ea-e5358ee99bcb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from typing import TypedDict\n", + "from IPython.display import Image, display\n", + "\n", + "from langgraph.graph import StateGraph, START, END\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.errors import NodeInterrupt\n", + "\n", + "\n", + "class State(TypedDict):\n", + " input: str\n", + "\n", + "\n", + "def step_1(state: State) -> State:\n", + " print(\"---Step 1---\")\n", + " return state\n", + "\n", + "\n", + "def step_2(state: State) -> State:\n", + " # Let's optionally raise a NodeInterrupt\n", + " # if the length of the input is longer than 5 characters\n", + " if len(state['input']) > 5:\n", + " raise NodeInterrupt(f\"Received input that is longer than 5 characters: {state['input']}\")\n", + " \n", + " print(\"---Step 2---\")\n", + " return state\n", + "\n", + "def step_3(state: State) -> State:\n", + " print(\"---Step 3---\")\n", + " return state\n", + "\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"step_1\", step_1)\n", + "builder.add_node(\"step_2\", step_2)\n", + "builder.add_node(\"step_3\", step_3)\n", + "builder.add_edge(START, \"step_1\")\n", + "builder.add_edge(\"step_1\", \"step_2\")\n", + "builder.add_edge(\"step_2\", \"step_3\")\n", + "builder.add_edge(\"step_3\", END)\n", + "\n", + "# Set up memory\n", + "memory = MemorySaver()\n", + "\n", + "# Compile the graph with memory\n", + "graph = builder.compile(checkpointer=memory)\n", + "\n", + "# View\n", + "display(Image(graph.get_graph().draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "id": "ad5521e1-0e58-42c5-9282-ff96f24ee6f6", + "metadata": {}, + "source": [ + "### Run the graph with dynamic interrupt" + ] + }, + { + "cell_type": "markdown", + "id": "83692c63-5c65-4562-9c65-5ad1935e339f", + "metadata": {}, + "source": [ + "First, let's run the graph with an input that <= 5 characters long. This should safely ignore the interrupt condition we defined and return the original input at the end of the graph execution." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b2d281f1-3349-4378-8918-7665fa7a7457", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello'}\n", + "---Step 1---\n", + "{'input': 'hello'}\n", + "---Step 2---\n", + "{'input': 'hello'}\n", + "---Step 3---\n", + "{'input': 'hello'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "2b66b926-47eb-401b-b37b-d80269d7214c", + "metadata": {}, + "source": [ + "If we inspect the graph at this point, we can see that there are no more tasks left to run and that the graph indeed finished execution." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4eac1455-e7ef-4a32-8c14-0d5789409689", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "()\n", + "()\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f8e03817-2135-4fb3-b881-fd6d2c378ccf", + "metadata": {}, + "source": [ + "Now, let's run the graph with an input that's longer than 5 characters. This should trigger the dynamic interrupt we defined via raising a `NodeInterrupt` error inside the `step_2` node." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c06192ad-13a4-4d2e-8e30-f1c08578fe77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello world'}\n", + "---Step 1---\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello world\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "\n", + "# Run the graph until the first interruption\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "173fd4f1-db97-44bb-a9e5-435ed042e3a3", + "metadata": {}, + "source": [ + "We can see that the graph now stopped while executing `step_2`. If we inspect the graph state at this point, we can see the information on what node is set to execute next (`step_2`), as well as what node raised the interrupt (also `step_2`), and additional information about the interrupt." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2058593c-178e-4a23-a4c4-860d4a9c2198", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('step_2',)\n", + "(PregelTask(id='365d4518-bcff-5abd-8ef5-8a0de7f510b0', name='step_2', error=None, interrupts=(Interrupt(value='Received input that is longer than 5 characters: hello world', when='during'),)),)\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "fc36d1be-ae2e-49c8-a17f-2b27be09618a", + "metadata": {}, + "source": [ + "If we try to resume the graph from the breakpoint, we will simply interrupt again as our inputs & graph state haven't changed." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "872e7a69-9784-4f81-90c6-6b6af2fa6480", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: to resume the graph from a dynamic interrupt we use the same syntax as with regular interrupts -- we pass None as the input\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3275f899-7039-4029-8814-0bb5c33fabfe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('step_2',)\n", + "(PregelTask(id='365d4518-bcff-5abd-8ef5-8a0de7f510b0', name='step_2', error=None, interrupts=(Interrupt(value='Received input that is longer than 5 characters: hello world', when='during'),)),)\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "a5862dea-2af2-48cb-9889-979b6c6af6aa", + "metadata": {}, + "source": [ + "### Update the graph state" + ] + }, + { + "cell_type": "markdown", + "id": "c8724ef6-877a-44b9-b96a-ae81efa2d9e4", + "metadata": {}, + "source": [ + "To get around it, we can do several things. \n", + "\n", + "First, we could simply run the graph on a different thread with a shorter input, like we did in the beginning. Alternatively, if we want to resume the graph execution from the breakpoint, we can update the state to have an input that's shorter than 5 characters (the condition for our interrupt)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ba8dc8d-b90e-45f5-92cd-2192fc66f270", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---Step 2---\n", + "{'input': 'foo'}\n", + "---Step 3---\n", + "{'input': 'foo'}\n", + "()\n", + "{'input': 'foo'}\n" + ] + } + ], + "source": [ + "# NOTE: this update will be applied as of the last successful node before the interrupt, i.e. `step_1`, right before the node with an interrupt\n", + "graph.update_state(config=thread_config, values={\"input\": \"foo\"})\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)\n", + "\n", + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.values)" + ] + }, + { + "cell_type": "markdown", + "id": "6f16980e-aef4-45c9-85eb-955568a93c5b", + "metadata": {}, + "source": [ + "You can also update the state **as node `step_2`** (interrupted node) which would skip over that node altogether" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9a48e564-d979-4ac2-b815-c667345a9f07", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello world'}\n", + "---Step 1---\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello world\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "\n", + "# Run the graph until the first interruption\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "17f973ab-00ce-4f16-a452-641e76625fde", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---Step 3---\n", + "{'input': 'hello world'}\n", + "()\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "# NOTE: this update will skip the node `step_2` altogether\n", + "graph.update_state(config=thread_config, values=None, as_node=\"step_2\")\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)\n", + "\n", + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.values)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}