diff --git a/docs/docs/how-tos/human_in_the_loop/interrupt.ipynb b/docs/docs/how-tos/human_in_the_loop/interrupt.ipynb new file mode 100644 index 000000000..06d448511 --- /dev/null +++ b/docs/docs/how-tos/human_in_the_loop/interrupt.ipynb @@ -0,0 +1,487 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0ff51c4b-5d5c-4b6e-8478-0ea489adb689", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# How to use `interrupt` for human-in-the-loop workflows\n", + "\n", + "To use an `interrupt` you must pass a checkpointer.\n", + "\n", + "1. Used with checkpointers\n", + "1. Used together with `Command` to resume.\n", + "2. Interrupt information is available when streaming. If using invoke with human in the loop need to inspect next\n", + "3. Cannot be used twice in a node.\n", + "\n", + "An interrupt can be used within a node like this:\n", + "\n", + "```python\n", + "async def some_node(state: State):\n", + " ...\n", + " # Any value that we want to surface as part of the interrupt\n", + " value = {\"foo\": \"bar\"} \n", + " answer = interrupt(value)\n", + " ...\n", + "```\n", + "\n", + "Graph execution willbe interrupted when the code reaches\n", + "the `interrup` function. It can be resumed by passing `Command`.\n", + "\n", + "```python\n", + "graph.invoke(Command(resume=some_value)))\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "17ecd33b-7879-47a8-9ad3-0b4ce1fdc0c9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cbc003a6-b45f-4526-bcf1-963d951797ae", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph" + ] + }, + { + "cell_type": "markdown", + "id": "7091bfdc-2754-4b3c-83b9-20cfb1d2be66", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "
\n", + "

Set up LangSmith for LangGraph development

\n", + "

\n", + " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", + "

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "8aa9eb6a-d54f-40e9-a618-0ad7290d15dc", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Interrupt and resume\n", + "\n", + "Here is a minimal example that shows how to `interrupt` and `resume` a graph consisting of one node." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ce902820-2739-402f-a784-867a44a3997c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> Entered the node: 1 # of times\n", + "{'__interrupt__': (Interrupt(value='what is your age?', resumable=True, ns=['node:d82f393c-4bba-241b-22aa-0cd65a88fd8b'], when='during'),)}\n" + ] + } + ], + "source": [ + "import uuid\n", + "import operator\n", + "from typing import TypedDict, Annotated, Optional\n", + "\n", + "from langgraph.graph import StateGraph\n", + "from langgraph.constants import START, INTERRUPT\n", + "from langgraph.types import interrupt, Command\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "class State(TypedDict):\n", + " \"\"\"The graph state.\"\"\"\n", + " foo: str\n", + " human_value: Optional[str]\n", + " \"\"\"Human value will be updated using an interrupt.\"\"\"\n", + " \n", + "counter = 0\n", + "\n", + "def node(state: State):\n", + " global counter\n", + " counter +=1 \n", + " print(f'> Entered the node: {counter} # of times')\n", + " answer = interrupt(\n", + " # This value will be sent to the client\n", + " # as part of the interrupt inforamtion.\n", + " 'what is your age?'\n", + " )\n", + " print(f'> Received an input from the interrupt: {answer}')\n", + " return {\"human_value\": answer}\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"node\", node)\n", + "builder.add_edge(START, \"node\")\n", + "\n", + "# A checkpointer must be enabled for interrupts to work!\n", + "graph = builder.compile(checkpointer=MemorySaver())\n", + "\n", + "config = {\n", + " \"thread_id\": uuid.uuid4(),\n", + "}\n", + "\n", + "for chunk in graph.stream({\"foo\": \"abc\"}, config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "8df4319f-f7e8-40dc-8943-91fa3fad31a0", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Let's resume graph execution from the given node:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c2ea37d8-4b92-442d-85e1-212e20123907", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> Entered the node: 2 # of times\n", + "> Received an input from the interrupt: some input from a human!!!\n", + "{'node': {'human_value': 'some input from a human!!!'}}\n" + ] + } + ], + "source": [ + "command = Command(resume=\"some input from a human!!!\")\n", + "\n", + "for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "0036a06d-c875-461b-917d-1cd48c233e7d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "!!! important \"Graph execution resumes at the start of a **node**\"\n", + "\n", + " Graph execution resumes from the **node** where the interrupt was raised rather than from the line where the `interrupt` was raised.\n", + "\n", + " As a result, you should see `> Entered the node: 2 # of times`." + ] + }, + { + "cell_type": "markdown", + "id": "9b40603f-5d01-41d7-afc7-2ead455a6803", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Validation of input with resume" + ] + }, + { + "cell_type": "code", + "execution_count": 127, + "id": "30b5821e-8bb8-4076-b477-60f39ea65445", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> Entered the node: 1 # of times\n", + "{'__interrupt__': (Interrupt(value='What is your age?', resumable=True, ns=['node:a1c64b81-7957-8c3c-7728-e73cb81837d5'], when='during'),)}\n" + ] + } + ], + "source": [ + "import uuid\n", + "import operator\n", + "from typing import TypedDict, Annotated, Optional, Literal\n", + "\n", + "from langgraph.graph import StateGraph\n", + "from langgraph.graph.state import GraphCommand\n", + "from langgraph.constants import START, INTERRUPT\n", + "from langgraph.types import interrupt, Command\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "class State(TypedDict):\n", + " \"\"\"The graph state.\"\"\"\n", + " foo: str\n", + " human_value: Optional[str]\n", + " \"\"\"Human value will be updated using an interrupt.\"\"\"\n", + " \n", + "\n", + "counter = 0\n", + "\n", + "def node(state: State) -> GraphCommand[Literal['node']]:\n", + " global counter\n", + " counter +=1 \n", + " print(f'> Entered the node: {counter} # of times')\n", + "\n", + " answer = interrupt(\n", + " \"What is your age?\"\n", + " )\n", + "\n", + " if not isinstance(answer, int) or answer < 0:\n", + " return GraphCommand(goto=\"node\")\n", + "\n", + " return GraphCommand(update={\"human_value\": answer})\n", + " \n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"node\", node)\n", + "builder.add_edge(START, \"node\")\n", + "\n", + "# A checkpointer must be enabled for interrupts to work!\n", + "graph = builder.compile(checkpointer=MemorySaver())\n", + "\n", + "config = {\n", + " \"thread_id\": uuid.uuid4(),\n", + "}\n", + "\n", + "for chunk in graph.stream({\"foo\": \"abc\"}, config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "0d95194e-e9f2-4abf-83dc-c29b5f1f6f2d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Let's resume with a bad input" + ] + }, + { + "cell_type": "code", + "execution_count": 128, + "id": "a79e330d-f849-44ea-af37-221efcffe1a3", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> Entered the node: 2 # of times\n", + "{'node': None}\n", + "> Entered the node: 3 # of times\n", + "{'__interrupt__': (Interrupt(value='What is your age?', resumable=True, ns=['node:995027b2-c8ad-8951-74ff-d9b86fca74bb'], when='during'),)}\n" + ] + } + ], + "source": [ + "bad_input = -20 # Negative number!\n", + "for chunk in graph.stream(Command(resume=bad_input), config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "code", + "execution_count": 130, + "id": "c9d01c77-7a9c-43aa-bbfa-c969b1d3e3f2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "ok_input = 5\n", + "for chunk in graph.stream(Command(resume=ok_input), config):\n", + " print(chunk)" + ] + }, + { + "cell_type": "markdown", + "id": "c7181eea-a0a8-43df-8ce6-b2172ea6cb21", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Usage with invoke / ainvoke\n", + "\n", + "If you're not using `stream` or `astream`, you will need to explicitly access the state of the graph to get information about the interrupt." + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "id": "c9ae2c5f-c81d-4188-9f7e-f3f90e675e12", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> Entered the node: 1 # of times\n" + ] + } + ], + "source": [ + "import uuid\n", + "import operator\n", + "from typing import TypedDict, Annotated, Optional\n", + "\n", + "from langgraph.graph import StateGraph\n", + "from langgraph.constants import START, INTERRUPT\n", + "from langgraph.types import interrupt, Command\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "class State(TypedDict):\n", + " \"\"\"The graph state.\"\"\"\n", + " foo: str\n", + " human_value: Optional[str]\n", + " \"\"\"Human value will be updated using an interrupt.\"\"\"\n", + " \n", + "counter = 0\n", + "\n", + "def node(state: State):\n", + " global counter\n", + " counter +=1 \n", + " print(f'> Entered the node: {counter} # of times')\n", + " answer = interrupt(\n", + " # This value will be sent to the client\n", + " # as part of the interrupt inforamtion.\n", + " 'what is your age?'\n", + " )\n", + " print(f'> Received an input from the interrupt: {answer}')\n", + " return {\"human_value\": answer}\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"node\", node)\n", + "builder.add_edge(START, \"node\")\n", + "\n", + "# A checkpointer must be enabled for interrupts to work!\n", + "graph = builder.compile(checkpointer=MemorySaver())\n", + "\n", + "config = {\n", + " \"thread_id\": uuid.uuid4(),\n", + "}\n", + "\n", + "for event in graph.invoke({\"foo\": \"abc\"}, config):\n", + " print" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}