{ "cells": [ { "cell_type": "markdown", "id": "90616e9e", "metadata": {}, "source": [ "# How to view and update past graph state\n", "\n", "Once you start [checkpointing](./persistence.ipynb) your graphs, you can easily\n", "**get** or **update** the state of the agent at any point in time. This permits\n", "a few things:\n", "\n", "1. You can surface a state during an interrupt to a user to let them accept an\n", " action.\n", "2. You can **rewind** the graph to reproduce or avoid issues.\n", "3. You can **modify** the state to embed your agent into a larger system, or to\n", " let the user better control its actions.\n", "\n", "The key methods used for this functionality are:\n", "\n", "- [getState](/langgraphjs/reference/classes/langgraph_pregel.Pregel.html#getState):\n", " fetch the values from the target config\n", "- [updateState](/langgraphjs/reference/classes/langgraph_pregel.Pregel.html#updateState):\n", " apply the given values to the target state\n", "\n", "**Note:** this requires passing in a checkpointer.\n", "\n", "\n", "\n", "This works for\n", "[StateGraph](/langgraphjs/reference/classes/langgraph.StateGraph.html)\n", "and all its subclasses, such as\n", "[MessageGraph](/langgraphjs/reference/classes/langgraph.MessageGraph.html).\n", "\n", "Below is an example.\n", "\n", "
\n", "

Note

\n", "

\n", " In this how-to, we will create our agent from scratch to be transparent (but verbose). You can accomplish similar functionality using the createReactAgent(model, tools=tool, checkpointer=checkpointer) (API doc) constructor. This may be more appropriate if you are used to LangChain's AgentExecutor class.\n", "

\n", "
\n", "\n", "## Setup\n", "\n", "This guide will use OpenAI's GPT-4o model. We will optionally set our API key\n", "for [LangSmith tracing](https://smith.langchain.com/), which will give us\n", "best-in-class observability." ] }, { "cell_type": "code", "execution_count": 1, "id": "9a7df1d0", "metadata": { "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Time Travel: LangGraphJS\n" ] } ], "source": [ "// process.env.OPENAI_API_KEY = \"sk_...\";\n", "\n", "// Optional, add tracing in LangSmith\n", "// process.env.LANGCHAIN_API_KEY = \"ls__...\";\n", "process.env.LANGCHAIN_CALLBACKS_BACKGROUND = \"true\";\n", "process.env.LANGCHAIN_TRACING_V2 = \"true\";\n", "process.env.LANGCHAIN_PROJECT = \"Time Travel: LangGraphJS\";" ] }, { "cell_type": "markdown", "id": "e79ba1c0", "metadata": {}, "source": [ "## Define the state\n", "\n", "The state is the interface for all of the nodes in our graph.\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "44968352", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "import { Annotation } from \"@langchain/langgraph\";\n", "import { BaseMessage } from \"@langchain/core/messages\";\n", "\n", "const StateAnnotation = Annotation.Root({\n", " messages: Annotation({\n", " reducer: (x, y) => x.concat(y),\n", " }),\n", "});" ] }, { "cell_type": "markdown", "id": "47c88187", "metadata": {}, "source": [ "## Set up the tools\n", "\n", "We will first define the tools we want to use. For this simple example, we will\n", "use create a placeholder search engine. However, it is really easy to create\n", "your own tools - see documentation\n", "[here](https://js.langchain.com/docs/how_to/custom_tools) on how to do\n", "that.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "b22edfc4", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "import { tool } from \"@langchain/core/tools\";\n", "import { z } from \"zod\";\n", "\n", "const searchTool = tool(async (_) => {\n", " // This is a placeholder for the actual implementation\n", " return \"Cold, with a low of 13 ℃\";\n", "}, {\n", " name: \"search\",\n", " description:\n", " \"Use to surf the web, fetch current information, check the weather, and retrieve other information.\",\n", " schema: z.object({\n", " query: z.string().describe(\"The query to use in your search.\"),\n", " }),\n", "});\n", "\n", "await searchTool.invoke({ query: \"What's the weather like?\" });\n", "\n", "const tools = [searchTool];" ] }, { "cell_type": "markdown", "id": "7c764430", "metadata": {}, "source": [ "We can now wrap these tools in a simple\n", "[ToolNode](/langgraphjs/reference/classes/prebuilt.ToolNode.html).\n", "This object will actually run the tools (functions) whenever they are invoked by\n", "our LLM.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "0cc63f1f", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "import { ToolNode } from \"@langchain/langgraph/prebuilt\";\n", "\n", "const toolNode = new ToolNode(tools);" ] }, { "cell_type": "markdown", "id": "cc409cd5", "metadata": {}, "source": [ "## Set up the model\n", "\n", "Now we will load the\n", "[chat model](https://js.langchain.com/docs/concepts/chat_models/).\n", "\n", "1. It should work with messages. We will represent all agent state in the form\n", " of messages, so it needs to be able to work well with them.\n", "2. It should work with\n", " [tool calling](https://js.langchain.com/docs/how_to/tool_calling/#passing-tools-to-llms),\n", " meaning it can return function arguments in its response.\n", "\n", "
\n", "

Note

\n", "

\n", " These model requirements are not general requirements for using LangGraph - they are just requirements for this one example.\n", "

\n", "
" ] }, { "cell_type": "code", "execution_count": 5, "id": "dae9ab9c", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "import { ChatOpenAI } from \"@langchain/openai\";\n", "\n", "const model = new ChatOpenAI({ model: \"gpt-4o\" });" ] }, { "cell_type": "markdown", "id": "b5cfd558", "metadata": {}, "source": [ "After we've done this, we should make sure the model knows that it has these\n", "tools available to call. We can do this by calling\n", "[bindTools](https://v01.api.js.langchain.com/classes/langchain_core_language_models_chat_models.BaseChatModel.html#bindTools).\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "ca438e74", "metadata": { "lines_to_next_cell": 2 }, "outputs": [], "source": [ "const boundModel = model.bindTools(tools);" ] }, { "cell_type": "markdown", "id": "4a2b8a4f", "metadata": {}, "source": [ "## Define the graph\n", "\n", "We can now put it all together. Time travel requires a checkpointer to save the\n", "state - otherwise you wouldn't have anything go `get` or `update`. We will use\n", "the\n", "[MemorySaver](/langgraphjs/reference/classes/index.MemorySaver.html),\n", "which \"saves\" checkpoints in-memory." ] }, { "cell_type": "code", "execution_count": 7, "id": "1a29ec2a", "metadata": {}, "outputs": [], "source": [ "import { END, START, StateGraph } from \"@langchain/langgraph\";\n", "import { AIMessage } from \"@langchain/core/messages\";\n", "import { RunnableConfig } from \"@langchain/core/runnables\";\n", "import { MemorySaver } from \"@langchain/langgraph\";\n", "\n", "const routeMessage = (state: typeof StateAnnotation.State) => {\n", " const { messages } = state;\n", " const lastMessage = messages[messages.length - 1] as AIMessage;\n", " // If no tools are called, we can finish (respond to the user)\n", " if (!lastMessage?.tool_calls?.length) {\n", " return END;\n", " }\n", " // Otherwise if there is, we continue and call the tools\n", " return \"tools\";\n", "};\n", "\n", "const callModel = async (\n", " state: typeof StateAnnotation.State,\n", " config?: RunnableConfig,\n", ") => {\n", " const { messages } = state;\n", " const response = await boundModel.invoke(messages, config);\n", " return { messages: [response] };\n", "};\n", "\n", "const workflow = new StateGraph(StateAnnotation)\n", " .addNode(\"agent\", callModel)\n", " .addNode(\"tools\", toolNode)\n", " .addEdge(START, \"agent\")\n", " .addConditionalEdges(\"agent\", routeMessage)\n", " .addEdge(\"tools\", \"agent\");\n", "\n", "// Here we only save in-memory\n", "let memory = new MemorySaver();\n", "const graph = workflow.compile({ checkpointer: memory });" ] }, { "cell_type": "markdown", "id": "a6dd42a3", "metadata": {}, "source": [ "## Interacting with the Agent\n", "\n", "We can now interact with the agent. Between interactions you can get and update\n", "state." ] }, { "cell_type": "code", "execution_count": 8, "id": "0749329a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hi I'm Jo.\n", "-----\n", "\n", "Hello, Jo! How can I assist you today?\n", "-----\n", "\n" ] } ], "source": [ "let config = { configurable: { thread_id: \"conversation-num-1\" } };\n", "let inputs = { messages: [{ role: \"user\", content: \"Hi I'm Jo.\" }] };\n", "for await (\n", " const { messages } of await graph.stream(inputs, {\n", " ...config,\n", " streamMode: \"values\",\n", " })\n", ") {\n", " let msg = messages[messages?.length - 1];\n", " if (msg?.content) {\n", " console.log(msg.content);\n", " } else if (msg?.tool_calls?.length > 0) {\n", " console.log(msg.tool_calls);\n", " } else {\n", " console.log(msg);\n", " }\n", " console.log(\"-----\\n\");\n", "}" ] }, { "cell_type": "markdown", "id": "221f323d", "metadata": {}, "source": [ "See LangSmith example run here\n", "https://smith.langchain.com/public/b3feb09b-bcd2-4ad5-ad1d-414106148448/r\n", "\n", "Here you can see the \"agent\" node ran, and then our edge returned `__end__` so\n", "the graph stopped execution there.\n", "\n", "Let's check the current graph state." ] }, { "cell_type": "code", "execution_count": 9, "id": "6ff5468d", "metadata": { "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " messages: [\n", " { role: 'user', content: \"Hi I'm Jo.\" },\n", " AIMessage {\n", " \"id\": \"chatcmpl-A3FGf3k3QQo9q0QjT6Oc5h1XplkHr\",\n", " \"content\": \"Hello, Jo! How can I assist you today?\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 12,\n", " \"promptTokens\": 68,\n", " \"totalTokens\": 80\n", " },\n", " \"finish_reason\": \"stop\",\n", " \"system_fingerprint\": \"fp_fde2829a40\"\n", " },\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "let checkpoint = await graph.getState(config);\n", "checkpoint.values;" ] }, { "cell_type": "markdown", "id": "571077e2", "metadata": {}, "source": [ "The current state is the two messages we've seen above, 1. the HumanMessage we\n", "sent in, 2. the AIMessage we got back from the model.\n", "\n", "The `next` values are empty since the graph has terminated (transitioned to the\n", "`__end__`)." ] }, { "cell_type": "code", "execution_count": 10, "id": "22b25946", "metadata": { "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[]\n" ] } ], "source": [ "checkpoint.next;" ] }, { "cell_type": "markdown", "id": "889cd8ce", "metadata": {}, "source": [ "## Let's get it to execute a tool\n", "\n", "When we call the graph again, it will create a checkpoint after each internal\n", "execution step. Let's get it to run a tool, then look at the checkpoint." ] }, { "cell_type": "code", "execution_count": 11, "id": "873b3438", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "What's the weather like in SF currently?\n", "-----\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[\n", " {\n", " name: 'search',\n", " args: { query: 'current weather in San Francisco' },\n", " type: 'tool_call',\n", " id: 'call_ZtmtDOyEXDCnXDgowlit5dSd'\n", " }\n", "]\n", "-----\n", "\n", "Cold, with a low of 13 ℃\n", "-----\n", "\n", "The current weather in San Francisco is cold, with a low of 13°C.\n", "-----\n", "\n" ] } ], "source": [ "inputs = { messages: [{ role: \"user\", content: \"What's the weather like in SF currently?\" }] };\n", "for await (\n", " const { messages } of await graph.stream(inputs, {\n", " ...config,\n", " streamMode: \"values\",\n", " })\n", ") {\n", " let msg = messages[messages?.length - 1];\n", " if (msg?.content) {\n", " console.log(msg.content);\n", " } else if (msg?.tool_calls?.length > 0) {\n", " console.log(msg.tool_calls);\n", " } else {\n", " console.log(msg);\n", " }\n", " console.log(\"-----\\n\");\n", "}" ] }, { "cell_type": "markdown", "id": "6384c1e3", "metadata": {}, "source": [ "See the trace of the above execution here:\n", "https://smith.langchain.com/public/0ef426fd-0da1-4c02-a50b-64ae1e68338e/r We can\n", "see it planned the tool execution (ie the \"agent\" node), then \"should_continue\"\n", "edge returned \"continue\" so we proceeded to \"action\" node, which executed the\n", "tool, and then \"agent\" node emitted the final response, which made\n", "\"should_continue\" edge return \"end\". Let's see how we can have more control over\n", "this." ] }, { "cell_type": "markdown", "id": "3a3fe0ce", "metadata": {}, "source": [ "### Pause before tools\n", "\n", "If you notice below, we now will add `interruptBefore=[\"action\"]` - this means\n", "that before any actions are taken we pause. This is a great moment to allow the\n", "user to correct and update the state! This is very useful when you want to have\n", "a human-in-the-loop to validate (and potentially change) the action to take." ] }, { "cell_type": "code", "execution_count": 12, "id": "736be42e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "What's the weather like in SF currently?\n", "-----\n", "\n", "[\n", " {\n", " name: 'search',\n", " args: { query: 'current weather in San Francisco' },\n", " type: 'tool_call',\n", " id: 'call_OsKnTv2psf879eeJ9vx5GeoY'\n", " }\n", "]\n", "-----\n", "\n" ] } ], "source": [ "memory = new MemorySaver();\n", "const graphWithInterrupt = workflow.compile({\n", " checkpointer: memory,\n", " interruptBefore: [\"tools\"],\n", "});\n", "\n", "inputs = { messages: [{ role: \"user\", content: \"What's the weather like in SF currently?\" }] };\n", "for await (\n", " const { messages } of await graphWithInterrupt.stream(inputs, {\n", " ...config,\n", " streamMode: \"values\",\n", " })\n", ") {\n", " let msg = messages[messages?.length - 1];\n", " if (msg?.content) {\n", " console.log(msg.content);\n", " } else if (msg?.tool_calls?.length > 0) {\n", " console.log(msg.tool_calls);\n", " } else {\n", " console.log(msg);\n", " }\n", " console.log(\"-----\\n\");\n", "}" ] }, { "cell_type": "markdown", "id": "bf27f2b4", "metadata": {}, "source": [ "## Get State\n", "\n", "You can fetch the latest graph checkpoint using\n", "[`getState(config)`](/langgraphjs/reference/classes/pregel.Pregel.html#getState)." ] }, { "cell_type": "code", "execution_count": 13, "id": "0f434f69", "metadata": { "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 'tools' ]\n" ] } ], "source": [ "let snapshot = await graphWithInterrupt.getState(config);\n", "snapshot.next;" ] }, { "cell_type": "markdown", "id": "1f78ad8f", "metadata": {}, "source": [ "## Resume\n", "\n", "You can resume by running the graph with a `null` input. The checkpoint is\n", "loaded, and with no new inputs, it will execute as if no interrupt had occurred." ] }, { "cell_type": "code", "execution_count": 14, "id": "fd4d7eff", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cold, with a low of 13 ℃\n", "-----\n", "\n", "Currently, it is cold in San Francisco, with a temperature around 13°C (55°F).\n", "-----\n", "\n" ] } ], "source": [ "for await (\n", " const { messages } of await graphWithInterrupt.stream(null, {\n", " ...snapshot.config,\n", " streamMode: \"values\",\n", " })\n", ") {\n", " let msg = messages[messages?.length - 1];\n", " if (msg?.content) {\n", " console.log(msg.content);\n", " } else if (msg?.tool_calls?.length > 0) {\n", " console.log(msg.tool_calls);\n", " } else {\n", " console.log(msg);\n", " }\n", " console.log(\"-----\\n\");\n", "}" ] }, { "cell_type": "markdown", "id": "2885d91d", "metadata": {}, "source": [ "## Check full history\n", "\n", "Let's browse the history of this thread, from newest to oldest.\n" ] }, { "cell_type": "code", "execution_count": 15, "id": "bc7acb70", "metadata": { "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " values: {\n", " messages: [\n", " [Object],\n", " AIMessage {\n", " \"id\": \"chatcmpl-A3FGhKzOZs0GYZ2yalNOCQZyPgbcp\",\n", " \"content\": \"\",\n", " \"additional_kwargs\": {\n", " \"tool_calls\": [\n", " {\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\",\n", " \"type\": \"function\",\n", " \"function\": \"[Object]\"\n", " }\n", " ]\n", " },\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 17,\n", " \"promptTokens\": 72,\n", " \"totalTokens\": 89\n", " },\n", " \"finish_reason\": \"tool_calls\",\n", " \"system_fingerprint\": \"fp_fde2829a40\"\n", " },\n", " \"tool_calls\": [\n", " {\n", " \"name\": \"search\",\n", " \"args\": {\n", " \"query\": \"current weather in San Francisco\"\n", " },\n", " \"type\": \"tool_call\",\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\"\n", " }\n", " ],\n", " \"invalid_tool_calls\": []\n", " },\n", " ToolMessage {\n", " \"content\": \"Cold, with a low of 13 ℃\",\n", " \"name\": \"search\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_call_id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\"\n", " },\n", " AIMessage {\n", " \"id\": \"chatcmpl-A3FGiYripPKtQLnAK1H3hWLSXQfOD\",\n", " \"content\": \"Currently, it is cold in San Francisco, with a temperature around 13°C (55°F).\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 21,\n", " \"promptTokens\": 105,\n", " \"totalTokens\": 126\n", " },\n", " \"finish_reason\": \"stop\",\n", " \"system_fingerprint\": \"fp_fde2829a40\"\n", " },\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", " ]\n", " },\n", " next: [],\n", " tasks: [],\n", " metadata: { source: 'loop', writes: { agent: [Object] }, step: 3 },\n", " config: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-9c3a-6bd1-8003-d7f030ff72b2'\n", " }\n", " },\n", " createdAt: '2024-09-03T04:17:20.653Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-9516-6200-8002-43d2c6dc603f'\n", " }\n", " }\n", "}\n", "--\n", "{\n", " values: {\n", " messages: [\n", " [Object],\n", " AIMessage {\n", " \"id\": \"chatcmpl-A3FGhKzOZs0GYZ2yalNOCQZyPgbcp\",\n", " \"content\": \"\",\n", " \"additional_kwargs\": {\n", " \"tool_calls\": [\n", " {\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\",\n", " \"type\": \"function\",\n", " \"function\": \"[Object]\"\n", " }\n", " ]\n", " },\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 17,\n", " \"promptTokens\": 72,\n", " \"totalTokens\": 89\n", " },\n", " \"finish_reason\": \"tool_calls\",\n", " \"system_fingerprint\": \"fp_fde2829a40\"\n", " },\n", " \"tool_calls\": [\n", " {\n", " \"name\": \"search\",\n", " \"args\": {\n", " \"query\": \"current weather in San Francisco\"\n", " },\n", " \"type\": \"tool_call\",\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\"\n", " }\n", " ],\n", " \"invalid_tool_calls\": []\n", " },\n", " ToolMessage {\n", " \"content\": \"Cold, with a low of 13 ℃\",\n", " \"name\": \"search\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_call_id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\"\n", " }\n", " ]\n", " },\n", " next: [ 'agent' ],\n", " tasks: [\n", " {\n", " id: '612efffa-3b16-530f-8a39-fd01c31e7b8b',\n", " name: 'agent',\n", " interrupts: []\n", " }\n", " ],\n", " metadata: { source: 'loop', writes: { tools: [Object] }, step: 2 },\n", " config: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-9516-6200-8002-43d2c6dc603f'\n", " }\n", " },\n", " createdAt: '2024-09-03T04:17:19.904Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-9455-6410-8001-1c78a97f63e6'\n", " }\n", " }\n", "}\n", "--\n", "{\n", " values: {\n", " messages: [\n", " [Object],\n", " AIMessage {\n", " \"id\": \"chatcmpl-A3FGhKzOZs0GYZ2yalNOCQZyPgbcp\",\n", " \"content\": \"\",\n", " \"additional_kwargs\": {\n", " \"tool_calls\": [\n", " {\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\",\n", " \"type\": \"function\",\n", " \"function\": \"[Object]\"\n", " }\n", " ]\n", " },\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 17,\n", " \"promptTokens\": 72,\n", " \"totalTokens\": 89\n", " },\n", " \"finish_reason\": \"tool_calls\",\n", " \"system_fingerprint\": \"fp_fde2829a40\"\n", " },\n", " \"tool_calls\": [\n", " {\n", " \"name\": \"search\",\n", " \"args\": {\n", " \"query\": \"current weather in San Francisco\"\n", " },\n", " \"type\": \"tool_call\",\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\"\n", " }\n", " ],\n", " \"invalid_tool_calls\": []\n", " }\n", " ]\n", " },\n", " next: [ 'tools' ],\n", " tasks: [\n", " {\n", " id: '767116b0-55b6-5af4-8f74-ce45fb6e31ed',\n", " name: 'tools',\n", " interrupts: []\n", " }\n", " ],\n", " metadata: { source: 'loop', writes: { agent: [Object] }, step: 1 },\n", " config: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-9455-6410-8001-1c78a97f63e6'\n", " }\n", " },\n", " createdAt: '2024-09-03T04:17:19.825Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-8c4b-6261-8000-c51e5807fbcd'\n", " }\n", " }\n", "}\n", "--\n", "{\n", " values: { messages: [ [Object] ] },\n", " next: [ 'agent' ],\n", " tasks: [\n", " {\n", " id: '5b0ed7d1-1bb7-5d78-b4fc-7a8ed40e7291',\n", " name: 'agent',\n", " interrupts: []\n", " }\n", " ],\n", " metadata: { source: 'loop', writes: null, step: 0 },\n", " config: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-8c4b-6261-8000-c51e5807fbcd'\n", " }\n", " },\n", " createdAt: '2024-09-03T04:17:18.982Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-8c4b-6260-ffff-6ec582916c42'\n", " }\n", " }\n", "}\n", "--\n", "{\n", " values: {},\n", " next: [ '__start__' ],\n", " tasks: [\n", " {\n", " id: 'a4250d5c-d025-5da1-b588-cae2b3f4a8c7',\n", " name: '__start__',\n", " interrupts: []\n", " }\n", " ],\n", " metadata: { source: 'input', writes: { messages: [Array] }, step: -1 },\n", " config: {\n", " configurable: {\n", " thread_id: 'conversation-num-1',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef69ab6-8c4b-6260-ffff-6ec582916c42'\n", " }\n", " },\n", " createdAt: '2024-09-03T04:17:18.982Z',\n", " parentConfig: undefined\n", "}\n", "--\n" ] } ], "source": [ "let toReplay;\n", "const states = await graphWithInterrupt.getStateHistory(config);\n", "for await (const state of states) {\n", " console.log(state);\n", " console.log(\"--\");\n", " if (state.values?.messages?.length === 2) {\n", " toReplay = state;\n", " }\n", "}\n", "if (!toReplay) {\n", " throw new Error(\"No state to replay\");\n", "}" ] }, { "cell_type": "markdown", "id": "342f0154", "metadata": {}, "source": [ "## Replay a past state\n", "\n", "To replay from this place we just need to pass its config back to the agent.\n" ] }, { "cell_type": "code", "execution_count": 16, "id": "c1cefbfa", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cold, with a low of 13 ℃\n", "-----\n", "\n", "The current weather in San Francisco is cold, with a low of 13°C.\n", "-----\n", "\n" ] } ], "source": [ "for await (\n", " const { messages } of await graphWithInterrupt.stream(null, {\n", " ...toReplay.config,\n", " streamMode: \"values\",\n", " })\n", ") {\n", " let msg = messages[messages?.length - 1];\n", " if (msg?.content) {\n", " console.log(msg.content);\n", " } else if (msg?.tool_calls?.length > 0) {\n", " console.log(msg.tool_calls);\n", " } else {\n", " console.log(msg);\n", " }\n", " console.log(\"-----\\n\");\n", "}" ] }, { "cell_type": "markdown", "id": "e870c084", "metadata": {}, "source": [ "## Branch off a past state\n", "\n", "Using LangGraph's checkpointing, you can do more than just replay past states.\n", "You can branch off previous locations to let the agent explore alternate\n", "trajectories or to let a user \"version control\" changes in a workflow.\n", "\n", "#### First, update a previous checkpoint\n", "\n", "Updating the state will create a **new** snapshot by applying the update to the\n", "previous checkpoint. Let's **add a tool message** to simulate calling the tool." ] }, { "cell_type": "code", "execution_count": 17, "id": "d7656840-3a4a-4a80-af74-214b35cfbadd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " messages: [\n", " {\n", " role: 'user',\n", " content: \"What's the weather like in SF currently?\"\n", " },\n", " AIMessage {\n", " \"id\": \"chatcmpl-A3FGhKzOZs0GYZ2yalNOCQZyPgbcp\",\n", " \"content\": \"\",\n", " \"additional_kwargs\": {\n", " \"tool_calls\": [\n", " {\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\",\n", " \"type\": \"function\",\n", " \"function\": \"[Object]\"\n", " }\n", " ]\n", " },\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 17,\n", " \"promptTokens\": 72,\n", " \"totalTokens\": 89\n", " },\n", " \"finish_reason\": \"tool_calls\",\n", " \"system_fingerprint\": \"fp_fde2829a40\"\n", " },\n", " \"tool_calls\": [\n", " {\n", " \"name\": \"search\",\n", " \"args\": {\n", " \"query\": \"current weather in San Francisco\"\n", " },\n", " \"type\": \"tool_call\",\n", " \"id\": \"call_OsKnTv2psf879eeJ9vx5GeoY\"\n", " }\n", " ],\n", " \"invalid_tool_calls\": []\n", " },\n", " {\n", " role: 'tool',\n", " content: \"It's sunny out, with a high of 38 ℃.\",\n", " tool_call_id: 'call_OsKnTv2psf879eeJ9vx5GeoY'\n", " }\n", " ]\n", "}\n", "[ 'agent' ]\n" ] } ], "source": [ "const tool_calls =\n", " toReplay.values.messages[toReplay.values.messages.length - 1].tool_calls;\n", "const branchConfig = await graphWithInterrupt.updateState(\n", " toReplay.config,\n", " {\n", " messages: [\n", " { role: \"tool\", content: \"It's sunny out, with a high of 38 ℃.\", tool_call_id: tool_calls[0].id },\n", " ],\n", " },\n", " // Updates are applied \"as if\" they were coming from a node. By default,\n", " // the updates will come from the last node to run. In our case, we want to treat\n", " // this update as if it came from the tools node, so that the next node to run will be\n", " // the agent.\n", " \"tools\",\n", ");\n", "\n", "const branchState = await graphWithInterrupt.getState(branchConfig);\n", "console.log(branchState.values);\n", "console.log(branchState.next);" ] }, { "cell_type": "markdown", "id": "4689abd9-1008-4d8b-902c-e956a5913e12", "metadata": {}, "source": [ "#### Now you can run from this branch\n", "\n", "Just use the updated config (containing the new checkpoint ID). The trajectory\n", "will follow the new branch." ] }, { "cell_type": "code", "execution_count": 18, "id": "bb95930f-07e5-4e32-8e38-2170d36ab1a0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The current weather in San Francisco is sunny, with a high of 38°C.\n", "-----\n", "\n" ] } ], "source": [ "for await (\n", " const { messages } of await graphWithInterrupt.stream(null, {\n", " ...branchConfig,\n", " streamMode: \"values\",\n", " })\n", ") {\n", " let msg = messages[messages?.length - 1];\n", " if (msg?.content) {\n", " console.log(msg.content);\n", " } else if (msg?.tool_calls?.length > 0) {\n", " console.log(msg.tool_calls);\n", " } else {\n", " console.log(msg);\n", " }\n", " console.log(\"-----\\n\");\n", "}" ] } ], "metadata": { "jupytext": { "encoding": "# -*- coding: utf-8 -*-" }, "kernelspec": { "display_name": "TypeScript", "language": "typescript", "name": "tslab" }, "language_info": { "codemirror_mode": { "mode": "typescript", "name": "javascript", "typescript": true }, "file_extension": ".ts", "mimetype": "text/typescript", "name": "typescript", "version": "3.7.2" } }, "nbformat": 4, "nbformat_minor": 5 }