diff --git a/pipeline/tools/docusaurus_parser.py b/pipeline/tools/docusaurus_parser.py index 2b0bd12f..cd30b62c 100644 --- a/pipeline/tools/docusaurus_parser.py +++ b/pipeline/tools/docusaurus_parser.py @@ -281,7 +281,7 @@ def replace_link(match: re.Match[str]) -> str: return f"{link_text}({new_url})" - # Handle relative links (./file or ../category/file) + # Handle relative links (/oss/file or ../category/file) if link_url.startswith(("./", "../")) and link_url.endswith(".md"): # Remove .md extension if present new_url = link_url[:-3] # Remove .md diff --git a/src/docs.json b/src/docs.json index d5623f2a..1eead6d7 100644 --- a/src/docs.json +++ b/src/docs.json @@ -56,13 +56,229 @@ }, "navigation": { "dropdowns": [ + { + "dropdown": "LangGraph", + "icon": "/images/brand/langgraph-pill.svg", + "description": "Framework for building reliable agents and workflows", + "tabs": [ + { + "tab": "Get started", + "groups": [ + { + "group": "Get started", + "pages": [ + "oss/overview", + "oss/quickstart", + "oss/run-an-agent", + "oss/template-applications" + ] + }, + { + "group": "General concepts", + "pages": [ + "oss/workflows-and-agents", + "oss/prebuilt-vs-low-level", + { + "group": "Common agent architectures", + "pages": [ + "oss/agentic-architectures", + "oss/agentic-rag", + "oss/agent-supervisor", + "oss/sql-agent" + ] + } + ] + }, + { + "group": "Additional resources", + "pages": [ + "oss/case-studies", + "oss/faq", + "oss/langgraph-academy" + ] + } + ] + }, + { + "tab": "Build agents", + "groups": [ + { + "group": "Basic configuration", + "pages": [ + "oss/prebuilts" + ] + }, + { + "group": "Low-level configuration", + "pages": [ + "oss/why-langgraph", + "oss/1-build-basic-chatbot", + "oss/2-add-tools", + "oss/3-add-memory", + "oss/4-human-in-the-loop", + "oss/5-customize-state", + "oss/6-time-travel" + ] + }, + { + "group": "Components", + "pages": [ + "oss/models", + { + "group": "Tools", + "pages": [ + "oss/tools", + "oss/call-tools" + ] + }, + { + "group": "MCP", + "pages": [ + "oss/mcp", + "oss/use-mcp" + ] + }, + { + "group": "Multi-agent", + "pages": [ + "oss/multi-agent", + "oss/multi-agent-prebuilts", + "oss/multi-agent-custom" + ] + } + ] + } + ] + }, + { + "tab": "Agent runtime", + "groups": [ + { + "group": "Capabilities", + "pages": [ + "oss/persistence", + "oss/durable-execution", + { + "group": "Streaming", + "pages": [ + "oss/streaming", + "oss/use-streaming" + ] + }, + { + "group": "Human-in-the-loop", + "pages": [ + "oss/human-in-the-loop", + "oss/add-human-in-the-loop" + ] + }, + { + "group": "Time travel", + "pages": [ + "oss/time-travel", + "oss/use-time-travel" + ] + }, + { + "group": "Memory and context", + "pages": [ + "oss/memory", + "oss/context", + "oss/add-memory" + ] + }, + { + "group": "Subgraphs", + "pages": [ + "oss/subgraphs", + "oss/use-subgraphs" + ] + } + ] + }, + { + "group": "Run and debug", + "pages": [ + "oss/local-server", + "oss/ui", + "oss/trace-agent", + "oss/evals" + ] + }, + { + "group": "LangGraph APIs", + "pages": [ + { + "group": "Graph API", + "pages": [ + "oss/graph-api", + "oss/use-graph-api" + ] + }, + { + "group": "Functional API", + "pages": [ + "oss/functional-api", + "oss/use-functional-api" + ] + }, + "oss/pregel" + ] + } + ] + }, + { + "tab": "Reference", + "groups": [ + { + "group": "LangGraph reference", + "pages": [ + "oss/reference/overview", + "oss/reference/graphs", + "oss/reference/functional-api", + "oss/reference/pregel", + "oss/reference/checkpointers", + "oss/reference/storage", + "oss/reference/caching", + "oss/reference/types", + "oss/reference/runtime", + "oss/reference/config", + "oss/reference/errors", + "oss/reference/constants", + "oss/reference/channels" + ] + }, + { + "group": "Prebuilt reference", + "pages": [ + "oss/reference/agents", + "oss/reference/supervisor", + "oss/reference/swarm", + "oss/reference/mcp" + ] + }, + { + "group": "Error troubleshooting", + "pages": [ + "oss/common-errors", + "oss/GRAPH_RECURSION_LIMIT", + "oss/INVALID_CHAT_HISTORY", + "oss/INVALID_CONCURRENT_GRAPH_UPDATE", + "oss/INVALID_GRAPH_NODE_RETURN_VALUE", + "oss/MULTIPLE_SUBGRAPHS" + ] + } + ] + } + ] + }, { "dropdown": "LangGraph Platform", "icon": "/images/brand/langgraph-platform-pill.svg", "description": "Platform for building and deploying AI agents", "tabs": [ { - "tab": "Get Started", + "tab": "Get started", "groups": [ { "group": "Overview", diff --git a/src/index.mdx b/src/index.mdx index c4ae7516..12e3f33d 100644 --- a/src/index.mdx +++ b/src/index.mdx @@ -8,7 +8,7 @@ mode: "wide" # Frameworks - + Open-source framework for developing applications powered by large language models (LLMs). - - - Low-level orchestration framework for building, managing, and deploying long-running, stateful agents. - - - Low-level orchestration framework for building, managing, and deploying long-running, stateful agents. @@ -43,21 +34,14 @@ mode: "wide" # Platforms - + Commercial platform for developing, deploying, and scaling long-running agents and worflows. - - Commercial platform for developing, deploying, and scaling long-running agents and workflows. - - **Prerequisites** - This guide assumes familiarity with the [LangGraph Platform](/langgraph-platform/index), [Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/), and [Cross-thread persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store) concepts. + This guide assumes familiarity with the [LangGraph Platform](/langgraph-platform/index), [Persistence](/oss/persistence), and [Cross-thread persistence](/oss/persistence#memory-store) concepts. @@ -13,7 +13,7 @@ sidebarTitle: Add TTLs to your LangGraph application TTLs are only supported for LangGraph platform deployments. This guide does not apply to LangGraph OSS. -The LangGraph Platform persists both [checkpoints](https://langchain-ai.github.io/langgraph/concepts/persistence/#checkpoints) (thread state) and [cross-thread memories](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store) (store items). Configure Time-to-Live (TTL) policies in `langgraph.json` to automatically manage the lifecycle of this data, preventing indefinite accumulation. +The LangGraph Platform persists both [checkpoints](/oss/persistence#checkpoints) (thread state) and [cross-thread memories](/oss/persistence#memory-store) (store items). Configure Time-to-Live (TTL) policies in `langgraph.json` to automatically manage the lifecycle of this data, preventing indefinite accumulation. ## Configuring Checkpoint TTL diff --git a/src/langgraph-platform/control-plane.mdx b/src/langgraph-platform/control-plane.mdx index a4536531..47b1ec6d 100644 --- a/src/langgraph-platform/control-plane.mdx +++ b/src/langgraph-platform/control-plane.mdx @@ -86,9 +86,9 @@ Database disk size for `Development` type deployments can be manually increased ### Database Provisioning -The control plane and [LangGraph Data Plane](/langgraph-platform/data-plane) "listener" application coordinate to automatically create a Postgres database for each deployment. The database serves as the [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store) for the deployment. +The control plane and [LangGraph Data Plane](/langgraph-platform/data-plane) "listener" application coordinate to automatically create a Postgres database for each deployment. The database serves as the [persistence layer](/oss/persistence#memory-store) for the deployment. -When implementing a LangGraph application, a [checkpointer](https://langchain-ai.github.io/langgraph/concepts/persistence/#checkpointer-libraries) does not need to be configured by the developer. Instead, a checkpointer is automatically configured for the graph. Any checkpointer configured for a graph will be replaced by the one that is automatically configured. +When implementing a LangGraph application, a [checkpointer](/oss/persistence#checkpointer-libraries) does not need to be configured by the developer. Instead, a checkpointer is automatically configured for the graph. Any checkpointer configured for a graph will be replaced by the one that is automatically configured. There is no direct access to the database. All access to the database occurs through the [LangGraph Server](/langgraph-platform/langgraph-server). diff --git a/src/langgraph-platform/data-plane.mdx b/src/langgraph-platform/data-plane.mdx index 4b43d4ac..847c9e11 100644 --- a/src/langgraph-platform/data-plane.mdx +++ b/src/langgraph-platform/data-plane.mdx @@ -26,7 +26,7 @@ In other words, the data plane "listener" reads the latest state of the control ## Postgres -Postgres is the persistence layer for all user, run, and long-term memory data in a LangGraph Server. This stores both checkpoints (see more info [here](https://langchain-ai.github.io/langgraph/concepts/persistence)), server resources (threads, runs, assistants and crons), as well as items saved in the long-term memory store (see more info [here](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store)). +Postgres is the persistence layer for all user, run, and long-term memory data in a LangGraph Server. This stores both checkpoints (see more info [here](/oss/persistence)), server resources (threads, runs, assistants and crons), as well as items saved in the long-term memory store (see more info [here](/oss/persistence#memory-store)). ## Redis diff --git a/src/langgraph-platform/deploy-self-hosted-full-platform.mdx b/src/langgraph-platform/deploy-self-hosted-full-platform.mdx index efda5056..4d4c7f13 100644 --- a/src/langgraph-platform/deploy-self-hosted-full-platform.mdx +++ b/src/langgraph-platform/deploy-self-hosted-full-platform.mdx @@ -2,7 +2,7 @@ title: How to deploy self-hosted full platform sidebarTitle: Deploy self-hosted full platform --- -Before deploying, review the [conceptual guide for the Self-Hosted Full Platform](/langgraph-platform/self-hosted-full-platform) deployment option. +Before deploying, review the [conceptual guide for the Self-Hosted Full Platform](/langgraph-platform/self-hosted) deployment option. **Important** diff --git a/src/langgraph-platform/human-in-the-loop-time-travel.mdx b/src/langgraph-platform/human-in-the-loop-time-travel.mdx index b7d109eb..2b4d09c9 100644 --- a/src/langgraph-platform/human-in-the-loop-time-travel.mdx +++ b/src/langgraph-platform/human-in-the-loop-time-travel.mdx @@ -3,13 +3,13 @@ title: Time travel using the server API sidebarTitle: Time travel using the server API --- -LangGraph provides the [**time travel**](https://langchain-ai.github.io/langgraph/concepts/time-travel/) functionality to resume execution from a prior checkpoint, either replaying the same state or modifying it to explore alternatives. In all cases, resuming past execution produces a new fork in the history. +LangGraph provides the [**time travel**](/oss/time-travel) functionality to resume execution from a prior checkpoint, either replaying the same state or modifying it to explore alternatives. In all cases, resuming past execution produces a new fork in the history. To time travel using the LangGraph Server API (via the LangGraph SDK): 1. **Run the graph** with initial inputs using [LangGraph SDK](/langgraph-platform/python-sdk)'s @[client.runs.wait] or @[client.runs.stream] APIs. 2. **Identify a checkpoint in an existing thread**: Use @[client.threads.get_history] method to retrieve the execution history for a specific `thread_id` and locate the desired `checkpoint_id`. - Alternatively, set a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/) before the node(s) where you want execution to pause. You can then find the most recent checkpoint recorded up to that breakpoint. + Alternatively, set a [breakpoint](/oss/add-human-in-the-loop) before the node(s) where you want execution to pause. You can then find the most recent checkpoint recorded up to that breakpoint. 3. **(Optional) modify the graph state**: Use the @[client.threads.update_state] method to modify the graph’s state at the checkpoint and resume execution from alternative state. 4. **Resume execution from the checkpoint**: Use the @[client.runs.wait] or @[client.runs.stream] APIs with an input of `None` and the appropriate `thread_id` and `checkpoint_id`. @@ -239,4 +239,4 @@ To time travel using the LangGraph Server API (via the LangGraph SDK): ## Learn more -* [**LangGraph time travel guide**](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/): learn more about using time travel in LangGraph. +* [**LangGraph time travel guide**](/oss/time-travel): learn more about using time travel in LangGraph. diff --git a/src/langgraph-platform/index.mdx b/src/langgraph-platform/index.mdx index a551e74d..ec14d7db 100644 --- a/src/langgraph-platform/index.mdx +++ b/src/langgraph-platform/index.mdx @@ -20,7 +20,7 @@ LangGraph Platform makes it easy to get your agent running in production — wh * **Support for long runs**: Regular server setups often encounter timeouts or disruptions when handling requests that take a long time to complete. LangGraph Server's API provides robust support for these tasks by sending regular heartbeat signals, preventing unexpected connection closures during prolonged processes. * **Handling Burstiness**: Certain applications, especially those with real-time user interaction, may experience "bursty" request loads where numerous requests hit the server simultaneously. LangGraph Server includes a task queue, ensuring requests are handled consistently without loss, even under heavy loads. * **[Double-texting](/langgraph-platform/interrupt-concurrent)**: In user-driven applications, it's common for users to send multiple messages rapidly. This "double texting" can disrupt agent flows if not handled properly. LangGraph Server offers built-in strategies to address and manage such interactions. -* **[Checkpointers and memory management](https://langchain-ai.github.io/langgraph/concepts/persistence/#checkpoints)**: For agents needing persistence (e.g., conversation memory), deploying a robust storage solution can be complex. LangGraph Platform includes optimized [checkpointers](https://langchain-ai.github.io/langgraph/concepts/persistence/#checkpoints) and a [memory store](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store), managing state across sessions without the need for custom solutions. +* **[Checkpointers and memory management](/oss/persistence#checkpoints)**: For agents needing persistence (e.g., conversation memory), deploying a robust storage solution can be complex. LangGraph Platform includes optimized [checkpointers](/oss/persistence#checkpoints) and a [memory store](/oss/persistence#memory-store), managing state across sessions without the need for custom solutions. * **[Human-in-the-loop support](/langgraph-platform/add-human-in-the-loop)**: In many applications, users require a way to intervene in agent processes. LangGraph Server provides specialized endpoints for human-in-the-loop scenarios, simplifying the integration of manual oversight into agent workflows. * **[LangGraph Studio](/langgraph-platform/langgraph-studio)**: Enables visualization, interaction, and debugging of agentic systems that implement the LangGraph Server API protocol. Studio also integrates with LangSmith to enable tracing, evaluation, and prompt engineering. * **[Deployment](/langgraph-platform/deployment-options)**: There are three ways to deploy on LangGraph Platform: [Cloud](/langgraph-platform/cloud), [Hybrid](/langgraph-platform/hybrid), and [Self-Hosted](/langgraph-platform/self-hosted). diff --git a/src/langgraph-platform/invoke-studio.mdx b/src/langgraph-platform/invoke-studio.mdx index e3ccca8f..7ecd24bc 100644 --- a/src/langgraph-platform/invoke-studio.mdx +++ b/src/langgraph-platform/invoke-studio.mdx @@ -5,7 +5,7 @@ sidebarTitle: Run application ## Prerequisites -* [Running agents](https://langchain-ai.github.io/langgraph/agents/run_agents/) +* [Running agents](/oss/run-an-agent) This guide shows how to submit a [run](/langgraph-platform/assistants#execution) to your application. @@ -15,7 +15,7 @@ This guide shows how to submit a [run](/langgraph-platform/assistants#execution) First define the input to your graph with in the "Input" section on the left side of the page, below the graph interface. -Studio will attempt to render a form for your input based on the graph's defined [state schema](https://langchain-ai.github.io/langgraph/concepts/low_level/#schema). To disable this, click the "View Raw" button, which will present you with a JSON editor. +Studio will attempt to render a form for your input based on the graph's defined [state schema](/oss/graph-api/#schema). To disable this, click the "View Raw" button, which will present you with a JSON editor. Click the up/down arrows at the top of the "Input" section to toggle through and use previously submitted inputs. @@ -39,7 +39,7 @@ For more information on breakpoints see [here](/langgraph-platform/add-human-in- ### Submit run -To submit the run with the specified input and run settings, click the "Submit" button. This will add a [run](/langgraph-platform/assistants#execution) to the existing selected [thread](https://langchain-ai.github.io/langgraph/concepts/persistence/#threads). If no thread is currently selected, a new one will be created. +To submit the run with the specified input and run settings, click the "Submit" button. This will add a [run](/langgraph-platform/assistants#execution) to the existing selected [thread](/oss/persistence#threads). If no thread is currently selected, a new one will be created. To cancel the ongoing run, click the "Cancel" button. diff --git a/src/langgraph-platform/iterate-graph-studio.mdx b/src/langgraph-platform/iterate-graph-studio.mdx index 7205ac42..6b21df4f 100644 --- a/src/langgraph-platform/iterate-graph-studio.mdx +++ b/src/langgraph-platform/iterate-graph-studio.mdx @@ -17,7 +17,7 @@ Studio allows you to edit prompts used inside individual nodes, directly from th ### Graph Configuration -Define your [configuration](https://langchain-ai.github.io/langgraph/how-tos/configuration/) to specify prompt fields and their associated nodes using `langgraph_nodes` and `langgraph_type` keys. +Define your [configuration](/oss/use-graph-api#add-runtime-configuration) to specify prompt fields and their associated nodes using `langgraph_nodes` and `langgraph_type` keys. #### Configuration Reference diff --git a/src/langgraph-platform/langgraph-basics/1-build-basic-chatbot.mdx b/src/langgraph-platform/langgraph-basics/1-build-basic-chatbot.mdx index 82f70796..10eb8625 100644 --- a/src/langgraph-platform/langgraph-basics/1-build-basic-chatbot.mdx +++ b/src/langgraph-platform/langgraph-basics/1-build-basic-chatbot.mdx @@ -3,7 +3,7 @@ title: Build a basic chatbot sidebarTitle: 1. Build a basic chatbot --- -import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let’s dive in! 🌟 @@ -58,7 +58,7 @@ Our graph can now handle two key tasks: **Concept** - When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages). + When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](/oss/graph-api#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages). ## 3. Add a node diff --git a/src/langgraph-platform/langgraph-basics/2-add-tools.mdx b/src/langgraph-platform/langgraph-basics/2-add-tools.mdx index a07e5106..7290b38b 100644 --- a/src/langgraph-platform/langgraph-basics/2-add-tools.mdx +++ b/src/langgraph-platform/langgraph-basics/2-add-tools.mdx @@ -3,7 +3,7 @@ title: Add tools sidebarTitle: 2. Add tools --- -import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; To handle queries that your chatbot can't answer "from memory", integrate a web search tool. The chatbot can use this tool to find relevant information and provide better responses. diff --git a/src/langgraph-platform/langgraph-basics/3-add-memory.mdx b/src/langgraph-platform/langgraph-basics/3-add-memory.mdx index 285b037b..fd132ce7 100644 --- a/src/langgraph-platform/langgraph-basics/3-add-memory.mdx +++ b/src/langgraph-platform/langgraph-basics/3-add-memory.mdx @@ -3,7 +3,7 @@ title: Add memory sidebarTitle: 3. Add memory --- -import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; This tutorial builds on [Add Tools](/langgraph-platform/langgraph-basics/2-add-tools). diff --git a/src/langgraph-platform/langgraph-basics/4-human-in-the-loop.mdx b/src/langgraph-platform/langgraph-basics/4-human-in-the-loop.mdx index 8f0b1c3a..3abb90a8 100644 --- a/src/langgraph-platform/langgraph-basics/4-human-in-the-loop.mdx +++ b/src/langgraph-platform/langgraph-basics/4-human-in-the-loop.mdx @@ -3,7 +3,7 @@ title: Add human-in-the-loop controls sidebarTitle: 4. Add human-in-the-loop controls --- -import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; This tutorial builds on [Add Memory](/langgraph-platform/langgraph-basics/3-add-memory). @@ -11,7 +11,7 @@ import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended. -LangGraph's [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](/langgraph-platform/add-human-in-the-loop) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](https://langchain-ai.github.io/langgraph/concepts/low_level/#command). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](/langgraph-platform/add-human-in-the-loop). +LangGraph's [persistence](/oss/persistence) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](/langgraph-platform/add-human-in-the-loop) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](/oss/graph-api#command). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](/langgraph-platform/add-human-in-the-loop). ## 1. Add the `human_assistance` tool @@ -165,12 +165,12 @@ snapshot.next return human_response["data"] ``` - Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](https://langchain-ai.github.io/langgraph/concepts/persistence/#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the Python kernel is running. + Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](/oss/persistence#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the Python kernel is running. ## 5. Resume execution -To resume execution, pass a [`Command`](https://langchain-ai.github.io/langgraph/concepts/low_level/#command) object containing data expected by the tool. The format of this data can be customized based on needs. For this example, use a dict with a key `"data"`: +To resume execution, pass a [`Command`](/oss/graph-api#command) object containing data expected by the tool. The format of this data can be customized based on needs. For this example, use a dict with a key `"data"`: ```python human_response = ( diff --git a/src/langgraph-platform/langgraph-basics/5-customize-state.mdx b/src/langgraph-platform/langgraph-basics/5-customize-state.mdx index 66dbc904..28a9678c 100644 --- a/src/langgraph-platform/langgraph-basics/5-customize-state.mdx +++ b/src/langgraph-platform/langgraph-basics/5-customize-state.mdx @@ -3,7 +3,7 @@ title: Customize state sidebarTitle: 5. Customize state --- -import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; In this tutorial, you will add additional fields to the state to define complex behavior without relying on the message list. The chatbot will use its search tool to find specific information and forward them to a human for review. @@ -35,7 +35,7 @@ Adding this information to the state makes it easily accessible by other graph n ## 2. Update the state inside the tool -Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](https://langchain-ai.github.io/langgraph/concepts/low_level/#using-inside-tools) to issue a state update from inside the tool. +Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](/oss/graph-api#using-inside-tools) to issue a state update from inside the tool. ```python from langchain_core.messages import ToolMessage diff --git a/src/langgraph-platform/langgraph-basics/6-time-travel.mdx b/src/langgraph-platform/langgraph-basics/6-time-travel.mdx index a0a90422..0bdc7e7a 100644 --- a/src/langgraph-platform/langgraph-basics/6-time-travel.mdx +++ b/src/langgraph-platform/langgraph-basics/6-time-travel.mdx @@ -3,7 +3,7 @@ title: Time travel sidebarTitle: 6. Time travel --- -import ChatModelTabs from '/snippets/chat_model_tabs.mdx'; +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; In a typical chatbot workflow, the user interacts with the bot one or more times to accomplish a task. [Memory](/langgraph-platform/langgraph-basics/3-add-memory) and a [human-in-the-loop](/langgraph-platform/langgraph-basics/4-human-in-the-loop) enable checkpoints in the graph state and control future responses. diff --git a/src/langgraph-platform/langgraph-server.mdx b/src/langgraph-platform/langgraph-server.mdx index f0153781..c1c38618 100644 --- a/src/langgraph-platform/langgraph-server.mdx +++ b/src/langgraph-platform/langgraph-server.mdx @@ -2,9 +2,9 @@ title: LangGraph Server --- -**LangGraph Server** offers an API for creating and managing agent-based applications. It is built on the concept of [assistants](/langgraph-platform/assistants), which are agents configured for specific tasks, and includes built-in [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store) and a **task queue**. This versatile API supports a wide range of agentic application use cases, from background processing to real-time interactions. +**LangGraph Server** offers an API for creating and managing agent-based applications. It is built on the concept of [assistants](/langgraph-platform/assistants), which are agents configured for specific tasks, and includes built-in [persistence](/oss/persistence#memory-store) and a **task queue**. This versatile API supports a wide range of agentic application use cases, from background processing to real-time interactions. -Use LangGraph Server to create and manage [assistants](/langgraph-platform/assistants), [threads](https://langchain-ai.github.io/langgraph/concepts/persistence/#threads), [runs](/langgraph-platform/assistants#execution), [cron jobs](/langgraph-platform/cron-jobs), [webhooks](/langgraph-platform/use-webhooks), and more. +Use LangGraph Server to create and manage [assistants](/langgraph-platform/assistants), [threads](/oss/persistence#threads), [runs](/langgraph-platform/assistants#execution), [cron jobs](/langgraph-platform/cron-jobs), [webhooks](/langgraph-platform/use-webhooks), and more. **API reference** @@ -27,7 +27,7 @@ Read the [application structure](/langgraph-platform/application-structure) guid ## Parts of a deployment -When you deploy LangGraph Server, you are deploying one or more [graphs](#graphs), a database for [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/), and a task queue. +When you deploy LangGraph Server, you are deploying one or more [graphs](#graphs), a database for [persistence](/oss/persistence), and a task queue. ### Graphs @@ -39,13 +39,13 @@ that can be served by the same graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default configuration settings. - We often think of a graph as implementing an [agent](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/), but a graph does not necessarily need to implement an agent. For example, a graph could implement a simple - chatbot that only supports back-and-forth conversation, without the ability to influence any application control flow. In reality, as applications get more complex, a graph will often implement a more complex flow that may use [multiple agents](https://langchain-ai.github.io/langgraph/concepts/multi_agent/) working in tandem. + We often think of a graph as implementing an [agent](/oss/agentic-architectures), but a graph does not necessarily need to implement an agent. For example, a graph could implement a simple + chatbot that only supports back-and-forth conversation, without the ability to influence any application control flow. In reality, as applications get more complex, a graph will often implement a more complex flow that may use [multiple agents](/oss/multi-agent) working in tandem. ### Persistence and task queue -LangGraph Server leverages a database for [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) and a task queue. +LangGraph Server leverages a database for [persistence](/oss/persistence) and a task queue. Currently, only [Postgres](https://www.postgresql.org/) is supported as a database for LangGraph Server and [Redis](https://redis.io/) as the task queue. diff --git a/src/langgraph-platform/langgraph-studio.mdx b/src/langgraph-platform/langgraph-studio.mdx index 317960e8..051c5fd9 100644 --- a/src/langgraph-platform/langgraph-studio.mdx +++ b/src/langgraph-platform/langgraph-studio.mdx @@ -23,8 +23,8 @@ Key features of LangGraph Studio: * [Manage threads](/langgraph-platform/threads-studio) * [Iterate on prompts](/langgraph-platform/iterate-graph-studio) * [Run experiments over a dataset](/langgraph-platform/run-evals-studio) -* Manage [long term memory](https://langchain-ai.github.io/langgraph/concepts/memory/) -* Debug agent state via [time travel](https://langchain-ai.github.io/langgraph/concepts/time-travel/) +* Manage [long term memory](/oss/memory) +* Debug agent state via [time travel](/oss/time-travel) LangGraph Studio works for graphs that are deployed on [LangGraph Platform](/langgraph-platform/deployment-quickstart) or for graphs that are running locally via the [LangGraph Server](/langgraph-platform/local-server). @@ -36,7 +36,7 @@ Graph mode exposes the full feature-set of Studio and is useful when you would l ### Chat mode -Chat mode is a simpler UI for iterating on and testing chat-specific agents. It is useful for business users and those who want to test overall agent behavior. Chat mode is only supported for graph's whose state includes or extends [`MessagesState`](https://langchain-ai.github.io/langgraph/how-tos/graph-api/#messagesstate). +Chat mode is a simpler UI for iterating on and testing chat-specific agents. It is useful for business users and those who want to test overall agent behavior. Chat mode is only supported for graph's whose state includes or extends [`MessagesState`](/oss/use-graph-api#messagesstate). ## Learn more diff --git a/src/langgraph-platform/manage-assistants-studio.mdx b/src/langgraph-platform/manage-assistants-studio.mdx index 81279b72..a5998d65 100644 --- a/src/langgraph-platform/manage-assistants-studio.mdx +++ b/src/langgraph-platform/manage-assistants-studio.mdx @@ -4,7 +4,7 @@ sidebarTitle: Manage assistants --- **Prerequisites** - * [Assistants Overview](https://langchain-ai.github.io/langgraph/concepts/assistants/) + * [Assistants Overview](/langgraph-platform/assistants/) LangGraph Studio lets you view, edit, and update your assistants, and allows you to run your graph using these assistant configurations. diff --git a/src/langgraph-platform/quick-start-studio.mdx b/src/langgraph-platform/quick-start-studio.mdx index 99373180..9bd46479 100644 --- a/src/langgraph-platform/quick-start-studio.mdx +++ b/src/langgraph-platform/quick-start-studio.mdx @@ -17,11 +17,11 @@ LangGraph Studio is accessed from the LangSmith UI, within the LangGraph Platfor For applications that are [deployed](/langgraph-platform/deployment-quickstart) on LangGraph Platform, you can access Studio as part of that deployment. To do so, navigate to the deployment in LangGraph Platform within the LangSmith UI and click the "LangGraph Studio" button. -This will load the Studio UI connected to your live deployment, allowing you to create, read, and update the [threads](https://langchain-ai.github.io/langgraph/concepts/persistence/#threads), [assistants](/langgraph-platform/assistants), and [memory](https://langchain-ai.github.io/langgraph/concepts/memory/) in that deployment. +This will load the Studio UI connected to your live deployment, allowing you to create, read, and update the [threads](/oss/persistence#threads), [assistants](/langgraph-platform/assistants), and [memory](/oss/memory) in that deployment. ## Local development server -To test your locally running application using LangGraph Studio, ensure your application is set up following [this guide](https://langchain-ai.github.io/langgraph/cloud/deployment/setup/). +To test your locally running application using LangGraph Studio, ensure your application is set up following [this guide](/langgraph-platform/deployment-quickstart). **LangSmith Tracing** diff --git a/src/langgraph-platform/run-evals-studio.mdx b/src/langgraph-platform/run-evals-studio.mdx index 57268497..05e7a248 100644 --- a/src/langgraph-platform/run-evals-studio.mdx +++ b/src/langgraph-platform/run-evals-studio.mdx @@ -2,7 +2,7 @@ title: Run experiments over a dataset sidebarTitle: Run experiments over a dataset --- -LangGraph Studio supports evaluations by allowing you to run your assistant over a pre-defined LangSmith dataset. This enables you to understand how your application performs over a variety of inputs, compare the results to reference outputs, and score the results using [evaluators](https://langchain-ai.github.io/langgraph/agents/evals/). +LangGraph Studio supports evaluations by allowing you to run your assistant over a pre-defined LangSmith dataset. This enables you to understand how your application performs over a variety of inputs, compare the results to reference outputs, and score the results using [evaluators](/oss/evals/). This guide shows you how to run an experiment end-to-end from Studio. @@ -11,7 +11,7 @@ This guide shows you how to run an experiment end-to-end from Studio. Before running an experiment, ensure you have the following: 1. **A LangSmith dataset**: Your dataset should contain the inputs you want to test and optionally, reference outputs for comparison. - * The schema for the inputs must match the required input schema for the assistant. For more information on schemas, see [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#schema). + * The schema for the inputs must match the required input schema for the assistant. For more information on schemas, see [here](/oss/use-graph-api#schema). * For more on creating datasets, see [How to Manage Datasets](https://docs.smith.langchain.com/evaluation/how_to_guides/manage_datasets_in_application#set-up-your-dataset). 2. **(Optional) Evaluators**: You can attach evaluators (e.g., LLM-as-a-Judge, heuristics, or custom functions) to your dataset in LangSmith. These will run automatically after the graph has processed all inputs. * To learn more, read about [Evaluation Concepts](https://docs.smith.langchain.com/evaluation/concepts#evaluators). diff --git a/src/langgraph-platform/same-thread.mdx b/src/langgraph-platform/same-thread.mdx index 366ae5e2..3c804dc2 100644 --- a/src/langgraph-platform/same-thread.mdx +++ b/src/langgraph-platform/same-thread.mdx @@ -6,7 +6,7 @@ In LangGraph Platform, a thread is not explicitly associated with a particular a This means that you can run multiple agents on the same thread, which allows a different agent to continue from an initial agent's progress. In this example, we will create two agents and then call them both on the same thread. -You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread by the first agent as context. +You'll see that the second agent will respond using information from the [checkpoint](/oss/graph-api#checkpointer-state) generated in the thread by the first agent as context. ## Setup diff --git a/src/langgraph-platform/semantic-search.mdx b/src/langgraph-platform/semantic-search.mdx index 9c4950d8..1fe29ca0 100644 --- a/src/langgraph-platform/semantic-search.mdx +++ b/src/langgraph-platform/semantic-search.mdx @@ -2,7 +2,7 @@ title: How to add semantic search to your LangGraph deployment sidebarTitle: Add semantic search to your LangGraph deployment --- -This guide explains how to add semantic search to your LangGraph deployment's cross-thread [store](https://langchain-ai.github.io/langgraph/concepts/persistence/#memory-store), so that your agent can search for memories and other documents by semantic similarity. +This guide explains how to add semantic search to your LangGraph deployment's cross-thread [store](/oss/persistence#memory-store), so that your agent can search for memories and other documents by semantic similarity. ## Prerequisites diff --git a/src/langgraph-platform/server-mcp.mdx b/src/langgraph-platform/server-mcp.mdx index 8e21a6d2..cd70ca05 100644 --- a/src/langgraph-platform/server-mcp.mdx +++ b/src/langgraph-platform/server-mcp.mdx @@ -190,7 +190,7 @@ After deployment, you can update the name and description using the LangGraph SD Define clear, minimal input and output schemas to avoid exposing unnecessary internal complexity to the LLM. -The default [MessagesState](https://langchain-ai.github.io/langgraph/concepts/low_level/#messagesstate) uses `AnyMessage`, which supports many message types but is too general for direct LLM exposure. +The default [MessagesState](/oss/graph-api#messagesstate) uses `AnyMessage`, which supports many message types but is too general for direct LLM exposure. Instead, define **custom agents or workflows** that use explicitly typed input and output structures. @@ -228,7 +228,7 @@ graph = builder.compile() print(graph.invoke({"question": "hi"})) ``` -For more details, see the [low-level concepts guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#state). +For more details, see the [low-level concepts guide](/oss/graph-api#state). ## Use user-scoped MCP tools in your deployment diff --git a/src/langgraph-platform/setup-javascript.mdx b/src/langgraph-platform/setup-javascript.mdx index 01f3fa89..bab1380f 100644 --- a/src/langgraph-platform/setup-javascript.mdx +++ b/src/langgraph-platform/setup-javascript.mdx @@ -2,7 +2,7 @@ title: How to set up a LangGraph.js application sidebarTitle: Set up a LangGraph.js application --- -A [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) application must be configured with a [LangGraph configuration file](/langgraph-platform/cli#configuration-file) in order to be deployed to LangGraph Platform (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph.js application for deployment using `package.json` to specify project dependencies. +A LangGraph.js application must be configured with a [LangGraph configuration file](/langgraph-platform/cli#configuration-file) in order to be deployed to LangGraph Platform (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph.js application for deployment using `package.json` to specify project dependencies. This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraphjs-studio-starter), which you can play around with to learn more about how to setup your LangGraph application for deployment. diff --git a/src/langgraph-platform/streaming.mdx b/src/langgraph-platform/streaming.mdx index 114984b8..77cd77c7 100644 --- a/src/langgraph-platform/streaming.mdx +++ b/src/langgraph-platform/streaming.mdx @@ -2,7 +2,7 @@ title: Streaming API sidebarTitle: Streaming API --- -[LangGraph SDK](/langgraph-platform/python-sdk) allows you to [stream outputs](https://langchain-ai.github.io/langgraph/concepts/streaming/) from the LangGraph API server. +[LangGraph SDK](/langgraph-platform/python-sdk) allows you to [stream outputs](/oss/streaming/) from the LangGraph API server. LangGraph SDK and LangGraph Server are a part of [LangGraph Platform](/langgraph-platform/index). @@ -217,11 +217,11 @@ Basic usage example: | Mode | Description | LangGraph Library Method | |----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| [`values`](#stream-graph-state) | Stream the full graph state after each [super-step](/langgraph-platform/graph-rebuild#graphs). | `.stream()` / `.astream()` with [`stream_mode="values"`](https://langchain-ai.github.io/langgraph/how-tos/streaming/#stream-graph-state) | -| [`updates`](#stream-graph-state) | Streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g., multiple nodes are run), those updates are streamed separately. | `.stream()` / `.astream()` with [`stream_mode="updates"`](https://langchain-ai.github.io/langgraph/how-tos/streaming/#stream-graph-state) | -| [`messages-tuple`](#messages) | Streams LLM tokens and metadata for the graph node where the LLM is invoked (useful for chat apps). | `.stream()` / `.astream()` with [`stream_mode="messages"`](https://langchain-ai.github.io/langgraph/how-tos/streaming/#messages) | -| [`debug`](#debug) | Streams as much information as possible throughout the execution of the graph. | `.stream()` / `.astream()` with [`stream_mode="debug"`](https://langchain-ai.github.io/langgraph/how-tos/streaming/#stream-graph-state) | -| [`custom`](#stream-custom-data) | Streams custom data from inside your graph | `.stream()` / `.astream()` with [`stream_mode="custom"`](https://langchain-ai.github.io/langgraph/how-tos/streaming/#stream-custom-data) | +| [`values`](#stream-graph-state) | Stream the full graph state after each [super-step](/langgraph-platform/graph-rebuild#graphs). | `.stream()` / `.astream()` with [`stream_mode="values"`](/oss/use-streaming#stream-graph-state) | +| [`updates`](#stream-graph-state) | Streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g., multiple nodes are run), those updates are streamed separately. | `.stream()` / `.astream()` with [`stream_mode="updates"`](/oss/use-streaming#stream-graph-state) | +| [`messages-tuple`](#messages) | Streams LLM tokens and metadata for the graph node where the LLM is invoked (useful for chat apps). | `.stream()` / `.astream()` with [`stream_mode="messages"`](/oss/use-streaming#messages) | +| [`debug`](#debug) | Streams as much information as possible throughout the execution of the graph. | `.stream()` / `.astream()` with [`stream_mode="debug"`](/oss/use-streaming#stream-graph-state) | +| [`custom`](#stream-custom-data) | Streams custom data from inside your graph | `.stream()` / `.astream()` with [`stream_mode="custom"`](/oss/use-streaming#stream-custom-data) | | [`events`](#stream-events) | Stream all events (including the state of the graph); mainly useful when migrating large LCEL apps. | `.astream_events()` | ### Stream multiple modes @@ -310,7 +310,7 @@ Use the stream modes `updates` and `values` to stream the state of the graph as **Stateful runs** - Examples below assume that you want to **persist the outputs** of a streaming run in the [checkpointer](https://langchain-ai.github.io/langgraph/concepts/persistence/) DB and have created a thread. To create a thread: + Examples below assume that you want to **persist the outputs** of a streaming run in the [checkpointer](/oss/persistence) DB and have created a thread. To create a thread: @@ -446,7 +446,7 @@ Use this to stream the **full state** of the graph after each step. ## Subgraphs -To include outputs from [subgraphs](https://langchain-ai.github.io/langgraph/concepts/subgraphs) in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. +To include outputs from [subgraphs](/oss/subgraphs) in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. ```python for chunk in client.runs.stream( @@ -744,8 +744,8 @@ The streamed output from [`messages-tuple` mode](#supported-stream-modes) is a t ### Filter LLM tokens -* To filter the streamed tokens by LLM invocation, you can [associate `tags` with LLM invocations](https://langchain-ai.github.io/langgraph/how-tos/streaming#filter-by-llm-invocation). -* To stream tokens only from specific nodes, use `stream_mode="messages"` and [filter the outputs by the `langgraph_node` field](https://langchain-ai.github.io/langgraph/how-tos/streaming#filter-by-node) in the streamed metadata. +* To filter the streamed tokens by LLM invocation, you can [associate `tags` with LLM invocations](/oss/use-streaming#filter-by-llm-invocation). +* To stream tokens only from specific nodes, use `stream_mode="messages"` and [filter the outputs by the `langgraph_node` field](/oss/use-streaming#filter-by-node) in the streamed metadata. ## Stream custom data @@ -843,7 +843,7 @@ To stream all events, including the state of the graph: ## Stateless runs -If you don't want to **persist the outputs** of a streaming run in the [checkpointer](https://langchain-ai.github.io/langgraph/concepts/persistence/) DB, you can create a stateless run without creating a thread: +If you don't want to **persist the outputs** of a streaming run in the [checkpointer](/oss/persistence) DB, you can create a stateless run without creating a thread: diff --git a/src/langgraph-platform/threads-studio.mdx b/src/langgraph-platform/threads-studio.mdx index bf67564b..bec5e762 100644 --- a/src/langgraph-platform/threads-studio.mdx +++ b/src/langgraph-platform/threads-studio.mdx @@ -2,7 +2,7 @@ title: Manage threads sidebarTitle: Manage threads --- -Studio allows you to view [threads](https://langchain-ai.github.io/langgraph/concepts/persistence/#threads) from the server and edit their state. +Studio allows you to view [threads](/oss/persistence#threads) from the server and edit their state. ## View threads diff --git a/src/langgraph-platform/use-stream-react.mdx b/src/langgraph-platform/use-stream-react.mdx index 0855a6ef..eeed6241 100644 --- a/src/langgraph-platform/use-stream-react.mdx +++ b/src/langgraph-platform/use-stream-react.mdx @@ -279,7 +279,7 @@ The [`useStream()`](https://langchain-ai.github.io/langgraphjs/reference/modules * Render a confirmation UI before executing a node * Wait for human input, allowing agent to ask the user with clarifying questions -Learn more about interrupts in the [How to handle interrupts](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/) guide. +Learn more about interrupts in the [How to handle interrupts](/oss/add-human-in-the-loop#pause-using-interrupt) guide. ```tsx const thread = useStream<{ messages: Message[] }, { InterruptType: string }>({ @@ -656,7 +656,7 @@ The [`useStream()`](https://langchain-ai.github.io/langgraphjs/reference/modules * `onError`: Called when an error occurs. * `onFinish`: Called when the stream is finished. * `onUpdateEvent`: Called when an update event is received. -* `onCustomEvent`: Called when a custom event is received. See the [streaming](https://langchain-ai.github.io/langgraph/how-tos/streaming/#stream-custom-data) guide to learn how to stream custom events. +* `onCustomEvent`: Called when a custom event is received. See the [streaming](/oss/use-streaming#stream-custom-data) guide to learn how to stream custom events. * `onMetadataEvent`: Called when a metadata event is received, which contains the Run ID and Thread ID. ## Learn More diff --git a/src/langgraph-platform/use-threads.mdx b/src/langgraph-platform/use-threads.mdx index b683dd68..5a5c044d 100644 --- a/src/langgraph-platform/use-threads.mdx +++ b/src/langgraph-platform/use-threads.mdx @@ -2,7 +2,7 @@ title: Use threads sidebarTitle: Use threads --- -In this guide, we will show how to create, view, and inspect [threads](https://langchain-ai.github.io/langgraph/concepts/persistence/#threads). +In this guide, we will show how to create, view, and inspect [threads](/oss/persistence#threads). ## Create a thread diff --git a/src/oss/1-build-basic-chatbot.mdx b/src/oss/1-build-basic-chatbot.mdx new file mode 100644 index 00000000..55a92b18 --- /dev/null +++ b/src/oss/1-build-basic-chatbot.mdx @@ -0,0 +1,455 @@ +--- +title: 1. Build a basic chatbot +--- + +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; + +In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let's dive in! 🌟 + +## Prerequisites + +Before you start this tutorial, ensure you have access to a LLM that supports +tool-calling features, such as [OpenAI](https://platform.openai.com/api-keys), +[Anthropic](https://console.anthropic.com/settings/keys), or +[Google Gemini](https://ai.google.dev/gemini-api/docs/api-key). + +## 1. Install packages + +Install the required packages: + +:::python +```bash +pip install -U langgraph langsmith +``` +::: + +:::js + + + ```bash + npm install @langchain/langgraph @langchain/core zod + ``` + + + ```bash + yarn add @langchain/langgraph @langchain/core zod + ``` + + + ```bash + pnpm add @langchain/langgraph @langchain/core zod + ``` + + + ```bash + bun add @langchain/langgraph @langchain/core zod + ``` + + +::: + + + 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. For more information on how to get started, see [LangSmith docs](https://docs.smith.langchain.com). + + +## 2. Create a `StateGraph` + +Now you can create a basic chatbot using LangGraph. This chatbot will respond directly to user messages. + +Start by creating a `StateGraph`. A `StateGraph` object defines the structure of our chatbot as a "state machine". We'll add `nodes` to represent the llm and functions our chatbot can call and `edges` to specify how the bot should transition between these functions. + +:::python +```python +from typing import Annotated + +from typing_extensions import TypedDict + +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages + + +class State(TypedDict): + # Messages have the type "list". The `add_messages` function + # in the annotation defines how this state key should be updated + # (in this case, it appends messages to the list, rather than overwriting them) + messages: Annotated[list, add_messages] + + +graph_builder = StateGraph(State) +``` +::: + +:::js +```typescript +import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State).compile(); +``` +::: + +Our graph can now handle two key tasks: + +1. Each `node` can receive the current `State` as input and output an update to the state. +2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt reducer function. + + + **Concept** + When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](/oss/graph-api#reducers) that handle state updates. In our example, `State` is a schema with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. + + To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages). + + +## 3. Add a node + +Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular functions. + +Let's first select a chat model: + +:::python + + + +::: + +:::js +```typescript +import { ChatOpenAI } from "@langchain/openai"; +// or import { ChatAnthropic } from "@langchain/anthropic"; + +const llm = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, +}); +``` +::: + +We can now incorporate the chat model into a simple node: + +:::python +```python + +def chatbot(state: State): + return {"messages": [llm.invoke(state["messages"])]} + + +# The first argument is the unique node name +# The second argument is the function or object that will be called whenever +# the node is used. +graph_builder.add_node("chatbot", chatbot) +``` +::: + +:::js +```typescript hl_lines="7-9" +import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state: z.infer) => { + return { messages: [await llm.invoke(state.messages)] }; + }) + .compile(); +``` +::: + +**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key "messages". This is the basic pattern for all LangGraph node functions. + +:::python +The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state. +::: + +:::js +The `addMessages` function used within `MessagesZodState` will append the LLM's response messages to whatever messages are already in the state. +::: + +## 4. Add an `entry` point + +Add an `entry` point to tell the graph **where to start its work** each time it is run: + +:::python +```python +graph_builder.add_edge(START, "chatbot") +``` +::: + +:::js +```typescript hl_lines="10" +import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state: z.infer) => { + return { messages: [await llm.invoke(state.messages)] }; + }) + .addEdge(START, "chatbot") + .compile(); +``` +::: + +## 5. Add an `exit` point + +Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity. + +:::python +```python +graph_builder.add_edge("chatbot", END) +``` +::: + +:::js +```typescript hl_lines="11" +import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state: z.infer) => { + return { messages: [await llm.invoke(state.messages)] }; + }) + .addEdge(START, "chatbot") + .addEdge("chatbot", END) + .compile(); +``` +::: + +This tells the graph to terminate after running the chatbot node. + +## 6. Compile the graph + +Before running the graph, we'll need to compile it. We can do so by calling `compile()` +on the graph builder. This creates a `CompiledGraph` we can invoke on our state. + +:::python +```python +graph = graph_builder.compile() +``` +::: + +:::js +```typescript hl_lines="12" +import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state: z.infer) => { + return { messages: [await llm.invoke(state.messages)] }; + }) + .addEdge(START, "chatbot") + .addEdge("chatbot", END) + .compile(); +``` +::: + + +## 7. Visualize the graph + +:::python +You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies. + +```python +from IPython.display import Image, display + +try: + display(Image(graph.get_graph().draw_mermaid_png())) +except Exception: + # This requires some extra dependencies and is optional + pass +``` +::: + +:::js +You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method. + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("basic-chatbot.png", imageBuffer); +``` +::: + +![basic chatbot diagram](/oss/images/basic-chatbot.png) + +## 8. Run the chatbot + +Now run the chatbot! + + + You can exit the chat loop at any time by typing `quit`, `exit`, or `q`. + + +:::python +```python +def stream_graph_updates(user_input: str): + for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}): + for value in event.values(): + print("Assistant:", value["messages"][-1].content) + + +while True: + try: + user_input = input("User: ") + if user_input.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + break + stream_graph_updates(user_input) + except: + # fallback if input() is not available + user_input = "What do you know about LangGraph?" + print("User: " + user_input) + stream_graph_updates(user_input) + break +``` +::: + +:::js +```typescript +import { HumanMessage } from "@langchain/core/messages"; + +async function streamGraphUpdates(userInput: string) { + const stream = await graph.stream({ + messages: [new HumanMessage(userInput)], + }); + +import * as readline from "node:readline/promises"; +import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; + +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state: z.infer) => { + return { messages: [await llm.invoke(state.messages)] }; + }) + .addEdge(START, "chatbot") + .addEdge("chatbot", END) + .compile(); + +async function generateText(content: string) { + const stream = await graph.stream( + { messages: [{ type: "human", content }] }, + { streamMode: "values" } + ); + + for await (const event of stream) { + for (const value of Object.values(event)) { + console.log( + "Assistant:", + value.messages[value.messages.length - 1].content + ); + const lastMessage = event.messages.at(-1); + if (lastMessage?.getType() === "ai") { + console.log(`Assistant: ${lastMessage.text}`); + } + } +} + +const prompt = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +while (true) { + const human = await prompt.question("User: "); + if (["quit", "exit", "q"].includes(human.trim())) break; + await generateText(human || "What do you know about LangGraph?"); +} + +prompt.close(); +``` +::: + +``` +Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions. +``` + +:::python +``` +Goodbye! +``` +::: + +**Congratulations!** You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a [LangSmith Trace](https://smith.langchain.com/public/7527e308-9502-4894-b347-f34385740d5a/r) for the call above. + +:::python +Below is the full code for this tutorial: + +```python +from typing import Annotated + +from langchain.chat_models import init_chat_model +from typing_extensions import TypedDict + +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages + + +class State(TypedDict): + messages: Annotated[list, add_messages] + + +graph_builder = StateGraph(State) + + +llm = init_chat_model("anthropic:claude-3-5-sonnet-latest") + + +def chatbot(state: State): + return {"messages": [llm.invoke(state["messages"])]} + + +# The first argument is the unique node name +# The second argument is the function or object that will be called whenever +# the node is used. +graph_builder.add_node("chatbot", chatbot) +graph_builder.add_edge(START, "chatbot") +graph_builder.add_edge("chatbot", END) +graph = graph_builder.compile() +``` +::: + +:::js +```typescript +import { StateGraph, START, END, MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; +import { ChatOpenAI } from "@langchain/openai"; + +const llm = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, +}); + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const graph = new StateGraph(State); + // The first argument is the unique node name + // The second argument is the function or object that will be called whenever + // the node is used. + .addNode("chatbot", async (state) => { + return { messages: [await llm.invoke(state.messages)] }; + }); + .addEdge(START, "chatbot"); + .addEdge("chatbot", END) + .compile(); +``` +::: + +## Next steps + +You may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll [add a web search tool](/oss/2-add-tools) to expand the bot's knowledge and make it more capable. diff --git a/src/oss/2-add-tools.mdx b/src/oss/2-add-tools.mdx new file mode 100644 index 00000000..da403ce6 --- /dev/null +++ b/src/oss/2-add-tools.mdx @@ -0,0 +1,677 @@ +--- +title: 2. Add tools +--- + +import chatTabs from '/snippets/chat-model-tabs.mdx'; + +To handle queries that your chatbot can't answer "from memory", integrate a web search tool. The chatbot can use this tool to find relevant information and provide better responses. + + + This tutorial builds on [Build a basic chatbot](/oss/1-build-basic-chatbot). + + +## Prerequisites + +Before you start this tutorial, ensure you have the following: + +:::python +* An API key for the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/). +::: + +:::js +* An API key for the [Tavily Search Engine](https://js.langchain.com/docs/integrations/tools/tavily_search/). +::: + +## 1. Install the search engine + +:::python +Install the requirements to use the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/): + +```bash +pip install -U langchain-tavily +``` +::: + +:::js +Install the requirements to use the [Tavily Search Engine](https://docs.tavily.com/): + + + + ```bash + npm install @langchain/tavily + ``` + + + ```bash + yarn add @langchain/tavily + ``` + + + ```bash + pnpm add @langchain/tavily + ``` + + + ```bash + bun add @langchain/tavily + ``` + + +::: + +## 2. Configure your environment + +Configure your environment with your search engine API key: + +:::python +```python +import os + +os.environ["TAVILY_API_KEY"] = "tvly-..." +``` +::: + +:::js +```typescript +process.env.TAVILY_API_KEY = "tvly-..."; +``` +::: + +## 3. Define the tool + +Define the web search tool: + +:::python +```python +from langchain_tavily import TavilySearch + +tool = TavilySearch(max_results=2) +tools = [tool] +tool.invoke("What's a 'node' in LangGraph?") +``` +::: + +:::js +```typescript +import { TavilySearch } from "@langchain/tavily"; + +const tool = new TavilySearch({ maxResults: 2 }); +const tools = [tool]; + +await tool.invoke({ query: "What's a 'node' in LangGraph?" }); +``` +::: + +The results are page summaries our chat bot can use to answer questions: + +:::python +``` +{'query': "What's a 'node' in LangGraph?", +'follow_up_questions': None, +'answer': None, +'images': [], +'results': [{'title': "Introduction to LangGraph: A Beginner's Guide - Medium", +'url': 'https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141', +'content': 'Stateful Graph: LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. We define nodes for classifying the input, handling greetings, and handling search queries. def classify_input_node(state): LangGraph is a versatile tool for building complex, stateful applications with LLMs. By understanding its core concepts and working through simple examples, beginners can start to leverage its power for their projects. Remember to pay attention to state management, conditional edges, and ensuring there are no dead-end nodes in your graph.', +'score': 0.7065353, +'raw_content': None}, +{'title': 'LangGraph Tutorial: What Is LangGraph and How to Use It?', +'url': 'https://www.datacamp.com/tutorial/langgraph-tutorial', +'content': 'LangGraph is a library within the LangChain ecosystem that provides a framework for defining, coordinating, and executing multiple LLM agents (or chains) in a structured and efficient manner. By managing the flow of data and the sequence of operations, LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination. Whether you need a chatbot that can handle various types of user requests or a multi-agent system that performs complex tasks, LangGraph provides the tools to build exactly what you need. LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.', +'score': 0.5008063, +'raw_content': None}], +'response_time': 1.38} +``` +::: + +:::js +```json +{ + "query": "What's a 'node' in LangGraph?", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "url": "https://blog.langchain.dev/langgraph/", + "title": "LangGraph - LangChain Blog", + "content": "TL;DR: LangGraph is module built on top of LangChain to better enable creation of cyclical graphs, often needed for agent runtimes. This state is updated by nodes in the graph, which return operations to attributes of this state (in the form of a key-value store). After adding nodes, you can then add edges to create the graph. An example of this may be in the basic agent runtime, where we always want the model to be called after we call a tool. The state of this graph by default contains concepts that should be familiar to you if you've used LangChain agents: `input`, `chat_history`, `intermediate_steps` (and `agent_outcome` to represent the most recent agent outcome)", + "score": 0.7407191, + "raw_content": null + }, + { + "url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141", + "title": "Introduction to LangGraph: A Beginner's Guide - Medium", + "content": "* **Stateful Graph:** LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. Image 10: Introduction to AI Agent with LangChain and LangGraph: A Beginner’s Guide Image 18: How to build LLM Agent with LangGraph — StateGraph and Reducer Image 20: Simplest Graphs using LangGraph Framework Image 24: Building a ReAct Agent with Langgraph: A Step-by-Step Guide Image 28: Building an Agentic RAG with LangGraph: A Step-by-Step Guide", + "score": 0.65279555, + "raw_content": null + } + ], + "response_time": 1.34 +} +``` +::: + +## 4. Define the graph + +:::python +For the `StateGraph` you created in the [first tutorial](/oss/1-build-basic-chatbot), add `bind_tools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine. +::: + +:::js +For the `StateGraph` you created in the [first tutorial](/oss/1-build-basic-chatbot), add `bindTools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine. +::: + +Let's first select our LLM: + +:::python + + + +::: + +:::js +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; + +const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); +``` +::: + +We can now incorporate it into a `StateGraph`: + +:::python +```python +from typing import Annotated + +from typing_extensions import TypedDict + +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages + +class State(TypedDict): + messages: Annotated[list, add_messages] + +graph_builder = StateGraph(State) + +# Modification: tell the LLM which tools it can call +# highlight-next-line +llm_with_tools = llm.bind_tools(tools) + +def chatbot(state: State): + return {"messages": [llm_with_tools.invoke(state["messages"])]} + +graph_builder.add_node("chatbot", chatbot) +``` +::: + +:::js +```typescript hl_lines="7-8" +import { StateGraph, MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const chatbot = async (state: z.infer) => { + // Modification: tell the LLM which tools it can call + const llmWithTools = llm.bindTools(tools); + + return { messages: [await llmWithTools.invoke(state.messages)] }; +}; +``` +::: + +## 5. Create a function to run the tools + +:::python +Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers. + +```python +import json + +from langchain_core.messages import ToolMessage + + +class BasicToolNode: + """A node that runs the tools requested in the last AIMessage.""" + + def __init__(self, tools: list) -> None: + self.tools_by_name = {tool.name: tool for tool in tools} + + def __call__(self, inputs: dict): + if messages := inputs.get("messages", []): + message = messages[-1] + else: + raise ValueError("No message found in input") + outputs = [] + for tool_call in message.tool_calls: + tool_result = self.tools_by_name[tool_call["name"]].invoke( + tool_call["args"] + ) + outputs.append( + ToolMessage( + content=json.dumps(tool_result), + name=tool_call["name"], + tool_call_id=tool_call["id"], + ) + ) + return {"messages": outputs} + + +tool_node = BasicToolNode(tools=[tool]) +graph_builder.add_node("tools", tool_node) +``` + + + If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode). + +::: + +:::js +Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `"tools"` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's tool calling support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers. + +```typescript +import type { StructuredToolInterface } from "@langchain/core/tools"; +import { isAIMessage, ToolMessage } from "@langchain/core/messages"; + +function createToolNode(tools: StructuredToolInterface[]) { + const toolByName: Record = {}; + for (const tool of tools) { + toolByName[tool.name] = tool; + } + + return async (inputs: z.infer) => { + const { messages } = inputs; + if (!messages || messages.length === 0) { + throw new Error("No message found in input"); + } + + const message = messages.at(-1); + if (!message || !isAIMessage(message) || !message.tool_calls) { + throw new Error("Last message is not an AI message with tool calls"); + } + + const outputs: ToolMessage[] = []; + for (const toolCall of message.tool_calls) { + if (!toolCall.id) throw new Error("Tool call ID is required"); + + const tool = toolByName[toolCall.name]; + if (!tool) throw new Error(`Tool ${toolCall.name} not found`); + + const result = await tool.invoke(toolCall.args); + + outputs.push( + new ToolMessage({ + content: JSON.stringify(result), + name: toolCall.name, + tool_call_id: toolCall.id, + }) + ); + } + + return { messages: outputs }; + }; +} +``` + + + If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html). + +::: + +## 6. Define the `conditional_edges` + +With the tool node added, now you can define the `conditional_edges`. + +**Edges** route the control flow from one node to the next. **Conditional edges** start from a single node and usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next. + +:::python +Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. +::: + +:::js +Next, define a router function called `routeTools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `addConditionalEdges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. +::: + +The condition will route to `tools` if tool calls are present and `END` if not. Because the condition can return `END`, you do not need to explicitly set a `finish_point` this time. + +:::python +```python +def route_tools( + state: State, +): + """ + Use in the conditional_edge to route to the ToolNode if the last message + has tool calls. Otherwise, route to the end. + """ + if isinstance(state, list): + ai_message = state[-1] + elif messages := state.get("messages", []): + ai_message = messages[-1] + else: + raise ValueError(f"No messages found in input state to tool_edge: {state}") + if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0: + return "tools" + return END + + +# The `tools_condition` function returns "tools" if the chatbot asks to use a tool, and "END" if +# it is fine directly responding. This conditional routing defines the main agent loop. +graph_builder.add_conditional_edges( + "chatbot", + route_tools, + # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node + # It defaults to the identity function, but if you + # want to use a node named something else apart from "tools", + # You can update the value of the dictionary to something else + # e.g., "tools": "my_tools" + {"tools": "tools", END: END}, +) +# Any time a tool is called, we return to the chatbot to decide the next step +graph_builder.add_edge("tools", "chatbot") +graph_builder.add_edge(START, "chatbot") +graph = graph_builder.compile() +``` + + + You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise. + +::: + +:::js +```typescript +import { END, START } from "@langchain/langgraph"; + +const routeTools = (state: z.infer) => { + /** + * Use as conditional edge to route to the ToolNode if the last message + * has tool calls. + */ + const lastMessage = state.messages.at(-1); + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { + return "tools"; + } + + /** Otherwise, route to the end. */ + return END; +}; + +const graph = new StateGraph(State) + .addNode("chatbot", chatbot) + + // The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if + // it is fine directly responding. This conditional routing defines the main agent loop. + .addNode("tools", createToolNode(tools)) + + // Start the graph with the chatbot + .addEdge(START, "chatbot") + + // The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if + // it is fine directly responding. + .addConditionalEdges("chatbot", routeTools, ["tools", END]) + + // Any time a tool is called, we need to return to the chatbot + .addEdge("tools", "chatbot") + .compile(); +``` + + + You can replace this with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) to be more concise. + +::: + + +## 7. Visualize the graph + +:::python +You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies. + +```python +from IPython.display import Image, display + +try: + display(Image(graph.get_graph().draw_mermaid_png())) +except Exception: + # This requires some extra dependencies and is optional + pass +``` +::: + +:::js +You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method. + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("chatbot-with-tools.png", imageBuffer); +``` +::: + +![chatbot-with-tools-diagram](/oss/images/chatbot-with-tools.png) + +## 8. Ask the bot questions + +Now you can ask the chatbot questions outside its training data: + +:::python +```python +def stream_graph_updates(user_input: str): + for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}): + for value in event.values(): + print("Assistant:", value["messages"][-1].content) + +while True: + try: + user_input = input("User: ") + if user_input.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + break + + stream_graph_updates(user_input) + except: + # fallback if input() is not available + user_input = "What do you know about LangGraph?" + print("User: " + user_input) + stream_graph_updates(user_input) + break +``` + +``` +Assistant: [{'text': "To provide you with accurate and up-to-date information about LangGraph, I'll need to search for the latest details. Let me do that for you.", 'type': 'text'}, {'id': 'toolu_01Q588CszHaSvvP2MxRq9zRD', 'input': {'query': 'LangGraph AI tool information'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] +Assistant: [{"url": "https://www.langchain.com/langgraph", "content": "LangGraph sets the foundation for how we can build and scale AI workloads \u2014 from conversational agents, complex task automation, to custom LLM-backed experiences that 'just work'. The next chapter in building complex production-ready features with LLMs is agentic, and with LangGraph and LangSmith, LangChain delivers an out-of-the-box solution ..."}, {"url": "https://github.com/langchain-ai/langgraph", "content": "Overview. LangGraph is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures ..."}] +Assistant: Based on the search results, I can provide you with information about LangGraph: + +1. Purpose: + LangGraph is a library designed for building stateful, multi-actor applications with Large Language Models (LLMs). It's particularly useful for creating agent and multi-agent workflows. + +2. Developer: + LangGraph is developed by LangChain, a company known for its tools and frameworks in the AI and LLM space. + +3. Key Features: + - Cycles: LangGraph allows the definition of flows that involve cycles, which is essential for most agentic architectures. + - Controllability: It offers enhanced control over the application flow. + - Persistence: The library provides ways to maintain state and persistence in LLM-based applications. + +4. Use Cases: + LangGraph can be used for various applications, including: + - Conversational agents + - Complex task automation + - Custom LLM-backed experiences + +5. Integration: + LangGraph works in conjunction with LangSmith, another tool by LangChain, to provide an out-of-the-box solution for building complex, production-ready features with LLMs. + +6. Significance: +... + LangGraph is noted to offer unique benefits compared to other LLM frameworks, particularly in its ability to handle cycles, provide controllability, and maintain persistence. + +LangGraph appears to be a significant tool in the evolving landscape of LLM-based application development, offering developers new ways to create more complex, stateful, and interactive AI systems. +Goodbye! +``` +::: + +:::js +```typescript +import readline from "node:readline/promises"; + +const prompt = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function generateText(content: string) { + const stream = await graph.stream( + { messages: [{ type: "human", content }] }, + { streamMode: "values" } + ); + + for await (const event of stream) { + const lastMessage = event.messages.at(-1); + + if (lastMessage?.getType() === "ai" || lastMessage?.getType() === "tool") { + console.log(`Assistant: ${lastMessage?.text}`); + } + } +} + +while (true) { + const human = await prompt.question("User: "); + if (["quit", "exit", "q"].includes(human.trim())) break; + await generateText(human || "What do you know about LangGraph?"); +} + +prompt.close(); +``` + +``` +User: What do you know about LangGraph? +Assistant: I'll search for the latest information about LangGraph for you. +Assistant: [{"title":"Introduction to LangGraph: A Beginner's Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","content":"..."}] +Assistant: Based on the search results, I can provide you with information about LangGraph: + +LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). Here are the key aspects: + +**Core Purpose:** +- LangGraph is specifically designed for creating agent and multi-agent workflows +- It provides a framework for defining, coordinating, and executing multiple LLM agents in a structured manner + +**Key Features:** +1. **Stateful Graph Architecture**: LangGraph revolves around a stateful graph where each node represents a step in computation, and the graph maintains state that is passed around and updated as the computation progresses + +2. **Conditional Edges**: It supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph + +3. **Cycles**: Unlike other LLM frameworks, LangGraph allows you to define flows that involve cycles, which is essential for most agentic architectures + +4. **Controllability**: It offers enhanced control over the application flow + +5. **Persistence**: The library provides ways to maintain state and persistence in LLM-based applications + +**Use Cases:** +- Conversational agents +- Complex task automation +- Custom LLM-backed experiences +- Multi-agent systems that perform complex tasks + +**Benefits:** +LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination, making it easier to build complex, production-ready features with LLMs. + +This makes LangGraph a significant tool in the evolving landscape of LLM-based application development. +``` +::: + +## 9. Use prebuilts + +For ease of use, adjust your code to replace the following with LangGraph prebuilt components. These have built in functionality like parallel API execution. + +:::python +* `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode) +* `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) + + + + +```python hl_lines="25 30" +from typing import Annotated + +from langchain_tavily import TavilySearch +from langchain_core.messages import BaseMessage +from typing_extensions import TypedDict + +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode, tools_condition + +class State(TypedDict): + messages: Annotated[list, add_messages] + +graph_builder = StateGraph(State) + +tool = TavilySearch(max_results=2) +tools = [tool] +llm_with_tools = llm.bind_tools(tools) + +def chatbot(state: State): + return {"messages": [llm_with_tools.invoke(state["messages"])]} + +graph_builder.add_node("chatbot", chatbot) + +tool_node = ToolNode(tools=[tool]) +graph_builder.add_node("tools", tool_node) + +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +# Any time a tool is called, we return to the chatbot to decide the next step +graph_builder.add_edge("tools", "chatbot") +graph_builder.add_edge(START, "chatbot") +graph = graph_builder.compile() +``` +::: + +:::js +* `createToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html) +* `routeTools` is replaced with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) + +```typescript +import { TavilySearch } from "@langchain/tavily"; +import { ChatOpenAI } from "@langchain/openai"; +import { StateGraph, START, MessagesZodState, END } from "@langchain/langgraph"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const tools = [new TavilySearch({ maxResults: 2 })]; + +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state) => ({ + messages: [await llm.invoke(state.messages)], + })) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile(); +``` +::: + +**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. + +:::python +To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r). +::: + +## Next steps + +The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](/oss/3-add-memory) to address this. diff --git a/src/oss/3-add-memory.mdx b/src/oss/3-add-memory.mdx new file mode 100644 index 00000000..68104733 --- /dev/null +++ b/src/oss/3-add-memory.mdx @@ -0,0 +1,410 @@ +--- +title: 3. Add memory +--- + +import chatTabs from '/snippets/chat-model-tabs.mdx'; + +The chatbot can now [use tools](/oss/2-add-tools) to answer user questions, but it does not remember the context of previous interactions. This limits its ability to have coherent, multi-turn conversations. + +LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off. + +We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But first, let's add checkpointing to enable multi-turn conversations. + + + This tutorial builds on [Add tools](/oss/2-add-tools). + + +## 1. Create a `MemorySaver` checkpointer + +Create a `MemorySaver` checkpointer: + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver + +memory = InMemorySaver() +``` +::: + +:::js +```typescript +import { MemorySaver } from "@langchain/langgraph"; + +const memory = new MemorySaver(); +``` +::: + +This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database. + +## 2. Compile the graph + +Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node: + +:::python +```python +graph = graph_builder.compile(checkpointer=memory) +``` +::: + +:::js +```typescript hl_lines="7" +const graph = new StateGraph(State) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); +``` +::: + +## 3. Interact with your chatbot + +Now you can interact with your bot! + +1. Pick a thread to use as the key for this conversation. + :::python + ```python + config = {"configurable": {"thread_id": "1"}} + ``` + ::: + :::js + ```typescript + const config = { configurable: { thread_id: "1" } }; + ``` + ::: +2. Call your chatbot: + :::python + ```python + user_input = "Hi there! My name is Will." + + # The config is the **second positional argument** to stream() or invoke()! + events = graph.stream( + {"messages": [{"role": "user", "content": user_input}]}, + config, + stream_mode="values", + ) + for event in events: + event["messages"][-1].pretty_print() + ``` + + ``` + ================================ Human Message ================================= + + Hi there! My name is Will. + ================================== Ai Message ================================== + + Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss? + ``` + + + The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{'messages': []}`). + + ::: + :::js + ```typescript + const userInput = "Hi there! My name is Will."; + + const events = await graph.stream( + { messages: [{ type: "human", content: userInput }] }, + { configurable: { thread_id: "1" }, streamMode: "values" } + ); + + for await (const event of events) { + const lastMessage = event.messages.at(-1); + console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`); + } + ``` + + ``` + human: Hi there! My name is Will. + ai: Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss? + ``` + + + + + + The config was provided as the **second parameter** when calling our graph. It importantly is _not_ nested within the graph inputs (`{"messages": []}`). + + ::: + +## 4. Ask a follow up question + +Ask a follow up question: + +:::python +```python +user_input = "Remember my name?" + +# The config is the **second positional argument** to stream() or invoke()! +events = graph.stream( + {"messages": [{"role": "user", "content": user_input}]}, + config, + stream_mode="values", +) +for event in events: + event["messages"][-1].pretty_print() +``` + +``` +================================ Human Message ================================= + +Remember my name? +================================== Ai Message ================================== + +Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks. +``` +::: + +:::js +```typescript +const userInput2 = "Remember my name?"; + +const events2 = await graph.stream( + { messages: [{ type: "human", content: userInput2 }] }, + { configurable: { thread_id: "1" }, streamMode: "values" } +); + +for await (const event of events2) { + const lastMessage = event.messages.at(-1); + console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`); +} +``` + +``` +human: Remember my name? +ai: Yes, your name is Will. How can I help you today? +``` +::: + +**Notice** that we aren't using an external list for memory: it's all handled by the checkpointer! You can inspect the full execution in this [LangSmith trace](https://smith.langchain.com/public/29ba22b5-6d40-4fbe-8d27-b369e3329c84/r) to see what's going on. + +Don't believe me? Try this using a different config. + +:::python +```python +# The only difference is we change the `thread_id` here to "2" instead of "1" +events = graph.stream( + {"messages": [{"role": "user", "content": user_input}]}, + # highlight-next-line + {"configurable": {"thread_id": "2"}}, + stream_mode="values", +) +for event in events: + event["messages"][-1].pretty_print() +``` + +``` +================================ Human Message ================================= + +Remember my name? +================================== Ai Message ================================== + +I apologize, but I don't have any previous context or memory of your name. As an AI assistant, I don't retain information from past conversations. Each interaction starts fresh. Could you please tell me your name so I can address you properly in this conversation? +``` +::: + +:::js +```typescript hl_lines="3-4" +const events3 = await graph.stream( + { messages: [{ type: "human", content: userInput2 }] }, + // The only difference is we change the `thread_id` here to "2" instead of "1" + { configurable: { thread_id: "2" }, streamMode: "values" } +); + +for await (const event of events3) { + const lastMessage = event.messages.at(-1); + console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`); +} +``` + +``` +human: Remember my name? +ai: I don't have the ability to remember personal information about users between interactions. However, I'm here to help you with any questions or topics you want to discuss! +``` +::: + +**Notice** that the **only** change we've made is to modify the `thread_id` in the config. See this call's [LangSmith trace](https://smith.langchain.com/public/51a62351-2f0a-4058-91cc-9996c5561428/r) for comparison. + +## 5. Inspect the state + +:::python +By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `get_state(config)`. + +```python +snapshot = graph.get_state(config) +snapshot +``` + +``` +StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Will.', additional_kwargs={}, response_metadata={}, id='8c1ca919-c553-4ebf-95d4-b59a2d61e078'), AIMessage(content="Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?", additional_kwargs={}, response_metadata={'id': 'msg_01WTQebPhNwmMrmmWojJ9KXJ', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 405, 'output_tokens': 32}}, id='run-58587b77-8c82-41e6-8a90-d62c444a261d-0', usage_metadata={'input_tokens': 405, 'output_tokens': 32, 'total_tokens': 437}), HumanMessage(content='Remember my name?', additional_kwargs={}, response_metadata={}, id='daba7df6-ad75-4d6b-8057-745881cea1ca'), AIMessage(content="Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.", additional_kwargs={}, response_metadata={'id': 'msg_01E41KitY74HpENRgXx94vag', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 444, 'output_tokens': 58}}, id='run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0', usage_metadata={'input_tokens': 444, 'output_tokens': 58, 'total_tokens': 502})]}, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef7d06e-93e0-6acc-8004-f2ac846575d2'}}, metadata={'source': 'loop', 'writes': {'chatbot': {'messages': [AIMessage(content="Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.", additional_kwargs={}, response_metadata={'id': 'msg_01E41KitY74HpENRgXx94vag', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 444, 'output_tokens': 58}}, id='run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0', usage_metadata={'input_tokens': 444, 'output_tokens': 58, 'total_tokens': 502})]}}, 'step': 4, 'parents': {}}, created_at='2024-09-27T19:30:10.820758+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef7d06e-859f-6206-8003-e1bd3c264b8f'}}, tasks=()) +``` + +``` +snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next) +``` +::: + +:::js +By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `getState(config)`. + +```typescript +await graph.getState({ configurable: { thread_id: "1" } }); +``` + +```typescript +{ + values: { + messages: [ + HumanMessage { + "id": "32fabcef-b3b8-481f-8bcb-fd83399a5f8d", + "content": "Hi there! My name is Will.", + "additional_kwargs": {}, + "response_metadata": {} + }, + AIMessage { + "id": "chatcmpl-BrPbTsCJbVqBvXWySlYoTJvM75Kv8", + "content": "Hello Will! How can I assist you today?", + "additional_kwargs": {}, + "response_metadata": {}, + "tool_calls": [], + "invalid_tool_calls": [] + }, + HumanMessage { + "id": "561c3aad-f8fc-4fac-94a6-54269a220856", + "content": "Remember my name?", + "additional_kwargs": {}, + "response_metadata": {} + }, + AIMessage { + "id": "chatcmpl-BrPbU4BhhsUikGbW37hYuF5vvnnE2", + "content": "Yes, I remember your name, Will! How can I help you today?", + "additional_kwargs": {}, + "response_metadata": {}, + "tool_calls": [], + "invalid_tool_calls": [] + } + ] + }, + next: [], + tasks: [], + metadata: { + source: 'loop', + step: 4, + parents: {}, + thread_id: '1' + }, + config: { + configurable: { + thread_id: '1', + checkpoint_id: '1f05cccc-9bb6-6270-8004-1d2108bcec77', + checkpoint_ns: '' + } + }, + createdAt: '2025-07-09T13:58:27.607Z', + parentConfig: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1f05cccc-78fa-68d0-8003-ffb01a76b599' + } + } +} +``` + +```typescript +import * as assert from "node:assert"; + +// Since the graph ended this turn, `next` is empty. +// If you fetch a state from within a graph invocation, next tells which node will execute next) +assert.deepEqual(snapshot.next, []); +``` +::: + +The snapshot above contains the current state values, corresponding config, and the `next` node to process. In our case, the graph has reached an `END` state, so `next` is empty. + +**Congratulations!** Your chatbot can now maintain conversation state across sessions thanks to LangGraph's checkpointing system. This opens up exciting possibilities for more natural, contextual interactions. LangGraph's checkpointing even handles **arbitrarily complex graph states**, which is much more expressive and powerful than simple chat memory. + +Check out the code snippet below to review the graph from this tutorial: + +:::python + + +```python hl_lines="36 37" +from typing import Annotated + +from langchain.chat_models import init_chat_model +from langchain_tavily import TavilySearch +from langchain_core.messages import BaseMessage +from typing_extensions import TypedDict + +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import StateGraph +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode, tools_condition + +class State(TypedDict): + messages: Annotated[list, add_messages] + +graph_builder = StateGraph(State) + +tool = TavilySearch(max_results=2) +tools = [tool] +llm_with_tools = llm.bind_tools(tools) + +def chatbot(state: State): + return {"messages": [llm_with_tools.invoke(state["messages"])]} + +graph_builder.add_node("chatbot", chatbot) + +tool_node = ToolNode(tools=[tool]) +graph_builder.add_node("tools", tool_node) + +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +graph_builder.add_edge("tools", "chatbot") +graph_builder.set_entry_point("chatbot") +memory = InMemorySaver() +graph = graph_builder.compile(checkpointer=memory) +``` +::: + +:::js +```typescript hl_lines="16 26" +import { END, MessagesZodState, START } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { TavilySearch } from "@langchain/tavily"; + +import { MemorySaver } from "@langchain/langgraph"; +import { StateGraph } from "@langchain/langgraph"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; +import { z } from "zod"; + +const State = z.object({ + messages: MessagesZodState.shape.messages, +}); + +const tools = [new TavilySearch({ maxResults: 2 })]; +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools); +const memory = new MemorySaver(); + +async function generateText(content: string) { + +const graph = new StateGraph(State) + .addNode("chatbot", async (state) => ({ + messages: [await llm.invoke(state.messages)], + })) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); +``` +::: + +## Next steps + +In the next tutorial, you will [add human-in-the-loop to the chatbot](/oss/4-human-in-the-loop) to handle situations where it may need guidance or verification before proceeding. diff --git a/src/oss/4-human-in-the-loop.mdx b/src/oss/4-human-in-the-loop.mdx new file mode 100644 index 00000000..bb7a3ce6 --- /dev/null +++ b/src/oss/4-human-in-the-loop.mdx @@ -0,0 +1,683 @@ +--- +title: 4. Add human-in-the-loop controls +--- + +import chatTabs from '/snippets/chat-model-tabs.mdx'; + +Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended. + +LangGraph's [persistence](/oss/persistence) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](/oss/add-human-in-the-loop) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](/oss/graph-api#command). + +:::python +`interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](/oss/add-human-in-the-loop). +::: + +:::js +`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](/oss/add-human-in-the-loop). +`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](/oss/add-human-in-the-loop). +::: + + + This tutorial builds on [Add memory](/oss/3-add-memory). + + +## 1. Add the `human_assistance` tool + +Starting with the existing code from the [Add memory to the chatbot](/oss/3-add-memory) tutorial, add the `human_assistance` tool to the chatbot. This tool uses `interrupt` to receive information from a human. + +Let's first select a chat model: + +:::python + + + +::: + +:::js +```typescript +// Add your API key here +process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY"; +``` +::: + +We can now incorporate it into our `StateGraph` with an additional tool: + +:::python +```python hl_lines="12 19 20 21 22 23" +from typing import Annotated + +from langchain_tavily import TavilySearch +from langchain_core.tools import tool +from typing_extensions import TypedDict + +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode, tools_condition + +from langgraph.types import Command, interrupt + +class State(TypedDict): + messages: Annotated[list, add_messages] + +graph_builder = StateGraph(State) + +@tool +def human_assistance(query: str) -> str: + """Request assistance from a human.""" + human_response = interrupt({"query": query}) + return human_response["data"] + +tool = TavilySearch(max_results=2) +tools = [tool, human_assistance] +llm_with_tools = llm.bind_tools(tools) + +def chatbot(state: State): + message = llm_with_tools.invoke(state["messages"]) + # Because we will be interrupting during tool execution, + # we disable parallel tool calling to avoid repeating any + # tool invocations when we resume. + assert len(message.tool_calls) <= 1 + return {"messages": [message]} + +graph_builder.add_node("chatbot", chatbot) + +tool_node = ToolNode(tools=tools) +graph_builder.add_node("tools", tool_node) + +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +graph_builder.add_edge("tools", "chatbot") +graph_builder.add_edge(START, "chatbot") +``` +::: + +:::js +```typescript hl_lines="1 7-19" +import { interrupt, MessagesZodState } from "@langchain/langgraph"; +import { ChatAnthropic } from "@langchain/anthropic"; +import { TavilySearch } from "@langchain/tavily"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + } +); + +const searchTool = new TavilySearch({ maxResults: 2 }); +const searchTool = new TavilySearch({ maxResults: 2 }); +const tools = [searchTool, humanAssistance]; + +const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", +}).bindTools(tools); +const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", +}).bindTools(tools); + +async function chatbot(state: z.infer) { +async function chatbot(state: z.infer) { + const message = await llmWithTools.invoke(state.messages); + + + // Because we will be interrupting during tool execution, + // we disable parallel tool calling to avoid repeating any + // tool invocations when we resume. + if (message.tool_calls && message.tool_calls.length > 1) { + throw new Error("Multiple tool calls not supported with interrupts"); + } + + return { messages: message }; +} +``` +::: + + + For more information and examples of human-in-the-loop workflows, see [Human-in-the-loop](/oss/human-in-the-loop). + + +## 2. Compile the graph + +We compile the graph with a checkpointer, as before: + +:::python +```python +memory = InMemorySaver() + +graph = graph_builder.compile(checkpointer=memory) +``` +::: + +:::js +```typescript hl_lines="3 11" +import { StateGraph, MemorySaver, START, END } from "@langchain/langgraph"; + +const memory = new MemorySaver(); + +const graph = new StateGraph(MessagesZodState) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); +const graph = new StateGraph(MessagesZodState) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); +``` +::: + + +## 3. Visualize the graph + +Visualizing the graph, you get the same layout as before – just with the added tool! + +:::python +```python +from IPython.display import Image, display + +try: + display(Image(graph.get_graph().draw_mermaid_png())) +except Exception: + # This requires some extra dependencies and is optional + pass +``` +::: + +:::js +```typescript +import * as fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("chatbot-with-tools.png", imageBuffer); +await fs.writeFile("chatbot-with-tools.png", imageBuffer); +``` +::: + +![chatbot-with-tools-diagram](/oss/images/chatbot-with-tools.png) + +## 4. Prompt the chatbot + +Now, prompt the chatbot with a question that will engage the new `human_assistance` tool: + +:::python +```python +user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?" +config = {"configurable": {"thread_id": "1"}} + +events = graph.stream( + {"messages": [{"role": "user", "content": user_input}]}, + config, + stream_mode="values", +) +for event in events: + if "messages" in event: + event["messages"][-1].pretty_print() +``` + +``` +================================ Human Message ================================= + +I need some expert guidance for building an AI agent. Could you request assistance for me? +================================== Ai Message ================================== + +[{'text': "Certainly! I'd be happy to request expert assistance for you regarding building an AI agent. To do this, I'll use the human_assistance function to relay your request. Let me do that for you now.", 'type': 'text'}, {'id': 'toolu_01ABUqneqnuHNuo1vhfDFQCW', 'input': {'query': 'A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?'}, 'name': 'human_assistance', 'type': 'tool_use'}] +Tool Calls: + human_assistance (toolu_01ABUqneqnuHNuo1vhfDFQCW) + Call ID: toolu_01ABUqneqnuHNuo1vhfDFQCW + Args: + query: A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic? +``` +::: + +:::js +```typescript +import { isAIMessage } from "@langchain/core/messages"; + +const userInput = + "I need some expert guidance for building an AI agent. Could you request assistance for me?"; + +const events = await graph.stream( + { messages: [{ role: "user", content: userInput }] }, + { configurable: { thread_id: "1" }, streamMode: "values" } + { configurable: { thread_id: "1" }, streamMode: "values" } +); + +for await (const event of events) { + if ("messages" in event) { + const lastMessage = event.messages.at(-1); + console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`); + + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { + const lastMessage = event.messages.at(-1); + console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`); + + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { + console.log("Tool calls:", lastMessage.tool_calls); + } + } +} +``` + +``` +[human]: I need some expert guidance for building an AI agent. Could you request assistance for me? +[ai]: I'll help you request human assistance for guidance on building an AI agent. +[ai]: I'll help you request human assistance for guidance on building an AI agent. +Tool calls: [ + { + name: 'humanAssistance', + args: { + query: 'I would like expert guidance on building an AI agent. Could you please provide assistance with this topic?' + query: 'I would like expert guidance on building an AI agent. Could you please provide assistance with this topic?' + }, + id: 'toolu_01Bpxc8rFVMhSaRosS6b85Ts', + type: 'tool_call' + id: 'toolu_01Bpxc8rFVMhSaRosS6b85Ts', + type: 'tool_call' + } +] +``` +::: + +The chatbot generated a tool call, but then execution has been interrupted. If you inspect the graph state, you see that it stopped at the tools node: + +:::python +```python +snapshot = graph.get_state(config) +snapshot.next +``` + +``` +('tools',) +``` +::: + +:::js +```typescript +const snapshot = await graph.getState({ configurable: { thread_id: "1" } }); +snapshot.next; +const snapshot = await graph.getState({ configurable: { thread_id: "1" } }); +snapshot.next; +``` + +```json +["tools"] +``` +::: + + + **Additional information** + :::python + Take a closer look at the `human_assistance` tool: + + ```python + @tool + def human_assistance(query: str) -> str: + """Request assistance from a human.""" + human_response = interrupt({"query": query}) + return human_response["data"] + ``` + + Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](/oss/persistence#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the Python kernel is running. + ::: + + :::js + Take a closer look at the `humanAssistance` tool: + + ```typescript hl_lines="3" + const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + }, + ); + ``` + + Take a closer look at the `humanAssistance` tool: + + ```javascript + const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + }, + ); + ``` + + Calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](/oss/persistence#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running. + ::: + + ## 5. Resume execution + + To resume execution, pass a [`Command`](/oss/graph-api#command) object containing data expected by the tool. The format of this data can be customized based on needs. + + :::python + + For this example, use a dict with a key `"data"`: + + ``` + + human_response = ( + "We, the experts are here to help! We'd recommend you check out LangGraph to build your agent." + " It's much more reliable and extensible than simple autonomous agents." + ) + + human_command = Command(resume={"data": human_response}) + + events = graph.stream(human_command, config, stream_mode="values") + for event in events: + if "messages" in event: + event["messages"][-1].pretty_print() + + ``` + ``` + + ================================== Ai Message ================================== + + [{'text': "Certainly! I'd be happy to request expert assistance for you regarding building an AI agent. To do this, I'll use the human_assistance function to relay your request. Let me do that for you now.", 'type': 'text'}, {'id': 'toolu_01ABUqneqnuHNuo1vhfDFQCW', 'input': {'query': 'A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?'}, 'name': 'human_assistance', 'type': 'tool_use'}] + Tool Calls: + human_assistance (toolu_01ABUqneqnuHNuo1vhfDFQCW) + Call ID: toolu_01ABUqneqnuHNuo1vhfDFQCW + Args: + query: A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic? + ================================= Tool Message ================================= + Name: human_assistance + + We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents. + ================================== Ai Message ================================== + + Thank you for your patience. I've received some expert advice regarding your request for guidance on building an AI agent. Here's what the experts have suggested: + + The experts recommend that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents. + + LangGraph is likely a framework or library designed specifically for creating AI agents with advanced capabilities. Here are a few points to consider based on this recommendation: + + 1. Reliability: The experts emphasize that LangGraph is more reliable than simpler autonomous agent approaches. This could mean it has better stability, error handling, or consistent performance. + 2. Extensibility: LangGraph is described as more extensible, which suggests that it probably offers a flexible architecture that allows you to easily add new features or modify existing ones as your agent's requirements evolve. + 3. Advanced capabilities: Given that it's recommended over "simple autonomous agents," LangGraph likely provides more sophisticated tools and techniques for building complex AI agents. + + ... + + 1. Look for tutorials or guides specifically focused on building AI agents with LangGraph. + 2. Check if there are any community forums or discussion groups where you can ask questions and get support from other developers using LangGraph. + + If you'd like more specific information about LangGraph or have any questions about this recommendation, please feel free to ask, and I can request further assistance from the experts. + Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... + + ``` + + ::: + + :::js + For this example, use an object with a key `"data"`: + + ``` + + import { Command } from "@langchain/langgraph"; + + const humanResponse = + "We, the experts are here to help! We'd recommend you check out LangGraph to build your agent." + + " It's much more reliable and extensible than simple autonomous agents."; + (" It's much more reliable and extensible than simple autonomous agents."); + + const humanCommand = new Command({ resume: { data: humanResponse } }); + + const resumeEvents = await graph.stream(humanCommand, { + configurable: { thread_id: "1" }, + streamMode: "values", + }); + const resumeEvents = await graph.stream(humanCommand, { + configurable: { thread_id: "1" }, + streamMode: "values", + }); + + for await (const event of resumeEvents) { + if ("messages" in event) { + const lastMessage = event.messages.at(-1); + console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`); + const lastMessage = event.messages.at(-1); + console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`); + } + } + + ``` + ``` + + [tool]: We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents. + [ai]: Thank you for your patience. I've received some expert advice regarding your request for guidance on building an AI agent. Here's what the experts have suggested: + + The experts recommend that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents. + + LangGraph is likely a framework or library designed specifically for creating AI agents with advanced capabilities. Here are a few points to consider based on this recommendation: + + 1. Reliability: The experts emphasize that LangGraph is more reliable than simpler autonomous agent approaches. This could mean it has better stability, error handling, or consistent performance. + 2. Extensibility: LangGraph is described as more extensible, which suggests that it probably offers a flexible architecture that allows you to easily add new features or modify existing ones as your agent's requirements evolve. + 3. Advanced capabilities: Given that it's recommended over "simple autonomous agents," LangGraph likely provides more sophisticated tools and techniques for building complex AI agents. + + ... + + ``` + + ::: + + The input has been received and processed as a tool message. Review this call's [LangSmith trace](https://smith.langchain.com/public/9f0f87e3-56a7-4dde-9c76-b71675624e91/r) to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that our chatbot can continue where it left off. + + **Congratulations!** You've used an `interrupt` to add human-in-the-loop execution to your chatbot, allowing for human oversight and intervention when needed. This opens up the potential UIs you can create with your AI systems. Since you have already added a **checkpointer**, as long as the underlying persistence layer is running, the graph can be paused **indefinitely** and resumed at any time as if nothing had happened. + + Check out the code snippet below to review the graph from this tutorial: + + :::python + + + + ``` + + from typing import Annotated + + from langchain_tavily import TavilySearch + from langchain_core.tools import tool + from typing_extensions import TypedDict + + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import StateGraph, START, END + from langgraph.graph.message import add_messages + from langgraph.prebuilt import ToolNode, tools_condition + from langgraph.types import Command, interrupt + + class State(TypedDict): + messages: Annotated[list, add_messages] + + graph_builder = StateGraph(State) + + @tool + def human_assistance(query: str) -> str: + """Request assistance from a human.""" + human_response = interrupt({"query": query}) + return human_response["data"] + + tool = TavilySearch(max_results=2) + tools = [tool, human_assistance] + llm_with_tools = llm.bind_tools(tools) + + def chatbot(state: State): + message = llm_with_tools.invoke(state["messages"]) + assert(len(message.tool_calls) <= 1) + return {"messages": [message]} + + graph_builder.add_node("chatbot", chatbot) + + tool_node = ToolNode(tools=tools) + graph_builder.add_node("tools", tool_node) + + graph_builder.add_conditional_edges( + "chatbot", + tools_condition, + ) + graph_builder.add_edge("tools", "chatbot") + graph_builder.add_edge(START, "chatbot") + + memory = InMemorySaver() + graph = graph_builder.compile(checkpointer=memory) + + ``` + + ::: + + :::js + + ``` + + import { + interrupt, + MessagesZodState, + StateGraph, + MemorySaver, + START, + END, + } from "@langchain/langgraph"; + import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; + import { isAIMessage } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { TavilySearch } from "@langchain/tavily"; + import { + interrupt, + MessagesZodState, + StateGraph, + MemorySaver, + START, + END, + } from "@langchain/langgraph"; + import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; + import { isAIMessage } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { TavilySearch } from "@langchain/tavily"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + + const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + } + ); + const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + } + ); + + const searchTool = new TavilySearch({ maxResults: 2 }); + const searchTool = new TavilySearch({ maxResults: 2 }); + const tools = [searchTool, humanAssistance]; + + const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", + }).bindTools(tools); + const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", + }).bindTools(tools); + + const chatbot = async (state: z.infer) => { + const chatbot = async (state: z.infer) => { + const message = await llmWithTools.invoke(state.messages); + + // Because we will be interrupting during tool execution, + // we disable parallel tool calling to avoid repeating any + // tool invocations when we resume. + + // Because we will be interrupting during tool execution, + // we disable parallel tool calling to avoid repeating any + // tool invocations when we resume. + if (message.tool_calls && message.tool_calls.length > 1) { + throw new Error("Multiple tool calls not supported with interrupts"); + } + + return { messages: message }; + + return { messages: message }; + }; + + ```typescript + const memory = new MemorySaver(); + + const graph = new StateGraph(MessagesZodState) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); + + const graph = new StateGraph(MessagesZodState) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); + ``` + ::: + + +## Next steps + +So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](/oss/5-customize-state). diff --git a/src/oss/5-customize-state.mdx b/src/oss/5-customize-state.mdx new file mode 100644 index 00000000..d464d8da --- /dev/null +++ b/src/oss/5-customize-state.mdx @@ -0,0 +1,647 @@ +--- +title: 5. Customize state +--- + +import chatTabs from '/snippets/chat-model-tabs.mdx'; + +In this tutorial, you will add additional fields to the state to define complex behavior without relying on the message list. The chatbot will use its search tool to find specific information and forward them to a human for review. + + + This tutorial builds on [Add human-in-the-loop controls](/oss/4-human-in-the-loop). + + +## 1. Add keys to the state + +Update the chatbot to research the birthday of an entity by adding `name` and `birthday` keys to the state: + +:::python +```python +from typing import Annotated + +from typing_extensions import TypedDict + +from langgraph.graph.message import add_messages + + +class State(TypedDict): + messages: Annotated[list, add_messages] + # highlight-next-line + name: str + # highlight-next-line + birthday: str +``` +::: + +:::js +```typescript +import { MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + messages: MessagesZodState.shape.messages, + // highlight-next-line + name: z.string(), + // highlight-next-line + birthday: z.string(), +}); +``` +::: + +Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer. + +## 2. Update the state inside the tool + +:::python +Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](/oss/graph-api#using-inside-tools) to issue a state update from inside the tool. + +```python +from langchain_core.messages import ToolMessage +from langchain_core.tools import InjectedToolCallId, tool + +from langgraph.types import Command, interrupt + +@tool +# Note that because we are generating a ToolMessage for a state update, we +# generally require the ID of the corresponding tool call. We can use +# LangChain's InjectedToolCallId to signal that this argument should not +# be revealed to the model in the tool's schema. +def human_assistance( + name: str, birthday: str, tool_call_id: Annotated[str, InjectedToolCallId] +) -> str: + """Request assistance from a human.""" + human_response = interrupt( + { + "question": "Is this correct?", + "name": name, + "birthday": birthday, + }, + ) + # If the information is correct, update the state as-is. + if human_response.get("correct", "").lower().startswith("y"): + verified_name = name + verified_birthday = birthday + response = "Correct" + # Otherwise, receive information from the human reviewer. + else: + verified_name = human_response.get("name", name) + verified_birthday = human_response.get("birthday", birthday) + response = f"Made a correction: {human_response}" + + # This time we explicitly update the state with a ToolMessage inside + # the tool. + state_update = { + "name": verified_name, + "birthday": verified_birthday, + "messages": [ToolMessage(response, tool_call_id=tool_call_id)], + } + # We return a Command object in the tool to update our state. + return Command(update=state_update) +``` +::: + +:::js +Now, populate the state keys inside of the `humanAssistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](/oss/graph-api#using-inside-tools) to issue a state update from inside the tool. + +```typescript +import { tool } from "@langchain/core/tools"; +import { ToolMessage } from "@langchain/core/messages"; +import { Command, interrupt } from "@langchain/langgraph"; + +const humanAssistance = tool( + async (input, config) => { + // Note that because we are generating a ToolMessage for a state update, + // we generally require the ID of the corresponding tool call. + // This is available in the tool's config. + const toolCallId = config?.toolCall?.id as string | undefined; + if (!toolCallId) throw new Error("Tool call ID is required"); + + const humanResponse = await interrupt({ + question: "Is this correct?", + name: input.name, + birthday: input.birthday, + }); + + // We explicitly update the state with a ToolMessage inside the tool. + const stateUpdate = (() => { + // If the information is correct, update the state as-is. + if (humanResponse.correct?.toLowerCase().startsWith("y")) { + return { + name: input.name, + birthday: input.birthday, + messages: [ + new ToolMessage({ content: "Correct", tool_call_id: toolCallId }), + ], + }; + } + + // Otherwise, receive information from the human reviewer. + return { + name: humanResponse.name || input.name, + birthday: humanResponse.birthday || input.birthday, + messages: [ + new ToolMessage({ + content: `Made a correction: ${JSON.stringify(humanResponse)}`, + tool_call_id: toolCallId, + }), + ], + }; + })(); + + // We return a Command object in the tool to update our state. + return new Command({ update: stateUpdate }); + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + name: z.string().describe("The name of the entity"), + birthday: z.string().describe("The birthday/release date of the entity"), + }), + } +); +``` +::: + +The rest of the graph stays the same. + +## 3. Prompt the chatbot + +:::python +Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields. + +```python +user_input = ( + "Can you look up when LangGraph was released? " + "When you have the answer, use the human_assistance tool for review." +) +config = {"configurable": {"thread_id": "1"}} + +events = graph.stream( + {"messages": [{"role": "user", "content": user_input}]}, + config, + stream_mode="values", +) +for event in events: + if "messages" in event: + event["messages"][-1].pretty_print() +``` +::: + +:::js +Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields. + +```typescript +import { isAIMessage } from "@langchain/core/messages"; + +const userInput = + "Can you look up when LangGraph was released? " + + "When you have the answer, use the humanAssistance tool for review."; + +const events = await graph.stream( + { messages: [{ role: "user", content: userInput }] }, + { configurable: { thread_id: "1" }, streamMode: "values" } +); + +for await (const event of events) { + if ("messages" in event) { + const lastMessage = event.messages.at(-1); + + console.log( + "=".repeat(32), + `${lastMessage?.getType()} Message`, + "=".repeat(32) + ); + console.log(lastMessage?.text); + + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { + console.log("Tool Calls:"); + for (const call of lastMessage.tool_calls) { + console.log(` ${call.name} (${call.id})`); + console.log(` Args: ${JSON.stringify(call.args)}`); + } + } + } +} +``` +::: + +``` +================================ Human Message ================================= + +Can you look up when LangGraph was released? When you have the answer, use the human_assistance tool for review. +================================== Ai Message ================================== + +[{'text': "Certainly! I'll start by searching for information about LangGraph's release date using the Tavily search function. Then, I'll use the human_assistance tool for review.", 'type': 'text'}, {'id': 'toolu_01JoXQPgTVJXiuma8xMVwqAi', 'input': {'query': 'LangGraph release date'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] +Tool Calls: + tavily_search_results_json (toolu_01JoXQPgTVJXiuma8xMVwqAi) + Call ID: toolu_01JoXQPgTVJXiuma8xMVwqAi + Args: + query: LangGraph release date +================================= Tool Message ================================= +Name: tavily_search_results_json + +[{"url": "https://blog.langchain.dev/langgraph-cloud/", "content": "We also have a new stable release of LangGraph. By LangChain 6 min read Jun 27, 2024 (Oct '24) Edit: Since the launch of LangGraph Platform, we now have multiple deployment options alongside LangGraph Studio - which now fall under LangGraph Platform. LangGraph Platform is synonymous with our Cloud SaaS deployment option."}, {"url": "https://changelog.langchain.com/announcements/langgraph-cloud-deploy-at-scale-monitor-carefully-iterate-boldly", "content": "LangChain - Changelog | ☁ 🚀 LangGraph Platform: Deploy at scale, monitor LangChain LangSmith LangGraph LangChain LangSmith LangGraph LangChain LangSmith LangGraph LangChain Changelog Sign up for our newsletter to stay up to date DATE: The LangChain Team LangGraph LangGraph Platform ☁ 🚀 LangGraph Platform: Deploy at scale, monitor carefully, iterate boldly DATE: June 27, 2024 AUTHOR: The LangChain Team LangGraph Platform is now in closed beta, offering scalable, fault-tolerant deployment for LangGraph agents. LangGraph Platform also includes a new playground-like studio for debugging agent failure modes and quick iteration: Join the waitlist today for LangGraph Platform. And to learn more, read our blog post announcement or check out our docs. Subscribe By clicking subscribe, you accept our privacy policy and terms and conditions."}] +================================== Ai Message ================================== + +[{'text': "Based on the search results, it appears that LangGraph was already in existence before June 27, 2024, when LangGraph Platform was announced. However, the search results don't provide a specific release date for the original LangGraph. \n\nGiven this information, I'll use the human_assistance tool to review and potentially provide more accurate information about LangGraph's initial release date.", 'type': 'text'}, {'id': 'toolu_01JDQAV7nPqMkHHhNs3j3XoN', 'input': {'name': 'Assistant', 'birthday': '2023-01-01'}, 'name': 'human_assistance', 'type': 'tool_use'}] +Tool Calls: + human_assistance (toolu_01JDQAV7nPqMkHHhNs3j3XoN) + Call ID: toolu_01JDQAV7nPqMkHHhNs3j3XoN + Args: + name: Assistant + birthday: 2023-01-01 +``` + +:::python +We've hit the `interrupt` in the `human_assistance` tool again. +::: + +:::js +We've hit the `interrupt` in the `humanAssistance` tool again. +::: + +## 4. Add human assistance + +The chatbot failed to identify the correct date, so supply it with information: + +:::python +```python +human_command = Command( + resume={ + "name": "LangGraph", + "birthday": "Jan 17, 2024", + }, +) + +events = graph.stream(human_command, config, stream_mode="values") +for event in events: + if "messages" in event: + event["messages"][-1].pretty_print() +``` +::: + +:::js +```typescript +import { Command } from "@langchain/langgraph"; + +const humanCommand = new Command({ + resume: { + name: "LangGraph", + birthday: "Jan 17, 2024", + }, +}); + +const resumeEvents = await graph.stream(humanCommand, { + configurable: { thread_id: "1" }, + streamMode: "values", +}); + +for await (const event of resumeEvents) { + if ("messages" in event) { + const lastMessage = event.messages.at(-1); + + console.log( + "=".repeat(32), + `${lastMessage?.getType()} Message`, + "=".repeat(32) + ); + console.log(lastMessage?.text); + + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { + console.log("Tool Calls:"); + for (const call of lastMessage.tool_calls) { + console.log(` ${call.name} (${call.id})`); + console.log(` Args: ${JSON.stringify(call.args)}`); + } + } + } +} +``` +::: + +``` +================================== Ai Message ================================== + +[{'text': "Based on the search results, it appears that LangGraph was already in existence before June 27, 2024, when LangGraph Platform was announced. However, the search results don't provide a specific release date for the original LangGraph. \n\nGiven this information, I'll use the human_assistance tool to review and potentially provide more accurate information about LangGraph's initial release date.", 'type': 'text'}, {'id': 'toolu_01JDQAV7nPqMkHHhNs3j3XoN', 'input': {'name': 'Assistant', 'birthday': '2023-01-01'}, 'name': 'human_assistance', 'type': 'tool_use'}] +Tool Calls: + human_assistance (toolu_01JDQAV7nPqMkHHhNs3j3XoN) + Call ID: toolu_01JDQAV7nPqMkHHhNs3j3XoN + Args: + name: Assistant + birthday: 2023-01-01 +================================= Tool Message ================================= +Name: human_assistance + +Made a correction: {'name': 'LangGraph', 'birthday': 'Jan 17, 2024'} +================================== Ai Message ================================== + +Thank you for the human assistance. I can now provide you with the correct information about LangGraph's release date. + +LangGraph was initially released on January 17, 2024. This information comes from the human assistance correction, which is more accurate than the search results I initially found. + +To summarize: +1. LangGraph's original release date: January 17, 2024 +2. LangGraph Platform announcement: June 27, 2024 + +It's worth noting that LangGraph had been in development and use for some time before the LangGraph Platform announcement, but the official initial release of LangGraph itself was on January 17, 2024. +``` + +Note that these fields are now reflected in the state: + +:::python +```python +snapshot = graph.get_state(config) + +{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")} +``` + +``` +{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'} +``` +::: + +:::js +```typescript +const snapshot = await graph.getState(config); + +const relevantState = Object.fromEntries( + Object.entries(snapshot.values).filter(([k]) => + ["name", "birthday"].includes(k) + ) +); +``` + +``` +{ name: 'LangGraph', birthday: 'Jan 17, 2024' } +``` +::: + +This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information). + +## 5. Manually update the state + +:::python +LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`: + +```python +graph.update_state(config, {"name": "LangGraph (library)"}) +``` + +``` +{'configurable': {'thread_id': '1', + 'checkpoint_ns': '', + 'checkpoint_id': '1efd4ec5-cf69-6352-8006-9278f1730162'}} +``` +::: + +:::js +LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`: + +```typescript +await graph.updateState( + { configurable: { thread_id: "1" } }, + { name: "LangGraph (library)" } +); +``` + +```typescript +{ + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1efd4ec5-cf69-6352-8006-9278f1730162' + } +} +``` +::: + +## 6. View the new value + +:::python +If you call `graph.get_state`, you can see the new value is reflected: + +```python +snapshot = graph.get_state(config) + +{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")} +``` + +``` +{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'} +``` +::: + +:::js +If you call `graph.getState`, you can see the new value is reflected: + +```typescript +const updatedSnapshot = await graph.getState(config); + +const updatedRelevantState = Object.fromEntries( + Object.entries(updatedSnapshot.values).filter(([k]) => + ["name", "birthday"].includes(k) + ) +); +``` + +```typescript +{ name: 'LangGraph (library)', birthday: 'Jan 17, 2024' } +``` +::: + +Manual state updates will [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to [control human-in-the-loop workflows](/oss/add-human-in-the-loop). Use of the `interrupt` function is generally recommended instead, as it allows data to be transmitted in a human-in-the-loop interaction independently of state updates. + +**Congratulations!** You've added custom keys to the state to facilitate a more complex workflow, and learned how to generate state updates from inside tools. + +Check out the code snippet below to review the graph from this tutorial: + +:::python + + +```python +from typing import Annotated + +from langchain_tavily import TavilySearch +from langchain_core.messages import ToolMessage +from langchain_core.tools import InjectedToolCallId, tool +from typing_extensions import TypedDict + +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode, tools_condition +from langgraph.types import Command, interrupt + +class State(TypedDict): + messages: Annotated[list, add_messages] + name: str + birthday: str + +@tool +def human_assistance( + name: str, birthday: str, tool_call_id: Annotated[str, InjectedToolCallId] +) -> str: + """Request assistance from a human.""" + human_response = interrupt( + { + "question": "Is this correct?", + "name": name, + "birthday": birthday, + }, + ) + if human_response.get("correct", "").lower().startswith("y"): + verified_name = name + verified_birthday = birthday + response = "Correct" + else: + verified_name = human_response.get("name", name) + verified_birthday = human_response.get("birthday", birthday) + response = f"Made a correction: {human_response}" + + state_update = { + "name": verified_name, + "birthday": verified_birthday, + "messages": [ToolMessage(response, tool_call_id=tool_call_id)], + } + return Command(update=state_update) + + +tool = TavilySearch(max_results=2) +tools = [tool, human_assistance] +llm_with_tools = llm.bind_tools(tools) + +def chatbot(state: State): + message = llm_with_tools.invoke(state["messages"]) + assert(len(message.tool_calls) <= 1) + return {"messages": [message]} + +graph_builder = StateGraph(State) +graph_builder.add_node("chatbot", chatbot) + +tool_node = ToolNode(tools=tools) +graph_builder.add_node("tools", tool_node) + +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +graph_builder.add_edge("tools", "chatbot") +graph_builder.add_edge(START, "chatbot") + +memory = InMemorySaver() +graph = graph_builder.compile(checkpointer=memory) +``` +::: + +:::js +```typescript +import { + Command, + interrupt, + MessagesZodState, + MemorySaver, + StateGraph, + END, + START, +} from "@langchain/langgraph"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; +import { ChatAnthropic } from "@langchain/anthropic"; +import { TavilySearch } from "@langchain/tavily"; +import { ToolMessage } from "@langchain/core/messages"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const State = z.object({ + messages: MessagesZodState.shape.messages, + name: z.string(), + birthday: z.string(), +}); + +const humanAssistance = tool( + async (input, config) => { + // Note that because we are generating a ToolMessage for a state update, we + // generally require the ID of the corresponding tool call. This is available + // in the tool's config. + const toolCallId = config?.toolCall?.id as string | undefined; + if (!toolCallId) throw new Error("Tool call ID is required"); + + const humanResponse = await interrupt({ + question: "Is this correct?", + name: input.name, + birthday: input.birthday, + }); + + // We explicitly update the state with a ToolMessage inside the tool. + const stateUpdate = (() => { + // If the information is correct, update the state as-is. + if (humanResponse.correct?.toLowerCase().startsWith("y")) { + return { + name: input.name, + birthday: input.birthday, + messages: [ + new ToolMessage({ content: "Correct", tool_call_id: toolCallId }), + ], + }; + } + + // Otherwise, receive information from the human reviewer. + return { + name: humanResponse.name || input.name, + birthday: humanResponse.birthday || input.birthday, + messages: [ + new ToolMessage({ + content: `Made a correction: ${JSON.stringify(humanResponse)}`, + tool_call_id: toolCallId, + }), + ], + }; + })(); + + // We return a Command object in the tool to update our state. + return new Command({ update: stateUpdate }); + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + name: z.string().describe("The name of the entity"), + birthday: z.string().describe("The birthday/release date of the entity"), + }), + } +); + +const searchTool = new TavilySearch({ maxResults: 2 }); + +const tools = [searchTool, humanAssistance]; +const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", +}).bindTools(tools); + +const memory = new MemorySaver(); + +const chatbot = async (state: z.infer) => { + const message = await llmWithTools.invoke(state.messages); + return { messages: message }; +}; + +const graph = new StateGraph(State) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); +``` +::: + +## Next steps + +There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](/oss/6-time-travel). diff --git a/src/oss/6-time-travel.mdx b/src/oss/6-time-travel.mdx new file mode 100644 index 00000000..d730e0c5 --- /dev/null +++ b/src/oss/6-time-travel.mdx @@ -0,0 +1,600 @@ +--- +title: 6. Time travel +--- + +import chatTabs from '/snippets/chat-model-tabs.mdx'; + +In a typical chatbot workflow, the user interacts with the bot one or more times to accomplish a task. [Memory](/oss/3-add-memory) and a [human-in-the-loop](/oss/4-human-in-the-loop) enable checkpoints in the graph state and control future responses. + +What if you want a user to be able to start from a previous response and explore a different outcome? Or what if you want users to be able to rewind your chatbot's work to fix mistakes or try a different strategy, something that is common in applications like autonomous software engineers? + +You can create these types of experiences using LangGraph's built-in **time travel** functionality. + + + This tutorial builds on [Customize state](/oss/5-customize-state). + + +## 1. Rewind your graph + +:::python +Rewind your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time. +::: + +:::js +Rewind your graph by fetching a checkpoint using the graph's `getStateHistory` method. You can then resume execution at this previous point in time. +::: + +:::python + + + +```python +from typing import Annotated + +from langchain_tavily import TavilySearch +from langchain_core.messages import BaseMessage +from typing_extensions import TypedDict + +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode, tools_condition + +class State(TypedDict): + messages: Annotated[list, add_messages] + +graph_builder = StateGraph(State) + +tool = TavilySearch(max_results=2) +tools = [tool] +llm_with_tools = llm.bind_tools(tools) + +def chatbot(state: State): + return {"messages": [llm_with_tools.invoke(state["messages"])]} + +graph_builder.add_node("chatbot", chatbot) + +tool_node = ToolNode(tools=[tool]) +graph_builder.add_node("tools", tool_node) + +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +graph_builder.add_edge("tools", "chatbot") +graph_builder.add_edge(START, "chatbot") + +memory = InMemorySaver() +graph = graph_builder.compile(checkpointer=memory) +``` +::: + +:::js +```typescript +import { + StateGraph, + START, + END, + MessagesZodState, + MemorySaver, +} from "@langchain/langgraph"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; +import { TavilySearch } from "@langchain/tavily"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; + +const State = z.object({ messages: MessagesZodState.shape.messages }); + +const tools = [new TavilySearch({ maxResults: 2 })]; +const llmWithTools = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools); +const memory = new MemorySaver(); + +const graph = new StateGraph(State) + .addNode("chatbot", async (state) => ({ + messages: [await llmWithTools.invoke(state.messages)], + })) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); +``` +::: + +## 2. Add steps + +Add steps to your graph. Every step will be checkpointed in its state history: + +:::python +```python +config = {"configurable": {"thread_id": "1"}} +events = graph.stream( + { + "messages": [ + { + "role": "user", + "content": ( + "I'm learning LangGraph. " + "Could you do some research on it for me?" + ), + }, + ], + }, + config, + stream_mode="values", +) +for event in events: + if "messages" in event: + event["messages"][-1].pretty_print() +``` + +``` +================================ Human Message ================================= + +I'm learning LangGraph. Could you do some research on it for me? +================================== Ai Message ================================== + +[{'text': "Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and accurate information, I'll use the Tavily search engine to look this up. Let me do that for you now.", 'type': 'text'}, {'id': 'toolu_01BscbfJJB9EWJFqGrN6E54e', 'input': {'query': 'LangGraph latest information and features'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] +Tool Calls: + tavily_search_results_json (toolu_01BscbfJJB9EWJFqGrN6E54e) + Call ID: toolu_01BscbfJJB9EWJFqGrN6E54e + Args: + query: LangGraph latest information and features +================================= Tool Message ================================= +Name: tavily_search_results_json + +[{"url": "https://blockchain.news/news/langchain-new-features-upcoming-events-update", "content": "LangChain, a leading platform in the AI development space, has released its latest updates, showcasing new use cases and enhancements across its ecosystem. According to the LangChain Blog, the updates cover advancements in LangGraph Platform, LangSmith's self-improving evaluators, and revamped documentation for LangGraph."}, {"url": "https://blog.langchain.dev/langgraph-platform-announce/", "content": "With these learnings under our belt, we decided to couple some of our latest offerings under LangGraph Platform. LangGraph Platform today includes LangGraph Server, LangGraph Studio, plus the CLI and SDK. ... we added features in LangGraph Server to deliver on a few key value areas. Below, we'll focus on these aspects of LangGraph Platform."}] +================================== Ai Message ================================== + +Thank you for your patience. I've found some recent information about LangGraph for you. Let me summarize the key points: + +1. LangGraph is part of the LangChain ecosystem, which is a leading platform in AI development. + +2. Recent updates and features of LangGraph include: + + a. LangGraph Platform: This seems to be a cloud-based version of LangGraph, though specific details weren't provided in the search results. +... +3. Keep an eye on LangGraph Platform developments, as cloud-based solutions often provide an easier starting point for learners. +4. Consider how LangGraph fits into the broader LangChain ecosystem, especially its interaction with tools like LangSmith. + +Is there any specific aspect of LangGraph you'd like to know more about? I'd be happy to do a more focused search on particular features or use cases. +Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... +``` + +```python +events = graph.stream( + { + "messages": [ + { + "role": "user", + "content": ( + "Ya that's helpful. Maybe I'll " + "build an autonomous agent with it!" + ), + }, + ], + }, + config, + stream_mode="values", +) +for event in events: + if "messages" in event: + event["messages"][-1].pretty_print() +``` + +``` +================================ Human Message ================================= + +Ya that's helpful. Maybe I'll build an autonomous agent with it! +================================== Ai Message ================================== + +[{'text': "That's an exciting idea! Building an autonomous agent with LangGraph is indeed a great application of this technology. LangGraph is particularly well-suited for creating complex, multi-step AI workflows, which is perfect for autonomous agents. Let me gather some more specific information about using LangGraph for building autonomous agents.", 'type': 'text'}, {'id': 'toolu_01QWNHhUaeeWcGXvA4eHT7Zo', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] +Tool Calls: + tavily_search_results_json (toolu_01QWNHhUaeeWcGXvA4eHT7Zo) + Call ID: toolu_01QWNHhUaeeWcGXvA4eHT7Zo + Args: + query: Building autonomous agents with LangGraph examples and tutorials +================================= Tool Message ================================= +Name: tavily_search_results_json + +[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}] +================================== Ai Message ================================== + +Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph: + +1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task. + +2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the "brain" of your agent, making decisions and generating responses. + +3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order. +... +6. Pay attention to how you structure the agent's decision-making process and workflow. +7. Don't forget to implement proper error handling and safety measures, especially if your agent will be interacting with external systems or making important decisions. + +Building an autonomous agent is an iterative process, so be prepared to refine and improve your agent over time. Good luck with your project! If you need any more specific information as you progress, feel free to ask. +Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... +``` +::: + +:::js +```typescript +import { randomUUID } from "node:crypto"; +const threadId = randomUUID(); + +let iter = 0; + +for (const userInput of [ + "I'm learning LangGraph. Could you do some research on it for me?", + "Ya that's helpful. Maybe I'll build an autonomous agent with it!", +]) { + iter += 1; + + console.log(`\n--- Conversation Turn ${iter} ---\n`); + const events = await graph.stream( + { messages: [{ role: "user", content: userInput }] }, + { configurable: { thread_id: threadId }, streamMode: "values" } + ); + + for await (const event of events) { + if ("messages" in event) { + const lastMessage = event.messages.at(-1); + + console.log( + "=".repeat(32), + `${lastMessage?.getType()} Message`, + "=".repeat(32) + ); + console.log(lastMessage?.text); + } + } +} +``` + +``` +--- Conversation Turn 1 --- + +================================ human Message ================================ +I'm learning LangGraph.js. Could you do some research on it for me? +================================ ai Message ================================ +I'll search for information about LangGraph.js for you. +================================ tool Message ================================ +{ + "query": "LangGraph.js framework TypeScript langchain what is it tutorial guide", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "url": "https://techcommunity.microsoft.com/blog/educatordeveloperblog/an-absolute-beginners-guide-to-langgraph-js/4212496", + "title": "An Absolute Beginner's Guide to LangGraph.js", + "content": "(...)", + "score": 0.79369855, + "raw_content": null + }, + { + "url": "https://langchain-ai.github.io/langgraphjs/", + "title": "LangGraph.js", + "content": "(...)", + "score": 0.78154784, + "raw_content": null + } + ], + "response_time": 2.37 +} +================================ ai Message ================================ +Let me provide you with an overview of LangGraph.js based on the search results: + +LangGraph.js is a JavaScript/TypeScript library that's part of the LangChain ecosystem, specifically designed for creating and managing complex LLM (Large Language Model) based workflows. Here are the key points about LangGraph.js: + +1. Purpose: +- It's a low-level orchestration framework for building controllable agents +- Particularly useful for creating agentic workflows where LLMs decide the course of action based on current state +- Helps model workflows as graphs with nodes and edges + +(...) + +--- Conversation Turn 2 --- + +================================ human Message ================================ +Ya that's helpful. Maybe I'll build an autonomous agent with it! +================================ ai Message ================================ +Let me search for specific information about building autonomous agents with LangGraph.js. +================================ tool Message ================================ +{ + "query": "how to build autonomous agents with LangGraph.js examples tutorial react agent", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "url": "https://ai.google.dev/gemini-api/docs/langgraph-example", + "title": "ReAct agent from scratch with Gemini 2.5 and LangGraph", + "content": "(...)", + "score": 0.7602419, + "raw_content": null + }, + { + "url": "https://www.youtube.com/watch?v=ZfjaIshGkmk", + "title": "Build Autonomous AI Agents with ReAct and LangGraph Tools", + "content": "(...)", + "score": 0.7471924, + "raw_content": null + } + ], + "response_time": 1.98 +} +================================ ai Message ================================ +Based on the search results, I can provide you with a practical overview of how to build an autonomous agent with LangGraph.js. Here's what you need to know: + +1. Basic Structure for Building an Agent: +- LangGraph.js provides a ReAct (Reason + Act) pattern implementation +- The basic components include: + - State management for conversation history + - Nodes for different actions + - Edges for decision-making flow + - Tools for specific functionalities + +(...) + +``` +::: + +## 3. Replay the full state history + +Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred. + +:::python +```python +to_replay = None +for state in graph.get_state_history(config): + print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next) + print("-" * 80) + if len(state.values["messages"]) == 6: + # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state. + to_replay = state +``` + +``` +Num Messages: 8 Next: () +-------------------------------------------------------------------------------- +Num Messages: 7 Next: ('chatbot',) +-------------------------------------------------------------------------------- +Num Messages: 6 Next: ('tools',) +-------------------------------------------------------------------------------- +Num Messages: 5 Next: ('chatbot',) +-------------------------------------------------------------------------------- +Num Messages: 4 Next: ('__start__',) +-------------------------------------------------------------------------------- +Num Messages: 4 Next: () +-------------------------------------------------------------------------------- +Num Messages: 3 Next: ('chatbot',) +-------------------------------------------------------------------------------- +Num Messages: 2 Next: ('tools',) +-------------------------------------------------------------------------------- +Num Messages: 1 Next: ('chatbot',) +-------------------------------------------------------------------------------- +Num Messages: 0 Next: ('__start__',) +-------------------------------------------------------------------------------- +``` +::: + +:::js +```typescript +import type { StateSnapshot } from "@langchain/langgraph"; + +let toReplay: StateSnapshot | undefined; +for await (const state of graph.getStateHistory({ + configurable: { thread_id: threadId }, +})) { + console.log( + `Num Messages: ${state.values.messages.length}, Next: ${JSON.stringify( + state.next + )}` + ); + console.log("-".repeat(80)); + if (state.values.messages.length === 6) { + // We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state. + toReplay = state; + } +} +``` + +``` +Num Messages: 8 Next: [] +-------------------------------------------------------------------------------- +Num Messages: 7 Next: ["chatbot"] +-------------------------------------------------------------------------------- +Num Messages: 6 Next: ["tools"] +-------------------------------------------------------------------------------- +Num Messages: 7, Next: ["chatbot"] +-------------------------------------------------------------------------------- +Num Messages: 6, Next: ["tools"] +-------------------------------------------------------------------------------- +Num Messages: 5, Next: ["chatbot"] +-------------------------------------------------------------------------------- +Num Messages: 4, Next: ["__start__"] +-------------------------------------------------------------------------------- +Num Messages: 4, Next: [] +-------------------------------------------------------------------------------- +Num Messages: 3, Next: ["chatbot"] +-------------------------------------------------------------------------------- +Num Messages: 2, Next: ["tools"] +-------------------------------------------------------------------------------- +Num Messages: 1, Next: ["chatbot"] +-------------------------------------------------------------------------------- +Num Messages: 0, Next: ["__start__"] +-------------------------------------------------------------------------------- +``` +::: + +Checkpoints are saved for every step of the graph. This **spans invocations** so you can rewind across a full thread's history. + +## Resume from a checkpoint + +:::python +Resume from the `to_replay` state, which is after the `chatbot` node in the second graph invocation. Resuming from this point will call the **action** node next. +::: + +:::js +Resume from the `toReplay` state, which is after a specific node in one of the graph invocations. Resuming from this point will call the next scheduled node. +::: + +:::python +```python +print(to_replay.next) +print(to_replay.config) +``` + +``` +('tools',) +{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}} +``` +::: + +:::js +Resume from the `toReplay` state, which is after the `chatbot` node in one of the graph invocations. Resuming from this point will call the next scheduled node. + +```typescript +console.log(toReplay.next); +console.log(toReplay.config); +``` + +``` +["tools"] +{ + configurable: { + thread_id: "007708b8-ea9b-4ff7-a7ad-3843364dbf75", + checkpoint_ns: "", + checkpoint_id: "1efd43e3-0c1f-6c4e-8006-891877d65740" + } +} +``` +::: + +## 4. Load a state from a moment-in-time + +:::python +The checkpoint's `to_replay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time. + +```python +# The `checkpoint_id` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer. +for event in graph.stream(None, to_replay.config, stream_mode="values"): + if "messages" in event: + event["messages"][-1].pretty_print() +``` + +``` +================================== Ai Message ================================== + +[{'text': "That's an exciting idea! Building an autonomous agent with LangGraph is indeed a great application of this technology. LangGraph is particularly well-suited for creating complex, multi-step AI workflows, which is perfect for autonomous agents. Let me gather some more specific information about using LangGraph for building autonomous agents.", 'type': 'text'}, {'id': 'toolu_01QWNHhUaeeWcGXvA4eHT7Zo', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] +Tool Calls: + tavily_search_results_json (toolu_01QWNHhUaeeWcGXvA4eHT7Zo) + Call ID: toolu_01QWNHhUaeeWcGXvA4eHT7Zo + Args: + query: Building autonomous agents with LangGraph examples and tutorials +================================= Tool Message ================================= +Name: tavily_search_results_json + +[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}] +================================== Ai Message ================================== + +Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph: + +1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task. + +2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the "brain" of your agent, making decisions and generating responses. + +3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order. +... + +Remember, building an autonomous agent is an iterative process. Start simple and gradually increase complexity as you become more comfortable with LangGraph and its capabilities. + +Would you like more information on any specific aspect of building your autonomous agent with LangGraph? +Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... +``` + +The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool. +::: + +:::js +The checkpoint's `toReplay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time. + +```typescript +// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer. +for await (const event of await graph.stream(null, { + ...toReplay?.config, + streamMode: "values", +})) { + if ("messages" in event) { + const lastMessage = event.messages.at(-1); + + console.log( + "=".repeat(32), + `${lastMessage?.getType()} Message`, + "=".repeat(32) + ); + console.log(lastMessage?.text); + } +} +``` + +``` +================================ ai Message ================================ +Let me search for specific information about building autonomous agents with LangGraph.js. +================================ tool Message ================================ +{ + "query": "how to build autonomous agents with LangGraph.js examples tutorial", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "url": "https://www.mongodb.com/developer/languages/typescript/build-javascript-ai-agent-langgraphjs-mongodb/", + "title": "Build a JavaScript AI Agent With LangGraph.js and MongoDB", + "content": "(...)", + "score": 0.7672197, + "raw_content": null + }, + { + "url": "https://medium.com/@lorevanoudenhove/how-to-build-ai-agents-with-langgraph-a-step-by-step-guide-5d84d9c7e832", + "title": "How to Build AI Agents with LangGraph: A Step-by-Step Guide", + "content": "(...)", + "score": 0.7407191, + "raw_content": null + } + ], + "response_time": 0.82 +} +================================ ai Message ================================ +Based on the search results, I can share some practical information about building autonomous agents with LangGraph.js. Here are some concrete examples and approaches: + +1. Example HR Assistant Agent: +- Can handle HR-related queries using employee information +- Features include: + - Starting and continuing conversations + - Looking up information using vector search + - Persisting conversation state using checkpoints + - Managing threaded conversations + +2. Energy Savings Calculator Agent: +- Functions as a lead generation tool for solar panel sales +- Capabilities include: + - Calculating potential energy savings + - Handling multi-step conversations + - Processing user inputs for personalized estimates + - Managing conversation state + +(...) +``` + +The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool. +::: + +**Congratulations!** You've now used time-travel checkpoint traversal in LangGraph. Being able to rewind and explore alternative paths opens up a world of possibilities for debugging, experimentation, and interactive applications. + +## Learn more + +Take your LangGraph journey further by exploring deployment and advanced features: + +* **[LangGraph Server quickstart](/langgraph-platform/local-server)**: Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI. +* **[LangGraph Platform quickstart](/langgraph-platform/deployment-quickstart)**: Deploy your LangGraph app using LangGraph Platform. diff --git a/src/oss/GRAPH_RECURSION_LIMIT.mdx b/src/oss/GRAPH_RECURSION_LIMIT.mdx new file mode 100644 index 00000000..8ae3f6fe --- /dev/null +++ b/src/oss/GRAPH_RECURSION_LIMIT.mdx @@ -0,0 +1,63 @@ +--- +title: GRAPH_RECURSION_LIMIT +--- +Your LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph) reached the maximum number of steps before hitting a stop condition. +This is often due to an infinite loop caused by code like the example below: + +:::python +```python +class State(TypedDict): + some_key: str + +builder = StateGraph(State) +builder.add_node("a", ...) +builder.add_node("b", ...) +builder.add_edge("a", "b") +builder.add_edge("b", "a") +... + +graph = builder.compile() +``` +::: + +:::js +```typescript +import { StateGraph } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + someKey: z.string(), +}); + +const builder = new StateGraph(State) + .addNode("a", ...) + .addNode("b", ...) + .addEdge("a", "b") + .addEdge("b", "a") + ... + +const graph = builder.compile(); +``` +::: + +However, complex graphs may hit the default limit naturally. + +## Troubleshooting + +* If you are not expecting your graph to go through many iterations, you likely have a cycle. Check your logic for infinite loops. + +:::python +* If you have a complex graph, you can pass in a higher `recursion_limit` value into your `config` object when invoking your graph like this: + +```python +graph.invoke({...}, {"recursion_limit": 100}) +``` +::: + +:::js +* If you have a complex graph, you can pass in a higher `recursionLimit` value into your `config` object when invoking your graph like this: + +```typescript +await graph.invoke({...}, { recursionLimit: 100 }); +``` +::: diff --git a/src/oss/INVALID_CHAT_HISTORY.mdx b/src/oss/INVALID_CHAT_HISTORY.mdx new file mode 100644 index 00000000..065c66ad --- /dev/null +++ b/src/oss/INVALID_CHAT_HISTORY.mdx @@ -0,0 +1,59 @@ +--- +title: INVALID_CHAT_HISTORY +--- +:::python +This error is raised in the prebuilt @[create_react_agent][create_react_agent] when the `call_model` graph node receives a malformed list of messages. Specifically, it is malformed when there are `AIMessages` with `tool_calls` (LLM requesting to call a tool) that do not have a corresponding `ToolMessage` (result of a tool invocation to return to the LLM). +::: + +:::js +This error is raised in the prebuilt @[createReactAgent][create_react_agent] when the `callModel` graph node receives a malformed list of messages. Specifically, it is malformed when there are `AIMessage`s with `tool_calls` (LLM requesting to call a tool) that do not have a corresponding `ToolMessage` (result of a tool invocation to return to the LLM). +::: + +There could be a few reasons you're seeing this error: + +:::python +1. You manually passed a malformed list of messages when invoking the graph, e.g. `graph.invoke({'messages': [AIMessage(..., tool_calls=[...])]})` +2. The graph was interrupted before receiving updates from the `tools` node (i.e. a list of ToolMessages) + and you invoked it with an input that is not None or a ToolMessage, + e.g. `graph.invoke({'messages': [HumanMessage(...)]}, config)`. + This interrupt could have been triggered in one of the following ways: + * You manually set `interrupt_before = ['tools']` in `create_react_agent` + * One of the tools raised an error that wasn't handled by the @[ToolNode][ToolNode] (`"tools"`) +::: + +:::js +1. You manually passed a malformed list of messages when invoking the graph, e.g. `graph.invoke({messages: [new AIMessage({..., tool_calls: [...]})]})` +2. The graph was interrupted before receiving updates from the `tools` node (i.e. a list of ToolMessages) + and you invoked it with an input that is not null or a ToolMessage, + e.g. `graph.invoke({messages: [new HumanMessage(...)]}, config)`. + This interrupt could have been triggered in one of the following ways: + * You manually set `interruptBefore: ['tools']` in `createReactAgent` + * One of the tools raised an error that wasn't handled by the @[ToolNode][ToolNode] (`"tools"`) +::: + +## Troubleshooting + +To resolve this, you can do one of the following: + +1. Don't invoke the graph with a malformed list of messages +2. In case of an interrupt (manual or due to an error) you can: + +:::python +* provide ToolMessages that match existing tool calls and call `graph.invoke({'messages': [ToolMessage(...)]})`. + **NOTE**: this will append the messages to the history and run the graph from the START node. + * manually update the state and resume the graph from the interrupt: + 1. get the list of most recent messages from the graph state with `graph.get_state(config)` + 2. modify the list of messages to either remove unanswered tool calls from AIMessages + +or add ToolMessages with tool_call_ids that match unanswered tool calls 3. call `graph.update_state(config, {'messages': ...})` with the modified list of messages 4. resume the graph, e.g. call `graph.invoke(None, config)` +::: + +:::js +* provide ToolMessages that match existing tool calls and call `graph.invoke({messages: [new ToolMessage(...)]})`. + **NOTE**: this will append the messages to the history and run the graph from the START node. + * manually update the state and resume the graph from the interrupt: + 1. get the list of most recent messages from the graph state with `graph.getState(config)` + 2. modify the list of messages to either remove unanswered tool calls from AIMessages + +or add ToolMessages with `toolCallId`s that match unanswered tool calls 3. call `graph.updateState(config, {messages: ...})` with the modified list of messages 4. resume the graph, e.g. call `graph.invoke(null, config)` +::: diff --git a/src/oss/INVALID_CONCURRENT_GRAPH_UPDATE.mdx b/src/oss/INVALID_CONCURRENT_GRAPH_UPDATE.mdx new file mode 100644 index 00000000..1d41a48a --- /dev/null +++ b/src/oss/INVALID_CONCURRENT_GRAPH_UPDATE.mdx @@ -0,0 +1,101 @@ +--- +title: INVALID_CONCURRENT_GRAPH_UPDATE +--- +A LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph) received concurrent updates to its state from multiple nodes to a state property that doesn't +support it. + +One way this can occur is if you are using a [fanout](/oss/graph-api#map-reduce-and-the-send-api) +or other parallel execution in your graph and you have defined a graph like this: + +:::python +```python hl_lines="2" +class State(TypedDict): + some_key: str + +def node(state: State): + return {"some_key": "some_string_value"} + +def other_node(state: State): + return {"some_key": "some_string_value"} + + +builder = StateGraph(State) +builder.add_node(node) +builder.add_node(other_node) +builder.add_edge(START, "node") +builder.add_edge(START, "other_node") +graph = builder.compile() +``` +::: + +:::js +```typescript hl_lines="2" +import { StateGraph, Annotation, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + someKey: z.string(), +}); + +const builder = new StateGraph(State) + .addNode("node", (state) => { + return { someKey: "some_string_value" }; + }) + .addNode("otherNode", (state) => { + return { someKey: "some_string_value" }; + }) + .addEdge(START, "node") + .addEdge(START, "otherNode"); + +const graph = builder.compile(); +``` +::: + +:::python +If a node in the above graph returns `{ "some_key": "some_string_value" }`, this will overwrite the state value for `"some_key"` with `"some_string_value"`. +However, if multiple nodes in e.g. a fanout within a single step return values for `"some_key"`, the graph will throw this error because +there is uncertainty around how to update the internal state. +::: + +:::js +If a node in the above graph returns `{ someKey: "some_string_value" }`, this will overwrite the state value for `someKey` with `"some_string_value"`. +However, if multiple nodes in e.g. a fanout within a single step return values for `someKey`, the graph will throw this error because +there is uncertainty around how to update the internal state. +::: + +To get around this, you can define a reducer that combines multiple values: + +:::python +```python hl_lines="5-6" +import operator +from typing import Annotated + +class State(TypedDict): + # The operator.add reducer fn makes this append-only + some_key: Annotated[list, operator.add] +``` +::: + +:::js +```typescript hl_lines="4-7" +import { withLangGraph } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + someKey: withLangGraph(z.array(z.string()), { + reducer: { + fn: (existing, update) => existing.concat(update), + }, + default: () => [], + }), +}); +``` +::: + +This will allow you to define logic that handles the same key returned from multiple nodes executed in parallel. + +## Troubleshooting + +The following may help resolve this error: + +* If your graph executes nodes in parallel, make sure you have defined relevant state keys with a reducer. diff --git a/src/oss/INVALID_GRAPH_NODE_RETURN_VALUE.mdx b/src/oss/INVALID_GRAPH_NODE_RETURN_VALUE.mdx new file mode 100644 index 00000000..c7ef41df --- /dev/null +++ b/src/oss/INVALID_GRAPH_NODE_RETURN_VALUE.mdx @@ -0,0 +1,84 @@ +--- +title: INVALID_GRAPH_NODE_RETURN_VALUE +--- +:::python +A LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph) +received a non-dict return type from a node. Here's an example: + +```python +class State(TypedDict): + some_key: str + +def bad_node(state: State): + # Should return a dict with a value for "some_key", not a list + return ["whoops"] + +builder = StateGraph(State) +builder.add_node(bad_node) +... + +graph = builder.compile() +``` + +Invoking the above graph will result in an error like this: + +```python +graph.invoke({ "some_key": "someval" }); +``` + +``` +InvalidUpdateError: Expected dict, got ['whoops'] +For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE +``` + +Nodes in your graph must return a dict containing one or more keys defined in your state. +::: + +:::js +A LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph) +received a non-object return type from a node. Here's an example: + +```typescript +import { z } from "zod"; +import { StateGraph } from "@langchain/langgraph"; + +const State = z.object({ + someKey: z.string(), +}); + +const badNode = (state: z.infer) => { + // Should return an object with a value for "someKey", not an array + return ["whoops"]; +}; + +const builder = new StateGraph(State).addNode("badNode", badNode); +// ... + +const graph = builder.compile(); +``` + +Invoking the above graph will result in an error like this: + +```typescript +await graph.invoke({ someKey: "someval" }); +``` + +``` +InvalidUpdateError: Expected object, got ['whoops'] +For troubleshooting, visit: https://langchain-ai.github.io/langgraphjs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE +``` + +Nodes in your graph must return an object containing one or more keys defined in your state. +::: + +## Troubleshooting + +The following may help resolve this error: + +:::python +* If you have complex logic in your node, make sure all code paths return an appropriate dict for your defined state. +::: + +:::js +* If you have complex logic in your node, make sure all code paths return an appropriate object for your defined state. +::: diff --git a/src/oss/MULTIPLE_SUBGRAPHS.mdx b/src/oss/MULTIPLE_SUBGRAPHS.mdx new file mode 100644 index 00000000..90306894 --- /dev/null +++ b/src/oss/MULTIPLE_SUBGRAPHS.mdx @@ -0,0 +1,20 @@ +--- +title: MULTIPLE_SUBGRAPHS +--- +You are calling subgraphs multiple times within a single LangGraph node with checkpointing enabled for each subgraph. + +This is currently not allowed due to internal restrictions on how checkpoint namespacing for subgraphs works. + +## Troubleshooting + +The following may help resolve this error: + +:::python +* If you don't need to interrupt/resume from a subgraph, pass `checkpointer=False` when compiling it like this: `.compile(checkpointer=False)` +::: + +:::js +* If you don't need to interrupt/resume from a subgraph, pass `checkpointer: false` when compiling it like this: `.compile({ checkpointer: false })` +::: + +* Don't imperatively call graphs multiple times in the same node, and instead use the [`Send`](/oss/graph-api#send) API. diff --git a/src/oss/add-human-in-the-loop.mdx b/src/oss/add-human-in-the-loop.mdx new file mode 100644 index 00000000..842f31a4 --- /dev/null +++ b/src/oss/add-human-in-the-loop.mdx @@ -0,0 +1,2233 @@ +--- +title: Enable human intervention +--- + +To review, edit, and approve tool calls in an agent or workflow, use interrupts to pause a graph and wait for human input. Interrupts use LangGraph's [persistence](/oss/persistence) layer, which saves the graph state, to indefinitely pause graph execution until you resume. + + + For more information about human-in-the-loop workflows, see the [Human-in-the-Loop](/oss/human-in-the-loop) conceptual guide. + + +## Pause using `interrupt` + +:::python +[Dynamic interrupts](/oss/human-in-the-loop#key-capabilities) (also known as dynamic breakpoints) are triggered based on the current state of the graph. You can set dynamic interrupts by calling @[`interrupt` function][interrupt] in the appropriate place. The graph will pause, which allows for human intervention, and then resumes the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context. + + + As of v1.0, `interrupt` is the recommended way to pause a graph. `NodeInterrupt` is deprecated and will be removed in v2.0. + +::: + +:::js +[Dynamic interrupts](/oss/human-in-the-loop#key-capabilities) (also known as dynamic breakpoints) are triggered based on the current state of the graph. You can set dynamic interrupts by calling @[`interrupt` function][interrupt] in the appropriate place. The graph will pause, which allows for human intervention, and then resumes the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context. +::: + +To use `interrupt` in your graph, you need to: + +1. [**Specify a checkpointer**](/oss/persistence#checkpoints) to save the graph state after each step. +2. **Call `interrupt()`** in the appropriate place. See the [Common Patterns](#common-patterns) section for examples. +3. **Run the graph** with a [**thread ID**](/oss/persistence#threads) until the `interrupt` is hit. +4. **Resume execution** using `invoke`/`stream` (see [**The `Command` primitive**](#resume-using-the-command-primitive)). + +:::python +```python +# highlight-next-line +from langgraph.types import interrupt, Command + +def human_node(state: State): + # highlight-next-line + value = interrupt( # (1)! + { + "text_to_revise": state["some_text"] # (2)! + } + ) + return { + "some_text": value # (3)! + } + + +graph = graph_builder.compile(checkpointer=checkpointer) # (4)! + +# Run the graph until the interrupt is hit. +config = {"configurable": {"thread_id": "some_id"}} +result = graph.invoke({"some_text": "original text"}, config=config) # (5)! +print(result['__interrupt__']) # (6)! +# > [ +# > Interrupt( +# > value={'text_to_revise': 'original text'}, +# > resumable=True, +# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960'] +# > ) +# > ] + +# highlight-next-line +print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! +# > {'some_text': 'Edited text'} +``` + +1. `interrupt(...)` pauses execution at `human_node`, surfacing the given payload to a human. +2. Any JSON serializable value can be passed to the `interrupt` function. Here, a dict containing the text to revise. +3. Once resumed, the return value of `interrupt(...)` is the human-provided input, which is used to update the state. +4. A checkpointer is required to persist graph state. In production, this should be durable (e.g., backed by a database). +5. The graph is invoked with some initial state. +6. When the graph hits the interrupt, it returns an `Interrupt` object with the payload and metadata. +7. The graph is resumed with a `Command(resume=...)`, injecting the human's input and continuing execution. +::: + +:::js +```typescript +// highlight-next-line +import { interrupt, Command } from "@langchain/langgraph"; + +const graph = graphBuilder + .addNode("humanNode", (state) => { + // highlight-next-line + const value = interrupt( + // (1)! + { + textToRevise: state.someText, // (2)! + } + ); + return { + someText: value, // (3)! + }; + }) + .addEdge(START, "humanNode") + .compile({ checkpointer }); // (4)! + +// Run the graph until the interrupt is hit. +const config = { configurable: { thread_id: "some_id" } }; +const result = await graph.invoke({ someText: "original text" }, config); // (5)! +console.log(result.__interrupt__); // (6)! +// > [ +// > { +// > value: { textToRevise: 'original text' }, +// > resumable: true, +// > ns: ['humanNode:6ce9e64f-edef-fe5d-f7dc-511fa9526960'], +// > when: 'during' +// > } +// > ] + +// highlight-next-line +console.log(await graph.invoke(new Command({ resume: "Edited text" }), config)); // (7)! +// > { someText: 'Edited text' } +``` + +1. `interrupt(...)` pauses execution at `humanNode`, surfacing the given payload to a human. +2. Any JSON serializable value can be passed to the `interrupt` function. Here, an object containing the text to revise. +3. Once resumed, the return value of `interrupt(...)` is the human-provided input, which is used to update the state. +4. A checkpointer is required to persist graph state. In production, this should be durable (e.g., backed by a database). +5. The graph is invoked with some initial state. +6. When the graph hits the interrupt, it returns an object with `__interrupt__` containing the payload and metadata. +7. The graph is resumed with a `Command({ resume: ... })`, injecting the human's input and continuing execution. +::: + + + :::python + ```python + from typing import TypedDict + import uuid + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.constants import START + from langgraph.graph import StateGraph + + # highlight-next-line + from langgraph.types import interrupt, Command + + + class State(TypedDict): + some_text: str + + + def human_node(state: State): + # highlight-next-line + value = interrupt( # (1)! + { + "text_to_revise": state["some_text"] # (2)! + } + ) + return { + "some_text": value # (3)! + } + + + # Build the graph + graph_builder = StateGraph(State) + graph_builder.add_node("human_node", human_node) + graph_builder.add_edge(START, "human_node") + checkpointer = InMemorySaver() # (4)! + graph = graph_builder.compile(checkpointer=checkpointer) + # Pass a thread ID to the graph to run it. + config = {"configurable": {"thread_id": uuid.uuid4()}} + # Run the graph until the interrupt is hit. + result = graph.invoke({"some_text": "original text"}, config=config) # (5)! + + print(result['__interrupt__']) # (6)! + # > [ + # > Interrupt( + # > value={'text_to_revise': 'original text'}, + # > resumable=True, + # > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960'] + # > ) + # > ] + print(result["__interrupt__"]) # (6)! + # > [Interrupt(value={'text_to_revise': 'original text'}, id='6d7c4048049254c83195429a3659661d')] + + # highlight-next-line + print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! + # > {'some_text': 'Edited text'} + ``` + + 1. `interrupt(...)` pauses execution at `human_node`, surfacing the given payload to a human. + 2. Any JSON serializable value can be passed to the `interrupt` function. Here, a dict containing the text to revise. + 3. Once resumed, the return value of `interrupt(...)` is the human-provided input, which is used to update the state. + 4. A checkpointer is required to persist graph state. In production, this should be durable (e.g., backed by a database). + 5. The graph is invoked with some initial state. + 6. When the graph hits the interrupt, it returns an `Interrupt` object with the payload and metadata. + 7. The graph is resumed with a `Command(resume=...)`, injecting the human's input and continuing execution. + ::: + + :::js + ```typescript + import { z } from "zod"; + import { v4 as uuidv4 } from "uuid"; + import { MemorySaver, StateGraph, START, interrupt, Command } from "@langchain/langgraph"; + + const StateAnnotation = z.object({ + someText: z.string(), + }); + + // Build the graph + const graphBuilder = new StateGraph(StateAnnotation) + .addNode("humanNode", (state) => { + // highlight-next-line + const value = interrupt( // (1)! + { + textToRevise: state.someText // (2)! + } + ); + return { + someText: value // (3)! + }; + }) + .addEdge(START, "humanNode"); + + const checkpointer = new MemorySaver(); // (4)! + + const graph = graphBuilder.compile({ checkpointer }); + + // Pass a thread ID to the graph to run it. + const config = { configurable: { thread_id: uuidv4() } }; + + // Run the graph until the interrupt is hit. + const result = await graph.invoke({ someText: "original text" }, config); // (5)! + + console.log(result.__interrupt__); // (6)! + // > [ + // > { + // > value: { textToRevise: 'original text' }, + // > resumable: true, + // > ns: ['humanNode:6ce9e64f-edef-fe5d-f7dc-511fa9526960'], + // > when: 'during' + // > } + // > ] + + // highlight-next-line + console.log(await graph.invoke(new Command({ resume: "Edited text" }), config)); // (7)! + // > { someText: 'Edited text' } + ``` + + 1. `interrupt(...)` pauses execution at `humanNode`, surfacing the given payload to a human. + 2. Any JSON serializable value can be passed to the `interrupt` function. Here, an object containing the text to revise. + 3. Once resumed, the return value of `interrupt(...)` is the human-provided input, which is used to update the state. + 4. A checkpointer is required to persist graph state. In production, this should be durable (e.g., backed by a database). + 5. The graph is invoked with some initial state. + 6. When the graph hits the interrupt, it returns an object with `__interrupt__` containing the payload and metadata. + 7. The graph is resumed with a `Command({ resume: ... })`, injecting the human's input and continuing execution. + ::: + + + + **New in 0.4.0** + :::python + `__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` and `ainvoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream` or `astream`. You can also use `graph.get_state(thread_id)` to get the interrupt value(s). + ::: + + :::js + `__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream`. You can also use `graph.getState(config)` to get the interrupt value(s). + ::: + + + + :::python + Interrupts resemble Python's input() function in terms of developer experience, but they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used. For this reason, interrupts are typically best placed at the start of a node or in a dedicated node. + ::: + + :::js + Interrupts are both powerful and ergonomic, but it's important to note that they do not automatically resume execution from the interrupt point. Instead, they rerun the entire where the interrupt was used. For this reason, interrupts are typically best placed at the state of a node or in a dedicated node. + ::: + + +## Resume using the `Command` primitive + +:::python + + Resuming from an `interrupt` is different from Python's `input()` function, where execution resumes from the exact point where the `input()` function was called. + +::: + +When the `interrupt` function is used within a graph, execution pauses at that point and awaits user input. + +:::python +To resume execution, use the @[`Command`][Command] primitive, which can be supplied via the `invoke` or `stream` methods. The graph resumes execution from the beginning of the node where `interrupt(...)` was initially called. This time, the `interrupt` function will return the value provided in `Command(resume=value)` rather than pausing again. All code from the beginning of the node to the `interrupt` will be re-executed. + +```python +# Resume graph execution by providing the user's input. +graph.invoke(Command(resume={"age": "25"}), thread_config) +``` +::: + +:::js +To resume execution, use the @[`Command`][Command] primitive, which can be supplied via the `invoke` or `stream` methods. The graph resumes execution from the beginning of the node where `interrupt(...)` was initially called. This time, the `interrupt` function will return the value provided in `Command(resume=value)` rather than pausing again. All code from the beginning of the node to the `interrupt` will be re-executed. + +```typescript +// Resume graph execution by providing the user's input. +await graph.invoke(new Command({ resume: { age: "25" } }), threadConfig); +``` +::: + +### Resume multiple interrupts with one invocation + +When nodes with interrupt conditions are run in parallel, it's possible to have multiple interrupts in the task queue. +For example, the following graph has two nodes run in parallel that require human input: + +![two nodes run in parallel that require human input](/oss/images/human_in_loop_parallel.png) + +:::python +Once your graph has been interrupted and is stalled, you can resume all the interrupts at once with `Command.resume`, passing a dictionary mapping of interrupt ids to resume values. + +```python +from typing import TypedDict +import uuid +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.constants import START +from langgraph.graph import StateGraph +from langgraph.types import interrupt, Command + + +class State(TypedDict): + text_1: str + text_2: str + + +def human_node_1(state: State): + value = interrupt({"text_to_revise": state["text_1"]}) + return {"text_1": value} + + +def human_node_2(state: State): + value = interrupt({"text_to_revise": state["text_2"]}) + return {"text_2": value} + + +graph_builder = StateGraph(State) +graph_builder.add_node("human_node_1", human_node_1) +graph_builder.add_node("human_node_2", human_node_2) + +# Add both nodes in parallel from START +graph_builder.add_edge(START, "human_node_1") +graph_builder.add_edge(START, "human_node_2") + +checkpointer = InMemorySaver() +graph = graph_builder.compile(checkpointer=checkpointer) + +thread_id = str(uuid.uuid4()) +config: RunnableConfig = {"configurable": {"thread_id": thread_id}} +result = graph.invoke( + {"text_1": "original text 1", "text_2": "original text 2"}, config=config +) + +# Resume with mapping of interrupt IDs to values +resume_map = { + i.interrupt_id: f"human input for prompt {i.value}" + for i in parent.get_state(thread_config).interrupts +} +print(graph.invoke(Command(resume=resume_map), config=config)) +# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'} +``` +::: + +:::js +```typescript +const state = await parentGraph.getState(threadConfig); +const resumeMap = Object.fromEntries( + state.interrupts.map((i) => [ + i.interruptId, + `human input for prompt ${i.value}`, + ]) +); + +await parentGraph.invoke(new Command({ resume: resumeMap }), threadConfig); +``` +::: + +## Common patterns + +Below we show different design patterns that can be implemented using `interrupt` and `Command`. + +### Approve or reject + +![Depending on the human's approval or rejection, the graph can proceed with the action or take an alternative path](/oss/images/approve-or-reject.png) + +Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action. + +:::python +```python +from typing import Literal +from langgraph.types import interrupt, Command + +def human_approval(state: State) -> Command[Literal["some_node", "another_node"]]: + is_approved = interrupt( + { + "question": "Is this correct?", + # Surface the output that should be + # reviewed and approved by the human. + "llm_output": state["llm_output"] + } + ) + + if is_approved: + return Command(goto="some_node") + else: + return Command(goto="another_node") + +# Add the node to the graph in an appropriate location +# and connect it to the relevant nodes. +graph_builder.add_node("human_approval", human_approval) +graph = graph_builder.compile(checkpointer=checkpointer) + +# After running the graph and hitting the interrupt, the graph will pause. +# Resume it with either an approval or rejection. +thread_config = {"configurable": {"thread_id": "some_id"}} +graph.invoke(Command(resume=True), config=thread_config) +``` +::: + +:::js +```typescript +import { interrupt, Command } from "@langchain/langgraph"; + +// Add the node to the graph in an appropriate location +// and connect it to the relevant nodes. +graphBuilder.addNode("humanApproval", (state) => { + const isApproved = interrupt({ + question: "Is this correct?", + // Surface the output that should be + // reviewed and approved by the human. + llmOutput: state.llmOutput, + }); + + if (isApproved) { + return new Command({ goto: "someNode" }); + } else { + return new Command({ goto: "anotherNode" }); + } +}); +const graph = graphBuilder.compile({ checkpointer }); + +// After running the graph and hitting the interrupt, the graph will pause. +// Resume it with either an approval or rejection. +const threadConfig = { configurable: { thread_id: "some_id" } }; +await graph.invoke(new Command({ resume: true }), threadConfig); +``` +::: + + + :::python + ```python + from typing import Literal, TypedDict + import uuid + + from langgraph.constants import START, END + from langgraph.graph import StateGraph + from langgraph.types import interrupt, Command + from langgraph.checkpoint.memory import InMemorySaver + + # Define the shared graph state + class State(TypedDict): + llm_output: str + decision: str + + # Simulate an LLM output node + def generate_llm_output(state: State) -> State: + return {"llm_output": "This is the generated output."} + + # Human approval node + def human_approval(state: State) -> Command[Literal["approved_path", "rejected_path"]]: + decision = interrupt({ + "question": "Do you approve the following output?", + "llm_output": state["llm_output"] + }) + + if decision == "approve": + return Command(goto="approved_path", update={"decision": "approved"}) + else: + return Command(goto="rejected_path", update={"decision": "rejected"}) + + # Next steps after approval + def approved_node(state: State) -> State: + print("✅ Approved path taken.") + return state + + # Alternative path after rejection + def rejected_node(state: State) -> State: + print("❌ Rejected path taken.") + return state + + # Build the graph + builder = StateGraph(State) + builder.add_node("generate_llm_output", generate_llm_output) + builder.add_node("human_approval", human_approval) + builder.add_node("approved_path", approved_node) + builder.add_node("rejected_path", rejected_node) + + builder.set_entry_point("generate_llm_output") + builder.add_edge("generate_llm_output", "human_approval") + builder.add_edge("approved_path", END) + builder.add_edge("rejected_path", END) + + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + + # Run until interrupt + config = {"configurable": {"thread_id": uuid.uuid4()}} + result = graph.invoke({}, config=config) + print(result["__interrupt__"]) + # Output: + # Interrupt(value={'question': 'Do you approve the following output?', 'llm_output': 'This is the generated output.'}, ...) + + # Simulate resuming with human input + # To test rejection, replace resume="approve" with resume="reject" + final_result = graph.invoke(Command(resume="approve"), config=config) + print(final_result) + ``` + ::: + + :::js + ```typescript + import { z } from "zod"; + import { v4 as uuidv4 } from "uuid"; + import { + StateGraph, + START, + END, + interrupt, + Command, + MemorySaver + } from "@langchain/langgraph"; + + // Define the shared graph state + const StateAnnotation = z.object({ + llmOutput: z.string(), + decision: z.string(), + }); + + // Simulate an LLM output node + function generateLlmOutput(state: z.infer) { + return { llmOutput: "This is the generated output." }; + } + + // Human approval node + function humanApproval(state: z.infer): Command { + const decision = interrupt({ + question: "Do you approve the following output?", + llmOutput: state.llmOutput + }); + + if (decision === "approve") { + return new Command({ + goto: "approvedPath", + update: { decision: "approved" } + }); + } else { + return new Command({ + goto: "rejectedPath", + update: { decision: "rejected" } + }); + } + } + + // Next steps after approval + function approvedNode(state: z.infer) { + console.log("✅ Approved path taken."); + return state; + } + + // Alternative path after rejection + function rejectedNode(state: z.infer) { + console.log("❌ Rejected path taken."); + return state; + } + + // Build the graph + const builder = new StateGraph(StateAnnotation) + .addNode("generateLlmOutput", generateLlmOutput) + .addNode("humanApproval", humanApproval, { + ends: ["approvedPath", "rejectedPath"] + }) + .addNode("approvedPath", approvedNode) + .addNode("rejectedPath", rejectedNode) + .addEdge(START, "generateLlmOutput") + .addEdge("generateLlmOutput", "humanApproval") + .addEdge("approvedPath", END) + .addEdge("rejectedPath", END); + + const checkpointer = new MemorySaver(); + const graph = builder.compile({ checkpointer }); + + // Run until interrupt + const config = { configurable: { thread_id: uuidv4() } }; + const result = await graph.invoke({}, config); + console.log(result.__interrupt__); + // Output: + // [{ + // value: { + // question: 'Do you approve the following output?', + // llmOutput: 'This is the generated output.' + // }, + // ... + // }] + + // Simulate resuming with human input + // To test rejection, replace resume: "approve" with resume: "reject" + const finalResult = await graph.invoke( + new Command({ resume: "approve" }), + config + ); + console.log(finalResult); + ``` + ::: + + +### Review and edit state + +![A human can review and edit the state of the graph. This is useful for correcting mistakes or updating the state with additional information](/oss/images/edit-graph-state-simple.png) + +:::python +```python +from langgraph.types import interrupt + +def human_editing(state: State): + ... + result = interrupt( + # Interrupt information to surface to the client. + # Can be any JSON serializable value. + { + "task": "Review the output from the LLM and make any necessary edits.", + "llm_generated_summary": state["llm_generated_summary"] + } + ) + + # Update the state with the edited text + return { + "llm_generated_summary": result["edited_text"] + } + +# Add the node to the graph in an appropriate location +# and connect it to the relevant nodes. +graph_builder.add_node("human_editing", human_editing) +graph = graph_builder.compile(checkpointer=checkpointer) + +... + +# After running the graph and hitting the interrupt, the graph will pause. +# Resume it with the edited text. +thread_config = {"configurable": {"thread_id": "some_id"}} +graph.invoke( + Command(resume={"edited_text": "The edited text"}), + config=thread_config +) +``` +::: + +:::js +```typescript +import { interrupt } from "@langchain/langgraph"; + +function humanEditing(state: z.infer) { + const result = interrupt({ + // Interrupt information to surface to the client. + // Can be any JSON serializable value. + task: "Review the output from the LLM and make any necessary edits.", + llmGeneratedSummary: state.llmGeneratedSummary, + }); + + // Update the state with the edited text + return { + llmGeneratedSummary: result.editedText, + }; +} + +// Add the node to the graph in an appropriate location +// and connect it to the relevant nodes. +graphBuilder.addNode("humanEditing", humanEditing); +const graph = graphBuilder.compile({ checkpointer }); + +// After running the graph and hitting the interrupt, the graph will pause. +// Resume it with the edited text. +const threadConfig = { configurable: { thread_id: "some_id" } }; +await graph.invoke( + new Command({ resume: { editedText: "The edited text" } }), + threadConfig +); +``` +::: + + + :::python + ```python + from typing import TypedDict + import uuid + + from langgraph.constants import START, END + from langgraph.graph import StateGraph + from langgraph.types import interrupt, Command + from langgraph.checkpoint.memory import InMemorySaver + + # Define the graph state + class State(TypedDict): + summary: str + + # Simulate an LLM summary generation + def generate_summary(state: State) -> State: + return { + "summary": "The cat sat on the mat and looked at the stars." + } + + # Human editing node + def human_review_edit(state: State) -> State: + result = interrupt({ + "task": "Please review and edit the generated summary if necessary.", + "generated_summary": state["summary"] + }) + return { + "summary": result["edited_summary"] + } + + # Simulate downstream use of the edited summary + def downstream_use(state: State) -> State: + print(f"✅ Using edited summary: {state['summary']}") + return state + + # Build the graph + builder = StateGraph(State) + builder.add_node("generate_summary", generate_summary) + builder.add_node("human_review_edit", human_review_edit) + builder.add_node("downstream_use", downstream_use) + + builder.set_entry_point("generate_summary") + builder.add_edge("generate_summary", "human_review_edit") + builder.add_edge("human_review_edit", "downstream_use") + builder.add_edge("downstream_use", END) + + # Set up in-memory checkpointing for interrupt support + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + + # Invoke the graph until it hits the interrupt + config = {"configurable": {"thread_id": uuid.uuid4()}} + result = graph.invoke({}, config=config) + + # Output interrupt payload + print(result["__interrupt__"]) + # Example output: + # > [ + # > Interrupt( + # > value={ + # > 'task': 'Please review and edit the generated summary if necessary.', + # > 'generated_summary': 'The cat sat on the mat and looked at the stars.' + # > }, + # > id='...' + # > ) + # > ] + + # Resume the graph with human-edited input + edited_summary = "The cat lay on the rug, gazing peacefully at the night sky." + resumed_result = graph.invoke( + Command(resume={"edited_summary": edited_summary}), + config=config + ) + print(resumed_result) + ``` + ::: + + :::js + ```typescript + import { z } from "zod"; + import { v4 as uuidv4 } from "uuid"; + import { + StateGraph, + START, + END, + interrupt, + Command, + MemorySaver + } from "@langchain/langgraph"; + + // Define the graph state + const StateAnnotation = z.object({ + summary: z.string(), + }); + + // Simulate an LLM summary generation + function generateSummary(state: z.infer) { + return { + summary: "The cat sat on the mat and looked at the stars." + }; + } + + // Human editing node + function humanReviewEdit(state: z.infer) { + const result = interrupt({ + task: "Please review and edit the generated summary if necessary.", + generatedSummary: state.summary + }); + return { + summary: result.editedSummary + }; + } + + // Simulate downstream use of the edited summary + function downstreamUse(state: z.infer) { + console.log(`✅ Using edited summary: ${state.summary}`); + return state; + } + + // Build the graph + const builder = new StateGraph(StateAnnotation) + .addNode("generateSummary", generateSummary) + .addNode("humanReviewEdit", humanReviewEdit) + .addNode("downstreamUse", downstreamUse) + .addEdge(START, "generateSummary") + .addEdge("generateSummary", "humanReviewEdit") + .addEdge("humanReviewEdit", "downstreamUse") + .addEdge("downstreamUse", END); + + // Set up in-memory checkpointing for interrupt support + const checkpointer = new MemorySaver(); + const graph = builder.compile({ checkpointer }); + + // Invoke the graph until it hits the interrupt + const config = { configurable: { thread_id: uuidv4() } }; + const result = await graph.invoke({}, config); + + // Output interrupt payload + console.log(result.__interrupt__); + // Example output: + // [{ + // value: { + // task: 'Please review and edit the generated summary if necessary.', + // generatedSummary: 'The cat sat on the mat and looked at the stars.' + // }, + // resumable: true, + // ... + // }] + + // Resume the graph with human-edited input + const editedSummary = "The cat lay on the rug, gazing peacefully at the night sky."; + const resumedResult = await graph.invoke( + new Command({ resume: { editedSummary } }), + config + ); + console.log(resumedResult); + ``` + ::: + + +### Review tool calls + +![A human can review and edit the output from the LLM before proceeding. This is particularly critical in applications where the tool calls requested by the LLM may be sensitive or require human oversight.](/oss/images/tool-call-review.png) + +To add a human approval step to a tool: + +1. Use `interrupt()` in the tool to pause execution. +2. Resume with a `Command` to continue based on human input. + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.types import interrupt +from langgraph.prebuilt import create_react_agent + +# An example of a sensitive tool that requires human review / approval +def book_hotel(hotel_name: str): + """Book a hotel""" + # highlight-next-line + response = interrupt( # (1)! + f"Trying to call `book_hotel` with args {{'hotel_name': {hotel_name}}}. " + "Please approve or suggest edits." + ) + if response["type"] == "accept": + pass + elif response["type"] == "edit": + hotel_name = response["args"]["hotel_name"] + else: + raise ValueError(f"Unknown response type: {response['type']}") + return f"Successfully booked a stay at {hotel_name}." + +# highlight-next-line +checkpointer = InMemorySaver() # (2)! + +agent = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + tools=[book_hotel], + # highlight-next-line + checkpointer=checkpointer, # (3)! +) +``` + +1. The @[`interrupt` function][interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback). +2. The `InMemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](/oss/add-memory#add-short-term-memory) and [human-in-the-loop](/oss/human-in-the-loop) capabilities. In this example, we use `InMemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database. +3. Initialize the agent with the `checkpointer`. +::: + +:::js +```typescript +import { MemorySaver } from "@langchain/langgraph"; +import { interrupt } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// An example of a sensitive tool that requires human review / approval +const bookHotel = tool( + async ({ hotelName }) => { + // highlight-next-line + const response = interrupt( + // (1)! + `Trying to call \`bookHotel\` with args {"hotelName": "${hotelName}"}. ` + + "Please approve or suggest edits." + ); + if (response.type === "accept") { + // Continue with original args + } else if (response.type === "edit") { + hotelName = response.args.hotelName; + } else { + throw new Error(`Unknown response type: ${response.type}`); + } + return `Successfully booked a stay at ${hotelName}.`; + }, + { + name: "bookHotel", + description: "Book a hotel", + schema: z.object({ + hotelName: z.string(), + }), + } +); + +// highlight-next-line +const checkpointer = new MemorySaver(); // (2)! + +const agent = createReactAgent({ + llm: model, + tools: [bookHotel], + // highlight-next-line + checkpointSaver: checkpointer, // (3)! +}); +``` + +1. The @[`interrupt` function][interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback). +2. The `MemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](/oss/add-memory#add-short-term-memory) and [human-in-the-loop](/oss/human-in-the-loop) capabilities. In this example, we use `MemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database. +3. Initialize the agent with the `checkpointSaver`. +::: + +Run the agent with the `stream()` method, passing the `config` object to specify the thread ID. This allows the agent to resume the same conversation on future invocations. + +:::python +```python +config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } +} + +for chunk in agent.stream( + {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]}, + # highlight-next-line + config +): + print(chunk) + print("\n") +``` +::: + +:::js +```typescript +const config = { + configurable: { + // highlight-next-line + thread_id: "1", + }, +}; + +const stream = await agent.stream( + { messages: [{ role: "user", content: "book a stay at McKittrick hotel" }] }, + // highlight-next-line + config +); + +for await (const chunk of stream) { + console.log(chunk); + console.log("\n"); +} +``` +::: + +> You should see that the agent runs until it reaches the `interrupt()` call, at which point it pauses and waits for human input. + +Resume the agent with a `Command` to continue based on human input. + +:::python +```python +from langgraph.types import Command + +for chunk in agent.stream( + # highlight-next-line + Command(resume={"type": "accept"}), # (1)! + # Command(resume={"type": "edit", "args": {"hotel_name": "McKittrick Hotel"}}), + config +): + print(chunk) + print("\n") +``` + +1. The @[`interrupt` function][interrupt] is used in conjunction with the @[`Command`][Command] object to resume the graph with a value provided by the human. +::: + +:::js +```typescript +import { Command } from "@langchain/langgraph"; + +const resumeStream = await agent.stream( + // highlight-next-line + new Command({ resume: { type: "accept" } }), // (1)! + // new Command({ resume: { type: "edit", args: { hotelName: "McKittrick Hotel" } } }), + config +); + +for await (const chunk of resumeStream) { + console.log(chunk); + console.log("\n"); +} +``` + +1. The @[`interrupt` function][interrupt] is used in conjunction with the @[`Command`][Command] object to resume the graph with a value provided by the human. +::: + +### Add interrupts to any tool + +You can create a wrapper to add interrupts to _any_ tool. The example below provides a reference implementation compatible with [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox) and [Agent Chat UI](https://github.com/langchain-ai/agent-chat-ui). + +:::python +```python title="Wrapper that adds human-in-the-loop to any tool" +from typing import Callable +from langchain_core.tools import BaseTool, tool as create_tool +from langchain_core.runnables import RunnableConfig +from langgraph.types import interrupt +from langgraph.prebuilt.interrupt import HumanInterruptConfig, HumanInterrupt + +def add_human_in_the_loop( + tool: Callable | BaseTool, + *, + interrupt_config: HumanInterruptConfig = None, +) -> BaseTool: + """Wrap a tool to support human-in-the-loop review.""" + if not isinstance(tool, BaseTool): + tool = create_tool(tool) + + if interrupt_config is None: + interrupt_config = { + "allow_accept": True, + "allow_edit": True, + "allow_respond": True, + } + + @create_tool( # (1)! + tool.name, + description=tool.description, + args_schema=tool.args_schema + ) + def call_tool_with_interrupt(config: RunnableConfig, **tool_input): + request: HumanInterrupt = { + "action_request": { + "action": tool.name, + "args": tool_input + }, + "config": interrupt_config, + "description": "Please review the tool call" + } + # highlight-next-line + response = interrupt([request])[0] # (2)! + # approve the tool call + if response["type"] == "accept": + tool_response = tool.invoke(tool_input, config) + # update tool call args + elif response["type"] == "edit": + tool_input = response["args"]["args"] + tool_response = tool.invoke(tool_input, config) + # respond to the LLM with user feedback + elif response["type"] == "response": + user_feedback = response["args"] + tool_response = user_feedback + else: + raise ValueError(f"Unsupported interrupt response type: {response['type']}") + + return tool_response + + return call_tool_with_interrupt +``` + +1. This wrapper creates a new tool that calls `interrupt()` **before** executing the wrapped tool. +2. `interrupt()` is using special input and output format that's expected by [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox): - a list of @[`HumanInterrupt`][HumanInterrupt] objects is sent to `AgentInbox` render interrupt information to the end user - resume value is provided by `AgentInbox` as a list (i.e., `Command(resume=[...])`) +::: + +:::js +```typescript title="Wrapper that adds human-in-the-loop to any tool" +import { StructuredTool, tool } from "@langchain/core/tools"; +import { RunnableConfig } from "@langchain/core/runnables"; +import { interrupt } from "@langchain/langgraph"; + +interface HumanInterruptConfig { + allowAccept?: boolean; + allowEdit?: boolean; + allowRespond?: boolean; +} + +interface HumanInterrupt { + actionRequest: { + action: string; + args: Record; + }; + config: HumanInterruptConfig; + description: string; +} + +function addHumanInTheLoop( + originalTool: StructuredTool, + interruptConfig: HumanInterruptConfig = { + allowAccept: true, + allowEdit: true, + allowRespond: true, + } +): StructuredTool { + // Wrap the original tool to support human-in-the-loop review + return tool( + // (1)! + async (toolInput: Record, config?: RunnableConfig) => { + const request: HumanInterrupt = { + actionRequest: { + action: originalTool.name, + args: toolInput, + }, + config: interruptConfig, + description: "Please review the tool call", + }; + + // highlight-next-line + const response = interrupt([request])[0]; // (2)! + + // approve the tool call + if (response.type === "accept") { + return await originalTool.invoke(toolInput, config); + } + // update tool call args + else if (response.type === "edit") { + const updatedArgs = response.args.args; + return await originalTool.invoke(updatedArgs, config); + } + // respond to the LLM with user feedback + else if (response.type === "response") { + return response.args; + } else { + throw new Error( + `Unsupported interrupt response type: ${response.type}` + ); + } + }, + { + name: originalTool.name, + description: originalTool.description, + schema: originalTool.schema, + } + ); +} +``` + +1. This wrapper creates a new tool that calls `interrupt()` **before** executing the wrapped tool. +2. `interrupt()` is using special input and output format that's expected by [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox): - a list of [`HumanInterrupt`] objects is sent to `AgentInbox` render interrupt information to the end user - resume value is provided by `AgentInbox` as a list (i.e., `Command({ resume: [...] })`) +::: + +You can use the wrapper to add `interrupt()` to any tool without having to add it _inside_ the tool: + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.prebuilt import create_react_agent + +# highlight-next-line +checkpointer = InMemorySaver() + +def book_hotel(hotel_name: str): + """Book a hotel""" + return f"Successfully booked a stay at {hotel_name}." + + +agent = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + tools=[ + # highlight-next-line + add_human_in_the_loop(book_hotel), # (1)! + ], + # highlight-next-line + checkpointer=checkpointer, +) + +config = {"configurable": {"thread_id": "1"}} + +# Run the agent +for chunk in agent.stream( + {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]}, + # highlight-next-line + config +): + print(chunk) + print("\n") +``` + +1. The `add_human_in_the_loop` wrapper is used to add `interrupt()` to the tool. This allows the agent to pause execution and wait for human input before proceeding with the tool call. +::: + +:::js +```typescript +import { MemorySaver } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const checkpointer = new MemorySaver(); + +const bookHotel = tool( + async ({ hotelName }) => { + return `Successfully booked a stay at ${hotelName}.`; + }, + { + name: "bookHotel", + description: "Book a hotel", + schema: z.object({ + hotelName: z.string(), + }), + } +); + +const agent = createReactAgent({ + llm: model, + tools: [ + // highlight-next-line + addHumanInTheLoop(bookHotel), // (1)! + ], + // highlight-next-line + checkpointSaver: checkpointer, +}); + +const config = { configurable: { thread_id: "1" } }; + +// Run the agent +const stream = await agent.stream( + { messages: [{ role: "user", content: "book a stay at McKittrick hotel" }] }, + // highlight-next-line + config +); + +for await (const chunk of stream) { + console.log(chunk); + console.log("\n"); +} +``` + +1. The `addHumanInTheLoop` wrapper is used to add `interrupt()` to the tool. This allows the agent to pause execution and wait for human input before proceeding with the tool call. +::: + +> You should see that the agent runs until it reaches the `interrupt()` call, +> at which point it pauses and waits for human input. + +Resume the agent with a `Command` to continue based on human input. + +:::python +```python +from langgraph.types import Command + +for chunk in agent.stream( + # highlight-next-line + Command(resume=[{"type": "accept"}]), + # Command(resume=[{"type": "edit", "args": {"args": {"hotel_name": "McKittrick Hotel"}}}]), + config +): + print(chunk) + print("\n") +``` +::: + +:::js +```typescript +import { Command } from "@langchain/langgraph"; + +const resumeStream = await agent.stream( + // highlight-next-line + new Command({ resume: [{ type: "accept" }] }), + // new Command({ resume: [{ type: "edit", args: { args: { hotelName: "McKittrick Hotel" } } }] }), + config +); + +for await (const chunk of resumeStream) { + console.log(chunk); + console.log("\n"); +} +``` +::: + +### Validate human input + +If you need to validate the input provided by the human within the graph itself (rather than on the client side), you can achieve this by using multiple interrupt calls within a single node. + +:::python +```python +from langgraph.types import interrupt + +def human_node(state: State): + """Human node with validation.""" + question = "What is your age?" + + while True: + answer = interrupt(question) + + # Validate answer, if the answer isn't valid ask for input again. + if not isinstance(answer, int) or answer < 0: + question = f"'{answer} is not a valid age. What is your age?" + answer = None + continue + else: + # If the answer is valid, we can proceed. + break + + print(f"The human in the loop is {answer} years old.") + return { + "age": answer + } +``` +::: + +:::js +```typescript +import { interrupt } from "@langchain/langgraph"; + +graphBuilder.addNode("humanNode", (state) => { + // Human node with validation. + let question = "What is your age?"; + + while (true) { + const answer = interrupt(question); + + // Validate answer, if the answer isn't valid ask for input again. + if (typeof answer !== "number" || answer < 0) { + question = `'${answer}' is not a valid age. What is your age?`; + continue; + } else { + // If the answer is valid, we can proceed. + break; + } + } + + console.log(`The human in the loop is ${answer} years old.`); + return { + age: answer, + }; +}); +``` +::: + + + :::python + ```python + from typing import TypedDict + import uuid + + from langgraph.constants import START, END + from langgraph.graph import StateGraph + from langgraph.types import interrupt, Command + from langgraph.checkpoint.memory import InMemorySaver + + # Define graph state + class State(TypedDict): + age: int + + # Node that asks for human input and validates it + def get_valid_age(state: State) -> State: + prompt = "Please enter your age (must be a non-negative integer)." + + while True: + user_input = interrupt(prompt) + + # Validate the input + try: + age = int(user_input) + if age < 0: + raise ValueError("Age must be non-negative.") + break # Valid input received + except (ValueError, TypeError): + prompt = f"'{user_input}' is not valid. Please enter a non-negative integer for age." + + return {"age": age} + + # Node that uses the valid input + def report_age(state: State) -> State: + print(f"✅ Human is {state['age']} years old.") + return state + + # Build the graph + builder = StateGraph(State) + builder.add_node("get_valid_age", get_valid_age) + builder.add_node("report_age", report_age) + + builder.set_entry_point("get_valid_age") + builder.add_edge("get_valid_age", "report_age") + builder.add_edge("report_age", END) + + # Create the graph with a memory checkpointer + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + + # Run the graph until the first interrupt + config = {"configurable": {"thread_id": uuid.uuid4()}} + result = graph.invoke({}, config=config) + print(result["__interrupt__"]) # First prompt: "Please enter your age..." + + # Simulate an invalid input (e.g., string instead of integer) + result = graph.invoke(Command(resume="not a number"), config=config) + print(result["__interrupt__"]) # Follow-up prompt with validation message + + # Simulate a second invalid input (e.g., negative number) + result = graph.invoke(Command(resume="-10"), config=config) + print(result["__interrupt__"]) # Another retry + + # Provide valid input + final_result = graph.invoke(Command(resume="25"), config=config) + print(final_result) # Should include the valid age + ``` + ::: + + :::js + ```typescript + import { z } from "zod"; + import { v4 as uuidv4 } from "uuid"; + import { + StateGraph, + START, + END, + interrupt, + Command, + MemorySaver + } from "@langchain/langgraph"; + + // Define graph state + const StateAnnotation = z.object({ + age: z.number(), + }); + + // Node that asks for human input and validates it + function getValidAge(state: z.infer) { + let prompt = "Please enter your age (must be a non-negative integer)."; + + while (true) { + const userInput = interrupt(prompt); + + // Validate the input + try { + const age = parseInt(userInput as string); + if (isNaN(age) || age < 0) { + throw new Error("Age must be non-negative."); + } + return { age }; + } catch (error) { + prompt = `'${userInput}' is not valid. Please enter a non-negative integer for age.`; + } + } + } + + // Node that uses the valid input + function reportAge(state: z.infer) { + console.log(`✅ Human is ${state.age} years old.`); + return state; + } + + // Build the graph + const builder = new StateGraph(StateAnnotation) + .addNode("getValidAge", getValidAge) + .addNode("reportAge", reportAge) + .addEdge(START, "getValidAge") + .addEdge("getValidAge", "reportAge") + .addEdge("reportAge", END); + + // Create the graph with a memory checkpointer + const checkpointer = new MemorySaver(); + const graph = builder.compile({ checkpointer }); + + // Run the graph until the first interrupt + const config = { configurable: { thread_id: uuidv4() } }; + let result = await graph.invoke({}, config); + console.log(result.__interrupt__); // First prompt: "Please enter your age..." + + // Simulate an invalid input (e.g., string instead of integer) + result = await graph.invoke(new Command({ resume: "not a number" }), config); + console.log(result.__interrupt__); // Follow-up prompt with validation message + + // Simulate a second invalid input (e.g., negative number) + result = await graph.invoke(new Command({ resume: "-10" }), config); + console.log(result.__interrupt__); // Another retry + + // Provide valid input + const finalResult = await graph.invoke(new Command({ resume: "25" }), config); + console.log(finalResult); // Should include the valid age + ``` + ::: + + +:::python +## Debug with interrupts + +To debug and test a graph, use [static interrupts](/oss/human-in-the-loop#key-capabilities) (also known as static breakpoints) to step through the graph execution one node at a time or to pause the graph execution at specific nodes. Static interrupts are triggered at defined points either before or after a node executes. You can set static interrupts by specifying `interrupt_before` and `interrupt_after` at compile time or run time. + + + Static interrupts are **not** recommended for human-in-the-loop workflows. Use [dynamic interrupts](#pause-using-interrupt) instead. + + + + + ```python + # highlight-next-line + graph = graph_builder.compile( # (1)! + # highlight-next-line + interrupt_before=["node_a"], # (2)! + # highlight-next-line + interrupt_after=["node_b", "node_c"], # (3)! + checkpointer=checkpointer, # (4)! + ) + + config = { + "configurable": { + "thread_id": "some_thread" + } + } + + # Run the graph until the breakpoint + graph.invoke(inputs, config=thread_config) # (5)! + + # Resume the graph + graph.invoke(None, config=thread_config) # (6)! + ``` + + 1. The breakpoints are set during `compile` time. + 2. `interrupt_before` specifies the nodes where execution should pause before the node is executed. + 3. `interrupt_after` specifies the nodes where execution should pause after the node is executed. + 4. A checkpointer is required to enable breakpoints. + 5. The graph is run until the first breakpoint is hit. + 6. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit. + + + ```python + # highlight-next-line + graph.invoke( # (1)! + inputs, + # highlight-next-line + interrupt_before=["node_a"], # (2)! + # highlight-next-line + interrupt_after=["node_b", "node_c"] # (3)! + config={ + "configurable": {"thread_id": "some_thread"} + }, + ) + + config = { + "configurable": { + "thread_id": "some_thread" + } + } + + # Run the graph until the breakpoint + graph.invoke(inputs, config=config) # (4)! + + # Resume the graph + graph.invoke(None, config=config) # (5)! + ``` + + 1. `graph.invoke` is called with the `interrupt_before` and `interrupt_after` parameters. This is a run-time configuration and can be changed for every invocation. + 2. `interrupt_before` specifies the nodes where execution should pause before the node is executed. + 3. `interrupt_after` specifies the nodes where execution should pause after the node is executed. + 4. The graph is run until the first breakpoint is hit. + 5. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit. + + + You cannot set static breakpoints at runtime for **sub-graphs**. + If you have a sub-graph, you must set the breakpoints at compilation time. + + + + + + + ```python + from IPython.display import Image, display + from typing_extensions import TypedDict + + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import StateGraph, START, END + + + class State(TypedDict): + input: str + + + def step_1(state): + print("---Step 1---") + pass + + + def step_2(state): + print("---Step 2---") + pass + + + def step_3(state): + print("---Step 3---") + pass + + + builder = StateGraph(State) + builder.add_node("step_1", step_1) + builder.add_node("step_2", step_2) + builder.add_node("step_3", step_3) + builder.add_edge(START, "step_1") + builder.add_edge("step_1", "step_2") + builder.add_edge("step_2", "step_3") + builder.add_edge("step_3", END) + + # Set up a checkpointer + checkpointer = InMemorySaver() # (1)! + + graph = builder.compile( + checkpointer=checkpointer, # (2)! + interrupt_before=["step_3"] # (3)! + ) + + # View + display(Image(graph.get_graph().draw_mermaid_png())) + + + # Input + initial_input = {"input": "hello world"} + + # Thread + thread = {"configurable": {"thread_id": "1"}} + + # Run the graph until the first interruption + for event in graph.stream(initial_input, thread, stream_mode="values"): + print(event) + + # This will run until the breakpoint + # You can get the state of the graph at this point + print(graph.get_state(config)) + + # You can continue the graph execution by passing in `None` for the input + for event in graph.stream(None, thread, stream_mode="values"): + print(event) + ``` + + +### Use static interrupts in LangGraph Studio + +You can use [LangGraph Studio](/langgraph-platform/langgraph-studio) to debug your graph. You can set static breakpoints in the UI and then run the graph. You can also use the UI to inspect the graph state at any point in the execution. + +![image](/oss/images/static-interrupt.png) + +LangGraph Studio is free with [locally deployed applications](/oss/local-server) using `langgraph dev`. +::: + +## Debug with interrupts + +To debug and test a graph, use [static interrupts](/oss/human-in-the-loop#key-capabilities) (also known as static breakpoints) to step through the graph execution one node at a time or to pause the graph execution at specific nodes. Static interrupts are triggered at defined points either before or after a node executes. You can set static interrupts by specifying `interrupt_before` and `interrupt_after` at compile time or run time. + + + Static interrupts are **not** recommended for human-in-the-loop workflows. Use [dynamic interrupts](#pause-using-interrupt) instead. + + + + + + ```python + # highlight-next-line + graph = graph_builder.compile( # (1)! + # highlight-next-line + interrupt_before=["node_a"], # (2)! + # highlight-next-line + interrupt_after=["node_b", "node_c"], # (3)! + checkpointer=checkpointer, # (4)! + ) + + config = { + "configurable": { + "thread_id": "some_thread" + } + } + + # Run the graph until the breakpoint + graph.invoke(inputs, config=thread_config) # (5)! + + # Resume the graph + graph.invoke(None, config=thread_config) # (6)! + ``` + + 1. The breakpoints are set during `compile` time. + 2. `interrupt_before` specifies the nodes where execution should pause before the node is executed. + 3. `interrupt_after` specifies the nodes where execution should pause after the node is executed. + 4. A checkpointer is required to enable breakpoints. + 5. The graph is run until the first breakpoint is hit. + 6. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit. + + + + + ```python + # highlight-next-line + graph.invoke( # (1)! + inputs, + # highlight-next-line + interrupt_before=["node_a"], # (2)! + # highlight-next-line + interrupt_after=["node_b", "node_c"] # (3)! + config={ + "configurable": {"thread_id": "some_thread"} + }, + ) + + config = { + "configurable": { + "thread_id": "some_thread" + } + } + + # Run the graph until the breakpoint + graph.invoke(inputs, config=config) # (4)! + + # Resume the graph + graph.invoke(None, config=config) # (5)! + ``` + + 1. `graph.invoke` is called with the `interrupt_before` and `interrupt_after` parameters. This is a run-time configuration and can be changed for every invocation. + 2. `interrupt_before` specifies the nodes where execution should pause before the node is executed. + 3. `interrupt_after` specifies the nodes where execution should pause after the node is executed. + 4. The graph is run until the first breakpoint is hit. + 5. The graph is resumed by passing in `None` for the input. This will run the graph until the next breakpoint is hit. + + + You cannot set static breakpoints at runtime for **sub-graphs**. + If you have a sub-graph, you must set the breakpoints at compilation time. + + + + + + + + ```python + from IPython.display import Image, display + from typing_extensions import TypedDict + + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import StateGraph, START, END + + + class State(TypedDict): + input: str + + + def step_1(state): + print("---Step 1---") + pass + + + def step_2(state): + print("---Step 2---") + pass + + + def step_3(state): + print("---Step 3---") + pass + + + builder = StateGraph(State) + builder.add_node("step_1", step_1) + builder.add_node("step_2", step_2) + builder.add_node("step_3", step_3) + builder.add_edge(START, "step_1") + builder.add_edge("step_1", "step_2") + builder.add_edge("step_2", "step_3") + builder.add_edge("step_3", END) + + # Set up a checkpointer + checkpointer = InMemorySaver() # (1)! + + graph = builder.compile( + checkpointer=checkpointer, # (2)! + interrupt_before=["step_3"] # (3)! + ) + + # View + display(Image(graph.get_graph().draw_mermaid_png())) + + + # Input + initial_input = {"input": "hello world"} + + # Thread + thread = {"configurable": {"thread_id": "1"}} + + # Run the graph until the first interruption + for event in graph.stream(initial_input, thread, stream_mode="values"): + print(event) + + # This will run until the breakpoint + # You can get the state of the graph at this point + print(graph.get_state(config)) + + # You can continue the graph execution by passing in `None` for the input + for event in graph.stream(None, thread, stream_mode="values"): + print(event) + ``` + + + +### Use static interrupts in LangGraph Studio + +You can use [LangGraph Studio](/langgraph-platform/langgraph-studio) to debug your graph. You can set static breakpoints in the UI and then run the graph. You can also use the UI to inspect the graph state at any point in the execution. + +![image](/oss/images/static-interrupt.png) + +LangGraph Studio is free with [locally deployed applications](/langgraph-platform/local-server) using `langgraph dev`. + +## Considerations + +When using human-in-the-loop, there are some considerations to keep in mind. + +### Using with code with side-effects + +Place code with side effects, such as API calls, after the `interrupt` or in a separate node to avoid duplication, as these are re-triggered every time the node is resumed. + + + + + :::python + ```python + from langgraph.types import interrupt + + def human_node(state: State): + """Human node with validation.""" + + answer = interrupt(question) + + api_call(answer) # OK as it's after the interrupt + ``` + ::: + + :::js + ```typescript + import { interrupt } from "@langchain/langgraph"; + + function humanNode(state: z.infer) { + // Human node with validation. + + const answer = interrupt(question); + + apiCall(answer); // OK as it's after the interrupt + } + ``` + ::: + + + + + :::python + ```python + from langgraph.types import interrupt + + def human_node(state: State): + """Human node with validation.""" + + answer = interrupt(question) + + return { + "answer": answer + } + + def api_call_node(state: State): + api_call(...) # OK as it's in a separate node + ``` + ::: + + :::js + ```typescript + import { interrupt } from "@langchain/langgraph"; + + function humanNode(state: z.infer) { + // Human node with validation. + + const answer = interrupt(question); + + return { + answer + }; + } + + function apiCallNode(state: z.infer) { + apiCall(state.answer); // OK as it's in a separate node + } + ``` + ::: + + + + +### Using with subgraphs called as functions + +When invoking a subgraph as a function, the parent graph will resume execution from the **beginning of the node** where the subgraph was invoked where the `interrupt` was triggered. Similarly, the **subgraph** will resume from the **beginning of the node** where the `interrupt()` function was called. + +:::python +```python +def node_in_parent_graph(state: State): + some_code() # <-- This will re-execute when the subgraph is resumed. + # Invoke a subgraph as a function. + # The subgraph contains an `interrupt` call. + subgraph_result = subgraph.invoke(some_input) + ... +``` +::: + +:::js +```typescript +async function nodeInParentGraph(state: z.infer) { + someCode(); // <-- This will re-execute when the subgraph is resumed. + // Invoke a subgraph as a function. + // The subgraph contains an `interrupt` call. + const subgraphResult = await subgraph.invoke(someInput); + // ... +} +``` +::: + + + + Say we have a parent graph with 3 nodes: + + **Parent Graph**: `node_1` → `node_2` (subgraph call) → `node_3` + + And the subgraph has 3 nodes, where the second node contains an `interrupt`: + + **Subgraph**: `sub_node_1` → `sub_node_2` (`interrupt`) → `sub_node_3` + + When resuming the graph, the execution will proceed as follows: + + 1. **Skip `node_1`** in the parent graph (already executed, graph state was saved in snapshot). + 2. **Re-execute `node_2`** in the parent graph from the start. + 3. **Skip `sub_node_1`** in the subgraph (already executed, graph state was saved in snapshot). + 4. **Re-execute `sub_node_2`** in the subgraph from the beginning. + 5. Continue with `sub_node_3` and subsequent nodes. + + Here is abbreviated example code that you can use to understand how subgraphs work with interrupts. + It counts the number of times each node is entered and prints the count. + + :::python + ```python + import uuid + from typing import TypedDict + + from langgraph.graph import StateGraph + from langgraph.constants import START + from langgraph.types import interrupt, Command + from langgraph.checkpoint.memory import InMemorySaver + + + class State(TypedDict): + """The graph state.""" + state_counter: int + + + counter_node_in_subgraph = 0 + + def node_in_subgraph(state: State): + """A node in the sub-graph.""" + global counter_node_in_subgraph + counter_node_in_subgraph += 1 # This code will **NOT** run again! + print(f"Entered `node_in_subgraph` a total of {counter_node_in_subgraph} times") + + counter_human_node = 0 + + def human_node(state: State): + global counter_human_node + counter_human_node += 1 # This code will run again! + print(f"Entered human_node in sub-graph a total of {counter_human_node} times") + answer = interrupt("what is your name?") + print(f"Got an answer of {answer}") + + + checkpointer = InMemorySaver() + + subgraph_builder = StateGraph(State) + subgraph_builder.add_node("some_node", node_in_subgraph) + subgraph_builder.add_node("human_node", human_node) + subgraph_builder.add_edge(START, "some_node") + subgraph_builder.add_edge("some_node", "human_node") + subgraph = subgraph_builder.compile(checkpointer=checkpointer) + + + counter_parent_node = 0 + + def parent_node(state: State): + """This parent node will invoke the subgraph.""" + global counter_parent_node + + counter_parent_node += 1 # This code will run again on resuming! + print(f"Entered `parent_node` a total of {counter_parent_node} times") + + # Please note that we're intentionally incrementing the state counter + # in the graph state as well to demonstrate that the subgraph update + # of the same key will not conflict with the parent graph (until + subgraph_state = subgraph.invoke(state) + return subgraph_state + + + builder = StateGraph(State) + builder.add_node("parent_node", parent_node) + builder.add_edge(START, "parent_node") + + # A checkpointer must be enabled for interrupts to work! + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + "thread_id": uuid.uuid4(), + } + } + + for chunk in graph.stream({"state_counter": 1}, config): + print(chunk) + + print('--- Resuming ---') + + for chunk in graph.stream(Command(resume="35"), config): + print(chunk) + ``` + + This will print out + + ```pycon + Entered `parent_node` a total of 1 times + Entered `node_in_subgraph` a total of 1 times + Entered human_node in sub-graph a total of 1 times + {'__interrupt__': (Interrupt(value='what is your name?', id='...'),)} + --- Resuming --- + Entered `parent_node` a total of 2 times + Entered human_node in sub-graph a total of 2 times + Got an answer of 35 + {'parent_node': {'state_counter': 1}} + ``` + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { + StateGraph, + START, + interrupt, + Command, + MemorySaver + } from "@langchain/langgraph"; + import { z } from "zod"; + + const StateAnnotation = z.object({ + stateCounter: z.number(), + }); + + // Global variable to track the number of attempts + let counterNodeInSubgraph = 0; + + function nodeInSubgraph(state: z.infer) { + // A node in the sub-graph. + counterNodeInSubgraph += 1; // This code will **NOT** run again! + console.log(`Entered 'nodeInSubgraph' a total of ${counterNodeInSubgraph} times`); + return {}; + } + + let counterHumanNode = 0; + + function humanNode(state: z.infer) { + counterHumanNode += 1; // This code will run again! + console.log(`Entered humanNode in sub-graph a total of ${counterHumanNode} times`); + const answer = interrupt("what is your name?"); + console.log(`Got an answer of ${answer}`); + return {}; + } + + const checkpointer = new MemorySaver(); + + const subgraphBuilder = new StateGraph(StateAnnotation) + .addNode("someNode", nodeInSubgraph) + .addNode("humanNode", humanNode) + .addEdge(START, "someNode") + .addEdge("someNode", "humanNode"); + const subgraph = subgraphBuilder.compile({ checkpointer }); + + let counterParentNode = 0; + + async function parentNode(state: z.infer) { + // This parent node will invoke the subgraph. + counterParentNode += 1; // This code will run again on resuming! + console.log(`Entered 'parentNode' a total of ${counterParentNode} times`); + + // Please note that we're intentionally incrementing the state counter + // in the graph state as well to demonstrate that the subgraph update + // of the same key will not conflict with the parent graph (until + const subgraphState = await subgraph.invoke(state); + return subgraphState; + } + + const builder = new StateGraph(StateAnnotation) + .addNode("parentNode", parentNode) + .addEdge(START, "parentNode"); + + // A checkpointer must be enabled for interrupts to work! + const graph = builder.compile({ checkpointer }); + + const config = { + configurable: { + thread_id: uuidv4(), + } + }; + + const stream = await graph.stream({ stateCounter: 1 }, config); + for await (const chunk of stream) { + console.log(chunk); + } + + console.log('--- Resuming ---'); + + const resumeStream = await graph.stream(new Command({ resume: "35" }), config); + for await (const chunk of resumeStream) { + console.log(chunk); + } + ``` + + This will print out + + ``` + Entered 'parentNode' a total of 1 times + Entered 'nodeInSubgraph' a total of 1 times + Entered humanNode in sub-graph a total of 1 times + { __interrupt__: [{ value: 'what is your name?', resumable: true, ns: ['parentNode:4c3a0248-21f0-1287-eacf-3002bc304db4', 'humanNode:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when: 'during' }] } + --- Resuming --- + Entered 'parentNode' a total of 2 times + Entered humanNode in sub-graph a total of 2 times + Got an answer of 35 + { parentNode: null } + ``` + ::: + + +### Using multiple interrupts in a single node + +Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](#validate-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully. + +When a node contains multiple interrupt calls, LangGraph keeps a list of resume values specific to the task executing the node. Whenever execution resumes, it starts at the beginning of the node. For each interrupt encountered, LangGraph checks if a matching value exists in the task's resume list. Matching is **strictly index-based**, so the order of interrupt calls within the node is critical. + +To avoid issues, refrain from dynamically changing the node's structure between executions. This includes adding, removing, or reordering interrupt calls, as such changes can result in mismatched indices. These problems often arise from unconventional patterns, such as mutating state via `Command(resume=..., update=SOME_STATE_MUTATION)` or relying on global variables to modify the node's structure dynamically. + +:::python + + ```python + import uuid + from typing import TypedDict, Optional + + from langgraph.graph import StateGraph + from langgraph.constants import START + from langgraph.types import interrupt, Command + from langgraph.checkpoint.memory import InMemorySaver + + + class State(TypedDict): + """The graph state.""" + + age: Optional[str] + name: Optional[str] + + + def human_node(state: State): + if not state.get('name'): + name = interrupt("what is your name?") + else: + name = "N/A" + + if not state.get('age'): + age = interrupt("what is your age?") + else: + age = "N/A" + + print(f"Name: {name}. Age: {age}") + + return { + "age": age, + "name": name, + } + + + builder = StateGraph(State) + builder.add_node("human_node", human_node) + builder.add_edge(START, "human_node") + + # A checkpointer must be enabled for interrupts to work! + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + "thread_id": uuid.uuid4(), + } + } + + for chunk in graph.stream({"age": None, "name": None}, config): + print(chunk) + + for chunk in graph.stream(Command(resume="John", update={"name": "foo"}), config): + print(chunk) + ``` + + ```pycon + {'__interrupt__': (Interrupt(value='what is your name?', id='...'),)} + Name: N/A. Age: John + {'human_node': {'age': 'John', 'name': 'N/A'}} + ``` + +::: diff --git a/src/oss/add-memory.mdx b/src/oss/add-memory.mdx new file mode 100644 index 00000000..68e677cf --- /dev/null +++ b/src/oss/add-memory.mdx @@ -0,0 +1,2769 @@ +--- +title: Add and manage memory +--- +AI applications need [memory](/oss/memory) to share context across multiple interactions. In LangGraph, you can add two types of memory: + +* [Add short-term memory](#add-short-term-memory) as a part of your agent's [state](/oss/graph-api#state) to enable multi-turn conversations. +* [Add long-term memory](#add-long-term-memory) to store user-specific or application-level data across sessions. + +## Add short-term memory + +**Short-term** memory (thread-level [persistence](/oss/persistence)) enables agents to track multi-turn conversations. To add short-term memory: + +:::python +```python +# highlight-next-line +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import StateGraph + +# highlight-next-line +checkpointer = InMemorySaver() + +builder = StateGraph(...) +# highlight-next-line +graph = builder.compile(checkpointer=checkpointer) + +graph.invoke( + {"messages": [{"role": "user", "content": "hi! i am Bob"}]}, + # highlight-next-line + {"configurable": {"thread_id": "1"}}, +) +``` +::: + +:::js +```typescript +import { MemorySaver, StateGraph } from "@langchain/langgraph"; + +const checkpointer = new MemorySaver(); + +const builder = new StateGraph(...); +const graph = builder.compile({ checkpointer }); + +await graph.invoke( + { messages: [{ role: "user", content: "hi! i am Bob" }] }, + { configurable: { thread_id: "1" } } +); +``` +::: + +### Use in production + +In production, use a checkpointer backed by a database: + +:::python +```python +from langgraph.checkpoint.postgres import PostgresSaver + +DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +# highlight-next-line +with PostgresSaver.from_conn_string(DB_URI) as checkpointer: + builder = StateGraph(...) + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) +``` +::: + +:::js +```typescript +import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + +const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; +const checkpointer = PostgresSaver.fromConnString(DB_URI); + +const builder = new StateGraph(...); +const graph = builder.compile({ checkpointer }); +``` +::: + + + :::python + ``` + pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres + ``` + + + You need to call `checkpointer.setup()` the first time you're using Postgres checkpointer + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + # highlight-next-line + from langgraph.checkpoint.postgres import PostgresSaver + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + # highlight-next-line + with PostgresSaver.from_conn_string(DB_URI) as checkpointer: + # checkpointer.setup() + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + ``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + # highlight-next-line + from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + # highlight-next-line + async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: + # await checkpointer.setup() + + async def call_model(state: MessagesState): + response = await model.ainvoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + ``` + + + ::: + + :::js + ``` + npm install @langchain/langgraph-checkpoint-postgres + ``` + + + You need to call `checkpointer.setup()` the first time you're using Postgres checkpointer + + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; + import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + + const model = new ChatAnthropic({ model: "claude-3-5-haiku-20241022" }); + + const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + const checkpointer = PostgresSaver.fromConnString(DB_URI); + // await checkpointer.setup(); + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", async (state) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }) + .addEdge(START, "call_model"); + + const graph = builder.compile({ checkpointer }); + + const config = { + configurable: { + thread_id: "1" + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "hi! I'm bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "what's my name?" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + ``` + ::: + + +:::python + + ``` + pip install -U pymongo langgraph langgraph-checkpoint-mongodb + ``` + + + **Setup** + To use the MongoDB checkpointer, you will need a MongoDB cluster. Follow [this guide](https://www.mongodb.com/docs/guides/atlas/cluster/) to create a cluster if you don't already have one. + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + # highlight-next-line + from langgraph.checkpoint.mongodb import MongoDBSaver + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "localhost:27017" + # highlight-next-line + with MongoDBSaver.from_conn_string(DB_URI) as checkpointer: + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + ``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + # highlight-next-line + from langgraph.checkpoint.mongodb.aio import AsyncMongoDBSaver + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "localhost:27017" + # highlight-next-line + async with AsyncMongoDBSaver.from_conn_string(DB_URI) as checkpointer: + + async def call_model(state: MessagesState): + response = await model.ainvoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + ``` + + + + + + ``` + pip install -U langgraph langgraph-checkpoint-redis + ``` + + + You need to call `checkpointer.setup()` the first time you're using Redis checkpointer + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + # highlight-next-line + from langgraph.checkpoint.redis import RedisSaver + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "redis://localhost:6379" + # highlight-next-line + with RedisSaver.from_conn_string(DB_URI) as checkpointer: + # checkpointer.setup() + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + ``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + # highlight-next-line + from langgraph.checkpoint.redis.aio import AsyncRedisSaver + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "redis://localhost:6379" + # highlight-next-line + async with AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer: + # await checkpointer.asetup() + + async def call_model(state: MessagesState): + response = await model.ainvoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + # highlight-next-line + graph = builder.compile(checkpointer=checkpointer) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + # highlight-next-line + config, + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + ``` + + + +::: + +### Use in subgraphs + +If your graph contains [subgraphs](/oss/subgraphs), you only need to provide the checkpointer when compiling the parent graph. LangGraph will automatically propagate the checkpointer to the child subgraphs. + +:::python +```python +from langgraph.graph import START, StateGraph +from langgraph.checkpoint.memory import InMemorySaver +from typing import TypedDict + +class State(TypedDict): + foo: str + +# Subgraph + +def subgraph_node_1(state: State): + return {"foo": state["foo"] + "bar"} + +subgraph_builder = StateGraph(State) +subgraph_builder.add_node(subgraph_node_1) +subgraph_builder.add_edge(START, "subgraph_node_1") +# highlight-next-line +subgraph = subgraph_builder.compile() + +# Parent graph + +builder = StateGraph(State) +# highlight-next-line +builder.add_node("node_1", subgraph) +builder.add_edge(START, "node_1") + +checkpointer = InMemorySaver() +# highlight-next-line +graph = builder.compile(checkpointer=checkpointer) +``` +::: + +:::js +```typescript +import { StateGraph, START, MemorySaver } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ foo: z.string() }); + +const subgraphBuilder = new StateGraph(State) + .addNode("subgraph_node_1", (state) => { + return { foo: state.foo + "bar" }; + }) + .addEdge(START, "subgraph_node_1"); +const subgraph = subgraphBuilder.compile(); + +const builder = new StateGraph(State) + .addNode("node_1", subgraph) + .addEdge(START, "node_1"); + +const checkpointer = new MemorySaver(); +const graph = builder.compile({ checkpointer }); +``` +::: + +If you want the subgraph to have its own memory, you can compile it with the appropriate checkpointer option. This is useful in [multi-agent](/oss/multi-agent) systems, if you want agents to keep track of their internal message histories. + +:::python +```python +subgraph_builder = StateGraph(...) +# highlight-next-line +subgraph = subgraph_builder.compile(checkpointer=True) +``` +::: + +:::js +```typescript +const subgraphBuilder = new StateGraph(...); +// highlight-next-line +const subgraph = subgraphBuilder.compile({ checkpointer: true }); +``` +::: + + +### Read short-term memory in tools + +LangGraph allows agents to access their short-term memory (state) inside the tools. + +:::python +```python +from typing import Annotated +from langgraph.prebuilt import InjectedState, create_react_agent + +class CustomState(AgentState): + # highlight-next-line + user_id: str + +def get_user_info( + # highlight-next-line + state: Annotated[CustomState, InjectedState] +) -> str: + """Look up user info.""" + # highlight-next-line + user_id = state["user_id"] + return "User is John Smith" if user_id == "user_123" else "Unknown user" + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_user_info], + # highlight-next-line + state_schema=CustomState, +) + +agent.invoke({ + "messages": "look up user information", + # highlight-next-line + "user_id": "user_123" +}) +``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import { + MessagesZodState, + LangGraphRunnableConfig, +} from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const CustomState = z.object({ + messages: MessagesZodState.shape.messages, + userId: z.string(), +}); + +const getUserInfo = tool( + async (_, config: LangGraphRunnableConfig) => { + const userId = config.configurable?.userId; + return userId === "user_123" ? "User is John Smith" : "Unknown user"; + }, + { + name: "get_user_info", + description: "Look up user info.", + schema: z.object({}), + } +); + +const agent = createReactAgent({ + llm: model, + tools: [getUserInfo], + stateSchema: CustomState, +}); + +await agent.invoke({ + messages: [{ role: "user", content: "look up user information" }], + userId: "user_123", +}); +``` +::: + +See the [Context](/oss/context) guide for more information. + + +### Write short-term memory from tools + +To modify the agent's short-term memory (state) during execution, you can return state updates directly from the tools. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. + +:::python +```python +from typing import Annotated +from langchain_core.tools import InjectedToolCallId +from langchain_core.runnables import RunnableConfig +from langchain_core.messages import ToolMessage +from langgraph.prebuilt import InjectedState, create_react_agent +from langgraph.prebuilt.chat_agent_executor import AgentState +from langgraph.types import Command + +class CustomState(AgentState): + # highlight-next-line + user_name: str + +def update_user_info( + tool_call_id: Annotated[str, InjectedToolCallId], + config: RunnableConfig +) -> Command: + """Look up and update user info.""" + user_id = config["configurable"].get("user_id") + name = "John Smith" if user_id == "user_123" else "Unknown user" + # highlight-next-line + return Command(update={ + # highlight-next-line + "user_name": name, + # update the message history + "messages": [ + ToolMessage( + "Successfully looked up user information", + tool_call_id=tool_call_id + ) + ] + }) + +def greet( + # highlight-next-line + state: Annotated[CustomState, InjectedState] +) -> str: + """Use this to greet the user once you found their info.""" + user_name = state["user_name"] + return f"Hello {user_name}!" + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[update_user_info, greet], + # highlight-next-line + state_schema=CustomState +) + +agent.invoke( + {"messages": [{"role": "user", "content": "greet the user"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} +) +``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import { + MessagesZodState, + LangGraphRunnableConfig, + Command, +} from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const CustomState = z.object({ + messages: MessagesZodState.shape.messages, + userName: z.string().optional(), +}); + +const updateUserInfo = tool( + async (_, config: LangGraphRunnableConfig) => { + const userId = config.configurable?.userId; + const name = userId === "user_123" ? "John Smith" : "Unknown user"; + return new Command({ + update: { + userName: name, + // update the message history + messages: [ + { + role: "tool", + content: "Successfully looked up user information", + tool_call_id: config.toolCall?.id, + }, + ], + }, + }); + }, + { + name: "update_user_info", + description: "Look up and update user info.", + schema: z.object({}), + } +); + +const greet = tool( + async (_, config: LangGraphRunnableConfig) => { + const userName = config.configurable?.userName; + return `Hello ${userName}!`; + }, + { + name: "greet", + description: "Use this to greet the user once you found their info.", + schema: z.object({}), + } +); + +const agent = createReactAgent({ + llm: model, + tools: [updateUserInfo, greet], + stateSchema: CustomState, +}); + +await agent.invoke( + { messages: [{ role: "user", content: "greet the user" }] }, + { configurable: { userId: "user_123" } } +); +``` +::: + +## Add long-term memory + +Use long-term memory to store user-specific or application-specific data across conversations. + +:::python +```python +# highlight-next-line +from langgraph.store.memory import InMemoryStore +from langgraph.graph import StateGraph + +# highlight-next-line +store = InMemoryStore() + +builder = StateGraph(...) +# highlight-next-line +graph = builder.compile(store=store) +``` +::: + +:::js +```typescript +import { InMemoryStore, StateGraph } from "@langchain/langgraph"; + +const store = new InMemoryStore(); + +const builder = new StateGraph(...); +const graph = builder.compile({ store }); +``` +::: + +### Use in production + +In production, use a store backed by a database: + +:::python +```python +from langgraph.store.postgres import PostgresStore + +DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +# highlight-next-line +with PostgresStore.from_conn_string(DB_URI) as store: + builder = StateGraph(...) + # highlight-next-line + graph = builder.compile(store=store) +``` +::: + +:::js +```typescript +import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres"; + +const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; +const store = PostgresStore.fromConnString(DB_URI); + +const builder = new StateGraph(...); +const graph = builder.compile({ store }); +``` +::: + + + :::python + ``` + pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres + ``` + + + You need to call `store.setup()` the first time you're using Postgres store + + + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres import PostgresSaver + # highlight-next-line + from langgraph.store.postgres import PostgresStore + from langgraph.store.base import BaseStore + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + + with ( + # highlight-next-line + PostgresStore.from_conn_string(DB_URI) as store, + PostgresSaver.from_conn_string(DB_URI) as checkpointer, + ): + # store.setup() + # checkpointer.setup() + + def call_model( + state: MessagesState, + config: RunnableConfig, + *, + # highlight-next-line + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + # highlight-next-line + memories = store.search(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + # highlight-next-line + store.put(namespace, str(uuid.uuid4()), {"data": memory}) + + response = model.invoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + # highlight-next-line + store=store, + ) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # highlight-next-line + "user_id": "1", + } + } + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + # highlight-next-line + "thread_id": "2", + "user_id": "1", + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + ``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver + # highlight-next-line + from langgraph.store.postgres.aio import AsyncPostgresStore + from langgraph.store.base import BaseStore + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + + async with ( + # highlight-next-line + AsyncPostgresStore.from_conn_string(DB_URI) as store, + AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer, + ): + # await store.setup() + # await checkpointer.setup() + + async def call_model( + state: MessagesState, + config: RunnableConfig, + *, + # highlight-next-line + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + # highlight-next-line + memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + # highlight-next-line + await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) + + response = await model.ainvoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + # highlight-next-line + store=store, + ) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # highlight-next-line + "user_id": "1", + } + } + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + # highlight-next-line + "thread_id": "2", + "user_id": "1", + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + ``` + + + ::: + + :::js + ``` + npm install @langchain/langgraph-checkpoint-postgres + ``` + + + You need to call `store.setup()` the first time you're using Postgres store + + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, MessagesZodState, START, LangGraphRunnableConfig } from "@langchain/langgraph"; + import { PostgresSaver, PostgresStore } from "@langchain/langgraph-checkpoint-postgres"; + import { z } from "zod"; + import { v4 as uuidv4 } from "uuid"; + + const model = new ChatAnthropic({ model: "claude-3-5-haiku-20241022" }); + + const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + + const store = PostgresStore.fromConnString(DB_URI); + const checkpointer = PostgresSaver.fromConnString(DB_URI); + // await store.setup(); + // await checkpointer.setup(); + + const callModel = async ( + state: z.infer, + config: LangGraphRunnableConfig, + ) => { + const userId = config.configurable?.userId; + const namespace = ["memories", userId]; + const memories = await config.store?.search(namespace, { query: state.messages.at(-1)?.content }); + const info = memories?.map(d => d.value.data).join("\n") || ""; + const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`; + + // Store new memories if the user asks the model to remember + const lastMessage = state.messages.at(-1); + if (lastMessage?.content?.toLowerCase().includes("remember")) { + const memory = "User name is Bob"; + await config.store?.put(namespace, uuidv4(), { data: memory }); + } + + const response = await model.invoke([ + { role: "system", content: systemMsg }, + ...state.messages + ]); + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); + + const graph = builder.compile({ + checkpointer, + store, + }); + + const config = { + configurable: { + thread_id: "1", + userId: "1", + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + + const config2 = { + configurable: { + thread_id: "2", + userId: "1", + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "what is my name?" }] }, + { ...config2, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + ``` + ::: + + +:::python + + ``` + pip install -U langgraph langgraph-checkpoint-redis + ``` + + + You need to call `store.setup()` the first time you're using Redis store + + + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis import RedisSaver + # highlight-next-line + from langgraph.store.redis import RedisStore + from langgraph.store.base import BaseStore + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "redis://localhost:6379" + + with ( + # highlight-next-line + RedisStore.from_conn_string(DB_URI) as store, + RedisSaver.from_conn_string(DB_URI) as checkpointer, + ): + store.setup() + checkpointer.setup() + + def call_model( + state: MessagesState, + config: RunnableConfig, + *, + # highlight-next-line + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + # highlight-next-line + memories = store.search(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + # highlight-next-line + store.put(namespace, str(uuid.uuid4()), {"data": memory}) + + response = model.invoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + # highlight-next-line + store=store, + ) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # highlight-next-line + "user_id": "1", + } + } + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + # highlight-next-line + "thread_id": "2", + "user_id": "1", + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + ``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis.aio import AsyncRedisSaver + # highlight-next-line + from langgraph.store.redis.aio import AsyncRedisStore + from langgraph.store.base import BaseStore + + model = init_chat_model(model="anthropic:claude-3-5-haiku-latest") + + DB_URI = "redis://localhost:6379" + + async with ( + # highlight-next-line + AsyncRedisStore.from_conn_string(DB_URI) as store, + AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer, + ): + # await store.setup() + # await checkpointer.asetup() + + async def call_model( + state: MessagesState, + config: RunnableConfig, + *, + # highlight-next-line + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + # highlight-next-line + memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + # highlight-next-line + await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) + + response = await model.ainvoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + # highlight-next-line + store=store, + ) + + config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # highlight-next-line + "user_id": "1", + } + } + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + # highlight-next-line + "thread_id": "2", + "user_id": "1", + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + # highlight-next-line + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + ``` + + + +::: + + +### Read long-term memory in tools + +:::python +```python title="A tool the agent can use to look up user information" +from langchain_core.runnables import RunnableConfig +from langgraph.config import get_store +from langgraph.prebuilt import create_react_agent +from langgraph.store.memory import InMemoryStore + +# highlight-next-line +store = InMemoryStore() # (1)! + +# highlight-next-line +store.put( # (2)! + ("users",), # (3)! + "user_123", # (4)! + { + "name": "John Smith", + "language": "English", + } # (5)! +) + +def get_user_info(config: RunnableConfig) -> str: + """Look up user info.""" + # Same as that provided to `create_react_agent` + # highlight-next-line + store = get_store() # (6)! + user_id = config["configurable"].get("user_id") + # highlight-next-line + user_info = store.get(("users",), user_id) # (7)! + return str(user_info.value) if user_info else "Unknown user" + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_user_info], + # highlight-next-line + store=store # (8)! +) + +# Run the agent +agent.invoke( + {"messages": [{"role": "user", "content": "look up user information"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} +) +``` + +1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](https://langchain-ai.github.io/langgraph/reference/store/) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. +2. For this example, we write some sample data to the store using the `put` method. Please see the @[BaseStore.put] API reference for more details. +3. The first argument is the namespace. This is used to group related data together. In this case, we are using the `users` namespace to group user data. +4. A key within the namespace. This example uses a user ID for the key. +5. The data that we want to store for the given user. +6. The `get_store` function is used to access the store. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created. +7. The `get` method is used to retrieve data from the store. The first argument is the namespace, and the second argument is the key. This will return a `StoreValue` object, which contains the value and metadata about the value. +8. The `store` is passed to the agent. This enables the agent to access the store when running tools. You can also use the `get_store` function to access the store from anywhere in your code. +::: + +:::js +```typescript title="A tool the agent can use to look up user information" +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import { LangGraphRunnableConfig, InMemoryStore } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const store = new InMemoryStore(); // (1)! + +await store.put( + // (2)! + ["users"], // (3)! + "user_123", // (4)! + { + name: "John Smith", + language: "English", + } // (5)! +); + +const getUserInfo = tool( + async (_, config: LangGraphRunnableConfig) => { + /**Look up user info.*/ + // Same as that provided to `createReactAgent` + const store = config.store; // (6)! + const userId = config.configurable?.userId; + const userInfo = await store?.get(["users"], userId); // (7)! + return userInfo?.value ? JSON.stringify(userInfo.value) : "Unknown user"; + }, + { + name: "get_user_info", + description: "Look up user info.", + schema: z.object({}), + } +); + +const agent = createReactAgent({ + llm: model, + tools: [getUserInfo], + store, // (8)! +}); + +// Run the agent +await agent.invoke( + { messages: [{ role: "user", content: "look up user information" }] }, + { configurable: { userId: "user_123" } } +); +``` + +1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](https://langchain-ai.github.io/langgraph/reference/store/) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. +2. For this example, we write some sample data to the store using the `put` method. Please see the @[BaseStore.put] API reference for more details. +3. The first argument is the namespace. This is used to group related data together. In this case, we are using the `users` namespace to group user data. +4. A key within the namespace. This example uses a user ID for the key. +5. The data that we want to store for the given user. +6. The store is accessible through the config. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created. +7. The `get` method is used to retrieve data from the store. The first argument is the namespace, and the second argument is the key. This will return a `StoreValue` object, which contains the value and metadata about the value. +8. The `store` is passed to the agent. This enables the agent to access the store when running tools. You can also use the store from the config to access it from anywhere in your code. +::: + + +### Write long-term memory from tools + +:::python +```python title="Example of a tool that updates user information" +from typing_extensions import TypedDict + +from langgraph.config import get_store +from langchain_core.runnables import RunnableConfig +from langgraph.prebuilt import create_react_agent +from langgraph.store.memory import InMemoryStore + +store = InMemoryStore() # (1)! + +class UserInfo(TypedDict): # (2)! + name: str + +def save_user_info(user_info: UserInfo, config: RunnableConfig) -> str: # (3)! + """Save user info.""" + # Same as that provided to `create_react_agent` + # highlight-next-line + store = get_store() # (4)! + user_id = config["configurable"].get("user_id") + # highlight-next-line + store.put(("users",), user_id, user_info) # (5)! + return "Successfully saved user info." + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[save_user_info], + # highlight-next-line + store=store +) + +# Run the agent +agent.invoke( + {"messages": [{"role": "user", "content": "My name is John Smith"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} # (6)! +) + +# You can access the store directly to get the value +store.get(("users",), "user_123").value +``` + +1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](https://langchain-ai.github.io/langgraph/reference/store/) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. +2. The `UserInfo` class is a `TypedDict` that defines the structure of the user information. The LLM will use this to format the response according to the schema. +3. The `save_user_info` function is a tool that allows an agent to update user information. This could be useful for a chat application where the user wants to update their profile information. +4. The `get_store` function is used to access the store. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created. +5. The `put` method is used to store data in the store. The first argument is the namespace, and the second argument is the key. This will store the user information in the store. +6. The `user_id` is passed in the config. This is used to identify the user whose information is being updated. +::: + +:::js +```typescript title="Example of a tool that updates user information" +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import { LangGraphRunnableConfig, InMemoryStore } from "@langchain/langgraph"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const store = new InMemoryStore(); // (1)! + +const UserInfo = z.object({ + // (2)! + name: z.string(), +}); + +const saveUserInfo = tool( + async ( + userInfo: z.infer, + config: LangGraphRunnableConfig + ) => { + // (3)! + /**Save user info.*/ + // Same as that provided to `createReactAgent` + const store = config.store; // (4)! + const userId = config.configurable?.userId; + await store?.put(["users"], userId, userInfo); // (5)! + return "Successfully saved user info."; + }, + { + name: "save_user_info", + description: "Save user info.", + schema: UserInfo, + } +); + +const agent = createReactAgent({ + llm: model, + tools: [saveUserInfo], + store, +}); + +// Run the agent +await agent.invoke( + { messages: [{ role: "user", content: "My name is John Smith" }] }, + { configurable: { userId: "user_123" } } // (6)! +); + +// You can access the store directly to get the value +const result = await store.get(["users"], "user_123"); +console.log(result?.value); +``` + +1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](https://langchain-ai.github.io/langgraph/reference/store/) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. +2. The `UserInfo` schema defines the structure of the user information. The LLM will use this to format the response according to the schema. +3. The `saveUserInfo` function is a tool that allows an agent to update user information. This could be useful for a chat application where the user wants to update their profile information. +4. The store is accessible through the config. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created. +5. The `put` method is used to store data in the store. The first argument is the namespace, and the second argument is the key. This will store the user information in the store. +6. The `userId` is passed in the config. This is used to identify the user whose information is being updated. +::: + +### Use semantic search + +Enable semantic search in your graph's memory store to let graph agents search for items in the store by semantic similarity. + +:::python +```python +from langchain.embeddings import init_embeddings +from langgraph.store.memory import InMemoryStore + +# Create store with semantic search enabled +embeddings = init_embeddings("openai:text-embedding-3-small") +store = InMemoryStore( + index={ + "embed": embeddings, + "dims": 1536, + } +) + +store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) +store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) + +items = store.search( + ("user_123", "memories"), query="I'm hungry", limit=1 +) +``` +::: + +:::js +```typescript +import { OpenAIEmbeddings } from "@langchain/openai"; +import { InMemoryStore } from "@langchain/langgraph"; + +// Create store with semantic search enabled +const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); +const store = new InMemoryStore({ + index: { + embeddings, + dims: 1536, + }, +}); + +await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); +await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); + +const items = await store.search(["user_123", "memories"], { + query: "I'm hungry", + limit: 1, +}); +``` +::: + + + :::python + ```python + from typing import Optional + + from langchain.embeddings import init_embeddings + from langchain.chat_models import init_chat_model + from langgraph.store.base import BaseStore + from langgraph.store.memory import InMemoryStore + from langgraph.graph import START, MessagesState, StateGraph + + llm = init_chat_model("openai:gpt-4o-mini") + + # Create store with semantic search enabled + embeddings = init_embeddings("openai:text-embedding-3-small") + store = InMemoryStore( + index={ + "embed": embeddings, + "dims": 1536, + } + ) + + store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) + store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) + + def chat(state, *, store: BaseStore): + # Search based on user's last message + items = store.search( + ("user_123", "memories"), query=state["messages"][-1].content, limit=2 + ) + memories = "\n".join(item.value["text"] for item in items) + memories = f"## Memories of user\n{memories}" if memories else "" + response = llm.invoke( + [ + {"role": "system", "content": f"You are a helpful assistant.\n{memories}"}, + *state["messages"], + ] + ) + return {"messages": [response]} + + + builder = StateGraph(MessagesState) + builder.add_node(chat) + builder.add_edge(START, "chat") + graph = builder.compile(store=store) + + for message, metadata in graph.stream( + input={"messages": [{"role": "user", "content": "I'm hungry"}]}, + stream_mode="messages", + ): + print(message.content, end="") + ``` + ::: + + :::js + ```typescript + import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; + import { StateGraph, START, MessagesZodState, InMemoryStore } from "@langchain/langgraph"; + import { z } from "zod"; + + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Create store with semantic search enabled + const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); + const store = new InMemoryStore({ + index: { + embeddings, + dims: 1536, + } + }); + + await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); + await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); + + const chat = async (state: z.infer, config) => { + // Search based on user's last message + const items = await config.store.search( + ["user_123", "memories"], + { query: state.messages.at(-1)?.content, limit: 2 } + ); + const memories = items.map(item => item.value.text).join("\n"); + const memoriesText = memories ? `## Memories of user\n${memories}` : ""; + + const response = await llm.invoke([ + { role: "system", content: `You are a helpful assistant.\n${memoriesText}` }, + ...state.messages, + ]); + + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("chat", chat) + .addEdge(START, "chat"); + const graph = builder.compile({ store }); + + for await (const [message, metadata] of await graph.stream( + { messages: [{ role: "user", content: "I'm hungry" }] }, + { streamMode: "messages" } + )) { + if (message.content) { + console.log(message.content); + } + } + ``` + ::: + + +## Manage short-term memory + +With [short-term memory](#add-short-term-memory) enabled, long conversations can exceed the LLM's context window. Common solutions are: + +* [Trim messages](#trim-messages): Remove first or last N messages (before calling LLM) +* [Delete messages](#delete-messages) from LangGraph state permanently +* [Summarize messages](#summarize-messages): Summarize earlier messages in the history and replace them with a summary +* [Manage checkpoints](#manage-checkpoints) to store and retrieve message history +* Custom strategies (e.g., message filtering, etc.) + +This allows the agent to keep track of the conversation without exceeding the LLM's context window. + +### Trim messages + +Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `maxTokens`) to use for handling the boundary. + + + + :::python + To trim message history in an agent, use @[`pre_model_hook`][create_react_agent] with the [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) function: + + ```python + # highlight-next-line + from langchain_core.messages.utils import ( + # highlight-next-line + trim_messages, + # highlight-next-line + count_tokens_approximately + # highlight-next-line + ) + from langgraph.prebuilt import create_react_agent + + # This function will be called every time before the node that calls LLM + def pre_model_hook(state): + trimmed_messages = trim_messages( + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=384, + start_on="human", + end_on=("human", "tool"), + ) + # highlight-next-line + return {"llm_input_messages": trimmed_messages} + + checkpointer = InMemorySaver() + agent = create_react_agent( + model, + tools, + # highlight-next-line + pre_model_hook=pre_model_hook, + checkpointer=checkpointer, + ) + ``` + ::: + + :::js + To trim message history in an agent, use `stateModifier` with the [`trimMessages`](https://js.langchain.com/docs/how_to/trim_messages/) function: + + ```typescript + import { trimMessages } from "@langchain/core/messages"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + // This function will be called every time before the node that calls LLM + const stateModifier = async (state) => { + return trimMessages(state.messages, { + strategy: "last", + maxTokens: 384, + startOn: "human", + endOn: ["human", "tool"], + }); + }; + + const checkpointer = new MemorySaver(); + const agent = createReactAgent({ + llm: model, + tools, + stateModifier, + checkpointer, + }); + ``` + ::: + + + :::python + To trim message history, use the [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) function: + + ```python + # highlight-next-line + from langchain_core.messages.utils import ( + # highlight-next-line + trim_messages, + # highlight-next-line + count_tokens_approximately + # highlight-next-line + ) + + def call_model(state: MessagesState): + # highlight-next-line + messages = trim_messages( + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=128, + start_on="human", + end_on=("human", "tool"), + ) + response = model.invoke(messages) + return {"messages": [response]} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + ... + ``` + ::: + + :::js + To trim message history, use the [`trimMessages`](https://js.langchain.com/docs/how_to/trim_messages/) function: + + ```typescript + import { trimMessages } from "@langchain/core/messages"; + + const callModel = async (state: z.infer) => { + const messages = trimMessages(state.messages, { + strategy: "last", + maxTokens: 128, + startOn: "human", + endOn: ["human", "tool"], + }); + const response = await model.invoke(messages); + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel); + // ... + ``` + ::: + + + + + :::python + ```python + # highlight-next-line + from langchain_core.messages.utils import ( + # highlight-next-line + trim_messages, + # highlight-next-line + count_tokens_approximately + # highlight-next-line + ) + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, START, MessagesState + + model = init_chat_model("anthropic:claude-3-7-sonnet-latest") + summarization_model = model.bind(max_tokens=128) + + def call_model(state: MessagesState): + # highlight-next-line + messages = trim_messages( + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=128, + start_on="human", + end_on=("human", "tool"), + ) + response = model.invoke(messages) + return {"messages": [response]} + + checkpointer = InMemorySaver() + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + graph = builder.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + graph.invoke({"messages": "hi, my name is bob"}, config) + graph.invoke({"messages": "write a short poem about cats"}, config) + graph.invoke({"messages": "now do the same but for dogs"}, config) + final_response = graph.invoke({"messages": "what's my name?"}, config) + + final_response["messages"][-1].pretty_print() + ``` + + ``` + ================================== Ai Message ================================== + + Your name is Bob, as you mentioned when you first introduced yourself. + ``` + ::: + + :::js + ```typescript + import { trimMessages } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, MessagesZodState, MemorySaver } from "@langchain/langgraph"; + import { z } from "zod"; + + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); + + const callModel = async (state: z.infer) => { + const messages = trimMessages(state.messages, { + strategy: "last", + maxTokens: 128, + startOn: "human", + endOn: ["human", "tool"], + }); + const response = await model.invoke(messages); + return { messages: [response] }; + }; + + const checkpointer = new MemorySaver(); + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); + const graph = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + await graph.invoke({ messages: [{ role: "user", content: "hi, my name is bob" }] }, config); + await graph.invoke({ messages: [{ role: "user", content: "write a short poem about cats" }] }, config); + await graph.invoke({ messages: [{ role: "user", content: "now do the same but for dogs" }] }, config); + const finalResponse = await graph.invoke({ messages: [{ role: "user", content: "what's my name?" }] }, config); + + console.log(finalResponse.messages.at(-1)?.content); + ``` + + ``` + Your name is Bob, as you mentioned when you first introduced yourself. + ``` + ::: + + +### Delete messages + +You can delete messages from the graph state to manage the message history. This is useful when you want to remove specific messages or clear the entire message history. + +:::python +To delete messages from the graph state, you can use the `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with @[`add_messages`][add_messages] [reducer](/oss/graph-api#reducers), like [`MessagesState`](/oss/graph-api#messagesstate). + +To remove specific messages: + +```python +# highlight-next-line +from langchain_core.messages import RemoveMessage + +def delete_messages(state): + messages = state["messages"] + if len(messages) > 2: + # remove the earliest two messages + # highlight-next-line + return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} +``` + +To remove **all** messages: + +```python +# highlight-next-line +from langgraph.graph.message import REMOVE_ALL_MESSAGES + +def delete_messages(state): + # highlight-next-line + return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]} +``` +::: + +:::js +To delete messages from the graph state, you can use the `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with @[`messagesStateReducer`][messagesStateReducer] [reducer](/oss/graph-api#reducers), like `MessagesZodState`. + +To remove specific messages: + +```typescript +import { RemoveMessage } from "@langchain/core/messages"; + +const deleteMessages = (state) => { + const messages = state.messages; + if (messages.length > 2) { + // remove the earliest two messages + return { + messages: messages + .slice(0, 2) + .map((m) => new RemoveMessage({ id: m.id })), + }; + } +}; +``` +::: + + + When deleting messages, **make sure** that the resulting message history is valid. Check the limitations of the LLM provider you're using. For example: + + * some providers expect message history to start with a `user` message + * most providers require `assistant` messages with tool calls to be followed by corresponding `tool` result messages. + + + + :::python + ```python + # highlight-next-line + from langchain_core.messages import RemoveMessage + + def delete_messages(state): + messages = state["messages"] + if len(messages) > 2: + # remove the earliest two messages + # highlight-next-line + return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_sequence([call_model, delete_messages]) + builder.add_edge(START, "call_model") + + checkpointer = InMemorySaver() + app = builder.compile(checkpointer=checkpointer) + + for event in app.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, + stream_mode="values" + ): + print([(message.type, message.content) for message in event["messages"]]) + + for event in app.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, + stream_mode="values" + ): + print([(message.type, message.content) for message in event["messages"]]) + ``` + + ``` + [('human', "hi! I'm bob")] + [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?')] + [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'), ('human', "what's my name?")] + [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'), ('human', "what's my name?"), ('ai', 'Your name is Bob.')] + [('human', "what's my name?"), ('ai', 'Your name is Bob.')] + ``` + ::: + + :::js + ```typescript + import { RemoveMessage } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, MessagesZodState, MemorySaver } from "@langchain/langgraph"; + import { z } from "zod"; + + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); + + const deleteMessages = (state: z.infer) => { + const messages = state.messages; + if (messages.length > 2) { + // remove the earliest two messages + return { messages: messages.slice(0, 2).map(m => new RemoveMessage({ id: m.id })) }; + } + return {}; + }; + + const callModel = async (state: z.infer) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addNode("delete_messages", deleteMessages) + .addEdge(START, "call_model") + .addEdge("call_model", "delete_messages"); + + const checkpointer = new MemorySaver(); + const app = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + + for await (const event of await app.stream( + { messages: [{ role: "user", content: "hi! I'm bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(event.messages.map(message => [message.getType(), message.content])); + } + + for await (const event of await app.stream( + { messages: [{ role: "user", content: "what's my name?" }] }, + { ...config, streamMode: "values" } + )) { + console.log(event.messages.map(message => [message.getType(), message.content])); + } + ``` + + ``` + [['human', "hi! I'm bob"]] + [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?']] + [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"]] + [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"], ['ai', 'Your name is Bob.']] + [['human', "what's my name?"], ['ai', 'Your name is Bob.']] + ``` + ::: + + +### Summarize messages + +The problem with trimming or removing messages, as shown above, is that you may lose information from culling of the message queue. Because of this, some applications benefit from a more sophisticated approach of summarizing the message history using a chat model. + +![](/oss/images/summary.png) + + + + :::python + To summarize message history in an agent, use @[`pre_model_hook`][create_react_agent] with a prebuilt [`SummarizationNode`](https://langchain-ai.github.io/langmem/reference/short_term/#langmem.short_term.SummarizationNode) abstraction: + + ```python + from langchain_anthropic import ChatAnthropic + from langmem.short_term import SummarizationNode, RunningSummary + from langchain_core.messages.utils import count_tokens_approximately + from langgraph.prebuilt import create_react_agent + from langgraph.prebuilt.chat_agent_executor import AgentState + from langgraph.checkpoint.memory import InMemorySaver + from typing import Any + + model = ChatAnthropic(model="claude-3-7-sonnet-latest") + + summarization_node = SummarizationNode( # (1)! + token_counter=count_tokens_approximately, + model=model, + max_tokens=384, + max_summary_tokens=128, + output_messages_key="llm_input_messages", + ) + + class State(AgentState): + # NOTE: we're adding this key to keep track of previous summary information + # to make sure we're not summarizing on every LLM call + # highlight-next-line + context: dict[str, RunningSummary] # (2)! + + + checkpointer = InMemorySaver() # (3)! + + agent = create_react_agent( + model=model, + tools=tools, + # highlight-next-line + pre_model_hook=summarization_node, # (4)! + # highlight-next-line + state_schema=State, # (5)! + checkpointer=checkpointer, + ) + ``` + + 1. The `InMemorySaver` is a checkpointer that stores the agent's state in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [checkpointer documentation](https://langchain-ai.github.io/langgraph/reference/checkpoints/) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready checkpointer for you. + 2. The `context` key is added to the agent's state. The key contains book-keeping information for the summarization node. It is used to keep track of the last summary information and ensure that the agent doesn't summarize on every LLM call, which can be inefficient. + 3. The `checkpointer` is passed to the agent. This enables the agent to persist its state across invocations. + 4. The `pre_model_hook` is set to the `SummarizationNode`. This node will summarize the message history before sending it to the LLM. The summarization node will automatically handle the summarization process and update the agent's state with the new summary. You can replace this with a custom implementation if you prefer. Please see the @[create_react_agent][create_react_agent] API reference for more details. + 5. The `state_schema` is set to the `State` class, which is the custom state that contains an extra `context` key. + ::: + + + :::python + Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can extend the [`MessagesState`](/oss/graph-api#working-with-messages-in-graph-state) to include a `summary` key: + + ```python + from langgraph.graph import MessagesState + class State(MessagesState): + summary: str + ``` + + Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarize_conversation` node can be called after some number of messages have accumulated in the `messages` state key. + + ```python + def summarize_conversation(state: State): + + # First, we get any existing summary + summary = state.get("summary", "") + + # Create our summarization prompt + if summary: + + # A summary already exists + summary_message = ( + f"This is a summary of the conversation to date: {summary}\n\n" + "Extend the summary by taking into account the new messages above:" + ) + + else: + summary_message = "Create a summary of the conversation above:" + + # Add prompt to our history + messages = state["messages"] + [HumanMessage(content=summary_message)] + response = model.invoke(messages) + + # Delete all but the 2 most recent messages + delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]] + return {"summary": response.content, "messages": delete_messages} + ``` + ::: + + :::js + Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can extend the [`MessagesZodState`](/oss/graph-api#working-with-messages-in-graph-state) to include a `summary` key: + + ```typescript + import { MessagesZodState } from "@langchain/langgraph"; + import { z } from "zod"; + + const State = MessagesZodState.merge(z.object({ + summary: z.string().optional(), + })); + ``` + + Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarizeConversation` node can be called after some number of messages have accumulated in the `messages` state key. + + ```typescript + import { RemoveMessage, HumanMessage } from "@langchain/core/messages"; + + const summarizeConversation = async (state: z.infer) => { + // First, we get any existing summary + const summary = state.summary || ""; + + // Create our summarization prompt + let summaryMessage: string; + if (summary) { + // A summary already exists + summaryMessage = + `This is a summary of the conversation to date: ${summary}\n\n` + + "Extend the summary by taking into account the new messages above:"; + } else { + summaryMessage = "Create a summary of the conversation above:"; + } + + // Add prompt to our history + const messages = [ + ...state.messages, + new HumanMessage({ content: summaryMessage }) + ]; + const response = await model.invoke(messages); + + // Delete all but the 2 most recent messages + const deleteMessages = state.messages + .slice(0, -2) + .map(m => new RemoveMessage({ id: m.id })); + + return { + summary: response.content, + messages: deleteMessages + }; + }; + ``` + ::: + + + + + :::python + ```python + from typing import Any, TypedDict + + from langchain.chat_models import init_chat_model + from langchain_core.messages import AnyMessage + from langchain_core.messages.utils import count_tokens_approximately + from langgraph.graph import StateGraph, START, MessagesState + from langgraph.checkpoint.memory import InMemorySaver + # highlight-next-line + from langmem.short_term import SummarizationNode, RunningSummary + + model = init_chat_model("anthropic:claude-3-7-sonnet-latest") + summarization_model = model.bind(max_tokens=128) + + class State(MessagesState): + # highlight-next-line + context: dict[str, RunningSummary] # (1)! + + class LLMInputState(TypedDict): # (2)! + summarized_messages: list[AnyMessage] + context: dict[str, RunningSummary] + + # highlight-next-line + summarization_node = SummarizationNode( + token_counter=count_tokens_approximately, + model=summarization_model, + max_tokens=256, + max_tokens_before_summary=256, + max_summary_tokens=128, + ) + + # highlight-next-line + def call_model(state: LLMInputState): # (3)! + response = model.invoke(state["summarized_messages"]) + return {"messages": [response]} + + checkpointer = InMemorySaver() + builder = StateGraph(State) + builder.add_node(call_model) + # highlight-next-line + builder.add_node("summarize", summarization_node) + builder.add_edge(START, "summarize") + builder.add_edge("summarize", "call_model") + graph = builder.compile(checkpointer=checkpointer) + + # Invoke the graph + config = {"configurable": {"thread_id": "1"}} + graph.invoke({"messages": "hi, my name is bob"}, config) + graph.invoke({"messages": "write a short poem about cats"}, config) + graph.invoke({"messages": "now do the same but for dogs"}, config) + final_response = graph.invoke({"messages": "what's my name?"}, config) + + final_response["messages"][-1].pretty_print() + print("\nSummary:", final_response["context"]["running_summary"].summary) + ``` + + 1. We will keep track of our running summary in the `context` field + + (expected by the `SummarizationNode`). + + 1. Define private state that will be used only for filtering + + the inputs to `call_model` node. + + 1. We're passing a private input state here to isolate the messages returned by the summarization node + + ``` + ================================== Ai Message ================================== + + From our conversation, I can see that you introduced yourself as Bob. That's the name you shared with me when we began talking. + + Summary: In this conversation, I was introduced to Bob, who then asked me to write a poem about cats. I composed a poem titled "The Mystery of Cats" that captured cats' graceful movements, independent nature, and their special relationship with humans. Bob then requested a similar poem about dogs, so I wrote "The Joy of Dogs," which highlighted dogs' loyalty, enthusiasm, and loving companionship. Both poems were written in a similar style but emphasized the distinct characteristics that make each pet special. + ``` + ::: + + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { + SystemMessage, + HumanMessage, + RemoveMessage, + type BaseMessage + } from "@langchain/core/messages"; + import { + MessagesZodState, + StateGraph, + START, + END, + MemorySaver, + } from "@langchain/langgraph"; + import { z } from "zod"; + import { v4 as uuidv4 } from "uuid"; + + const memory = new MemorySaver(); + + // We will add a `summary` attribute (in addition to `messages` key, + // which MessagesZodState already has) + const GraphState = z.object({ + messages: MessagesZodState.shape.messages, + summary: z.string().default(""), + }); + + // We will use this model for both the conversation and the summarization + const model = new ChatAnthropic({ model: "claude-3-haiku-20240307" }); + + // Define the logic to call the model + const callModel = async (state: z.infer) => { + // If a summary exists, we add this in as a system message + const { summary } = state; + let { messages } = state; + if (summary) { + const systemMessage = new SystemMessage({ + id: uuidv4(), + content: `Summary of conversation earlier: ${summary}`, + }); + messages = [systemMessage, ...messages]; + } + const response = await model.invoke(messages); + // We return an object, because this will get added to the existing state + return { messages: [response] }; + }; + + // We now define the logic for determining whether to end or summarize the conversation + const shouldContinue = (state: z.infer) => { + const messages = state.messages; + // If there are more than six messages, then we summarize the conversation + if (messages.length > 6) { + return "summarize_conversation"; + } + // Otherwise we can just end + return END; + }; + + const summarizeConversation = async (state: z.infer) => { + // First, we summarize the conversation + const { summary, messages } = state; + let summaryMessage: string; + if (summary) { + // If a summary already exists, we use a different system prompt + // to summarize it than if one didn't + summaryMessage = + `This is summary of the conversation to date: ${summary}\n\n` + + "Extend the summary by taking into account the new messages above:"; + } else { + summaryMessage = "Create a summary of the conversation above:"; + } + + const allMessages = [ + ...messages, + new HumanMessage({ id: uuidv4(), content: summaryMessage }), + ]; + + const response = await model.invoke(allMessages); + + // We now need to delete messages that we no longer want to show up + // I will delete all but the last two messages, but you can change this + const deleteMessages = messages + .slice(0, -2) + .map((m) => new RemoveMessage({ id: m.id! })); + + if (typeof response.content !== "string") { + throw new Error("Expected a string response from the model"); + } + + return { summary: response.content, messages: deleteMessages }; + }; + + // Define a new graph + const workflow = new StateGraph(GraphState) + // Define the conversation node and the summarize node + .addNode("conversation", callModel) + .addNode("summarize_conversation", summarizeConversation) + // Set the entrypoint as conversation + .addEdge(START, "conversation") + // We now add a conditional edge + .addConditionalEdges( + // First, we define the start node. We use `conversation`. + // This means these are the edges taken after the `conversation` node is called. + "conversation", + // Next, we pass in the function that will determine which node is called next. + shouldContinue, + ) + // We now add a normal edge from `summarize_conversation` to END. + // This means that after `summarize_conversation` is called, we end. + .addEdge("summarize_conversation", END); + + // Finally, we compile it! + const app = workflow.compile({ checkpointer: memory }); + ``` + ::: + + +### Manage checkpoints + +You can view and delete the information stored by the checkpointer. + + +#### View thread state + +:::python + + + ```python + config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # highlight-next-line + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" + + } + } + # highlight-next-line + graph.get_state(config) + ``` + + ``` + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() + ) + ``` + + + ```python + config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # highlight-next-line + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" + + } + } + # highlight-next-line + checkpointer.get_tuple(config) + ``` + + ``` + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:24.680462+00:00', + 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + }, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + pending_writes=[] + ) + ``` + + +::: + +:::js +```typescript +const config = { + configurable: { + thread_id: "1", + // optionally provide an ID for a specific checkpoint, + // otherwise the latest checkpoint is shown + // checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a" + }, +}; +await graph.getState(config); +``` + +``` +{ + values: { messages: [HumanMessage(...), AIMessage(...), HumanMessage(...), AIMessage(...)] }, + next: [], + config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } }, + metadata: { + source: 'loop', + writes: { call_model: { messages: AIMessage(...) } }, + step: 4, + parents: {}, + thread_id: '1' + }, + createdAt: '2025-05-05T16:01:24.680462+00:00', + parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } }, + tasks: [], + interrupts: [] +} +``` +::: + + +#### View the history of the thread + +:::python + + + ```python + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + # highlight-next-line + list(graph.get_state_history(config)) + ``` + + ``` + [ + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")]}, + next=('call_model',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.863421+00:00', + parent_config={...} + tasks=(PregelTask(id='8ab4155e-6b15-b885-9ce5-bed69a2c305c', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Your name is Bob.')}),), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, + next=('__start__',), + config={...}, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.863173+00:00', + parent_config={...} + tasks=(PregelTask(id='24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "what's my name?"}]}),), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, + next=(), + config={...}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.862295+00:00', + parent_config={...} + tasks=(), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob")]}, + next=('call_model',), + config={...}, + metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:22.278960+00:00', + parent_config={...} + tasks=(PregelTask(id='8cbd75e0-3720-b056-04f7-71ac805140a0', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}),), + interrupts=() + ), + StateSnapshot( + values={'messages': []}, + next=('__start__',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:22.277497+00:00', + parent_config=None, + tasks=(PregelTask(id='d458367b-8265-812c-18e2-33001d199ce6', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}),), + interrupts=() + ) + ] + ``` + + + ```python + config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } + } + # highlight-next-line + list(checkpointer.list(config)) + ``` + + ``` + [ + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:24.680462+00:00', + 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + }, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + pending_writes=[] + ), + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:23.863421+00:00', + 'id': '1f029ca3-1790-6b0a-8003-baf965b6a38f', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")], 'branch:to:call_model': None} + }, + metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[('8ab4155e-6b15-b885-9ce5-bed69a2c305c', 'messages', AIMessage(content='Your name is Bob.'))] + ), + CheckpointTuple( + config={...}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:23.863173+00:00', + 'id': '1f029ca3-1790-616e-8002-9e021694a0cd', + 'channel_versions': {'__start__': '00000000000000000000000000000004.0.5736472536395331', 'messages': '00000000000000000000000000000003.0.7056767754077798', 'branch:to:call_model': '00000000000000000000000000000003.0.22059023329132854'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}}, + 'channel_values': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}, 'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]} + }, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[('24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', 'messages', [{'role': 'user', 'content': "what's my name?"}]), ('24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', 'branch:to:call_model', None)] + ), + CheckpointTuple( + config={...}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:23.862295+00:00', + 'id': '1f029ca3-178d-6f54-8001-d7b180db0c89', + 'channel_versions': {'__start__': '00000000000000000000000000000002.0.18673090920108737', 'messages': '00000000000000000000000000000003.0.7056767754077798', 'branch:to:call_model': '00000000000000000000000000000003.0.22059023329132854'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]} + }, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[] + ), + CheckpointTuple( + config={...}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:22.278960+00:00', + 'id': '1f029ca3-0874-6612-8000-339f2abc83b1', + 'channel_versions': {'__start__': '00000000000000000000000000000002.0.18673090920108737', 'messages': '00000000000000000000000000000002.0.30296526818059655', 'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob")], 'branch:to:call_model': None} + }, + metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[('8cbd75e0-3720-b056-04f7-71ac805140a0', 'messages', AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'))] + ), + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:22.277497+00:00', + 'id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565', + 'channel_versions': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, + 'versions_seen': {'__input__': {}}, + 'channel_values': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}} + }, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, + parent_config=None, + pending_writes=[('d458367b-8265-812c-18e2-33001d199ce6', 'messages', [{'role': 'user', 'content': "hi! I'm bob"}]), ('d458367b-8265-812c-18e2-33001d199ce6', 'branch:to:call_model', None)] + ) + ] + ``` + + +::: + +:::js +```typescript +const config = { + configurable: { + thread_id: "1", + }, +}; + +const history = []; +for await (const state of graph.getStateHistory(config)) { + history.push(state); +} +``` +::: + +#### Delete all checkpoints for a thread + +:::python +```python +thread_id = "1" +checkpointer.delete_thread(thread_id) +``` +::: + +:::js +```typescript +const threadId = "1"; +await checkpointer.deleteThread(threadId); +``` +::: + +:::python +## Prebuilt memory tools + +**LangMem** is a LangChain-maintained library that offers tools for managing long-term memories in your agent. See the [LangMem documentation](https://langchain-ai.github.io/langmem/) for usage examples. +::: diff --git a/src/oss/agent-supervisor.mdx b/src/oss/agent-supervisor.mdx new file mode 100644 index 00000000..52c64bd1 --- /dev/null +++ b/src/oss/agent-supervisor.mdx @@ -0,0 +1,780 @@ +--- +title: Build a multi-agent supervisor +sidebarTitle: Multi-agent supervisor +--- +[**Supervisor**](/oss/multi-agent#supervisor) is a multi-agent architecture where **specialized** agents are coordinated by a central **supervisor agent**. The supervisor agent controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. + +In this tutorial, you will build a supervisor system with two agents — a research and a math expert. By the end of the tutorial you will: + +1. Build specialized research and math agents +2. Build a supervisor for orchestrating them with the prebuilt [`langgraph-supervisor`](/oss/agent-supervisor) +3. Build a supervisor from scratch +4. Implement advanced task delegation + +![diagram](/oss/images/diagram.png) + +## Setup + +First, let's install required packages and set our API keys + +```python +%%capture --no-stderr +%pip install -U langgraph langgraph-supervisor langchain-tavily "langchain[openai]" +``` + +```python +import getpass +import os + + +def _set_if_undefined(var: str): + if not os.environ.get(var): + os.environ[var] = getpass.getpass(f"Please provide your {var}") + + +_set_if_undefined("OPENAI_API_KEY") +_set_if_undefined("TAVILY_API_KEY") +``` + + + Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph. + + +## 1. Create worker agents + +First, let's create our specialized worker agents — research agent and math agent: + +* Research agent will have access to a web search tool using [Tavily API](https://tavily.com/) +* Math agent will have access to simple math tools (`add`, `multiply`, `divide`) + +### Research agent + +For web search, we will use `TavilySearch` tool from `langchain-tavily`: + +```python +from langchain_tavily import TavilySearch + +web_search = TavilySearch(max_results=3) +web_search_results = web_search.invoke("who is the mayor of NYC?") + +print(web_search_results["results"][0]["content"]) +``` + +**Output:** + +``` +Find events, attractions, deals, and more at nyctourism.com Skip Main Navigation Menu The Official Website of the City of New York Text Size Powered by Translate SearchSearch Primary Navigation The official website of NYC Home NYC Resources NYC311 Office of the Mayor Events Connect Jobs Search Office of the Mayor | Mayor's Bio | City of New York Secondary Navigation MayorBiographyNewsOfficials Eric L. Adams 110th Mayor of New York City Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve public safety, and build a stronger, healthier city that delivers for all New Yorkers. As the representative of one of the nation's largest counties, Eric fought tirelessly to grow the local economy, invest in schools, reduce inequality, improve public safety, and advocate for smart policies and better government that delivers for all New Yorkers. +``` + +To create individual worker agents, we will use LangGraph's prebuilt [agent](/oss/quickstart). + +```python +from langgraph.prebuilt import create_react_agent + +research_agent = create_react_agent( + model="openai:gpt-4.1", + tools=[web_search], + prompt=( + "You are a research agent.\n\n" + "INSTRUCTIONS:\n" + "- Assist ONLY with research-related tasks, DO NOT do any math\n" + "- After you're done with your tasks, respond to the supervisor directly\n" + "- Respond ONLY with the results of your work, do NOT include ANY other text." + ), + name="research_agent", +) +``` + +Let's [run the agent](/oss/run-an-agent) to verify that it behaves as expected. + + + **We'll use `pretty_print_messages` helper to render the streamed agent outputs nicely** + ```python + from langchain_core.messages import convert_to_messages + + + def pretty_print_message(message, indent=False): + pretty_message = message.pretty_repr(html=True) + if not indent: + print(pretty_message) + return + + indented = "\n".join("\t" + c for c in pretty_message.split("\n")) + print(indented) + + + def pretty_print_messages(update, last_message=False): + is_subgraph = False + if isinstance(update, tuple): + ns, update = update + # skip parent graph updates in the printouts + if len(ns) == 0: + return + + graph_id = ns[-1].split(":")[0] + print(f"Update from subgraph {graph_id}:") + print("\n") + is_subgraph = True + + for node_name, node_update in update.items(): + update_label = f"Update from node {node_name}:" + if is_subgraph: + update_label = "\t" + update_label + + print(update_label) + print("\n") + + messages = convert_to_messages(node_update["messages"]) + if last_message: + messages = messages[-1:] + + for m in messages: + pretty_print_message(m, indent=is_subgraph) + print("\n") + ``` + + ```python + for chunk in research_agent.stream( + {"messages": [{"role": "user", "content": "who is the mayor of NYC?"}]} + ): + pretty_print_messages(chunk) + ``` + + **Output:** + + ``` + Update from node agent: + + + ================================== Ai Message ================================== + Name: research_agent + Tool Calls: + tavily_search (call_U748rQhQXT36sjhbkYLSXQtJ) + Call ID: call_U748rQhQXT36sjhbkYLSXQtJ + Args: + query: current mayor of New York City + search_depth: basic + + + Update from node tools: + + + ================================= Tool Message ================================== + Name: tavily_search + + {"query": "current mayor of New York City", "follow_up_questions": null, "answer": null, "images": [], "results": [{"title": "List of mayors of New York City - Wikipedia", "url": "https://en.wikipedia.org/wiki/List_of_mayors_of_New_York_City", "content": "The mayor of New York City is the chief executive of the Government of New York City, as stipulated by New York City's charter.The current officeholder, the 110th in the sequence of regular mayors, is Eric Adams, a member of the Democratic Party.. During the Dutch colonial period from 1624 to 1664, New Amsterdam was governed by the Director of Netherland.", "score": 0.9039154, "raw_content": null}, {"title": "Office of the Mayor | Mayor's Bio | City of New York - NYC.gov", "url": "https://www.nyc.gov/office-of-the-mayor/bio.page", "content": "Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve", "score": 0.8405867, "raw_content": null}, {"title": "Eric Adams - Wikipedia", "url": "https://en.wikipedia.org/wiki/Eric_Adams", "content": "Eric Leroy Adams (born September 1, 1960) is an American politician and former police officer who has served as the 110th mayor of New York City since 2022. Adams was an officer in the New York City Transit Police and then the New York City Police Department (``` + ``` + + +### Math agent + +For math agent tools we will use [vanilla Python functions](/oss/call-tools#define-a-tool): + +```python +def add(a: float, b: float): + """Add two numbers.""" + return a + b + + +def multiply(a: float, b: float): + """Multiply two numbers.""" + return a * b + + +def divide(a: float, b: float): + """Divide two numbers.""" + return a / b + + +math_agent = create_react_agent( + model="openai:gpt-4.1", + tools=[add, multiply, divide], + prompt=( + "You are a math agent.\n\n" + "INSTRUCTIONS:\n" + "- Assist ONLY with math-related tasks\n" + "- After you're done with your tasks, respond to the supervisor directly\n" + "- Respond ONLY with the results of your work, do NOT include ANY other text." + ), + name="math_agent", +) +``` + +Let's run the math agent: + +```python +for chunk in math_agent.stream( + {"messages": [{"role": "user", "content": "what's (3 + 5) x 7"}]} +): + pretty_print_messages(chunk) +``` + +**Output:** + +``` +Update from node agent: + + +================================== Ai Message ================================== +Name: math_agent +Tool Calls: + add (call_p6OVLDHB4LyCNCxPOZzWR15v) + Call ID: call_p6OVLDHB4LyCNCxPOZzWR15v + Args: + a: 3 + b: 5 + + +Update from node tools: + + +================================= Tool Message ================================== +Name: add + +8.0 + + +Update from node agent: + + +================================== Ai Message ================================== +Name: math_agent +Tool Calls: + multiply (call_EoaWHMLFZAX4AkajQCtZvbli) + Call ID: call_EoaWHMLFZAX4AkajQCtZvbli + Args: + a: 8 + b: 7 + + +Update from node tools: + + +================================= Tool Message ================================== +Name: multiply + +56.0 + + +Update from node agent: + + +================================== Ai Message ================================== +Name: math_agent + +56 + + +``` + +## 2. Create supervisor with `langgraph-supervisor` + +To implement out multi-agent system, we will use @[`create_supervisor`][create_supervisor] from the prebuilt `langgraph-supervisor` library: + +```python +from langgraph_supervisor import create_supervisor +from langchain.chat_models import init_chat_model + +supervisor = create_supervisor( + model=init_chat_model("openai:gpt-4.1"), + agents=[research_agent, math_agent], + prompt=( + "You are a supervisor managing two agents:\n" + "- a research agent. Assign research-related tasks to this agent\n" + "- a math agent. Assign math-related tasks to this agent\n" + "Assign work to one agent at a time, do not call agents in parallel.\n" + "Do not do any work yourself." + ), + add_handoff_back_messages=True, + output_mode="full_history", +).compile() +``` + +```python +from IPython.display import display, Image + +display(Image(supervisor.get_graph().draw_mermaid_png())) +``` + +![Graph](/oss/images/output.png) + +**Note:** When you run this code, it will generate and display a visual representation of the supervisor graph showing the flow between the supervisor and worker agents. + +Let's now run it with a query that requires both agents: + +* research agent will look up the necessary GDP information +* math agent will perform division to find the percentage of NY state GDP, as requested + +```python +for chunk in supervisor.stream( + { + "messages": [ + { + "role": "user", + "content": "find US and New York state GDP in 2024. what % of US GDP was New York state?", + } + ] + }, +): + pretty_print_messages(chunk, last_message=True) + +final_message_history = chunk["supervisor"]["messages"] +``` + +**Output:** + +``` +Update from node supervisor: + + +================================= Tool Message ================================== +Name: transfer_to_research_agent + +Successfully transferred to research_agent + + +Update from node research_agent: + + +================================= Tool Message ================================== +Name: transfer_back_to_supervisor + +Successfully transferred back to supervisor + + +Update from node supervisor: + + +================================= Tool Message ================================== +Name: transfer_to_math_agent + +Successfully transferred to math_agent + + +Update from node math_agent: + + +================================= Tool Message ================================== +Name: transfer_back_to_supervisor + +Successfully transferred back to supervisor + + +Update from node supervisor: + + +================================== Ai Message ================================== +Name: supervisor + +In 2024, the US GDP was $29.18 trillion and New York State's GDP was $2.297 trillion. New York State accounted for approximately 7.87% of the total US GDP in 2024. + + +``` + +## 3. Create supervisor from scratch + +Let's now implement this same multi-agent system from scratch. We will need to: + +1. [Set up how the supervisor communicates](#set-up-agent-communication) with individual agents +2. [Create the supervisor agent](#create-supervisor-agent) +3. Combine supervisor and worker agents into a [single multi-agent graph](#create-multi-agent-graph). + +### Set up agent communication + +We will need to define a way for the supervisor agent to communicate with the worker agents. A common way to implement this in multi-agent architectures is using **handoffs**, where one agent *hands off* control to another. Handoffs allow you to specify: + +* **destination**: target agent to transfer to +* **payload**: information to pass to that agent + +We will implement handoffs via **handoff tools** and give these tools to the supervisor agent: when the supervisor calls these tools, it will hand off control to a worker agent, passing the full message history to that agent. + +```python +from typing import Annotated +from langchain_core.tools import tool, InjectedToolCallId +from langgraph.prebuilt import InjectedState +from langgraph.graph import StateGraph, START, MessagesState +from langgraph.types import Command + + +def create_handoff_tool(*, agent_name: str, description: str | None = None): + name = f"transfer_to_{agent_name}" + description = description or f"Ask {agent_name} for help." + + @tool(name, description=description) + def handoff_tool( + state: Annotated[MessagesState, InjectedState], + tool_call_id: Annotated[str, InjectedToolCallId], + ) -> Command: + tool_message = { + "role": "tool", + "content": f"Successfully transferred to {agent_name}", + "name": name, + "tool_call_id": tool_call_id, + } + # highlight-next-line + return Command( + # highlight-next-line + goto=agent_name, # (1)! + # highlight-next-line + update={**state, "messages": state["messages"] + [tool_message]}, # (2)! + # highlight-next-line + graph=Command.PARENT, # (3)! + ) + + return handoff_tool + + +# Handoffs +assign_to_research_agent = create_handoff_tool( + agent_name="research_agent", + description="Assign task to a researcher agent.", +) + +assign_to_math_agent = create_handoff_tool( + agent_name="math_agent", + description="Assign task to a math agent.", +) +``` + +1. Name of the agent or node to hand off to. +2. Take the agent's messages and add them to the parent's state as part of the handoff. The next agent will see the parent state. +3. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph. + +### Create supervisor agent + +Then, let's create the supervisor agent with the handoff tools we just defined. We will use the prebuilt @[`create_react_agent`][create_react_agent]: + +```python +supervisor_agent = create_react_agent( + model="openai:gpt-4.1", + tools=[assign_to_research_agent, assign_to_math_agent], + prompt=( + "You are a supervisor managing two agents:\n" + "- a research agent. Assign research-related tasks to this agent\n" + "- a math agent. Assign math-related tasks to this agent\n" + "Assign work to one agent at a time, do not call agents in parallel.\n" + "Do not do any work yourself." + ), + name="supervisor", +) +``` + +### Create multi-agent graph + +Putting this all together, let's create a graph for our overall multi-agent system. We will add the supervisor and the individual agents as subgraph [nodes](/oss/graph-api#nodes). + +```python +from langgraph.graph import END + +# Define the multi-agent supervisor graph +supervisor = ( + StateGraph(MessagesState) + # NOTE: `destinations` is only needed for visualization and doesn't affect runtime behavior + .add_node(supervisor_agent, destinations=("research_agent", "math_agent", END)) + .add_node(research_agent) + .add_node(math_agent) + .add_edge(START, "supervisor") + # always return back to the supervisor + .add_edge("research_agent", "supervisor") + .add_edge("math_agent", "supervisor") + .compile() +) +``` + +Notice that we've added explicit [edges](/oss/graph-api#edges) from worker agents back to the supervisor — this means that they are guaranteed to return control back to the supervisor. If you want the agents to respond directly to the user (i.e., turn the system into a router, you can remove these edges). + +```python +from IPython.display import display, Image + +display(Image(supervisor.get_graph().draw_mermaid_png())) +``` + +![Graph](/oss/images/multi-output.png) + +**Note:** When you run this code, it will generate and display a visual representation of the multi-agent supervisor graph showing the flow between the supervisor and worker agents. + +With the multi-agent graph created, let's now run it! + +```python +for chunk in supervisor.stream( + { + "messages": [ + { + "role": "user", + "content": "find US and New York state GDP in 2024. what % of US GDP was New York state?", + } + ] + }, +): + pretty_print_messages(chunk, last_message=True) + +final_message_history = chunk["supervisor"]["messages"] +``` + +**Output:** + +``` +Update from node supervisor: + + +================================= Tool Message ================================== +Name: transfer_to_research_agent + +Successfully transferred to research_agent + + +Update from node research_agent: + + +================================== Ai Message ================================== +Name: research_agent + +- US GDP in 2024 is projected to be about $28.18 trillion USD (Statista; CBO projection). +- New York State's nominal GDP for 2024 is estimated at approximately $2.16 trillion USD (various economic reports). +- New York State's share of US GDP in 2024 is roughly 7.7%. + +Sources: +- https://www.statista.com/statistics/216985/forecast-of-us-gross-domestic-product/ +- https://nyassembly.gov/Reports/WAM/2025economic_revenue/2025_report.pdf?v=1740533306 + + +Update from node supervisor: + + +================================= Tool Message ================================== +Name: transfer_to_math_agent + +Successfully transferred to math_agent + + +Update from node math_agent: + + +================================== Ai Message ================================== +Name: math_agent + +US GDP in 2024: $28.18 trillion +New York State GDP in 2024: $2.16 trillion +Percentage of US GDP from New York State: 7.67% + + +Update from node supervisor: + + +================================== Ai Message ================================== +Name: supervisor + +Here are your results: + +- 2024 US GDP (projected): $28.18 trillion USD +- 2024 New York State GDP (estimated): $2.16 trillion USD +- New York State's share of US GDP: approximately 7.7% + +If you need the calculation steps or sources, let me know! + + +``` + +Let's examine the full resulting message history: + +```python +for message in final_message_history: + message.pretty_print() +``` + +**Output:** + +``` +================================ Human Message ================================== + +find US and New York state GDP in 2024. what % of US GDP was New York state? +================================== Ai Message =================================== +Name: supervisor +Tool Calls: + transfer_to_research_agent (call_KlGgvF5ahlAbjX8d2kHFjsC3) + Call ID: call_KlGgvF5ahlAbjX8d2kHFjsC3 + Args: +================================= Tool Message ================================== +Name: transfer_to_research_agent + +Successfully transferred to research_agent +================================== Ai Message =================================== +Name: research_agent +Tool Calls: + tavily_search (call_ZOaTVUA6DKrOjWQldLhtrsO2) + Call ID: call_ZOaTVUA6DKrOjWQldLhtrsO2 + Args: + query: US GDP 2024 estimate or actual + search_depth: advanced + tavily_search (call_QsRAasxW9K03lTlqjuhNLFbZ) + Call ID: call_QsRAasxW9K03lTlqjuhNLFbZ + Args: + query: New York state GDP 2024 estimate or actual + search_depth: advanced +================================= Tool Message ================================== +Name: tavily_search + +{"query": "US GDP 2024 estimate or actual", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://www.advisorperspectives.com/dshort/updates/2025/05/29/gdp-gross-domestic-product-q1-2025-second-estimate", "title": "Q1 GDP Second Estimate: Real GDP at -0.2%, Higher Than Expected", "content": "> Real gross domestic product (GDP) decreased at an annual rate of 0.2 percent in the first quarter of 2025 (January, February, and March), according to the second estimate released by the U.S. Bureau of Economic Analysis. In the fourth quarter of 2024, real GDP increased 2.4 percent. The decrease in real GDP in the first quarter primarily reflected an increase in imports, which are a subtraction in the calculation of GDP, and a decrease in government spending. These movements were partly [...] by [Harry Mamaysky](https://www.advisor``` +``` + + + You can see that the supervisor system appends **all** of the individual agent messages (i.e., their internal tool-calling loop) to the full message history. This means that on every supervisor turn, supervisor agent sees this full history. If you want more control over: + + * **how inputs are passed to agents**: you can use LangGraph @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. See the [task delegation](#4-create-delegation-tasks) example below + * **how agent outputs are added**: you can control how much of the agent's internal message history is added to the overall supervisor message history by wrapping the agent in a separate node function: + ```python + def call_research_agent(state): + # return agent's final response, + # excluding inner monologue + response = research_agent.invoke(state) + # highlight-next-line + return {"messages": response["messages"][-1]} + ``` + + +## 4. Create delegation tasks + +So far the individual agents relied on **interpreting full message history** to determine their tasks. An alternative approach is to ask the supervisor to **formulate a task explicitly**. We can do so by adding a `task_description` parameter to the `handoff_tool` function. + +```python +from langgraph.types import Send + + +def create_task_description_handoff_tool( + *, agent_name: str, description: str | None = None +): + name = f"transfer_to_{agent_name}" + description = description or f"Ask {agent_name} for help." + + @tool(name, description=description) + def handoff_tool( + # this is populated by the supervisor LLM + task_description: Annotated[ + str, + "Description of what the next agent should do, including all of the relevant context.", + ], + # these parameters are ignored by the LLM + state: Annotated[MessagesState, InjectedState], + ) -> Command: + task_description_message = {"role": "user", "content": task_description} + agent_input = {**state, "messages": [task_description_message]} + return Command( + # highlight-next-line + goto=[Send(agent_name, agent_input)], + graph=Command.PARENT, + ) + + return handoff_tool + + +assign_to_research_agent_with_description = create_task_description_handoff_tool( + agent_name="research_agent", + description="Assign task to a researcher agent.", +) + +assign_to_math_agent_with_description = create_task_description_handoff_tool( + agent_name="math_agent", + description="Assign task to a math agent.", +) + +supervisor_agent_with_description = create_react_agent( + model="openai:gpt-4.1", + tools=[ + assign_to_research_agent_with_description, + assign_to_math_agent_with_description, + ], + prompt=( + "You are a supervisor managing two agents:\n" + "- a research agent. Assign research-related tasks to this assistant\n" + "- a math agent. Assign math-related tasks to this assistant\n" + "Assign work to one agent at a time, do not call agents in parallel.\n" + "Do not do any work yourself." + ), + name="supervisor", +) + +supervisor_with_description = ( + StateGraph(MessagesState) + .add_node( + supervisor_agent_with_description, destinations=("research_agent", "math_agent") + ) + .add_node(research_agent) + .add_node(math_agent) + .add_edge(START, "supervisor") + .add_edge("research_agent", "supervisor") + .add_edge("math_agent", "supervisor") + .compile() +) +``` + + + We're using @[`Send()`][Send] primitive in the `handoff_tool`. This means that instead of receiving the full `supervisor` graph state as input, each worker agent only sees the contents of the `Send` payload. In this example, we're sending the task description as a single "human" message. + + +Let's now running it with the same input query: + +```python +for chunk in supervisor_with_description.stream( + { + "messages": [ + { + "role": "user", + "content": "find US and New York state GDP in 2024. what % of US GDP was New York state?", + } + ] + }, + subgraphs=True, +): + pretty_print_messages(chunk, last_message=True) +``` + +**Output:** + +``` +Update from subgraph supervisor: + + + ate from node agent: + + + =============================== Ai Message ================================== + e: supervisor + l Calls: + nsfer_to_research_agent (call_tk8q8py8qK6MQz6Kj6mijKua) + l ID: call_tk8q8py8qK6MQz6Kj6mijKua + s: + k_description: Find the 2024 GDP (Gross Domestic Product) for both the United States and New York state, using the most up-to-date and reputable sources available. Provide both GDP values and cite the data sources. + + +Update from subgraph research_agent: + + + ate from node agent: + + + =============================== Ai Message ================================== + e: research_agent + l Calls: + ily_search (call_KqvhSvOIhAvXNsT6BOwbPlRB) + l ID: call_KqvhSvOIhAvXNsT6BOwbPlRB + s: + ry: 2024 United States GDP value from a reputable source + rch_depth: advanced + ily_search (call_kbbAWBc9KwCWKHmM5v04H88t) + l ID: call_kbbAWBc9KwCWKHmM5v04H88t + s: + ry: 2024 New York state GDP value from a reputable source + rch_depth: advanced + + +Update from subgraph research_agent: + + + ate from node tools: + + + ============================== Tool Message ================================== + e: tavily_search + + uery": "2024 United States GDP value from a reputable source", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://www.focus-economics.com/countries/united-states/", "title": "United States Economy Overview - Focus Economics", "content": "The United States' Macroeconomic Analysis:\n------------------------------------------\n\n**Nominal GDP of USD 29,185 billion in 2024.**\n\n**Nominal GDP of USD 29,179 billion in 2024.**\n\n**GDP per capita of USD 86,635 compared to the global average of USD 10,589.**\n\n**GDP per capita of USD 86,652 compared to the global average of USD 10,589.**\n\n**Average real GDP growth of 2.5% over the last decade.**\n\n**Average real GDP growth of ``` +``` diff --git a/src/oss/agentic-architectures.mdx b/src/oss/agentic-architectures.mdx new file mode 100644 index 00000000..499e3df5 --- /dev/null +++ b/src/oss/agentic-architectures.mdx @@ -0,0 +1,120 @@ +--- +title: Agent architectures +sidebarTitle: Overview +--- + +Many LLM applications implement a particular control flow of steps before and / or after LLM calls. As an example, [RAG](https://github.com/langchain-ai/rag-from-scratch) performs retrieval of documents relevant to a user question, and passes those documents to an LLM in order to ground the model's response in the provided document context. + +Instead of hard-coding a fixed control flow, we sometimes want LLM systems that can pick their own control flow to solve more complex problems! This is one definition of an [agent](https://blog.langchain.dev/what-is-an-agent/): *an agent is a system that uses an LLM to decide the control flow of an application.* There are many ways that an LLM can control application: + +* An LLM can route between two potential paths +* An LLM can decide which of many tools to call +* An LLM can decide whether the generated answer is sufficient or more work is needed + +As a result, there are many different types of [agent architectures](https://blog.langchain.dev/what-is-a-cognitive-architecture/), which give an LLM varying levels of control. + +![Agent Types](/oss/images/agent_types.png) + +## Router + +A router allows an LLM to select a single step from a specified set of options. This is an agent architecture that exhibits a relatively limited level of control because the LLM usually focuses on making a single decision and produces a specific output from a limited set of pre-defined options. Routers typically employ a few different concepts to achieve this. + +### Structured Output + +Structured outputs with LLMs work by providing a specific format or schema that the LLM should follow in its response. This is similar to tool calling, but more general. While tool calling typically involves selecting and using predefined functions, structured outputs can be used for any type of formatted response. Common methods to achieve structured outputs include: + +1. Prompt engineering: Instructing the LLM to respond in a specific format via the system prompt. +2. Output parsers: Using post-processing to extract structured data from LLM responses. +3. Tool calling: Leveraging built-in tool calling capabilities of some LLMs to generate structured outputs. + +Structured outputs are crucial for routing as they ensure the LLM's decision can be reliably interpreted and acted upon by the system. Learn more about [structured outputs in this how-to guide](https://python.langchain.com/docs/how_to/structured_output/). + +## Tool-calling agent + +While a router allows an LLM to make a single decision, more complex agent architectures expand the LLM's control in two key ways: + +1. Multi-step decision making: The LLM can make a series of decisions, one after another, instead of just one. +2. Tool access: The LLM can choose from and use a variety of tools to accomplish tasks. + +[ReAct](https://arxiv.org/abs/2210.03629) is a popular general purpose agent architecture that combines these expansions, integrating three core concepts. + +1. [Tool calling](#tool-calling): Allowing the LLM to select and use various tools as needed. +2. [Memory](#memory): Enabling the agent to retain and use information from previous steps. +3. [Planning](#planning): Empowering the LLM to create and follow multi-step plans to achieve goals. + +This architecture allows for more complex and flexible agent behaviors, going beyond simple routing to enable dynamic problem-solving with multiple steps. Unlike the original [paper](https://arxiv.org/abs/2210.03629), today's agents rely on LLMs' [tool calling](#tool-calling) capabilities and operate on a list of [messages](/oss/graph-api#why-use-messages). + +In LangGraph, you can use the prebuilt [agent](/oss/prebuilts#2-create-an-agent) to get started with tool-calling agents. + +### Tool calling + +Tools are useful whenever you want an agent to interact with external systems. External systems (e.g., APIs) often require a particular input schema or payload, rather than natural language. When we bind an API, for example, as a tool, we give the model awareness of the required input schema. The model will choose to call a tool based upon the natural language input from the user and it will return an output that adheres to the tool's required schema. + +[Many LLM providers support tool calling](https://python.langchain.com/docs/integrations/chat/) and [tool calling interface](https://blog.langchain.dev/improving-core-tool-interfaces-and-docs-in-langchain/) in LangChain is simple: you can simply pass any Python `function` into `ChatModel.bind_tools(function)`. + +![Tools](/oss/images/tool_call.png) + +### Memory + +[Memory](/oss/add-memory) is crucial for agents, enabling them to retain and utilize information across multiple steps of problem-solving. It operates on different scales: + +1. [Short-term memory](/oss/add-memory#add-short-term-memory): Allows the agent to access information acquired during earlier steps in a sequence. +2. [Long-term memory](/oss/add-memory#add-long-term-memory): Enables the agent to recall information from previous interactions, such as past messages in a conversation. + +LangGraph provides full control over memory implementation: + +* [`State`](/oss/graph-api#state): User-defined schema specifying the exact structure of memory to retain. +* [`Checkpointer`](/oss/persistence#checkpoints): Mechanism to store state at every step across different interactions within a session. +* [`Store`](/oss/persistence#memory-store): Mechanism to store user-specific or application-level data across sessions. + +This flexible approach allows you to tailor the memory system to your specific agent architecture needs. Effective memory management enhances an agent's ability to maintain context, learn from past experiences, and make more informed decisions over time. For a practical guide on adding and managing memory, see [Memory](/oss/add-memory). + +### Planning + +In a tool-calling [agent](/oss/agentic-architectures#what-is-an-agent), an LLM is called repeatedly in a while-loop. At each step the agent decides which tools to call, and what the inputs to those tools should be. Those tools are then executed, and the outputs are fed back into the LLM as observations. The while-loop terminates when the agent decides it has enough information to solve the user request and it is not worth calling any more tools. + +## Custom agent architectures + +While routers and tool-calling agents (like ReAct) are common, [customizing agent architectures](https://blog.langchain.dev/why-you-should-outsource-your-agentic-infrastructure-but-own-your-cognitive-architecture/) often leads to better performance for specific tasks. LangGraph offers several powerful features for building tailored agent systems: + +### Human-in-the-loop + +Human involvement can significantly enhance agent reliability, especially for sensitive tasks. This can involve: + +* Approving specific actions +* Providing feedback to update the agent's state +* Offering guidance in complex decision-making processes + +Human-in-the-loop patterns are crucial when full automation isn't feasible or desirable. Learn more in our [human-in-the-loop guide](/oss/human-in-the-loop). + +### Parallelization + +Parallel processing is vital for efficient multi-agent systems and complex tasks. LangGraph supports parallelization through its [Send](/oss/graph-api#send) API, enabling: + +* Concurrent processing of multiple states +* Implementation of map-reduce-like operations +* Efficient handling of independent subtasks + +For practical implementation, see our [map-reduce tutorial](/oss/graph-api#map-reduce-and-the-send-api) + +### Subgraphs + +[Subgraphs](/oss/subgraphs) are essential for managing complex agent architectures, particularly in [multi-agent systems](/oss/multi-agent). They allow: + +* Isolated state management for individual agents +* Hierarchical organization of agent teams +* Controlled communication between agents and the main system + +Subgraphs communicate with the parent graph through overlapping keys in the state schema. This enables flexible, modular agent design. For implementation details, refer to our [subgraph how-to guide](/oss/subgraphs). + +### Reflection + +Reflection mechanisms can significantly improve agent reliability by: + +1. Evaluating task completion and correctness +2. Providing feedback for iterative improvement +3. Enabling self-correction and learning + +While often LLM-based, reflection can also use deterministic methods. For instance, in coding tasks, compilation errors can serve as feedback. This approach is demonstrated in [this video using LangGraph for self-corrective code generation](https://www.youtube.com/watch?v=MvNdgmM7uyc). + +By leveraging these features, LangGraph enables the creation of sophisticated, task-specific agent architectures that can handle complex workflows, collaborate effectively, and continuously improve their performance. diff --git a/src/oss/agentic-rag.mdx b/src/oss/agentic-rag.mdx new file mode 100644 index 00000000..7def1239 --- /dev/null +++ b/src/oss/agentic-rag.mdx @@ -0,0 +1,503 @@ +--- +title: Build a RAG agent +sidebarTitle: Agentic RAG +--- +In this tutorial we will build a [retrieval agent](https://python.langchain.com/docs/tutorials/qa_chat_history). Retrieval agents are useful when you want an LLM to make a decision about whether to retrieve context from a vectorstore or respond to the user directly. + +By the end of the tutorial we will have done the following: + +1. Fetch and preprocess documents that will be used for retrieval. +2. Index those documents for semantic search and create a retriever tool for the agent. +3. Build an agentic RAG system that can decide when to use the retriever tool. + +![Screenshot 2024-02-14 at 3.43.58 PM.png](/oss/images/screenshot_2024_02_14_3_43_58_pm.png) + +## Setup + +Let's download the required packages and set our API keys: + +```python +%%capture --no-stderr +%pip install -U --quiet langgraph "langchain[openai]" langchain-community langchain-text-splitters +``` + +```python +import getpass +import os + + +def _set_env(key: str): + if key not in os.environ: + os.environ[key] = getpass.getpass(f"{key}:") + + +_set_env("OPENAI_API_KEY") +``` + + + Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph. + + +## 1. Preprocess documents + +1. Fetch documents to use in our RAG system. We will use three of the most recent pages from [Lilian Weng's excellent blog](https://lilianweng.github.io/). We'll start by fetching the content of the pages using `WebBaseLoader` utility: + ```python + from langchain_community.document_loaders import WebBaseLoader + + urls = [ + "https://lilianweng.github.io/posts/2024-11-28-reward-hacking/", + "https://lilianweng.github.io/posts/2024-07-07-hallucination/", + "https://lilianweng.github.io/posts/2024-04-12-diffusion-video/", + ] + + docs = [WebBaseLoader(url).load() for url in urls] + ``` + ```python + docs[0][0].page_content.strip()[:1000] + ``` +2. Split the fetched documents into smaller chunks for indexing into our vectorstore: + ```python + from langchain_text_splitters import RecursiveCharacterTextSplitter + + docs_list = [item for sublist in docs for item in sublist] + + text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( + chunk_size=100, chunk_overlap=50 + ) + doc_splits = text_splitter.split_documents(docs_list) + ``` + ```python + doc_splits[0].page_content.strip() + ``` + +## 2. Create a retriever tool + +Now that we have our split documents, we can index them into a vector store that we'll use for semantic search. + +1. Use an in-memory vector store and OpenAI embeddings: + ```python + from langchain_core.vectorstores import InMemoryVectorStore + from langchain_openai import OpenAIEmbeddings + + vectorstore = InMemoryVectorStore.from_documents( + documents=doc_splits, embedding=OpenAIEmbeddings() + ) + retriever = vectorstore.as_retriever() + ``` +2. Create a retriever tool using LangChain's prebuilt `create_retriever_tool`: + ```python + from langchain.tools.retriever import create_retriever_tool + + retriever_tool = create_retriever_tool( + retriever, + "retrieve_blog_posts", + "Search and return information about Lilian Weng blog posts.", + ) + ``` +3. Test the tool: + ```python + retriever_tool.invoke({"query": "types of reward hacking"}) + ``` + +## 3. Generate query + +Now we will start building components ([nodes](/oss/graph-api#nodes) and [edges](/oss/graph-api#edges)) for our agentic RAG graph. + +:::python +Note that the components will operate on the [`MessagesState`](/oss/graph-api#messagesstate) — graph state that contains a `messages` key with a list of [chat messages](https://python.langchain.com/docs/concepts/messages/). +::: + +:::js +Note that the components will operate on the `MessagesZodState` — graph state that contains a `messages` key with a list of [chat messages](https://js.langchain.com/docs/concepts/messages/). +::: + +1. Build a `generate_query_or_respond` node. It will call an LLM to generate a response based on the current graph state (list of messages). Given the input messages, it will decide to retrieve using the retriever tool, or respond directly to the user. Note that we're giving the chat model access to the `retriever_tool` we created earlier via `.bind_tools`: + ```python + from langgraph.graph import MessagesState + from langchain.chat_models import init_chat_model + + response_model = init_chat_model("openai:gpt-4.1", temperature=0) + + + def generate_query_or_respond(state: MessagesState): + """Call the model to generate a response based on the current state. Given + the question, it will decide to retrieve using the retriever tool, or simply respond to the user. + """ + response = ( + response_model + # highlight-next-line + .bind_tools([retriever_tool]).invoke(state["messages"]) + ) + return {"messages": [response]} + ``` +2. Try it on a random input: + ```python + input = {"messages": [{"role": "user", "content": "hello!"}]} + generate_query_or_respond(input)["messages"][-1].pretty_print() + ``` + **Output:** + ``` + ================================== Ai Message ================================== + + Hello! How can I help you today? + ``` +3. Ask a question that requires semantic search: + ```python + input = { + "messages": [ + { + "role": "user", + "content": "What does Lilian Weng say about types of reward hacking?", + } + ] + } + generate_query_or_respond(input)["messages"][-1].pretty_print() + ``` + **Output:** + ``` + ================================== Ai Message ================================== + Tool Calls: + retrieve_blog_posts (call_tYQxgfIlnQUDMdtAhdbXNwIM) + Call ID: call_tYQxgfIlnQUDMdtAhdbXNwIM + Args: + query: types of reward hacking + ``` + +## 4. Grade documents + +1. Add a [conditional edge](/oss/graph-api#conditional-edges) — `grade_documents` — to determine whether the retrieved documents are relevant to the question. We will use a model with a structured output schema `GradeDocuments` for document grading. The `grade_documents` function will return the name of the node to go to based on the grading decision (`generate_answer` or `rewrite_question`): + ```python + from pydantic import BaseModel, Field + from typing import Literal + + GRADE_PROMPT = ( + "You are a grader assessing relevance of a retrieved document to a user question. \n " + "Here is the retrieved document: \n\n {context} \n\n" + "Here is the user question: {question} \n" + "If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n" + "Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question." + ) + + + # highlight-next-line + class GradeDocuments(BaseModel): + """Grade documents using a binary score for relevance check.""" + + binary_score: str = Field( + description="Relevance score: 'yes' if relevant, or 'no' if not relevant" + ) + + + grader_model = init_chat_model("openai:gpt-4.1", temperature=0) + + + def grade_documents( + state: MessagesState, + ) -> Literal["generate_answer", "rewrite_question"]: + """Determine whether the retrieved documents are relevant to the question.""" + question = state["messages"][0].content + context = state["messages"][-1].content + + prompt = GRADE_PROMPT.format(question=question, context=context) + response = ( + grader_model + # highlight-next-line + .with_structured_output(GradeDocuments).invoke( + [{"role": "user", "content": prompt}] + ) + ) + score = response.binary_score + + if score == "yes": + return "generate_answer" + else: + return "rewrite_question" + ``` +2. Run this with irrelevant documents in the tool response: + ```python + from langchain_core.messages import convert_to_messages + + input = { + "messages": convert_to_messages( + [ + { + "role": "user", + "content": "What does Lilian Weng say about types of reward hacking?", + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "1", + "name": "retrieve_blog_posts", + "args": {"query": "types of reward hacking"}, + } + ], + }, + {"role": "tool", "content": "meow", "tool_call_id": "1"}, + ] + ) + } + grade_documents(input) + ``` +3. Confirm that the relevant documents are classified as such: + ```python + input = { + "messages": convert_to_messages( + [ + { + "role": "user", + "content": "What does Lilian Weng say about types of reward hacking?", + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "1", + "name": "retrieve_blog_posts", + "args": {"query": "types of reward hacking"}, + } + ], + }, + { + "role": "tool", + "content": "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering", + "tool_call_id": "1", + }, + ] + ) + } + grade_documents(input) + ``` + +## 5. Rewrite question + +1. Build the `rewrite_question` node. The retriever tool can return potentially irrelevant documents, which indicates a need to improve the original user question. To do so, we will call the `rewrite_question` node: + ```python + REWRITE_PROMPT = ( + "Look at the input and try to reason about the underlying semantic intent / meaning.\n" + "Here is the initial question:" + "\n ------- \n" + "{question}" + "\n ------- \n" + "Formulate an improved question:" + ) + + + def rewrite_question(state: MessagesState): + """Rewrite the original user question.""" + messages = state["messages"] + question = messages[0].content + prompt = REWRITE_PROMPT.format(question=question) + response = response_model.invoke([{"role": "user", "content": prompt}]) + return {"messages": [{"role": "user", "content": response.content}]} + ``` +2. Try it out: + ```python + input = { + "messages": convert_to_messages( + [ + { + "role": "user", + "content": "What does Lilian Weng say about types of reward hacking?", + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "1", + "name": "retrieve_blog_posts", + "args": {"query": "types of reward hacking"}, + } + ], + }, + {"role": "tool", "content": "meow", "tool_call_id": "1"}, + ] + ) + } + + response = rewrite_question(input) + print(response["messages"][-1]["content"]) + ``` + **Output:** + ``` + What are the different types of reward hacking described by Lilian Weng, and how does she explain them? + ``` + +## 6. Generate an answer + +1. Build `generate_answer` node: if we pass the grader checks, we can generate the final answer based on the original question and the retrieved context: + ```python + GENERATE_PROMPT = ( + "You are an assistant for question-answering tasks. " + "Use the following pieces of retrieved context to answer the question. " + "If you don't know the answer, just say that you don't know. " + "Use three sentences maximum and keep the answer concise.\n" + "Question: {question} \n" + "Context: {context}" + ) + + + def generate_answer(state: MessagesState): + """Generate an answer.""" + question = state["messages"][0].content + context = state["messages"][-1].content + prompt = GENERATE_PROMPT.format(question=question, context=context) + response = response_model.invoke([{"role": "user", "content": prompt}]) + return {"messages": [response]} + ``` +2. Try it: + ```python + input = { + "messages": convert_to_messages( + [ + { + "role": "user", + "content": "What does Lilian Weng say about types of reward hacking?", + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "1", + "name": "retrieve_blog_posts", + "args": {"query": "types of reward hacking"}, + } + ], + }, + { + "role": "tool", + "content": "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering", + "tool_call_id": "1", + }, + ] + ) + } + + response = generate_answer(input) + response["messages"][-1].pretty_print() + ``` + **Output:** + ``` + ================================== Ai Message ================================== + + Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors. + ``` + +## 7. Assemble the graph + +* Start with a `generate_query_or_respond` and determine if we need to call `retriever_tool` +* Route to next step using `tools_condition`: + * If `generate_query_or_respond` returned `tool_calls`, call `retriever_tool` to retrieve context + * Otherwise, respond directly to the user +* Grade retrieved document content for relevance to the question (`grade_documents`) and route to next step: + * If not relevant, rewrite the question using `rewrite_question` and then call `generate_query_or_respond` again + * If relevant, proceed to `generate_answer` and generate final response using the `ToolMessage` with the retrieved document context + +```python +from langgraph.graph import StateGraph, START, END +from langgraph.prebuilt import ToolNode +from langgraph.prebuilt import tools_condition + +workflow = StateGraph(MessagesState) + +# Define the nodes we will cycle between +workflow.add_node(generate_query_or_respond) +workflow.add_node("retrieve", ToolNode([retriever_tool])) +workflow.add_node(rewrite_question) +workflow.add_node(generate_answer) + +workflow.add_edge(START, "generate_query_or_respond") + +# Decide whether to retrieve +workflow.add_conditional_edges( + "generate_query_or_respond", + # Assess LLM decision (call `retriever_tool` tool or respond to the user) + tools_condition, + { + # Translate the condition outputs to nodes in our graph + "tools": "retrieve", + END: END, + }, +) + +# Edges taken after the `action` node is called. +workflow.add_conditional_edges( + "retrieve", + # Assess agent decision + grade_documents, +) +workflow.add_edge("generate_answer", END) +workflow.add_edge("rewrite_question", "generate_query_or_respond") + +# Compile +graph = workflow.compile() +``` + +Visualize the graph: + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Graph](/oss/images/agentic-rag-output.png) + +## 8. Run the agentic RAG + +```python +for chunk in graph.stream( + { + "messages": [ + { + "role": "user", + "content": "What does Lilian Weng say about types of reward hacking?", + } + ] + } +): + for node, update in chunk.items(): + print("Update from node", node) + update["messages"][-1].pretty_print() + print("\n\n") +``` + +**Output:** + +``` +Update from node generate_query_or_respond +================================== Ai Message ================================== +Tool Calls: + retrieve_blog_posts (call_NYu2vq4km9nNNEFqJwefWKu1) + Call ID: call_NYu2vq4km9nNNEFqJwefWKu1 + Args: + query: types of reward hacking + + + +Update from node retrieve +================================= Tool Message ================================== +Name: retrieve_blog_posts + +(Note: Some work defines reward tampering as a distinct category of misalignment behavior from reward hacking. But I consider reward hacking as a broader concept here.) +At a high level, reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering. + +Why does Reward Hacking Exist?# + +Pan et al. (2022) investigated reward hacking as a function of agent capabilities, including (1) model size, (2) action space resolution, (3) observation space noise, and (4) training time. They also proposed a taxonomy of three types of misspecified proxy rewards: + +Let's Define Reward Hacking# +Reward shaping in RL is challenging. Reward hacking occurs when an RL agent exploits flaws or ambiguities in the reward function to obtain high rewards without genuinely learning the intended behaviors or completing the task as designed. In recent years, several related concepts have been proposed, all referring to some form of reward hacking: + + + +Update from node generate_answer +================================== Ai Message ================================== + +Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors. +``` diff --git a/src/oss/application-structure.mdx b/src/oss/application-structure.mdx new file mode 100644 index 00000000..7e57478a --- /dev/null +++ b/src/oss/application-structure.mdx @@ -0,0 +1,160 @@ +--- +title: Application Structure +--- + +## Overview + +A LangGraph application consists of one or more graphs, a configuration file (`langgraph.json`), a file that specifies dependencies, and an optional `.env` file that specifies environment variables. + +This guide shows a typical structure of an application and shows how the required information to deploy an application using the LangGraph Platform is specified. + +## Key Concepts + +To deploy using the LangGraph Platform, the following information should be provided: + +1. A [LangGraph configuration file](#configuration-file-concepts) (`langgraph.json`) that specifies the dependencies, graphs, and environment variables to use for the application. +2. The [graphs](#graphs) that implement the logic of the application. +3. A file that specifies [dependencies](#dependencies) required to run the application. +4. [Environment variables](#environment-variables) that are required for the application to run. + +## File Structure + +Below are examples of directory structures for applications: + +:::python + + + ```plaintext + my-app/ + ├── my_agent # all project code lies within here + │ ├── utils # utilities for your graph + │ │ ├── __init__.py + │ │ ├── tools.py # tools for your graph + │ │ ├── nodes.py # node functions for your graph + │ │ └── state.py # state definition of your graph + │ ├── __init__.py + │ └── agent.py # code for constructing your graph + ├── .env # environment variables + ├── requirements.txt # package dependencies + └── langgraph.json # configuration file for LangGraph + ``` + + + ```plaintext + my-app/ + ├── my_agent # all project code lies within here + │ ├── utils # utilities for your graph + │ │ ├── __init__.py + │ │ ├── tools.py # tools for your graph + │ │ ├── nodes.py # node functions for your graph + │ │ └── state.py # state definition of your graph + │ ├── __init__.py + │ └── agent.py # code for constructing your graph + ├── .env # environment variables + ├── langgraph.json # configuration file for LangGraph + └── pyproject.toml # dependencies for your project + ``` + + +::: + +:::js +```plaintext +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for your graph +│ │ └── state.ts # state definition of your graph +│ └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` +::: + + + The directory structure of a LangGraph application can vary depending on the programming language and the package manager used. + + + +## Configuration File + +The `langgraph.json` file is a JSON file that specifies the dependencies, graphs, environment variables, and other settings required to deploy a LangGraph application. + +See the [LangGraph configuration file reference](/langgraph-platform/cli#configuration-file) for details on all supported keys in the JSON file. + + + The [LangGraph CLI](/langgraph-platform/cli) defaults to using the configuration file `langgraph.json` in the current directory. + + +### Examples + +:::python +* The dependencies involve a custom local package and the `langchain_openai` package. +* A single graph will be loaded from the file `./your_package/your_file.py` with the variable `variable`. +* The environment variables are loaded from the `.env` file. + +```json +{ + "dependencies": ["langchain_openai", "./your_package"], + "graphs": { + "my_agent": "./your_package/your_file.py:agent" + }, + "env": "./.env" +} +``` +::: + +:::js +* The dependencies will be loaded from a dependency file in the local directory (e.g., `package.json`). +* A single graph will be loaded from the file `./your_package/your_file.js` with the function `agent`. +* The environment variable `OPENAI_API_KEY` is set inline. + +```json +{ + "dependencies": ["."], + "graphs": { + "my_agent": "./your_package/your_file.js:agent" + }, + "env": { + "OPENAI_API_KEY": "secret-key" + } +} +``` +::: + +## Dependencies + +:::python +A LangGraph application may depend on other Python packages. +::: + +:::js +A LangGraph application may depend on other TypeScript/JavaScript libraries. +::: + +You will generally need to specify the following information for dependencies to be set up correctly: + +:::python +1. A file in the directory that specifies the dependencies (e.g. `requirements.txt`, `pyproject.toml`, or `package.json`). +::: + +:::js +1. A file in the directory that specifies the dependencies (e.g. `package.json`). +::: + +1. A `dependencies` key in the [LangGraph configuration file](#configuration-file-concepts) that specifies the dependencies required to run the LangGraph application. +2. Any additional binaries or system libraries can be specified using `dockerfile_lines` key in the [LangGraph configuration file](#configuration-file-concepts). + +## Graphs + +Use the `graphs` key in the [LangGraph configuration file](#configuration-file-concepts) to specify which graphs will be available in the deployed LangGraph application. + +You can specify one or more graphs in the configuration file. Each graph is identified by a name (which should be unique) and a path for either: (1) the compiled graph or (2) a function that makes a graph is defined. + +## Environment Variables + +If you're working with a deployed LangGraph application locally, you can configure environment variables in the `env` key of the [LangGraph configuration file](#configuration-file-concepts). + +For a production deployment, you will typically want to configure the environment variables in the deployment environment. diff --git a/src/oss/call-tools.mdx b/src/oss/call-tools.mdx new file mode 100644 index 00000000..96caaadd --- /dev/null +++ b/src/oss/call-tools.mdx @@ -0,0 +1,2339 @@ +--- +title: Call tools +--- +[Tools](/oss/tools) encapsulate a callable function and its input schema. These can be passed to compatible chat models, allowing the model to decide whether to invoke a tool and determine the appropriate arguments. + +You can [define your own tools](#define-a-tool) or use [prebuilt tools](#prebuilt-tools) + +## Define a tool + +:::python +Define a basic tool with the [@tool](https://python.langchain.com/api_reference/core/tools/langchain_core.tools.convert.tool.html) decorator: + +```python +from langchain_core.tools import tool + +# highlight-next-line +@tool +def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b +``` +::: + +:::js +Define a basic tool with the [tool](https://js.langchain.com/docs/api/core/tools/classes/tool.html) function: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers.", + schema: z.object({ + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }), + } +); +``` +::: + +## Run a tool + +Tools conform to the [Runnable interface](https://python.langchain.com/docs/concepts/runnables/), which means you can run a tool using the `invoke` method: + +:::python +```python +multiply.invoke({"a": 6, "b": 7}) # returns 42 +``` +::: + +:::js +```typescript +await multiply.invoke({ a: 6, b: 7 }); // returns 42 +``` +::: + +If the tool is invoked with `type="tool_call"`, it will return a [ToolMessage](https://python.langchain.com/docs/concepts/messages/#toolmessage): + +:::python +```python +tool_call = { + "type": "tool_call", + "id": "1", + "args": {"a": 42, "b": 7} +} +multiply.invoke(tool_call) # returns a ToolMessage object +``` + +Output: + +```pycon +ToolMessage(content='294', name='multiply', tool_call_id='1') +``` +::: + +:::js +```typescript +const toolCall = { + type: "tool_call", + id: "1", + name: "multiply", + args: { a: 42, b: 7 }, +}; +await multiply.invoke(toolCall); // returns a ToolMessage object +``` + +Output: + +``` +ToolMessage { + content: "294", + name: "multiply", + tool_call_id: "1" +} +``` +::: + +## Use in an agent + +:::python +To create a tool-calling agent, you can use the prebuilt @[create_react_agent][create_react_agent]: + +```python +from langchain_core.tools import tool +# highlight-next-line +from langgraph.prebuilt import create_react_agent + +@tool +def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + +# highlight-next-line +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet", + tools=[multiply] +) +agent.invoke({"messages": [{"role": "user", "content": "what's 42 x 7?"}]}) +``` +::: + +:::js +To create a tool-calling agent, you can use the prebuilt [createReactAgent](https://js.langchain.com/docs/api/langgraph_prebuilt/functions/createReactAgent.html): + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +// highlight-next-line +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers.", + schema: z.object({ + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }), + } +); + +// highlight-next-line +const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: [multiply], +}); + +await agent.invoke({ + messages: [{ role: "user", content: "what's 42 x 7?" }], +}); +``` +::: + +:::python +### Dynamically select tools + +Configure tool availability at runtime based on context: + +```python +from dataclasses import dataclass +from typing import Literal + +from langchain.chat_models import init_chat_model +from langchain_core.tools import tool + +from langgraph.prebuilt import create_react_agent +from langgraph.prebuilt.chat_agent_executor import AgentState +from langgraph.runtime import Runtime + + +@dataclass +class CustomContext: + tools: list[Literal["weather", "compass"]] + + +@tool +def weather() -> str: + """Returns the current weather conditions.""" + return "It's nice and sunny." + + +@tool +def compass() -> str: + """Returns the direction the user is facing.""" + return "North" + +model = init_chat_model("anthropic:claude-sonnet-4-20250514") + +# highlight-next-line +def configure_model(state: AgentState, runtime: Runtime[CustomContext]): + """Configure the model with tools based on runtime context.""" + selected_tools = [ + tool + for tool in [weather, compass] + if tool.name in runtime.context.tools + ] + return model.bind_tools(selected_tools) + + +agent = create_react_agent( + # Dynamically configure the model with tools based on runtime context + # highlight-next-line + configure_model, + # Initialize with all tools available + # highlight-next-line + tools=[weather, compass] +) + +output = agent.invoke( + { + "messages": [ + { + "role": "user", + "content": "Who are you and what tools do you have access to?", + } + ] + }, + # highlight-next-line + context=CustomContext(tools=["weather"]), # Only enable the weather tool +) + +print(output["messages"][-1].text()) +``` + + + **New in langgraph>=0.6** + +::: + +## Use in a workflow + +If you are writing a custom workflow, you will need to: + +1. register the tools with the chat model +2. call the tool if the model decides to use it + +:::python +Use `model.bind_tools()` to register the tools with the model. + +```python +from langchain.chat_models import init_chat_model + +model = init_chat_model(model="claude-3-5-haiku-latest") + +# highlight-next-line +model_with_tools = model.bind_tools([multiply]) +``` +::: + +:::js +Use `model.bindTools()` to register the tools with the model. + +```typescript +import { ChatOpenAI } from "@langchain/openai"; + +const model = new ChatOpenAI({ model: "gpt-4o" }); + +// highlight-next-line +const modelWithTools = model.bindTools([multiply]); +``` +::: + +LLMs automatically determine if a tool invocation is necessary and handle calling the tool with the appropriate arguments. + + + :::python + ```python + from langchain_core.tools import tool + from langchain.chat_models import init_chat_model + + @tool + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + + model = init_chat_model(model="claude-3-5-haiku-latest") + # highlight-next-line + model_with_tools = model.bind_tools([multiply]) + + response_message = model_with_tools.invoke("what's 42 x 7?") + tool_call = response_message.tool_calls[0] + + multiply.invoke(tool_call) + ``` + + ```pycon + ToolMessage( + content='294', + name='multiply', + tool_call_id='toolu_0176DV4YKSD8FndkeuuLj36c' + ) + ``` + ::: + + :::js + ```typescript + import { tool } from "@langchain/core/tools"; + import { ChatOpenAI } from "@langchain/openai"; + import { z } from "zod"; + + const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers.", + schema: z.object({ + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }), + } + ); + + const model = new ChatOpenAI({ model: "gpt-4o" }); + // highlight-next-line + const modelWithTools = model.bindTools([multiply]); + + const responseMessage = await modelWithTools.invoke("what's 42 x 7?"); + const toolCall = responseMessage.tool_calls[0]; + + await multiply.invoke(toolCall); + ``` + + ``` + ToolMessage { + content: "294", + name: "multiply", + tool_call_id: "toolu_0176DV4YKSD8FndkeuuLj36c" + } + ``` + ::: + + +#### ToolNode + +:::python +To execute tools in custom workflows, use the prebuilt @[`ToolNode`][ToolNode] or implement your own custom node. + +`ToolNode` is a specialized node for executing tools in a workflow. It provides the following features: + +* Supports both synchronous and asynchronous tools. +* Executes multiple tools concurrently. +* Handles errors during tool execution (`handle_tool_errors=True`, enabled by default). See [handling tool errors](#handle-errors) for more details. + +`ToolNode` operates on [`MessagesState`](/oss/graph-api#messagesstate): + +* **Input**: `MessagesState`, where the last message is an `AIMessage` containing the `tool_calls` parameter. +* **Output**: `MessagesState` updated with the resulting [`ToolMessage`](https://python.langchain.com/docs/concepts/messages/#toolmessage) from executed tools. + +```python +# highlight-next-line +from langgraph.prebuilt import ToolNode + +def get_weather(location: str): + """Call to get the current weather.""" + if location.lower() in ["sf", "san francisco"]: + return "It's 60 degrees and foggy." + else: + return "It's 90 degrees and sunny." + +def get_coolest_cities(): + """Get a list of coolest cities""" + return "nyc, sf" + +# highlight-next-line +tool_node = ToolNode([get_weather, get_coolest_cities]) +tool_node.invoke({"messages": [...]}) +``` +::: + +:::js +To execute tools in custom workflows, use the prebuilt [`ToolNode`](https://js.langchain.com/docs/api/langgraph_prebuilt/classes/ToolNode.html) or implement your own custom node. + +`ToolNode` is a specialized node for executing tools in a workflow. It provides the following features: + +* Supports both synchronous and asynchronous tools. +* Executes multiple tools concurrently. +* Handles errors during tool execution (`handleToolErrors: true`, enabled by default). See [handling tool errors](#handle-errors) for more details. +* **Input**: `MessagesZodState`, where the last message is an `AIMessage` containing the `tool_calls` parameter. +* **Output**: `MessagesZodState` updated with the resulting [`ToolMessage`](https://js.langchain.com/docs/concepts/messages/#toolmessage) from executed tools. + +```typescript +// highlight-next-line +import { ToolNode } from "@langchain/langgraph/prebuilt"; + +const getWeather = tool( + (input) => { + if (["sf", "san francisco"].includes(input.location.toLowerCase())) { + return "It's 60 degrees and foggy."; + } else { + return "It's 90 degrees and sunny."; + } + }, + { + name: "get_weather", + description: "Call to get the current weather.", + schema: z.object({ + location: z.string().describe("Location to get the weather for."), + }), + } +); + +const getCoolestCities = tool( + () => { + return "nyc, sf"; + }, + { + name: "get_coolest_cities", + description: "Get a list of coolest cities", + schema: z.object({ + noOp: z.string().optional().describe("No-op parameter."), + }), + } +); + +// highlight-next-line +const toolNode = new ToolNode([getWeather, getCoolestCities]); +await toolNode.invoke({ messages: [...] }); +``` +::: + + + :::python + ```python + from langchain_core.messages import AIMessage + from langgraph.prebuilt import ToolNode + + # Define tools + @tool + def get_weather(location: str): + """Call to get the current weather.""" + if location.lower() in ["sf", "san francisco"]: + return "It's 60 degrees and foggy." + else: + return "It's 90 degrees and sunny." + + # highlight-next-line + tool_node = ToolNode([get_weather]) + + message_with_single_tool_call = AIMessage( + content="", + tool_calls=[ + { + "name": "get_weather", + "args": {"location": "sf"}, + "id": "tool_call_id", + "type": "tool_call", + } + ], + ) + + tool_node.invoke({"messages": [message_with_single_tool_call]}) + ``` + + ``` + {'messages': [ToolMessage(content="It's 60 degrees and foggy.", name='get_weather', tool_call_id='tool_call_id')]} + ``` + ::: + + :::js + ```typescript + import { AIMessage } from "@langchain/core/messages"; + import { ToolNode } from "@langchain/langgraph/prebuilt"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + + // Define tools + const getWeather = tool( + (input) => { + if (["sf", "san francisco"].includes(input.location.toLowerCase())) { + return "It's 60 degrees and foggy."; + } else { + return "It's 90 degrees and sunny."; + } + }, + { + name: "get_weather", + description: "Call to get the current weather.", + schema: z.object({ + location: z.string().describe("Location to get the weather for."), + }), + } + ); + + // highlight-next-line + const toolNode = new ToolNode([getWeather]); + + const messageWithSingleToolCall = new AIMessage({ + content: "", + tool_calls: [ + { + name: "get_weather", + args: { location: "sf" }, + id: "tool_call_id", + type: "tool_call", + } + ], + }); + + await toolNode.invoke({ messages: [messageWithSingleToolCall] }); + ``` + + ``` + { messages: [ToolMessage { content: "It's 60 degrees and foggy.", name: "get_weather", tool_call_id: "tool_call_id" }] } + ``` + ::: + + + + :::python + ```python + from langchain_core.messages import AIMessage + from langgraph.prebuilt import ToolNode + + # Define tools + + def get_weather(location: str): + """Call to get the current weather.""" + if location.lower() in ["sf", "san francisco"]: + return "It's 60 degrees and foggy." + else: + return "It's 90 degrees and sunny." + + def get_coolest_cities(): + """Get a list of coolest cities""" + return "nyc, sf" + + # highlight-next-line + tool_node = ToolNode([get_weather, get_coolest_cities]) + + message_with_multiple_tool_calls = AIMessage( + content="", + tool_calls=[ + { + "name": "get_coolest_cities", + "args": {}, + "id": "tool_call_id_1", + "type": "tool_call", + }, + { + "name": "get_weather", + "args": {"location": "sf"}, + "id": "tool_call_id_2", + "type": "tool_call", + }, + ], + ) + + # highlight-next-line + tool_node.invoke({"messages": [message_with_multiple_tool_calls]}) # (1)! + ``` + + 1. `ToolNode` will execute both tools in parallel + + ``` + { + 'messages': [ + ToolMessage(content='nyc, sf', name='get_coolest_cities', tool_call_id='tool_call_id_1'), + ToolMessage(content="It's 60 degrees and foggy.", name='get_weather', tool_call_id='tool_call_id_2') + ] + } + ``` + ::: + + :::js + ```typescript + import { AIMessage } from "@langchain/core/messages"; + import { ToolNode } from "@langchain/langgraph/prebuilt"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + + // Define tools + const getWeather = tool( + (input) => { + if (["sf", "san francisco"].includes(input.location.toLowerCase())) { + return "It's 60 degrees and foggy."; + } else { + return "It's 90 degrees and sunny."; + } + }, + { + name: "get_weather", + description: "Call to get the current weather.", + schema: z.object({ + location: z.string().describe("Location to get the weather for."), + }), + } + ); + + const getCoolestCities = tool( + () => { + return "nyc, sf"; + }, + { + name: "get_coolest_cities", + description: "Get a list of coolest cities", + schema: z.object({ + noOp: z.string().optional().describe("No-op parameter."), + }), + } + ); + + // highlight-next-line + const toolNode = new ToolNode([getWeather, getCoolestCities]); + + const messageWithMultipleToolCalls = new AIMessage({ + content: "", + tool_calls: [ + { + name: "get_coolest_cities", + args: {}, + id: "tool_call_id_1", + type: "tool_call", + }, + { + name: "get_weather", + args: { location: "sf" }, + id: "tool_call_id_2", + type: "tool_call", + }, + ], + }); + + // highlight-next-line + await toolNode.invoke({ messages: [messageWithMultipleToolCalls] }); // (1)! + ``` + + 1. `ToolNode` will execute both tools in parallel + + ``` + { + messages: [ + ToolMessage { content: "nyc, sf", name: "get_coolest_cities", tool_call_id: "tool_call_id_1" }, + ToolMessage { content: "It's 60 degrees and foggy.", name: "get_weather", tool_call_id: "tool_call_id_2" } + ] + } + ``` + ::: + + + + :::python + ```python + from langchain.chat_models import init_chat_model + from langgraph.prebuilt import ToolNode + + def get_weather(location: str): + """Call to get the current weather.""" + if location.lower() in ["sf", "san francisco"]: + return "It's 60 degrees and foggy." + else: + return "It's 90 degrees and sunny." + + # highlight-next-line + tool_node = ToolNode([get_weather]) + + model = init_chat_model(model="claude-3-5-haiku-latest") + # highlight-next-line + model_with_tools = model.bind_tools([get_weather]) # (1)! + + + # highlight-next-line + response_message = model_with_tools.invoke("what's the weather in sf?") + tool_node.invoke({"messages": [response_message]}) + ``` + + 1. Use `.bind_tools()` to attach the tool schema to the chat model + + ``` + {'messages': [ToolMessage(content="It's 60 degrees and foggy.", name='get_weather', tool_call_id='toolu_01Pnkgw5JeTRxXAU7tyHT4UW')]} + ``` + ::: + + :::js + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { ToolNode } from "@langchain/langgraph/prebuilt"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + + const getWeather = tool( + (input) => { + if (["sf", "san francisco"].includes(input.location.toLowerCase())) { + return "It's 60 degrees and foggy."; + } else { + return "It's 90 degrees and sunny."; + } + }, + { + name: "get_weather", + description: "Call to get the current weather.", + schema: z.object({ + location: z.string().describe("Location to get the weather for."), + }), + } + ); + + // highlight-next-line + const toolNode = new ToolNode([getWeather]); + + const model = new ChatOpenAI({ model: "gpt-4o" }); + // highlight-next-line + const modelWithTools = model.bindTools([getWeather]); // (1)! + + // highlight-next-line + const responseMessage = await modelWithTools.invoke("what's the weather in sf?"); + await toolNode.invoke({ messages: [responseMessage] }); + ``` + + 1. Use `.bindTools()` to attach the tool schema to the chat model + + ``` + { messages: [ToolMessage { content: "It's 60 degrees and foggy.", name: "get_weather", tool_call_id: "toolu_01Pnkgw5JeTRxXAU7tyHT4UW" }] } + ``` + ::: + + + + This is an example of creating a tool-calling agent from scratch using `ToolNode`. You can also use LangGraph's prebuilt [agent](/oss/prebuilts). + + :::python + ```python + from langchain.chat_models import init_chat_model + from langgraph.prebuilt import ToolNode + from langgraph.graph import StateGraph, MessagesState, START, END + + def get_weather(location: str): + """Call to get the current weather.""" + if location.lower() in ["sf", "san francisco"]: + return "It's 60 degrees and foggy." + else: + return "It's 90 degrees and sunny." + + # highlight-next-line + tool_node = ToolNode([get_weather]) + + model = init_chat_model(model="claude-3-5-haiku-latest") + # highlight-next-line + model_with_tools = model.bind_tools([get_weather]) + + def should_continue(state: MessagesState): + messages = state["messages"] + last_message = messages[-1] + if last_message.tool_calls: + return "tools" + return END + + def call_model(state: MessagesState): + messages = state["messages"] + response = model_with_tools.invoke(messages) + return {"messages": [response]} + + builder = StateGraph(MessagesState) + + # Define the two nodes we will cycle between + builder.add_node("call_model", call_model) + # highlight-next-line + builder.add_node("tools", tool_node) + + builder.add_edge(START, "call_model") + builder.add_conditional_edges("call_model", should_continue, ["tools", END]) + builder.add_edge("tools", "call_model") + + graph = builder.compile() + + graph.invoke({"messages": [{"role": "user", "content": "what's the weather in sf?"}]}) + ``` + + ``` + { + 'messages': [ + HumanMessage(content="what's the weather in sf?"), + AIMessage( + content=[{'text': "I'll help you check the weather in San Francisco right now.", 'type': 'text'}, {'id': 'toolu_01A4vwUEgBKxfFVc5H3v1CNs', 'input': {'location': 'San Francisco'}, 'name': 'get_weather', 'type': 'tool_use'}], + tool_calls=[{'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'toolu_01A4vwUEgBKxfFVc5H3v1CNs', 'type': 'tool_call'}] + ), + ToolMessage(content="It's 60 degrees and foggy."), + AIMessage(content="The current weather in San Francisco is 60 degrees and foggy. Typical San Francisco weather with its famous marine layer!") + ] + } + ``` + ::: + + :::js + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { ToolNode } from "@langchain/langgraph/prebuilt"; + import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { isAIMessage } from "@langchain/core/messages"; + + const getWeather = tool( + (input) => { + if (["sf", "san francisco"].includes(input.location.toLowerCase())) { + return "It's 60 degrees and foggy."; + } else { + return "It's 90 degrees and sunny."; + } + }, + { + name: "get_weather", + description: "Call to get the current weather.", + schema: z.object({ + location: z.string().describe("Location to get the weather for."), + }), + } + ); + + // highlight-next-line + const toolNode = new ToolNode([getWeather]); + + const model = new ChatOpenAI({ model: "gpt-4o" }); + // highlight-next-line + const modelWithTools = model.bindTools([getWeather]); + + const shouldContinue = (state: z.infer) => { + const messages = state.messages; + const lastMessage = messages.at(-1); + if (lastMessage && isAIMessage(lastMessage) && lastMessage.tool_calls?.length) { + return "tools"; + } + return END; + }; + + const callModel = async (state: z.infer) => { + const messages = state.messages; + const response = await modelWithTools.invoke(messages); + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + // Define the two nodes we will cycle between + .addNode("agent", callModel) + // highlight-next-line + .addNode("tools", toolNode) + .addEdge(START, "agent") + .addConditionalEdges("agent", shouldContinue, ["tools", END]) + .addEdge("tools", "agent"); + + const graph = builder.compile(); + + await graph.invoke({ + messages: [{ role: "user", content: "what's the weather in sf?" }] + }); + ``` + + ``` + { + messages: [ + HumanMessage { content: "what's the weather in sf?" }, + AIMessage { + content: [{ text: "I'll help you check the weather in San Francisco right now.", type: "text" }, { id: "toolu_01A4vwUEgBKxfFVc5H3v1CNs", input: { location: "San Francisco" }, name: "get_weather", type: "tool_use" }], + tool_calls: [{ name: "get_weather", args: { location: "San Francisco" }, id: "toolu_01A4vwUEgBKxfFVc5H3v1CNs", type: "tool_call" }] + }, + ToolMessage { content: "It's 60 degrees and foggy." }, + AIMessage { content: "The current weather in San Francisco is 60 degrees and foggy. Typical San Francisco weather with its famous marine layer!" } + ] + } + ``` + ::: + + +## Tool customization + +For more control over tool behavior, use the `@tool` decorator. + +### Parameter descriptions + +:::python +Auto-generate descriptions from docstrings: + +```python +# highlight-next-line +from langchain_core.tools import tool + +# highlight-next-line +@tool("multiply_tool", parse_docstring=True) +def multiply(a: int, b: int) -> int: + """Multiply two numbers. + + Args: + a: First operand + b: Second operand + """ + return a * b +``` +::: + +:::js +Auto-generate descriptions from schema: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers.", + schema: z.object({ + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }), + } +); +``` +::: + +### Explicit input schema + +:::python +Define schemas using `args_schema`: + +```python +from pydantic import BaseModel, Field +from langchain_core.tools import tool + +class MultiplyInputSchema(BaseModel): + """Multiply two numbers""" + a: int = Field(description="First operand") + b: int = Field(description="Second operand") + +# highlight-next-line +@tool("multiply_tool", args_schema=MultiplyInputSchema) +def multiply(a: int, b: int) -> int: + return a * b +``` +::: + +### Tool name + +Override the default tool name using the first argument or name property: + +:::python +```python +from langchain_core.tools import tool + +# highlight-next-line +@tool("multiply_tool") +def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b +``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply_tool", // Custom name + description: "Multiply two numbers.", + schema: z.object({ + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }), + } +); +``` +::: + +## Context management + +Tools within LangGraph sometimes require context data, such as runtime-only arguments (e.g., user IDs or session details), that should not be controlled by the model. LangGraph provides three methods for managing such context: + +| Type | Usage Scenario | Mutable | Lifetime | +| --------------------------------------- | ---------------------------------------- | ------- | ------------------------ | +| [Configuration](#configuration) | Static, immutable runtime data | ❌ | Single invocation | +| [Short-term memory](#short-term-memory) | Dynamic, changing data during invocation | ✅ | Single invocation | +| [Long-term memory](#long-term-memory) | Persistent, cross-session data | ✅ | Across multiple sessions | + +### Configuration + +:::python +Use configuration when you have **immutable** runtime data that tools require, such as user identifiers. You pass these arguments via [`RunnableConfig`](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) at invocation and access them in the tool: + +```python +from langchain_core.tools import tool +from langchain_core.runnables import RunnableConfig + +@tool +# highlight-next-line +def get_user_info(config: RunnableConfig) -> str: + """Retrieve user information based on user ID.""" + user_id = config["configurable"].get("user_id") + return "User is John Smith" if user_id == "user_123" else "Unknown user" + +# Invocation example with an agent +agent.invoke( + {"messages": [{"role": "user", "content": "look up user info"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} +) +``` +::: + +:::js +Use configuration when you have **immutable** runtime data that tools require, such as user identifiers. You pass these arguments via [`LangGraphRunnableConfig`](https://js.langchain.com/docs/api/langgraph/interfaces/LangGraphRunnableConfig.html) at invocation and access them in the tool: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + +const getUserInfo = tool( + // highlight-next-line + async (_, config: LangGraphRunnableConfig) => { + const userId = config?.configurable?.user_id; + return userId === "user_123" ? "User is John Smith" : "Unknown user"; + }, + { + name: "get_user_info", + description: "Retrieve user information based on user ID.", + schema: z.object({}), + } +); + +// Invocation example with an agent +await agent.invoke( + { messages: [{ role: "user", content: "look up user info" }] }, + // highlight-next-line + { configurable: { user_id: "user_123" } } +); +``` +::: + + + :::python + ```python + from langchain_core.runnables import RunnableConfig + from langchain_core.tools import tool + from langgraph.prebuilt import create_react_agent + + def get_user_info( + # highlight-next-line + config: RunnableConfig, + ) -> str: + """Look up user info.""" + # highlight-next-line + user_id = config["configurable"].get("user_id") + return "User is John Smith" if user_id == "user_123" else "Unknown user" + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_user_info], + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "look up user information"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} + ) + ``` + ::: + + :::js + ```typescript + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + import { ChatAnthropic } from "@langchain/anthropic"; + + const getUserInfo = tool( + // highlight-next-line + async (_, config: LangGraphRunnableConfig) => { + // highlight-next-line + const userId = config?.configurable?.user_id; + return userId === "user_123" ? "User is John Smith" : "Unknown user"; + }, + { + name: "get_user_info", + description: "Look up user info.", + schema: z.object({}), + } + ); + + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: [getUserInfo], + }); + + await agent.invoke( + { messages: [{ role: "user", content: "look up user information" }] }, + // highlight-next-line + { configurable: { user_id: "user_123" } } + ); + ``` + ::: + + +### Short-term memory + +Short-term memory maintains **dynamic** state that changes during a single execution. + +:::python +To **access** (read) the graph state inside the tools, you can use a special parameter **annotation** — @[`InjectedState`][InjectedState]: + +```python +from typing import Annotated, NotRequired +from langchain_core.tools import tool +from langgraph.prebuilt import InjectedState, create_react_agent +from langgraph.prebuilt.chat_agent_executor import AgentState + +class CustomState(AgentState): + # The user_name field in short-term state + user_name: NotRequired[str] + +@tool +def get_user_name( + # highlight-next-line + state: Annotated[CustomState, InjectedState] +) -> str: + """Retrieve the current user-name from state.""" + # Return stored name or a default if not set + return state.get("user_name", "Unknown user") + +# Example agent setup +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_user_name], + state_schema=CustomState, +) + +# Invocation: reads the name from state (initially empty) +agent.invoke({"messages": "what's my name?"}) +``` +::: + +:::js +To **access** (read) the graph state inside the tools, you can use the @[`getContextVariable`][getContextVariable] function: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import { getContextVariable } from "@langchain/core/context"; +import { MessagesZodState } from "@langchain/langgraph"; +import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + +const getUserName = tool( + // highlight-next-line + async (_, config: LangGraphRunnableConfig) => { + // highlight-next-line + const currentState = getContextVariable("currentState") as z.infer< + typeof MessagesZodState + > & { userName?: string }; + return currentState?.userName || "Unknown user"; + }, + { + name: "get_user_name", + description: "Retrieve the current user name from state.", + schema: z.object({}), + } +); +``` +::: + +:::python +Use a tool that returns a `Command` to **update** `user_name` and append a confirmation message: + +```python +from typing import Annotated +from langgraph.types import Command +from langchain_core.messages import ToolMessage +from langchain_core.tools import tool, InjectedToolCallId + +@tool +def update_user_name( + new_name: str, + tool_call_id: Annotated[str, InjectedToolCallId] +) -> Command: + """Update user-name in short-term memory.""" + # highlight-next-line + return Command(update={ + # highlight-next-line + "user_name": new_name, + # highlight-next-line + "messages": [ + # highlight-next-line + ToolMessage(f"Updated user name to {new_name}", tool_call_id=tool_call_id) + # highlight-next-line + ] + # highlight-next-line + }) +``` +::: + +:::js +To **update** short-term memory, you can use tools that return a `Command` to update state: + +```typescript +import { Command } from "@langchain/langgraph"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const updateUserName = tool( + async (input) => { + // highlight-next-line + return new Command({ + // highlight-next-line + update: { + // highlight-next-line + userName: input.newName, + // highlight-next-line + messages: [ + // highlight-next-line + { + // highlight-next-line + role: "assistant", + // highlight-next-line + content: `Updated user name to ${input.newName}`, + // highlight-next-line + }, + // highlight-next-line + ], + // highlight-next-line + }, + // highlight-next-line + }); + }, + { + name: "update_user_name", + description: "Update user name in short-term memory.", + schema: z.object({ + newName: z.string().describe("The new user name"), + }), + } +); +``` +::: + + + :::python + If you want to use tools that return `Command` and update graph state, you can either use prebuilt @[`create_react_agent`][create_react_agent] / @[`ToolNode`][ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.: + + ```python + def call_tools(state): + ... + commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls] + return commands + ``` + ::: + + :::js + If you want to use tools that return `Command` and update graph state, you can either use prebuilt @[`createReactAgent`][create_react_agent] / @[ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.: + + ```typescript + const callTools = async (state: State) => { + // ... + const commands = await Promise.all( + toolCalls.map(toolCall => toolsByName[toolCall.name].invoke(toolCall)) + ); + return commands; + }; + ``` + ::: + + +### Long-term memory + +Use [long-term memory](/oss/memory#long-term-memory) to store user-specific or application-specific data across conversations. This is useful for applications like chatbots, where you want to remember user preferences or other information. + +To use long-term memory, you need to: + +1. [Configure a store](/oss/add-memory#add-long-term-memory) to persist data across invocations. +2. Access the store from within tools. + +:::python +To **access** information in the store: + +```python +from langchain_core.runnables import RunnableConfig +from langchain_core.tools import tool +from langgraph.graph import StateGraph +# highlight-next-line +from langgraph.config import get_store + +@tool +def get_user_info(config: RunnableConfig) -> str: + """Look up user info.""" + # Same as that provided to `builder.compile(store=store)` + # or `create_react_agent` + # highlight-next-line + store = get_store() + user_id = config["configurable"].get("user_id") + # highlight-next-line + user_info = store.get(("users",), user_id) + return str(user_info.value) if user_info else "Unknown user" + +builder = StateGraph(...) +... +graph = builder.compile(store=store) +``` +::: + +:::js +To **access** information in the store: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + +const getUserInfo = tool( + async (_, config: LangGraphRunnableConfig) => { + // Same as that provided to `builder.compile({ store })` + // or `createReactAgent` + // highlight-next-line + const store = config.store; + if (!store) throw new Error("Store not provided"); + + const userId = config?.configurable?.user_id; + // highlight-next-line + const userInfo = await store.get(["users"], userId); + return userInfo?.value ? JSON.stringify(userInfo.value) : "Unknown user"; + }, + { + name: "get_user_info", + description: "Look up user info.", + schema: z.object({}), + } +); +``` +::: + + + :::python + ```python + from langchain_core.runnables import RunnableConfig + from langchain_core.tools import tool + from langgraph.config import get_store + from langgraph.prebuilt import create_react_agent + from langgraph.store.memory import InMemoryStore + + # highlight-next-line + store = InMemoryStore() # (1)! + + # highlight-next-line + store.put( # (2)! + ("users",), # (3)! + "user_123", # (4)! + { + "name": "John Smith", + "language": "English", + } # (5)! + ) + + @tool + def get_user_info(config: RunnableConfig) -> str: + """Look up user info.""" + # Same as that provided to `create_react_agent` + # highlight-next-line + store = get_store() # (6)! + user_id = config["configurable"].get("user_id") + # highlight-next-line + user_info = store.get(("users",), user_id) # (7)! + return str(user_info.value) if user_info else "Unknown user" + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_user_info], + # highlight-next-line + store=store # (8)! + ) + + # Run the agent + agent.invoke( + {"messages": [{"role": "user", "content": "look up user information"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} + ) + ``` + + 1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the store documentation for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. + 2. For this example, we write some sample data to the store using the `put` method. Please see the @[BaseStore.put] API reference for more details. + 3. The first argument is the namespace. This is used to group related data together. In this case, we are using the `users` namespace to group user data. + 4. A key within the namespace. This example uses a user ID for the key. + 5. The data that we want to store for the given user. + 6. The `get_store` function is used to access the store. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created. + 7. The `get` method is used to retrieve data from the store. The first argument is the namespace, and the second argument is the key. This will return a `StoreValue` object, which contains the value and metadata about the value. + 8. The `store` is passed to the agent. This enables the agent to access the store when running tools. You can also use the `get_store` function to access the store from anywhere in your code. + ::: + + :::js + ```typescript + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { InMemoryStore } from "@langchain/langgraph"; + import { ChatAnthropic } from "@langchain/anthropic"; + import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + + // highlight-next-line + const store = new InMemoryStore(); // (1)! + + // highlight-next-line + await store.put( // (2)! + ["users"], // (3)! + "user_123", // (4)! + { + name: "John Smith", + language: "English", + } // (5)! + ); + + const getUserInfo = tool( + async (_, config: LangGraphRunnableConfig) => { + // Same as that provided to `createReactAgent` + // highlight-next-line + const store = config.store; // (6)! + if (!store) throw new Error("Store not provided"); + + const userId = config?.configurable?.user_id; + // highlight-next-line + const userInfo = await store.get(["users"], userId); // (7)! + return userInfo?.value ? JSON.stringify(userInfo.value) : "Unknown user"; + }, + { + name: "get_user_info", + description: "Look up user info.", + schema: z.object({}), + } + ); + + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: [getUserInfo], + // highlight-next-line + store: store // (8)! + }); + + // Run the agent + await agent.invoke( + { messages: [{ role: "user", content: "look up user information" }] }, + // highlight-next-line + { configurable: { user_id: "user_123" } } + ); + ``` + + 1. The `InMemoryStore` is a store that stores data in memory. In production, you would typically use a database or other persistent storage. Please review the [store documentation](https://python.langchain.com/api_reference/langgraph/index.html#module-langgraph.store) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. + 2. For this example, we write some sample data to the store using the `put` method. Please see the [BaseStore.put](https://js.langchain.com/docs/api/langgraph_store/classes/BaseStore.html#put) API reference for more details. + 3. The first argument is the namespace. This is used to group related data together. In this case, we are using the `users` namespace to group user data. + 4. A key within the namespace. This example uses a user ID for the key. + 5. The data that we want to store for the given user. + 6. The store is accessible from the config object that is passed to the tool. This enables the tool to access the store when running. + 7. The `get` method is used to retrieve data from the store. The first argument is the namespace, and the second argument is the key. This will return a `StoreValue` object, which contains the value and metadata about the value. + 8. The `store` is passed to the agent. This enables the agent to access the store when running tools. + ::: + + +:::python +To **update** information in the store: + +```python +from langchain_core.runnables import RunnableConfig +from langchain_core.tools import tool +from langgraph.graph import StateGraph +# highlight-next-line +from langgraph.config import get_store + +@tool +def save_user_info(user_info: str, config: RunnableConfig) -> str: + """Save user info.""" + # Same as that provided to `builder.compile(store=store)` + # or `create_react_agent` + # highlight-next-line + store = get_store() + user_id = config["configurable"].get("user_id") + # highlight-next-line + store.put(("users",), user_id, user_info) + return "Successfully saved user info." + +builder = StateGraph(...) +... +graph = builder.compile(store=store) +``` +::: + +:::js +To **update** information in the store: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; +import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + +const saveUserInfo = tool( + async (input, config: LangGraphRunnableConfig) => { + // Same as that provided to `builder.compile({ store })` + // or `createReactAgent` + // highlight-next-line + const store = config.store; + if (!store) throw new Error("Store not provided"); + + const userId = config?.configurable?.user_id; + // highlight-next-line + await store.put(["users"], userId, input.userInfo); + return "Successfully saved user info."; + }, + { + name: "save_user_info", + description: "Save user info.", + schema: z.object({ + userInfo: z.string().describe("User information to save"), + }), + } +); +``` +::: + + + :::python + ```python + from typing_extensions import TypedDict + + from langchain_core.tools import tool + from langgraph.config import get_store + from langchain_core.runnables import RunnableConfig + from langgraph.prebuilt import create_react_agent + from langgraph.store.memory import InMemoryStore + + store = InMemoryStore() # (1)! + + class UserInfo(TypedDict): # (2)! + name: str + + @tool + def save_user_info(user_info: UserInfo, config: RunnableConfig) -> str: # (3)! + """Save user info.""" + # Same as that provided to `create_react_agent` + # highlight-next-line + store = get_store() # (4)! + user_id = config["configurable"].get("user_id") + # highlight-next-line + store.put(("users",), user_id, user_info) # (5)! + return "Successfully saved user info." + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[save_user_info], + # highlight-next-line + store=store + ) + + # Run the agent + agent.invoke( + {"messages": [{"role": "user", "content": "My name is John Smith"}]}, + # highlight-next-line + config={"configurable": {"user_id": "user_123"}} # (6)! + ) + + # You can access the store directly to get the value + store.get(("users",), "user_123").value + ``` + + 1. The `InMemoryStore` is a store that stores data in memory. In a production setting, you would typically use a database or other persistent storage. Please review the [store documentation](https://python.langchain.com/api_reference/langgraph/index.html#module-langgraph.store) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. + 2. The `UserInfo` class is a `TypedDict` that defines the structure of the user information. The LLM will use this to format the response according to the schema. + 3. The `save_user_info` function is a tool that allows an agent to update user information. This could be useful for a chat application where the user wants to update their profile information. + 4. The `get_store` function is used to access the store. You can call it from anywhere in your code, including tools and prompts. This function returns the store that was passed to the agent when it was created. + 5. The `put` method is used to store data in the store. The first argument is the namespace, and the second argument is the key. This will store the user information in the store. + 6. The `user_id` is passed in the config. This is used to identify the user whose information is being updated. + ::: + + :::js + ```typescript + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { InMemoryStore } from "@langchain/langgraph"; + import { ChatAnthropic } from "@langchain/anthropic"; + import type { LangGraphRunnableConfig } from "@langchain/langgraph"; + + const store = new InMemoryStore(); // (1)! + + const UserInfoSchema = z.object({ // (2)! + name: z.string(), + }); + + const saveUserInfo = tool( + async (input, config: LangGraphRunnableConfig) => { // (3)! + // Same as that provided to `createReactAgent` + // highlight-next-line + const store = config.store; // (4)! + if (!store) throw new Error("Store not provided"); + + const userId = config?.configurable?.user_id; + // highlight-next-line + await store.put(["users"], userId, input); // (5)! + return "Successfully saved user info."; + }, + { + name: "save_user_info", + description: "Save user info.", + schema: UserInfoSchema, + } + ); + + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: [saveUserInfo], + // highlight-next-line + store: store + }); + + // Run the agent + await agent.invoke( + { messages: [{ role: "user", content: "My name is John Smith" }] }, + // highlight-next-line + { configurable: { user_id: "user_123" } } // (6)! + ); + + // You can access the store directly to get the value + const userInfo = await store.get(["users"], "user_123"); + console.log(userInfo?.value); + ``` + + 1. The `InMemoryStore` is a store that stores data in memory. In production, you would typically use a database or other persistent storage. Please review the [store documentation](https://python.langchain.com/api_reference/langgraph/index.html#module-langgraph.store) for more options. If you're deploying with **LangGraph Platform**, the platform will provide a production-ready store for you. + 2. The `UserInfoSchema` is a Zod schema that defines the structure of the user information. The LLM will use this to format the response according to the schema. + 3. The `saveUserInfo` function is a tool that allows an agent to update user information. This could be useful for a chat application where the user wants to update their profile information. + 4. The store is accessible from the config object that is passed to the tool. This enables the tool to access the store when running. + 5. The `put` method is used to store data in the store. The first argument is the namespace, and the second argument is the key. This will store the user information in the store. + 6. The `user_id` is passed in the config. This is used to identify the user whose information is being updated. + ::: + + +## Advanced tool features + +### Immediate return + +:::python +Use `return_direct=True` to immediately return a tool's result without executing additional logic. + +This is useful for tools that should not trigger further processing or tool calls, allowing you to return results directly to the user. + +```python +# highlight-next-line +@tool(return_direct=True) +def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b +``` +::: + +:::js +Use `returnDirect: true` to immediately return a tool's result without executing additional logic. + +This is useful for tools that should not trigger further processing or tool calls, allowing you to return results directly to the user. + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const add = tool( + (input) => { + return input.a + input.b; + }, + { + name: "add", + description: "Add two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + // highlight-next-line + returnDirect: true, + } +); +``` +::: + + + :::python + ```python + from langchain_core.tools import tool + from langgraph.prebuilt import create_react_agent + + # highlight-next-line + @tool(return_direct=True) + def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[add] + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "what's 3 + 5?"}]} + ) + ``` + ::: + + :::js + ```typescript + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { ChatAnthropic } from "@langchain/anthropic"; + + // highlight-next-line + const add = tool( + (input) => { + return input.a + input.b; + }, + { + name: "add", + description: "Add two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + // highlight-next-line + returnDirect: true, + } + ); + + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: [add] + }); + + await agent.invoke({ + messages: [{ role: "user", content: "what's 3 + 5?" }] + }); + ``` + ::: + + + + **Using without prebuilt components** + :::python + If you are building a custom workflow and are not relying on `create_react_agent` or `ToolNode`, you will also + need to implement the control flow to handle `return_direct=True`. + ::: + + :::js + If you are building a custom workflow and are not relying on `createReactAgent` or `ToolNode`, you will also + need to implement the control flow to handle `returnDirect: true`. + ::: + + +### Force tool use + +If you need to force a specific tool to be used, you will need to configure this at the **model** level using the `tool_choice` parameter in the bind_tools method. + +Force specific tool usage via tool_choice: + +:::python +```python +@tool(return_direct=True) +def greet(user_name: str) -> int: + """Greet user.""" + return f"Hello {user_name}!" + +tools = [greet] + +configured_model = model.bind_tools( + tools, + # Force the use of the 'greet' tool + # highlight-next-line + tool_choice={"type": "tool", "name": "greet"} +) +``` +::: + +:::js +```typescript +const greet = tool( + (input) => { + return `Hello ${input.userName}!`; + }, + { + name: "greet", + description: "Greet user.", + schema: z.object({ + userName: z.string(), + }), + returnDirect: true, + } +); + +const tools = [greet]; + +const configuredModel = model.bindTools( + tools, + // Force the use of the 'greet' tool + // highlight-next-line + { tool_choice: { type: "tool", name: "greet" } } +); +``` +::: + + + :::python + To force the agent to use specific tools, you can set the `tool_choice` option in `model.bind_tools()`: + + ```python + from langchain_core.tools import tool + + # highlight-next-line + @tool(return_direct=True) + def greet(user_name: str) -> int: + """Greet user.""" + return f"Hello {user_name}!" + + tools = [greet] + + agent = create_react_agent( + # highlight-next-line + model=model.bind_tools(tools, tool_choice={"type": "tool", "name": "greet"}), + tools=tools + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "Hi, I am Bob"}]} + ) + ``` + ::: + + :::js + To force the agent to use specific tools, you can set the `tool_choice` option in `model.bindTools()`: + + ```typescript + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { ChatOpenAI } from "@langchain/openai"; + + // highlight-next-line + const greet = tool( + (input) => { + return `Hello ${input.userName}!`; + }, + { + name: "greet", + description: "Greet user.", + schema: z.object({ + userName: z.string(), + }), + // highlight-next-line + returnDirect: true, + } + ); + + const tools = [greet]; + const model = new ChatOpenAI({ model: "gpt-4o" }); + + const agent = createReactAgent({ + // highlight-next-line + llm: model.bindTools(tools, { tool_choice: { type: "tool", name: "greet" } }), + tools: tools + }); + + await agent.invoke({ + messages: [{ role: "user", content: "Hi, I am Bob" }] + }); + ``` + ::: + + + + **Avoid infinite loops** + :::python + Forcing tool usage without stopping conditions can create infinite loops. Use one of the following safeguards: + + * Mark the tool with [`return_direct=True`](#immediate-return) to end the loop after execution. + * Set [`recursion_limit`](/oss/graph-api#recursion-limit) to restrict the number of execution steps. + ::: + + :::js + Forcing tool usage without stopping conditions can create infinite loops. Use one of the following safeguards: + + * Mark the tool with [`returnDirect: true`](#immediate-return) to end the loop after execution. + * Set [`recursionLimit`](/oss/graph-api#recursion-limit) to restrict the number of execution steps. + ::: + + + + **Tool choice configuration** + The `tool_choice` parameter is used to configure which tool should be used by the model when it decides to call a tool. This is useful when you want to ensure that a specific tool is always called for a particular task or when you want to override the model's default behavior of choosing a tool based on its internal logic. + + Note that not all models support this feature, and the exact configuration may vary depending on the model you are using. + + +### Disable parallel calls + +:::python +For supported providers, you can disable parallel tool calling by setting `parallel_tool_calls=False` via the `model.bind_tools()` method: + +```python +model.bind_tools( + tools, + # highlight-next-line + parallel_tool_calls=False +) +``` +::: + +:::js +For supported providers, you can disable parallel tool calling by setting `parallel_tool_calls: false` via the `model.bindTools()` method: + +```typescript +model.bindTools( + tools, + // highlight-next-line + { parallel_tool_calls: false } +); +``` +::: + + + :::python + ```python + from langchain.chat_models import init_chat_model + + def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + + model = init_chat_model("anthropic:claude-3-5-sonnet-latest", temperature=0) + tools = [add, multiply] + agent = create_react_agent( + # disable parallel tool calls + # highlight-next-line + model=model.bind_tools(tools, parallel_tool_calls=False), + tools=tools + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "what's 3 + 5 and 4 * 7?"}]} + ) + ``` + ::: + + :::js + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + const add = tool( + (input) => { + return input.a + input.b; + }, + { + name: "add", + description: "Add two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + } + ); + + const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers.", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + } + ); + + const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }); + const tools = [add, multiply]; + + const agent = createReactAgent({ + // disable parallel tool calls + // highlight-next-line + llm: model.bindTools(tools, { parallel_tool_calls: false }), + tools: tools + }); + + await agent.invoke({ + messages: [{ role: "user", content: "what's 3 + 5 and 4 * 7?" }] + }); + ``` + ::: + + +### Handle errors + +:::python +LangGraph provides built-in error handling for tool execution through the prebuilt @[ToolNode][ToolNode] component, used both independently and in prebuilt agents. + +By **default**, `ToolNode` catches exceptions raised during tool execution and returns them as `ToolMessage` objects with a status indicating an error. + +```python +from langchain_core.messages import AIMessage +from langgraph.prebuilt import ToolNode + +def multiply(a: int, b: int) -> int: + if a == 42: + raise ValueError("The ultimate error") + return a * b + +# Default error handling (enabled by default) +tool_node = ToolNode([multiply]) + +message = AIMessage( + content="", + tool_calls=[{ + "name": "multiply", + "args": {"a": 42, "b": 7}, + "id": "tool_call_id", + "type": "tool_call" + }] +) + +result = tool_node.invoke({"messages": [message]}) +``` + +Output: + +```pycon +{'messages': [ + ToolMessage( + content="Error: ValueError('The ultimate error')\n Please fix your mistakes.", + name='multiply', + tool_call_id='tool_call_id', + status='error' + ) +]} +``` +::: + +:::js +LangGraph provides built-in error handling for tool execution through the prebuilt [ToolNode](https://js.langchain.com/docs/api/langgraph_prebuilt/classes/ToolNode.html) component, used both independently and in prebuilt agents. + +By **default**, `ToolNode` catches exceptions raised during tool execution and returns them as `ToolMessage` objects with a status indicating an error. + +```typescript +import { AIMessage } from "@langchain/core/messages"; +import { ToolNode } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const multiply = tool( + (input) => { + if (input.a === 42) { + throw new Error("The ultimate error"); + } + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + } +); + +// Default error handling (enabled by default) +const toolNode = new ToolNode([multiply]); + +const message = new AIMessage({ + content: "", + tool_calls: [ + { + name: "multiply", + args: { a: 42, b: 7 }, + id: "tool_call_id", + type: "tool_call", + }, + ], +}); + +const result = await toolNode.invoke({ messages: [message] }); +``` + +Output: + +``` +{ messages: [ + ToolMessage { + content: "Error: The ultimate error\n Please fix your mistakes.", + name: "multiply", + tool_call_id: "tool_call_id", + status: "error" + } +]} +``` +::: + +#### Disable error handling + +To propagate exceptions directly, disable error handling: + +:::python +```python +tool_node = ToolNode([multiply], handle_tool_errors=False) +``` +::: + +:::js +```typescript +const toolNode = new ToolNode([multiply], { handleToolErrors: false }); +``` +::: + +With error handling disabled, exceptions raised by tools will propagate up, requiring explicit management. + +#### Custom error messages + +Provide a custom error message by setting the error handling parameter to a string: + +:::python +```python +tool_node = ToolNode( + [multiply], + handle_tool_errors="Can't use 42 as the first operand, please switch operands!" +) +``` + +Example output: + +```python +{'messages': [ + ToolMessage( + content="Can't use 42 as the first operand, please switch operands!", + name='multiply', + tool_call_id='tool_call_id', + status='error' + ) +]} +``` +::: + +:::js +```typescript +const toolNode = new ToolNode([multiply], { + handleToolErrors: + "Can't use 42 as the first operand, please switch operands!", +}); +``` + +Example output: + +```typescript +{ messages: [ + ToolMessage { + content: "Can't use 42 as the first operand, please switch operands!", + name: "multiply", + tool_call_id: "tool_call_id", + status: "error" + } +]} +``` +::: + +#### Error handling in agents + +:::python +Error handling in prebuilt agents (`create_react_agent`) leverages `ToolNode`: + +```python +from langgraph.prebuilt import create_react_agent + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[multiply] +) + +# Default error handling +agent.invoke({"messages": [{"role": "user", "content": "what's 42 x 7?"}]}) +``` + +To disable or customize error handling in prebuilt agents, explicitly pass a configured `ToolNode`: + +```python +custom_tool_node = ToolNode( + [multiply], + handle_tool_errors="Cannot use 42 as a first operand!" +) + +agent_custom = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=custom_tool_node +) + +agent_custom.invoke({"messages": [{"role": "user", "content": "what's 42 x 7?"}]}) +``` +::: + +:::js +Error handling in prebuilt agents (`createReactAgent`) leverages `ToolNode`: + +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatAnthropic } from "@langchain/anthropic"; + +const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: [multiply], +}); + +// Default error handling +await agent.invoke({ + messages: [{ role: "user", content: "what's 42 x 7?" }], +}); +``` + +To disable or customize error handling in prebuilt agents, explicitly pass a configured `ToolNode`: + +```typescript +const customToolNode = new ToolNode([multiply], { + handleToolErrors: "Cannot use 42 as a first operand!", +}); + +const agentCustom = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }), + tools: customToolNode, +}); + +await agentCustom.invoke({ + messages: [{ role: "user", content: "what's 42 x 7?" }], +}); +``` +::: + +### Handle large numbers of tools + +As the number of available tools grows, you may want to limit the scope of the LLM's selection, to decrease token consumption and to help manage sources of error in LLM reasoning. + +To address this, you can dynamically adjust the tools available to a model by retrieving relevant tools at runtime using semantic search. + +See [`langgraph-bigtool`](https://github.com/langchain-ai/langgraph-bigtool) prebuilt library for a ready-to-use implementation. + +## Prebuilt tools + +### LLM provider tools + +:::python +You can use prebuilt tools from model providers by passing a dictionary with tool specs to the `tools` parameter of `create_react_agent`. For example, to use the `web_search_preview` tool from OpenAI: + +```python +from langgraph.prebuilt import create_react_agent + +agent = create_react_agent( + model="openai:gpt-4o-mini", + tools=[{"type": "web_search_preview"}] +) +response = agent.invoke( + {"messages": ["What was a positive news story from today?"]} +) +``` + +Please consult the documentation for the specific model you are using to see which tools are available and how to use them. +::: + +:::js +You can use prebuilt tools from model providers by passing a dictionary with tool specs to the `tools` parameter of `createReactAgent`. For example, to use the `web_search_preview` tool from OpenAI: + +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatOpenAI } from "@langchain/openai"; + +const agent = createReactAgent({ + llm: new ChatOpenAI({ model: "gpt-4o-mini" }), + tools: [{ type: "web_search_preview" }], +}); + +const response = await agent.invoke({ + messages: [ + { role: "user", content: "What was a positive news story from today?" }, + ], +}); +``` + +Please consult the documentation for the specific model you are using to see which tools are available and how to use them. +::: + +### LangChain tools + +Additionally, LangChain supports a wide range of prebuilt tool integrations for interacting with APIs, databases, file systems, web data, and more. These tools extend the functionality of agents and enable rapid development. + +:::python +You can browse the full list of available integrations in the [LangChain integrations directory](https://python.langchain.com/docs/integrations/tools/). + +Some commonly used tool categories include: + +* **Search**: Bing, SerpAPI, Tavily +* **Code interpreters**: Python REPL, Node.js REPL +* **Databases**: SQL, MongoDB, Redis +* **Web data**: Web scraping and browsing +* **APIs**: OpenWeatherMap, NewsAPI, and others + +These integrations can be configured and added to your agents using the same `tools` parameter shown in the examples above. +::: + +:::js +You can browse the full list of available integrations in the [LangChain integrations directory](https://js.langchain.com/docs/integrations/tools/). + +Some commonly used tool categories include: + +* **Search**: Tavily, SerpAPI +* **Code interpreters**: Web browsers, calculators +* **Databases**: SQL, vector databases +* **Web data**: Web scraping and browsing +* **APIs**: Various API integrations + +These integrations can be configured and added to your agents using the same `tools` parameter shown in the examples above. +::: diff --git a/src/oss/case-studies.mdx b/src/oss/case-studies.mdx new file mode 100644 index 00000000..cb4cbdb3 --- /dev/null +++ b/src/oss/case-studies.mdx @@ -0,0 +1,48 @@ +--- +title: Case studies +--- +This list of companies using LangGraph and their success stories is compiled from public sources. If your company uses LangGraph, we'd love for you to share your story and add it to the list. You’re also welcome to contribute updates based on publicly available information from other companies, such as blog posts or press releases. + +| Company | Industry | Use case | Reference | +| --- | --- | --- | --- | +| [AirTop](https://www.airtop.ai/) | Software & Technology (GenAI Native) | Browser automation for AI agents | [Case study, 2024](https://blog.langchain.dev/customers-airtop/) | +| [AppFolio](https://www.appfolio.com/) | Real Estate | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-appfolio/) | +| [Athena Intelligence](https://www.athenaintel.com/) | Software & Technology (GenAI Native) | Research & summarization | [Case study, 2024](https://blog.langchain.dev/customers-athena-intelligence/) | +| [BlackRock](https://www.blackrock.com/) | Financial Services | Copilot for domain-specific task | [Interrupt talk, 2025](https://youtu.be/oyqeCHFM5U4?feature=shared) | +| [Captide](https://www.captide.co/) | Software & Technology (GenAI Native) | Data extraction | [Case study, 2025](https://blog.langchain.dev/how-captide-is-redefining-equity-research-with-agentic-workflows-built-on-langgraph-and-langsmith/) | +| [Cisco CX](https://www.cisco.com/site/us/en/services/modern-data-center/index.html?CCID=cc005911&DTID=eivtotr001480&OID=srwsas032775) | Software & Technology | Customer support | [Interrupt Talk, 2025](https://youtu.be/gPhyPRtIMn0?feature=shared) | +| [Cisco Outshift](https://outshift.cisco.com/) | Software & Technology | DevOps | [Video story, 2025](https://www.youtube.com/watch?v=htcb-vGR_x0); [Case study, 2025](https://blog.langchain.com/cisco-outshift/); [Blog post, 2025](https://outshift.cisco.com/blog/build-react-agent-application-for-devops-tasks-using-rest-apis) | +| [Cisco TAC](https://www.cisco.com/c/en/us/support/index.html) | Software & Technology | Customer support | [Video story, 2025](https://youtu.be/EAj0HBDGqaE?feature=shared) | +| [City of Hope](https://www.cityofhope.org/) | Non-profit | Copilot for domain-specific task | [Video story, 2025](https://youtu.be/9ABwtK2gIZU?feature=shared) | +| [C.H. Robinson](https://www.chrobinson.com/en-us/) | Logistics | Automation | [Case study, 2025](https://blog.langchain.dev/customers-chrobinson/) | +| [Definely](https://www.definely.com/) | Legal | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.com/customers-definely/) | +| [Docent Pro](https://docentpro.com/) | Travel | GenAI embedded product experiences | [Case study, 2025](https://blog.langchain.com/customers-docentpro/) | +| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) | +| [Exa](https://exa.ai/) | Software & Technology (GenAI Native) | Search | [Case study, 2025](https://blog.langchain.com/exa/) | +| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) | +| [Harmonic](https://harmonic.ai/) | Software & Technology | Search | [Case study, 2025](https://blog.langchain.com/customers-harmonic/) | +| [Inconvo](https://inconvo.ai/?ref=blog.langchain.dev) | Software & Technology | Code generation | [Case study, 2025](https://blog.langchain.dev/customers-inconvo/) | +| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) | +| [J.P. Morgan](https://www.jpmorganchase.com/) | Financial Services | Copilot for domain-specific task | [Interrupt talk, 2025](https://youtu.be/yMalr0jiOAc?feature=shared) | +| [Klarna](https://www.klarna.com/) | Fintech | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/customers-klarna/) | +| [Komodo Health](https://www.komodohealth.com/) | Healthcare | Copilot for domain-specific task | [Blog post](https://www.komodohealth.com/perspectives/new-gen-ai-assistant-empowers-the-enterprise/) | +| [LinkedIn](https://www.linkedin.com/) | Social Media | Code generation; Search & discovery | [Interrupt talk, 2025](https://youtu.be/NmblVxyBhi8?feature=shared); [Blog post, 2025](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics); [Blog post, 2024](https://www.linkedin.com/blog/engineering/generative-ai/behind-the-platform-the-journey-to-create-the-linkedin-genai-application-tech-stack) | +| [Minimal](https://gominimal.ai/) | E-commerce | Customer support | [Case study, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) | +| [Modern Treasury](https://www.moderntreasury.com/) | Fintech | GenAI embedded product experiences | [Video story, 2025](https://youtu.be/AwAiffXqaCU?feature=shared) | +| [Monday](https://monday.com/) | Software & Technology | GenAI embedded product experiences | [Interrupt talk, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) | +| [Morningstar](https://www.morningstar.com/) | Financial Services | Research & summarization | [Video story, 2025](https://youtu.be/6LidoFXCJPs?feature=shared) | +| [OpenRecovery](https://www.openrecovery.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-openrecovery/) | +| [Pigment](https://www.pigment.com/) | Fintech | GenAI embedded product experiences | [Video story, 2025](https://youtu.be/5JVSO2KYOmE?feature=shared) | +| [Prosper](https://www.prosper.com/) | Fintech | Customer support | [Video story, 2025](https://youtu.be/9RFNOYtkwsc?feature=shared) | +| [Qodo](https://www.qodo.ai/) | Software & Technology (GenAI Native) | Code generation | [Blog post, 2025](https://www.qodo.ai/blog/why-we-chose-langgraph-to-build-our-coding-agent/) | +| [Rakuten](https://www.rakuten.com/) | E-commerce / Fintech | Copilot for domain-specific task | [Video story, 2025](https://youtu.be/gD1LIjCkuA8?feature=shared); [Blog post, 2025](https://rakuten.today/blog/from-ai-hype-to-real-world-tools-rakuten-teams-up-with-langchain.html) | +| [Replit](https://replit.com/) | Software & Technology | Code generation | [Blog post, 2024](https://blog.langchain.dev/customers-replit/); [Breakout agent story, 2024](https://www.langchain.com/breakoutagents/replit); [Fireside chat video, 2024](https://www.youtube.com/watch?v=ViykMqljjxU) | +| [Rexera](https://www.rexera.com/) | Real Estate (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-rexera/) | +| [Abu Dhabi Government](https://www.tamm.abudhabi/) | Government | Search | [Case study, 2025](https://blog.langchain.com/customers-abu-dhabi-government/) | +| [Tradestack](https://www.tradestack.uk/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-tradestack/) | +| [Uber](https://www.uber.com/) | Transportation | Developer productivity; Code generation | [Interrupt talk, 2025](https://youtu.be/Bugs0dVcNI8?feature=shared); [Presentation, 2024](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/); [Video, 2024](https://www.youtube.com/watch?v=8rkA5vWUE4Y) | +| [Unify](https://www.unifygtm.com/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Interrupt talk, 2025](https://youtu.be/pKk-LfhujwI?feature=shared); [Blog post, 2024](https://blog.langchain.dev/unify-launches-agents-for-account-qualification-using-langgraph-and-langsmith/) | +| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Video story, 2025](https://www.youtube.com/watch?v=vrjJ6NuyTWA); [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) | +| [Vodafone](https://www.vodafone.com/) | Telecommunications | Code generation; internal search | [Case study, 2025](https://blog.langchain.dev/customers-vodafone/) | +| [WebToon](https://www.webtoons.com/en/) | Media & Entertainment | Data extraction | [Case study, 2025](https://blog.langchain.com/customers-webtoon/) | +| [11x](https://www.11x.ai/) | Software & Technology (GenAI Native) | Research & outreach | [Interrupt talk, 2025](https://youtu.be/fegwPmaAPQk?feature=shared) | diff --git a/src/oss/common-errors.mdx b/src/oss/common-errors.mdx new file mode 100644 index 00000000..b0af85e3 --- /dev/null +++ b/src/oss/common-errors.mdx @@ -0,0 +1,13 @@ +--- +title: Error troubleshooting +sidebarTitle: Overview +--- + +This page contains guides around resolving common errors you may find while building with LangGraph. +Errors referenced below will have an `lc_error_code` property corresponding to one of the below codes when they are thrown in code. + +* [GRAPH_RECURSION_LIMIT](/oss/GRAPH_RECURSION_LIMIT) +* [INVALID_CONCURRENT_GRAPH_UPDATE](/oss/INVALID_CONCURRENT_GRAPH_UPDATE) +* [INVALID_GRAPH_NODE_RETURN_VALUE](/oss/INVALID_GRAPH_NODE_RETURN_VALUE) +* [MULTIPLE_SUBGRAPHS](/oss/MULTIPLE_SUBGRAPHS) +* [INVALID_CHAT_HISTORY](/oss/INVALID_CHAT_HISTORY) diff --git a/src/oss/community-agents.mdx b/src/oss/community-agents.mdx new file mode 100644 index 00000000..b7af7905 --- /dev/null +++ b/src/oss/community-agents.mdx @@ -0,0 +1,76 @@ +[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!) +--- +title: Community Agents +--- + +If you’re looking for other prebuilt libraries, explore the community-built options +below. These libraries can extend LangGraph's functionality in various ways. + +## 📚 Available Libraries + +[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!) + +:::python +| Name | GitHub URL | Description | Weekly Downloads | Stars | +| --- | --- | --- | --- | --- | +| **trustcall** | https://github.com/hinthornw/trustcall | Tenacious tool calling built on LangGraph. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/hinthornw/trustcall?style=social) +| **breeze-agent** | https://github.com/andrestorres123/breeze-agent | A streamlined research system built inspired on STORM and built on LangGraph. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/andrestorres123/breeze-agent?style=social) +| **langgraph-supervisor** | https://github.com/langchain-ai/langgraph-supervisor-py | Build supervisor multi-agent systems with LangGraph. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraph-supervisor-py?style=social) +| **langmem** | https://github.com/langchain-ai/langmem | Build agents that learn and adapt from interactions over time. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langmem?style=social) +| **langchain-mcp-adapters** | https://github.com/langchain-ai/langchain-mcp-adapters | Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langchain-mcp-adapters?style=social) +| **open-deep-research** | https://github.com/langchain-ai/open_deep_research | Open source assistant for iterative web research and report writing. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/open_deep_research?style=social) +| **langgraph-swarm** | https://github.com/langchain-ai/langgraph-swarm-py | Build swarm-style multi-agent systems using LangGraph. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraph-swarm-py?style=social) +| **delve-taxonomy-generator** | https://github.com/andrestorres123/delve | A taxonomy generator for unstructured data | -12345 | ![GitHub stars](https://img.shields.io/github/stars/andrestorres123/delve?style=social) +| **nodeology** | https://github.com/xyin-anl/Nodeology | Enable researcher to build scientific workflows easily with simplified interface. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/xyin-anl/Nodeology?style=social) +| **langgraph-bigtool** | https://github.com/langchain-ai/langgraph-bigtool | Build LangGraph agents with large numbers of tools. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraph-bigtool?style=social) +| **ai-data-science-team** | https://github.com/business-science/ai-data-science-team | An AI-powered data science team of agents to help you perform common data science tasks 10X faster. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/business-science/ai-data-science-team?style=social) +| **langgraph-reflection** | https://github.com/langchain-ai/langgraph-reflection | LangGraph agent that runs a reflection step. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraph-reflection?style=social) +| **langgraph-codeact** | https://github.com/langchain-ai/langgraph-codeact | LangGraph implementation of CodeAct agent that generates and executes code instead of tool calling. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraph-codeact?style=social) + +## ✨ Contributing Your Library + +Have you built an awesome open-source library using LangGraph? We'd love to feature +your project on the official LangGraph documentation pages! 🏆 + +To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) file. + +**Guidelines** + +* Your repo must be distributed as an installable package on PyPI 📦 +* The repo should either use the Graph API (exposing a `StateGraph` instance) or + the Functional API (exposing an `entrypoint`). +* The package must include documentation (e.g., a `README.md` or docs site) + explaining how to use it. + +We'll review your contribution and merge it in! + +Thanks for contributing! 🚀 +::: + +:::js +| Name | GitHub URL | Description | Weekly Downloads | Stars | +| --- | --- | --- | --- | --- | +| **@langchain/mcp-adapters** | https://github.com/langchain-ai/langchainjs | Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents. | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langchainjs?style=social) +| **@langchain/langgraph-supervisor** | https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-supervisor | Build supervisor multi-agent systems with LangGraph | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraphjs?style=social) +| **@langchain/langgraph-swarm** | https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-swarm | Build multi-agent swarms with LangGraph | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraphjs?style=social) +| **@langchain/langgraph-cua** | https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-cua | Build computer use agents with LangGraph | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraphjs?style=social) + +## ✨ Contributing Your Library + +Have you built an awesome open-source library using LangGraph? We'd love to feature +your project on the official LangGraph documentation pages! 🏆 + +To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) file. + +**Guidelines** + +* Your repo must be distributed as an installable package on npm 📦 +* The repo should either use the Graph API (exposing a `StateGraph` instance) or + the Functional API (exposing an `entrypoint`). +* The package must include documentation (e.g., a `README.md` or docs site) + explaining how to use it. + +We'll review your contribution and merge it in! + +Thanks for contributing! 🚀 +::: diff --git a/src/oss/context.mdx b/src/oss/context.mdx new file mode 100644 index 00000000..09e527dd --- /dev/null +++ b/src/oss/context.mdx @@ -0,0 +1,312 @@ +--- +title: Context overview +--- +**Context engineering** is the practice of building dynamic systems that provide the right information and tools, in the right format, so that an AI application can accomplish a task. Context can be characterized along two key dimensions: + +1. By **mutability**: + * **Static context**: Immutable data that doesn't change during execution (e.g., user metadata, database connections, tools) + * **Dynamic context**: Mutable data that evolves as the application runs (e.g., conversation history, intermediate results, tool call observations) +2. By **lifetime**: + * **Runtime context**: Data scoped to a single run or invocation + * **Cross-conversation context**: Data that persists across multiple conversations or sessions + + + **Runtime context vs LLM context** + Runtime context refers to local context: data and dependencies your code needs to run. It does **not** refer to: + + * The LLM context, which is the data passed into the LLM's prompt. + * The "context window", which is the maximum number of tokens that can be passed to the LLM. + + Runtime context can be used to optimize the LLM context. For example, you can use user metadata + in the runtime context to fetch user preferences and feed them into the context window. + + +LangGraph provides three ways to manage context, which combines the mutability and lifetime dimensions: + +:::python +| Context type | Description | Mutability | Lifetime | Access method | +| ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------- | ------------------ | --------------------------------------- | +| [**Static runtime context**](#static-runtime-context) | User metadata, tools, db connections passed at startup | Static | Single run | `context` argument to `invoke`/`stream` | +| [**Dynamic runtime context (state)**](#dynamic-runtime-context-state) | Mutable data that evolves during a single run | Dynamic | Single run | LangGraph state object | +| [**Dynamic cross-conversation context (store)**](#dynamic-cross-conversation-context-store) | Persistent data shared across conversations | Dynamic | Cross-conversation | LangGraph store | + +## Static runtime context + +**Static runtime context** represents immutable data like user metadata, tools, and database connections that are passed to an application at the start of a run via the `context` argument to `invoke`/`stream`. This data does not change during execution. + + + **New in LangGraph v0.6: `context` replaces `config['configurable']`** + Runtime context is now passed to the `context` argument of `invoke`/`stream`, + which replaces the previous pattern of passing application configuration to `config['configurable']`. + + +```python +@dataclass +class ContextSchema: + user_name: str + +graph.invoke( # (1)! + {"messages": [{"role": "user", "content": "hi!"}]}, # (2)! + # highlight-next-line + context={"user_name": "John Smith"} # (3)! +) +``` + +1. This is the invocation of the agent or graph. The `invoke` method runs the underlying graph with the provided input. +2. This example uses messages as an input, which is common, but your application may use different input structures. +3. This is where you pass the runtime data. The `context` parameter allows you to provide additional dependencies that the agent can use during its execution. + + + + ```python + from langchain_core.messages import AnyMessage + from langgraph.runtime import get_runtime + from langgraph.prebuilt.chat_agent_executor import AgentState + from langgraph.prebuilt import create_react_agent + + # highlight-next-line + def prompt(state: AgentState) -> list[AnyMessage]: + runtime = get_runtime(ContextSchema) + system_msg = f"You are a helpful assistant. Address the user as {runtime.context.user_name}." + return [{"role": "system", "content": system_msg}] + state["messages"] + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + prompt=prompt, + context_schema=ContextSchema + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + context={"user_name": "John Smith"} + ) + ``` + + * See [Agents](/oss/prebuilts) for details. + + + ```python + from langgraph.runtime import Runtime + + # highlight-next-line + def node(state: State, config: Runtime[ContextSchema]): + user_name = runtime.context.user_name + ... + ``` + + * See [the Graph API](/oss/graph-api#add-runtime-configuration) for details. + + + ```python + from langgraph.runtime import get_runtime + + @tool + # highlight-next-line + def get_user_email() -> str: + """Retrieve user information based on user ID.""" + # simulate fetching user info from a database + runtime = get_runtime(ContextSchema) + email = get_user_email_from_db(runtime.context.user_name) + return email + ``` + + See the [tool calling guide](/oss/call-tools#configuration) for details. + + + + + The `Runtime` object can be used to access static context and other utilities like the active store and stream writer. + See the [Runtime][langgraph.runtime.Runtime] documentation for details. + +::: + +:::js +| Context type | Description | Mutability | Lifetime | +| ------------------------------------------------------------------------------------------- | --------------------------------------------- | ---------- | ------------------ | +| [**Config**](#config-static-context) | data passed at the start of a run | Static | Single run | +| [**Dynamic runtime context (state)**](#dynamic-runtime-context-state) | Mutable data that evolves during a single run | Dynamic | Single run | +| [**Dynamic cross-conversation context (store)**](#dynamic-cross-conversation-context-store) | Persistent data shared across conversations | Dynamic | Cross-conversation | + + +## Config + +Config is for immutable data like user metadata or API keys. Use this when you have values that don't change mid-run. + +Specify configuration using a key called **"configurable"** which is reserved for this purpose. + +```typescript +await graph.invoke( + // (1)! + { messages: [{ role: "user", content: "hi!" }] }, // (2)! + // highlight-next-line + { configurable: { user_id: "user_123" } } // (3)! +); +``` +::: + + +## Dynamic runtime context + +**Dynamic runtime context** represents mutable data that can evolve during a single run and is managed through the LangGraph state object. This includes conversation history, intermediate results, and values derived from tools or LLM outputs. In LangGraph, the state object acts as [short-term memory](/oss/memory) during a run. + + + + Example shows how to incorporate state into an agent **prompt**. + + State can also be accessed by the agent's **tools**, which can read or update the state as needed. See [tool calling guide](/oss/call-tools#short-term-memory) for details. + + :::python + ```python + from langchain_core.messages import AnyMessage + from langchain_core.runnables import RunnableConfig + from langgraph.prebuilt import create_react_agent + from langgraph.prebuilt.chat_agent_executor import AgentState + + # highlight-next-line + class CustomState(AgentState): # (1)! + user_name: str + + def prompt( + # highlight-next-line + state: CustomState + ) -> list[AnyMessage]: + user_name = state["user_name"] + system_msg = f"You are a helpful assistant. User's name is {user_name}" + return [{"role": "system", "content": system_msg}] + state["messages"] + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[...], + # highlight-next-line + state_schema=CustomState, # (2)! + prompt=prompt + ) + + agent.invoke({ + "messages": "hi!", + "user_name": "John Smith" + }) + ``` + + 1. Define a custom state schema that extends `AgentState` or `MessagesState`. + 2. Pass the custom state schema to the agent. This allows the agent to access and modify the state during execution. + ::: + + :::js + ```typescript + import type { BaseMessage } from "@langchain/core/messages"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { MessagesZodState } from "@langchain/langgraph"; + import { z } from "zod"; + + // highlight-next-line + const CustomState = z.object({ // (1)! + messages: MessagesZodState.shape.messages, + userName: z.string(), + }); + + const prompt = ( + // highlight-next-line + state: z.infer + ): BaseMessage[] => { + const userName = state.userName; + const systemMsg = `You are a helpful assistant. User's name is ${userName}`; + return [{ role: "system", content: systemMsg }, ...state.messages]; + }; + + const agent = createReactAgent({ + llm: model, + tools: [...], + // highlight-next-line + stateSchema: CustomState, // (2)! + stateModifier: prompt, + }); + + await agent.invoke({ + messages: [{ role: "user", content: "hi!" }], + userName: "John Smith", + }); + ``` + + 1. Define a custom state schema that extends `MessagesZodState` or creates a new schema. + 2. Pass the custom state schema to the agent. This allows the agent to access and modify the state during execution. + ::: + + + :::python + ```python + from typing_extensions import TypedDict + from langchain_core.messages import AnyMessage + from langgraph.graph import StateGraph + + # highlight-next-line + class CustomState(TypedDict): # (1)! + messages: list[AnyMessage] + extra_field: int + + # highlight-next-line + def node(state: CustomState): # (2)! + messages = state["messages"] + ... + return { # (3)! + # highlight-next-line + "extra_field": state["extra_field"] + 1 + } + + builder = StateGraph(State) + builder.add_node(node) + builder.set_entry_point("node") + graph = builder.compile() + ``` + + 1. Define a custom state + 2. Access the state in any node or tool + 3. The Graph API is designed to work as easily as possible with state. The return value of a node represents a requested update to the state. + ::: + + :::js + ```typescript + import type { BaseMessage } from "@langchain/core/messages"; + import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; + import { z } from "zod"; + + // highlight-next-line + const CustomState = z.object({ // (1)! + messages: MessagesZodState.shape.messages, + extraField: z.number(), + }); + + const builder = new StateGraph(CustomState) + .addNode("node", async (state) => { // (2)! + const messages = state.messages; + // ... + return { // (3)! + // highlight-next-line + extraField: state.extraField + 1, + }; + }) + .addEdge(START, "node"); + + const graph = builder.compile(); + ``` + + 1. Define a custom state + 2. Access the state in any node or tool + 3. The Graph API is designed to work as easily as possible with state. The return value of a node represents a requested update to the state. + ::: + + + + + **Turning on memory** + Please see the [memory guide](/oss/add-memory) for more details on how to enable memory. This is a powerful feature that allows you to persist the agent's state across multiple invocations. Otherwise, the state is scoped only to a single run. + + + +## Dynamic cross-conversation context + +**Dynamic cross-conversation context** represents persistent, mutable data that spans across multiple conversations or sessions and is managed through the LangGraph store. This includes user profiles, preferences, and historical interactions. The LangGraph store acts as [long-term memory](/oss/memory#long-term-memory) across multiple runs. This can be used to read or update persistent facts (e.g., user profiles, preferences, prior interactions). + +For more information, see the [Memory guide](/oss/add-memory). diff --git a/src/oss/durable-execution.mdx b/src/oss/durable-execution.mdx new file mode 100644 index 00000000..ab10768c --- /dev/null +++ b/src/oss/durable-execution.mdx @@ -0,0 +1,331 @@ +--- +title: Durable execution +--- + +**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](/oss/human-in-the-loop), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later). + +LangGraph's built-in [persistence](/oss/persistence) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](/oss/human-in-the-loop) interactions -- it can be resumed from its last recorded state. + + + If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures. + To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](/oss/functional-api#task). You can use [tasks](/oss/functional-api#task) from both the [StateGraph (Graph API)](/oss/graph-api) and the [Functional API](/oss/functional-api). + + +## Requirements + +To leverage durable execution in LangGraph, you need to: + +1. Enable [persistence](/oss/persistence) in your workflow by specifying a [checkpointer](/oss/persistence#checkpointer-libraries) that will save workflow progress. +2. Specify a [thread identifier](/oss/persistence#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow. + +:::python +1. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay). +::: + +:::js +1. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay). +::: + +## Determinism and Consistent Replay + +When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped. + +As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](/oss/functional-api#task) or [nodes](/oss/graph-api#nodes). + +To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines: + +* **Avoid Repeating Work**: If a [node](/oss/graph-api#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer. +* **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes. +* **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution. + +:::python +For some examples of pitfalls to avoid, see the [Common Pitfalls](/oss/functional-api#common-pitfalls) section in the functional API, which shows +how to structure your code using **tasks** to avoid these issues. The same principles apply to the @[StateGraph (Graph API)][StateGraph]. +::: + +:::js +For some examples of pitfalls to avoid, see the [Common Pitfalls](/oss/functional-api#common-pitfalls) section in the functional API, which shows +how to structure your code using **tasks** to avoid these issues. The same principles apply to the @[StateGraph (Graph API)][StateGraph]. +::: + +## Durability modes + +LangGraph supports three durability modes that allow you to balance performance and data consistency based on your application's requirements. The durability modes, from least to most durable, are as follows: + +* [`"exit"`](#exit) +* [`"async"`](#async) +* [`"sync"`](#sync) + +A higher durability mode add more overhead to the workflow execution. + + + **Added in v0.6.0** + Use the `durability` parameter instead of `checkpoint_during` (deprecated in v0.6.0) for persistence policy management: + + * `durability="async"` replaces `checkpoint_during=True` + * `durability="exit"` replaces `checkpoint_during=False` + + for persistence policy management, with the following mapping: + + * `checkpoint_during=True` -> `durability="async"` + * `checkpoint_during=False` -> `durability="exit"` + + +### `"exit"` + +Changes are persisted only when graph execution completes (either successfully or with an error). This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from mid-execution failures or interrupt the graph execution. + +### `"async"` + +Changes are persisted asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that checkpoints might not be written if the process crashes during execution. + +### `"sync"` + +Changes are persisted synchronously before the next step starts. This ensures that every checkpoint is written before continuing execution, providing high durability at the cost of some performance overhead. + +You can specify the durability mode when calling any graph execution method: + +:::python +```python +graph.stream( + {"input": "test"}, + durability="sync" +) +``` +::: + +## Using tasks in nodes + +If a [node](/oss/graph-api#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes. + +:::python + + + ```python + from typing import NotRequired + from typing_extensions import TypedDict + import uuid + + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import StateGraph, START, END + import requests + + # Define a TypedDict to represent the state + class State(TypedDict): + url: str + result: NotRequired[str] + + def call_api(state: State): + """Example node that makes an API request.""" + # highlight-next-line + result = requests.get(state['url']).text[:100] # Side-effect + return { + "result": result + } + + # Create a StateGraph builder and add a node for the call_api function + builder = StateGraph(State) + builder.add_node("call_api", call_api) + + # Connect the start and end nodes to the call_api node + builder.add_edge(START, "call_api") + builder.add_edge("call_api", END) + + # Specify a checkpointer + checkpointer = InMemorySaver() + + # Compile the graph with the checkpointer + graph = builder.compile(checkpointer=checkpointer) + + # Define a config with a thread ID. + thread_id = uuid.uuid4() + config = {"configurable": {"thread_id": thread_id}} + + # Invoke the graph + graph.invoke({"url": "https://www.example.com"}, config) + ``` + + + ```python + from typing import NotRequired + from typing_extensions import TypedDict + import uuid + + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.func import task + from langgraph.graph import StateGraph, START, END + import requests + + # Define a TypedDict to represent the state + class State(TypedDict): + urls: list[str] + result: NotRequired[list[str]] + + + @task + def _make_request(url: str): + """Make a request.""" + # highlight-next-line + return requests.get(url).text[:100] + + def call_api(state: State): + """Example node that makes an API request.""" + # highlight-next-line + requests = [_make_request(url) for url in state['urls']] + results = [request.result() for request in requests] + return { + "results": results + } + + # Create a StateGraph builder and add a node for the call_api function + builder = StateGraph(State) + builder.add_node("call_api", call_api) + + # Connect the start and end nodes to the call_api node + builder.add_edge(START, "call_api") + builder.add_edge("call_api", END) + + # Specify a checkpointer + checkpointer = InMemorySaver() + + # Compile the graph with the checkpointer + graph = builder.compile(checkpointer=checkpointer) + + # Define a config with a thread ID. + thread_id = uuid.uuid4() + config = {"configurable": {"thread_id": thread_id}} + + # Invoke the graph + graph.invoke({"urls": ["https://www.example.com"]}, config) + ``` + + +::: + +:::js + + + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { MemorySaver } from "@langchain/langgraph"; + import { v4 as uuidv4 } from "uuid"; + import { z } from "zod"; + + // Define a Zod schema to represent the state + const State = z.object({ + url: z.string(), + result: z.string().optional(), + }); + + const callApi = async (state: z.infer) => { + // highlight-next-line + const response = await fetch(state.url); + const text = await response.text(); + const result = text.slice(0, 100); // Side-effect + return { + result, + }; + }; + + // Create a StateGraph builder and add a node for the callApi function + const builder = new StateGraph(State) + .addNode("callApi", callApi) + .addEdge(START, "callApi") + .addEdge("callApi", END); + + // Specify a checkpointer + const checkpointer = new MemorySaver(); + + // Compile the graph with the checkpointer + const graph = builder.compile({ checkpointer }); + + // Define a config with a thread ID. + const threadId = uuidv4(); + const config = { configurable: { thread_id: threadId } }; + + // Invoke the graph + await graph.invoke({ url: "https://www.example.com" }, config); + ``` + + + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { MemorySaver } from "@langchain/langgraph"; + import { task } from "@langchain/langgraph"; + import { v4 as uuidv4 } from "uuid"; + import { z } from "zod"; + + // Define a Zod schema to represent the state + const State = z.object({ + urls: z.array(z.string()), + results: z.array(z.string()).optional(), + }); + + const makeRequest = task("makeRequest", async (url: string) => { + // highlight-next-line + const response = await fetch(url); + const text = await response.text(); + return text.slice(0, 100); + }); + + const callApi = async (state: z.infer) => { + // highlight-next-line + const requests = state.urls.map((url) => makeRequest(url)); + const results = await Promise.all(requests); + return { + results, + }; + }; + + // Create a StateGraph builder and add a node for the callApi function + const builder = new StateGraph(State) + .addNode("callApi", callApi) + .addEdge(START, "callApi") + .addEdge("callApi", END); + + // Specify a checkpointer + const checkpointer = new MemorySaver(); + + // Compile the graph with the checkpointer + const graph = builder.compile({ checkpointer }); + + // Define a config with a thread ID. + const threadId = uuidv4(); + const config = { configurable: { thread_id: threadId } }; + + // Invoke the graph + await graph.invoke({ urls: ["https://www.example.com"] }, config); + ``` + + +::: + +## Resuming Workflows + +Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios: + +:::python +* **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](/oss/human-in-the-loop) for more details. +* **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](/oss/use-functional-api#resuming-after-an-error) with the functional API). +::: + +:::js +* **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](/oss/human-in-the-loop) for more details. +* **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `null` as the input value (see this [example](/oss/use-functional-api#resuming-after-an-error) with the functional API). +::: + +## Starting Points for Resuming Workflows + +:::python +* If you're using a @[StateGraph (Graph API)][StateGraph], the starting point is the beginning of the [**node**](/oss/graph-api#nodes) where execution stopped. +* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted. + Inside the subgraph, the starting point will be the specific [**node**](/oss/graph-api#nodes) where execution stopped. +* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](/oss/functional-api#entrypoint) where execution stopped. +::: + +:::js +* If you're using a [StateGraph (Graph API)](/oss/graph-api), the starting point is the beginning of the [**node**](/oss/graph-api#nodes) where execution stopped. +* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted. + Inside the subgraph, the starting point will be the specific [**node**](/oss/graph-api#nodes) where execution stopped. +* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](/oss/functional-api#entrypoint) where execution stopped. +::: diff --git a/src/oss/evals.mdx b/src/oss/evals.mdx new file mode 100644 index 00000000..5f35b8d6 --- /dev/null +++ b/src/oss/evals.mdx @@ -0,0 +1,238 @@ +--- +title: Evaluate agent performance +--- + +To evaluate your agent's performance you can use `LangSmith` [evaluations](https://docs.smith.langchain.com/evaluation). You would need to first define an evaluator function to judge the results from an agent, such as final outputs or trajectory. Depending on your evaluation technique, this may or may not involve a reference output: + +:::python +```python +def evaluator(*, outputs: dict, reference_outputs: dict): + # compare agent outputs against reference outputs + output_messages = outputs["messages"] + reference_messages = reference_outputs["messages"] + score = compare_messages(output_messages, reference_messages) + return {"key": "evaluator_score", "score": score} +``` +::: + +:::js +```typescript +type EvaluatorParams = { + outputs: Record; + referenceOutputs: Record; +}; + +function evaluator({ outputs, referenceOutputs }: EvaluatorParams) { + // compare agent outputs against reference outputs + const outputMessages = outputs.messages; + const referenceMessages = referenceOutputs.messages; + const score = compareMessages(outputMessages, referenceMessages); + return { key: "evaluator_score", score: score }; +} +``` +::: + +To get started, you can use prebuilt evaluators from `AgentEvals` package: + +:::python +```bash +pip install -U agentevals +``` +::: + +:::js +```bash +npm install agentevals +``` +::: + +## Create evaluator + +A common way to evaluate agent performance is by comparing its trajectory (the order in which it calls its tools) against a reference trajectory: + +:::python +```python +import json +# highlight-next-line +from agentevals.trajectory.match import create_trajectory_match_evaluator + +outputs = [ + { + "role": "assistant", + "tool_calls": [ + { + "function": { + "name": "get_weather", + "arguments": json.dumps({"city": "san francisco"}), + } + }, + { + "function": { + "name": "get_directions", + "arguments": json.dumps({"destination": "presidio"}), + } + } + ], + } +] +reference_outputs = [ + { + "role": "assistant", + "tool_calls": [ + { + "function": { + "name": "get_weather", + "arguments": json.dumps({"city": "san francisco"}), + } + }, + ], + } +] + +# Create the evaluator +evaluator = create_trajectory_match_evaluator( + # highlight-next-line + trajectory_match_mode="superset", # (1)! +) + +# Run the evaluator +result = evaluator( + outputs=outputs, reference_outputs=reference_outputs +) +``` +::: + +:::js +```typescript +import { createTrajectoryMatchEvaluator } from "agentevals/trajectory/match"; + +const outputs = [ + { + role: "assistant", + tool_calls: [ + { + function: { + name: "get_weather", + arguments: JSON.stringify({ city: "san francisco" }), + }, + }, + { + function: { + name: "get_directions", + arguments: JSON.stringify({ destination: "presidio" }), + }, + }, + ], + }, +]; + +const referenceOutputs = [ + { + role: "assistant", + tool_calls: [ + { + function: { + name: "get_weather", + arguments: JSON.stringify({ city: "san francisco" }), + }, + }, + ], + }, +]; + +// Create the evaluator +const evaluator = createTrajectoryMatchEvaluator({ + // Specify how the trajectories will be compared. `superset` will accept output trajectory as valid if it's a superset of the reference one. Other options include: strict, unordered and subset + trajectoryMatchMode: "superset", // (1)! +}); + +// Run the evaluator +const result = evaluator({ + outputs: outputs, + referenceOutputs: referenceOutputs, +}); +``` +::: + +1. Specify how the trajectories will be compared. `superset` will accept output trajectory as valid if it's a superset of the reference one. Other options include: [strict](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#strict-match), [unordered](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#unordered-match) and [subset](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#subset-and-superset-match) + +As a next step, learn more about how to [customize trajectory match evaluator](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#agent-trajectory-match). + +### LLM-as-a-judge + +You can use LLM-as-a-judge evaluator that uses an LLM to compare the trajectory against the reference outputs and output a score: + +:::python +```python +import json +from agentevals.trajectory.llm import ( + # highlight-next-line + create_trajectory_llm_as_judge, + TRAJECTORY_ACCURACY_PROMPT_WITH_REFERENCE +) + +evaluator = create_trajectory_llm_as_judge( + prompt=TRAJECTORY_ACCURACY_PROMPT_WITH_REFERENCE, + model="openai:o3-mini" +) +``` +::: + +:::js +```typescript +import { + createTrajectoryLlmAsJudge, + TRAJECTORY_ACCURACY_PROMPT_WITH_REFERENCE, +} from "agentevals/trajectory/llm"; + +const evaluator = createTrajectoryLlmAsJudge({ + prompt: TRAJECTORY_ACCURACY_PROMPT_WITH_REFERENCE, + model: "openai:o3-mini", +}); +``` +::: + +## Run evaluator + +To run an evaluator, you will first need to create a [LangSmith dataset](https://docs.smith.langchain.com/evaluation/concepts#datasets). To use the prebuilt AgentEvals evaluators, you will need a dataset with the following schema: + +* **input**: `{"messages": [...]}` input messages to call the agent with. +* **output**: `{"messages": [...]}` expected message history in the agent output. For trajectory evaluation, you can choose to keep only assistant messages. + +:::python +```python +from langsmith import Client +from langgraph.prebuilt import create_react_agent +from agentevals.trajectory.match import create_trajectory_match_evaluator + +client = Client() +agent = create_react_agent(...) +evaluator = create_trajectory_match_evaluator(...) + +experiment_results = client.evaluate( + lambda inputs: agent.invoke(inputs), + # replace with your dataset name + data="", + evaluators=[evaluator] +) +``` +::: + +:::js +```typescript +import { Client } from "langsmith"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { createTrajectoryMatchEvaluator } from "agentevals/trajectory/match"; + +const client = new Client(); +const agent = createReactAgent({...}); +const evaluator = createTrajectoryMatchEvaluator({...}); + +const experimentResults = await client.evaluate( + (inputs) => agent.invoke(inputs), + // replace with your dataset name + { data: "" }, + { evaluators: [evaluator] } +); +``` +::: diff --git a/src/oss/faq.mdx b/src/oss/faq.mdx new file mode 100644 index 00000000..782b828f --- /dev/null +++ b/src/oss/faq.mdx @@ -0,0 +1,67 @@ +--- +title: FAQ +--- + +Common questions and their answers! + +## Do I need to use LangChain to use LangGraph? What’s the difference? + +No. LangGraph is an orchestration framework for complex agentic systems and is more low-level and controllable than LangChain agents. LangChain provides a standard interface to interact with models and other components, useful for straight-forward chains and retrieval flows. + +## How is LangGraph different from other agent frameworks? + +Other agentic frameworks can work for simple, generic tasks but fall short for complex tasks. LangGraph provides a more expressive framework to handle your unique tasks without restricting you to a single black-box cognitive architecture. + +## Does LangGraph impact the performance of my app? + +LangGraph will not add any overhead to your code and is specifically designed with streaming workflows in mind. + +## Is LangGraph open source? Is it free? + +Yes. LangGraph is an MIT-licensed open-source library and is free to use. + +## How are LangGraph and LangGraph Platform different? + +LangGraph is a stateful, orchestration framework that brings added control to agent workflows. LangGraph Platform is a service for deploying and scaling LangGraph applications, with an opinionated API for building agent UXs, plus an integrated developer studio. + +| Features | LangGraph (open source) | LangGraph Platform | +| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| Description | Stateful orchestration framework for agentic applications | Scalable infrastructure for deploying LangGraph applications | +| SDKs | Python and JavaScript | Python and JavaScript | +| HTTP APIs | None | Yes - useful for retrieving & updating state or long-term memory, or creating a configurable assistant | +| Streaming | Basic | Dedicated mode for token-by-token messages | +| Checkpointer | Community contributed | Supported out-of-the-box | +| Persistence Layer | Self-managed | Managed Postgres with efficient storage | +| Deployment | Self-managed |
  • Cloud SaaS
  • Free self-hosted
  • Enterprise (paid self-hosted)
| +| Scalability | Self-managed | Auto-scaling of task queues and servers | +| Fault-tolerance | Self-managed | Automated retries | +| Concurrency Control | Simple threading | Supports double-texting | +| Scheduling | None | Cron scheduling | +| Monitoring | None | Integrated with LangSmith for observability | +| IDE integration | LangGraph Studio | LangGraph Studio | + +## Is LangGraph Platform open source? + +No. LangGraph Platform is proprietary software. + +There is a free, self-hosted version of LangGraph Platform with access to basic features. The Cloud SaaS deployment option and the Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more. + +For more information, see our [LangGraph Platform pricing page](https://www.langchain.com/pricing-langgraph-platform). + +## Does LangGraph work with LLMs that don't support tool calling? + +Yes! You can use LangGraph with any LLMs. The main reason we use LLMs that support tool calling is that this is often the most convenient way to have the LLM make its decision about what to do. If your LLM does not support tool calling, you can still use it - you just need to write a bit of logic to convert the raw LLM string response to a decision about what to do. + +## Does LangGraph work with OSS LLMs? + +Yes! LangGraph is totally ambivalent to what LLMs are used under the hood. The main reason we use closed LLMs in most of the tutorials is that they seamlessly support tool calling, while OSS LLMs often don't. But tool calling is not necessary (see [this section](#does-langgraph-work-with-llms-that-dont-support-tool-calling)) so you can totally use LangGraph with OSS LLMs. + +## Can I use LangGraph Studio without logging in to LangSmith + +Yes! You can use the [development version of LangGraph Server](/oss/local-server) to run the backend locally. +This will connect to the studio frontend hosted as part of LangSmith. +If you set an environment variable of `LANGSMITH_TRACING=false`, then no traces will be sent to LangSmith. + +## What does "nodes executed" mean for LangGraph Platform usage? + +**Nodes Executed** is the aggregate number of nodes in a LangGraph application that are called and completed successfully during an invocation of the application. If a node in the graph is not called during execution or ends in an error state, these nodes will not be counted. If a node is called and completes successfully multiple times, each occurrence will be counted. diff --git a/src/oss/functional-api.mdx b/src/oss/functional-api.mdx new file mode 100644 index 00000000..1482fd23 --- /dev/null +++ b/src/oss/functional-api.mdx @@ -0,0 +1,1134 @@ +--- +title: Functional API overview +sidebarTitle: Overview +--- + +The **Functional API** allows you to add LangGraph's key features — [persistence](/oss/persistence), [memory](/oss/add-memory), [human-in-the-loop](/oss/human-in-the-loop), and [streaming](/oss/streaming) — to your applications with minimal changes to your existing code. + +It is designed to integrate these features into existing code that may use standard language primitives for branching and control flow, such as `if` statements, `for` loops, and function calls. Unlike many data orchestration frameworks that require restructuring code into an explicit pipeline or DAG, the Functional API allows you to incorporate these capabilities without enforcing a rigid execution model. + +The Functional API uses two key building blocks: + +:::python +* **`@entrypoint`** – Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts. +* **`@task`** – Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously. +::: + +:::js +* **`entrypoint`** – An entrypoint encapsulates workflow logic and manages execution flow, including handling long-running tasks and interrupts. +* **`task`** – Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously. +::: + +This provides a minimal abstraction for building workflows with state management and streaming. + + + For information on how to use the functional API, see [Use Functional API](/oss/use-functional-api). + + +## Functional API vs. Graph API + +For users who prefer a more declarative approach, LangGraph's [Graph API](/oss/graph-api) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application. + +Here are some key differences: + +* **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write. +* **Short-term memory**: The **GraphAPI** requires declaring a [**State**](/oss/graph-api#state) and may require defining [**reducers**](/oss/graph-api#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions. +* **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](/oss/graph-api). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint. +* **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime. + +## Example + +Below we demonstrate a simple application that writes an essay and [interrupts](/oss/add-human-in-the-loop) to request human review. + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.func import entrypoint, task +from langgraph.types import interrupt + +@task +def write_essay(topic: str) -> str: + """Write an essay about the given topic.""" + time.sleep(1) # A placeholder for a long-running task. + return f"An essay about topic: {topic}" + +@entrypoint(checkpointer=InMemorySaver()) +def workflow(topic: str) -> dict: + """A simple workflow that writes an essay and asks for a review.""" + essay = write_essay("cat").result() + is_approved = interrupt({ + # Any json-serializable payload provided to interrupt as argument. + # It will be surfaced on the client side as an Interrupt when streaming data + # from the workflow. + "essay": essay, # The essay we want reviewed. + # We can add any additional information that we need. + # For example, introduce a key called "action" with some instructions. + "action": "Please approve/reject the essay", + }) + + return { + "essay": essay, # The essay that was generated + "is_approved": is_approved, # Response from HIL + } +``` +::: + +:::js +```typescript +import { MemorySaver, entrypoint, task, interrupt } from "@langchain/langgraph"; + +const writeEssay = task("writeEssay", async (topic: string) => { + // A placeholder for a long-running task. + await new Promise((resolve) => setTimeout(resolve, 1000)); + return `An essay about topic: ${topic}`; +}); + +const workflow = entrypoint( + { checkpointer: new MemorySaver(), name: "workflow" }, + async (topic: string) => { + const essay = await writeEssay(topic); + const isApproved = interrupt({ + // Any json-serializable payload provided to interrupt as argument. + // It will be surfaced on the client side as an Interrupt when streaming data + // from the workflow. + essay, // The essay we want reviewed. + // We can add any additional information that we need. + // For example, introduce a key called "action" with some instructions. + action: "Please approve/reject the essay", + }); + + return { + essay, // The essay that was generated + isApproved, // Response from HIL + }; + } +); +``` +::: + + + This workflow will write an essay about the topic "cat" and then pause to get a review from a human. The workflow can be interrupted for an indefinite amount of time until a review is provided. + + When the workflow is resumed, it executes from the very start, but because the result of the `writeEssay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed. + + :::python + ```python + import time + import uuid + from langgraph.func import entrypoint, task + from langgraph.types import interrupt + from langgraph.checkpoint.memory import InMemorySaver + + + @task + def write_essay(topic: str) -> str: + """Write an essay about the given topic.""" + time.sleep(1) # This is a placeholder for a long-running task. + return f"An essay about topic: {topic}" + + @entrypoint(checkpointer=InMemorySaver()) + def workflow(topic: str) -> dict: + """A simple workflow that writes an essay and asks for a review.""" + essay = write_essay("cat").result() + is_approved = interrupt( + { + # Any json-serializable payload provided to interrupt as argument. + # It will be surfaced on the client side as an Interrupt when streaming data + # from the workflow. + "essay": essay, # The essay we want reviewed. + # We can add any additional information that we need. + # For example, introduce a key called "action" with some instructions. + "action": "Please approve/reject the essay", + } + ) + return { + "essay": essay, # The essay that was generated + "is_approved": is_approved, # Response from HIL + } + + + thread_id = str(uuid.uuid4()) + config = {"configurable": {"thread_id": thread_id}} + for item in workflow.stream("cat", config): + print(item) + # > {'write_essay': 'An essay about topic: cat'} + # > { + # > '__interrupt__': ( + # > Interrupt( + # > value={ + # > 'essay': 'An essay about topic: cat', + # > 'action': 'Please approve/reject the essay' + # > }, + # > id='b9b2b9d788f482663ced6dc755c9e981' + # > ), + # > ) + # > } + ``` + + An essay has been written and is ready for review. Once the review is provided, we can resume the workflow: + + ```python + from langgraph.types import Command + + # Get review from a user (e.g., via a UI) + # In this case, we're using a bool, but this can be any json-serializable value. + human_review = True + + for item in workflow.stream(Command(resume=human_review), config): + print(item) + ``` + + ```pycon + {'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}} + ``` + + The workflow has been completed and the review has been added to the essay. + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { MemorySaver, entrypoint, task, interrupt } from "@langchain/langgraph"; + + const writeEssay = task("writeEssay", async (topic: string) => { + // This is a placeholder for a long-running task. + await new Promise(resolve => setTimeout(resolve, 1000)); + return `An essay about topic: ${topic}`; + }); + + const workflow = entrypoint( + { checkpointer: new MemorySaver(), name: "workflow" }, + async (topic: string) => { + const essay = await writeEssay(topic); + const isApproved = interrupt({ + // Any json-serializable payload provided to interrupt as argument. + // It will be surfaced on the client side as an Interrupt when streaming data + // from the workflow. + essay, // The essay we want reviewed. + // We can add any additional information that we need. + // For example, introduce a key called "action" with some instructions. + action: "Please approve/reject the essay", + }); + + return { + essay, // The essay that was generated + isApproved, // Response from HIL + }; + } + ); + + const threadId = uuidv4(); + + const config = { + configurable: { + thread_id: threadId + } + }; + + for await (const item of workflow.stream("cat", config)) { + console.log(item); + } + ``` + + ```console + { writeEssay: 'An essay about topic: cat' } + { + __interrupt__: [{ + value: { essay: 'An essay about topic: cat', action: 'Please approve/reject the essay' }, + resumable: true, + ns: ['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], + when: 'during' + }] + } + ``` + + An essay has been written and is ready for review. Once the review is provided, we can resume the workflow: + + ```typescript + import { Command } from "@langchain/langgraph"; + + // Get review from a user (e.g., via a UI) + // In this case, we're using a bool, but this can be any json-serializable value. + const humanReview = true; + + for await (const item of workflow.stream(new Command({ resume: humanReview }), config)) { + console.log(item); + } + ``` + + ```console + { workflow: { essay: 'An essay about topic: cat', isApproved: true } } + ``` + + The workflow has been completed and the review has been added to the essay. + ::: + + +## Entrypoint + +:::python +The @[`@entrypoint`][entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling _long-running tasks_ and [interrupts](/oss/human-in-the-loop). +::: + +:::js +The @[`entrypoint`][entrypoint] function can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling _long-running tasks_ and [interrupts](/oss/human-in-the-loop). +::: + +### Definition + +:::python +An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator. + +The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument. + +Decorating a function with an `entrypoint` produces a @[`Pregel`][Pregel.stream] instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing). + +You will usually want to pass a **checkpointer** to the `@entrypoint` decorator to enable persistence and use features like **human-in-the-loop**. + + + + ```python + from langgraph.func import entrypoint + + @entrypoint(checkpointer=checkpointer) + def my_workflow(some_input: dict) -> int: + # some logic that may involve long-running tasks like API calls, + # and may be interrupted for human-in-the-loop. + ... + return result + ``` + + + ```python + from langgraph.func import entrypoint + + @entrypoint(checkpointer=checkpointer) + async def my_workflow(some_input: dict) -> int: + # some logic that may involve long-running tasks like API calls, + # and may be interrupted for human-in-the-loop + ... + return result + ``` + + +::: + +:::js +An **entrypoint** is defined by calling the `entrypoint` function with configuration and a function. + +The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use an object as the input type for the first argument. + +Creating an entrypoint with a function produces a workflow instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing). + +You will often want to pass a **checkpointer** to the `entrypoint` function to enable persistence and use features like **human-in-the-loop**. + +```typescript +import { entrypoint } from "@langchain/langgraph"; + +const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (someInput: Record): Promise => { + // some logic that may involve long-running tasks like API calls, + // and may be interrupted for human-in-the-loop + return result; + } +); +``` +::: + + + **Serialization** + The **inputs** and **outputs** of entrypoints must be JSON-serializable to support checkpointing. Please see the [serialization](#serialization) section for more details. + + +:::python +### Injectable parameters + +When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include: + +| Parameter | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **previous** | Access the state associated with the previous `checkpoint` for the given thread. See [short-term-memory](#short-term-memory). | +| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](/oss/use-functional-api#long-term-memory). | +| **writer** | Use to access the StreamWriter when working with Async Python < 3.11. See [streaming with functional API for details](/oss/use-functional-api#streaming). | +| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. | + + + Declare the parameters with the appropriate name and type annotation. + + + + ```python + from langchain_core.runnables import RunnableConfig + from langgraph.func import entrypoint + from langgraph.store.base import BaseStore + from langgraph.store.memory import InMemoryStore + + in_memory_store = InMemoryStore(...) # An instance of InMemoryStore for long-term memory + + @entrypoint( + checkpointer=checkpointer, # Specify the checkpointer + store=in_memory_store # Specify the store + ) + def my_workflow( + some_input: dict, # The input (e.g., passed via `invoke`) + *, + previous: Any = None, # For short-term memory + store: BaseStore, # For long-term memory + writer: StreamWriter, # For streaming custom data + config: RunnableConfig # For accessing the configuration passed to the entrypoint + ) -> ...: + ``` + +::: + +### Executing + +:::python +Using the [`@entrypoint`](#entrypoint) yields a @[`Pregel`][Pregel.stream] object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods. + + + + ```python + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + my_workflow.invoke(some_input, config) # Wait for the result synchronously + ``` + + + ```python + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + await my_workflow.ainvoke(some_input, config) # Await result asynchronously + ``` + + + ```python + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + for chunk in my_workflow.stream(some_input, config): + print(chunk) + ``` + + + ```python + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + async for chunk in my_workflow.astream(some_input, config): + print(chunk) + ``` + + +::: + +:::js +Using the [`entrypoint`](#entrypoint) function will return an object that can be executed using the `invoke` and `stream` methods. + + + + ```typescript + const config = { + configurable: { + thread_id: "some_thread_id" + } + }; + await myWorkflow.invoke(someInput, config); // Wait for the result + ``` + + + ```typescript + const config = { + configurable: { + thread_id: "some_thread_id" + } + }; + + for await (const chunk of myWorkflow.stream(someInput, config)) { + console.log(chunk); + } + ``` + + +::: + +### Resuming + +:::python +Resuming an execution after an @[interrupt][interrupt] can be done by passing a **resume** value to the @[Command] primitive. + + + + ```python + from langgraph.types import Command + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + my_workflow.invoke(Command(resume=some_resume_value), config) + ``` + + + ```python + from langgraph.types import Command + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + await my_workflow.ainvoke(Command(resume=some_resume_value), config) + ``` + + + ```python + from langgraph.types import Command + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + for chunk in my_workflow.stream(Command(resume=some_resume_value), config): + print(chunk) + ``` + + + ```python + from langgraph.types import Command + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + async for chunk in my_workflow.astream(Command(resume=some_resume_value), config): + print(chunk) + ``` + + +::: + +:::js +Resuming an execution after an @[interrupt][interrupt] can be done by passing a **resume** value to the @[`Command`][Command] primitive. + + + + ```typescript + import { Command } from "@langchain/langgraph"; + + const config = { + configurable: { + thread_id: "some_thread_id" + } + }; + + await myWorkflow.invoke(new Command({ resume: someResumeValue }), config); + ``` + + + ```typescript + import { Command } from "@langchain/langgraph"; + + const config = { + configurable: { + thread_id: "some_thread_id" + } + }; + + const stream = await myWorkflow.stream( + new Command({ resume: someResumableValue }), + config, + ) + + for await (const chunk of stream) { + console.log(chunk); + } + ``` + + +::: + +:::python +**Resuming after an error** + +To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config). + +This assumes that the underlying **error** has been resolved and execution can proceed successfully. + + + + ```python + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + my_workflow.invoke(None, config) + ``` + + + ```python + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + await my_workflow.ainvoke(None, config) + ``` + + + ```python + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + for chunk in my_workflow.stream(None, config): + print(chunk) + ``` + + + ```python + + config = { + "configurable": { + "thread_id": "some_thread_id" + } + } + + async for chunk in my_workflow.astream(None, config): + print(chunk) + ``` + + +::: + +:::js +**Resuming after an error** + +To resume after an error, run the `entrypoint` with `null` and the same **thread id** (config). + +This assumes that the underlying **error** has been resolved and execution can proceed successfully. + + + + ```typescript + const config = { + configurable: { + thread_id: "some_thread_id" + } + }; + + await myWorkflow.invoke(null, config); + ``` + + + ```typescript + const config = { + configurable: { + thread_id: "some_thread_id" + } + }; + + for await (const chunk of myWorkflow.stream(null, config)) { + console.log(chunk); + } + ``` + + +::: + +### Short-term memory + +When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](/oss/persistence#checkpoints). + +:::python +This allows accessing the state from the previous invocation using the `previous` parameter. + +By default, the `previous` parameter is the return value of the previous invocation. + +```python +@entrypoint(checkpointer=checkpointer) +def my_workflow(number: int, *, previous: Any = None) -> int: + previous = previous or 0 + return number + previous + +config = { + "configurable": { + "thread_id": "some_thread_id" + } +} + +my_workflow.invoke(1, config) # 1 (previous was None) +my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation) +``` +::: + +:::js +This allows accessing the state from the previous invocation using the `getPreviousState` function. + +By default, the `getPreviousState` function returns the return value of the previous invocation. + +```typescript +import { entrypoint, getPreviousState } from "@langchain/langgraph"; + +const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (number: number) => { + const previous = getPreviousState() ?? 0; + return number + previous; + } +); + +const config = { + configurable: { + thread_id: "some_thread_id", + }, +}; + +await myWorkflow.invoke(1, config); // 1 (previous was undefined) +await myWorkflow.invoke(2, config); // 3 (previous was 1 from the previous invocation) +``` +::: + +#### `entrypoint.final` + +:::python +@[`entrypoint.final`][entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**. + +The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`. + +```python +@entrypoint(checkpointer=checkpointer) +def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]: + previous = previous or 0 + # This will return the previous value to the caller, saving + # 2 * number to the checkpoint, which will be used in the next invocation + # for the `previous` parameter. + return entrypoint.final(value=previous, save=2 * number) + +config = { + "configurable": { + "thread_id": "1" + } +} + +my_workflow.invoke(3, config) # 0 (previous was None) +my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation) +``` +::: + +:::js +@[`entrypoint.final`][entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**. + +The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. + +```typescript +import { entrypoint, getPreviousState } from "@langchain/langgraph"; + +const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (number: number) => { + const previous = getPreviousState() ?? 0; + // This will return the previous value to the caller, saving + // 2 * number to the checkpoint, which will be used in the next invocation + // for the `previous` parameter. + return entrypoint.final({ + value: previous, + save: 2 * number, + }); + } +); + +const config = { + configurable: { + thread_id: "1", + }, +}; + +await myWorkflow.invoke(3, config); // 0 (previous was undefined) +await myWorkflow.invoke(1, config); // 6 (previous was 3 * 2 from the previous invocation) +``` +::: + +## Task + +A **task** represents a discrete unit of work, such as an API call or data processing step. It has two key characteristics: + +* **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking. +* **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](/oss/persistence) for more details). + +### Definition + +:::python +Tasks are defined using the `@task` decorator, which wraps a regular Python function. + +```python +from langgraph.func import task + +@task() +def slow_computation(input_value): + # Simulate a long-running operation + ... + return result +``` +::: + +:::js +Tasks are defined using the `task` function, which wraps a regular function. + +```typescript +import { task } from "@langchain/langgraph"; + +const slowComputation = task("slowComputation", async (inputValue: any) => { + // Simulate a long-running operation + return result; +}); +``` +::: + + + **Serialization** + The **outputs** of tasks must be JSON-serializable to support checkpointing. + + +### Execution + +**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](/oss/graph-api#nodes). + +Tasks _cannot_ be called directly from the main application code. + +:::python +When you call a **task**, it returns _immediately_ with a future object. A future is a placeholder for a result that will be available later. + +To obtain the result of a **task**, you can either wait for it synchronously (using `result()`) or await it asynchronously (using `await`). + + + + ```python + @entrypoint(checkpointer=checkpointer) + def my_workflow(some_input: int) -> int: + future = slow_computation(some_input) + return future.result() # Wait for the result synchronously + ``` + + + ```python + @entrypoint(checkpointer=checkpointer) + async def my_workflow(some_input: int) -> int: + return await slow_computation(some_input) # Await result asynchronously + ``` + + +::: + +:::js +When you call a **task**, it returns a Promise that can be awaited. + +```typescript +const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (someInput: number): Promise => { + return await slowComputation(someInput); + } +); +``` +::: + +## When to use a task + +**Tasks** are useful in the following scenarios: + +* **Checkpointing**: When you need to save the result of a long-running operation to a checkpoint, so you don't need to recompute it when resuming the workflow. +* **Human-in-the-loop**: If you're building a workflow that requires human intervention, you MUST use **tasks** to encapsulate any randomness (e.g., API calls) to ensure that the workflow can be resumed correctly. See the [determinism](#determinism) section for more details. +* **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs). +* **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](https://docs.smith.langchain.com/). +* **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic. + +## Serialization + +There are two key aspects to serialization in LangGraph: + +1. `entrypoint` inputs and outputs must be JSON-serializable. +2. `task` outputs must be JSON-serializable. + +:::python +These requirements are necessary for enabling checkpointing and workflow resumption. Use python primitives like dictionaries, lists, strings, numbers, and booleans to ensure that your inputs and outputs are serializable. +::: + +:::js +These requirements are necessary for enabling checkpointing and workflow resumption. Use primitives like objects, arrays, strings, numbers, and booleans to ensure that your inputs and outputs are serializable. +::: + +Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution. + +Providing non-serializable inputs or outputs will result in a runtime error when a workflow is configured with a checkpointer. + +## Determinism + +To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same _sequence of steps_, even if **task** results are non-deterministic. + +LangGraph achieves this behavior by persisting **task** and [**subgraph**](/oss/subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the _same sequence of steps_, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same. + +While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them. + +## Idempotency + +Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication. + +## Common Pitfalls + +### Handling side effects + +Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to ensure they are not executed multiple times when resuming a workflow. + + + + In this example, a side effect (writing to a file) is directly included in the workflow, so it will be executed a second time when resuming the workflow. + + :::python + ```python + @entrypoint(checkpointer=checkpointer) + def my_workflow(inputs: dict) -> int: + # This code will be executed a second time when resuming the workflow. + # Which is likely not what you want. + # highlight-next-line + with open("output.txt", "w") as f: + # highlight-next-line + f.write("Side effect executed") + value = interrupt("question") + return value + ``` + ::: + + :::js + ```typescript + import { entrypoint, interrupt } from "@langchain/langgraph"; + import fs from "fs"; + + const myWorkflow = entrypoint( + { checkpointer, name: "workflow }, + async (inputs: Record) => { + // This code will be executed a second time when resuming the workflow. + // Which is likely not what you want. + fs.writeFileSync("output.txt", "Side effect executed"); + const value = interrupt("question"); + return value; + } + ); + ``` + ::: + + + In this example, the side effect is encapsulated in a task, ensuring consistent execution upon resumption. + + :::python + ```python + from langgraph.func import task + + # highlight-next-line + @task + # highlight-next-line + def write_to_file(): + with open("output.txt", "w") as f: + f.write("Side effect executed") + + @entrypoint(checkpointer=checkpointer) + def my_workflow(inputs: dict) -> int: + # The side effect is now encapsulated in a task. + write_to_file().result() + value = interrupt("question") + return value + ``` + ::: + + :::js + ```typescript + import { entrypoint, task, interrupt } from "@langchain/langgraph"; + import * as fs from "fs"; + + const writeToFile = task("writeToFile", async () => { + fs.writeFileSync("output.txt", "Side effect executed"); + }); + + const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (inputs: Record) => { + // The side effect is now encapsulated in a task. + await writeToFile(); + const value = interrupt("question"); + return value; + } + ); + ``` + ::: + + + +### Non-deterministic control flow + +Operations that might give different results each time (like getting current time or random numbers) should be encapsulated in tasks to ensure that on resume, the same result is returned. + +* In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ... +* Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ... + +:::python +This is especially important when using **human-in-the-loop** workflows with multiple interrupts calls. LangGraph keeps a list of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value. This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts. +::: + +:::js +This is especially important when using **human-in-the-loop** workflows with multiple interrupt calls. LangGraph keeps a list of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value. This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts. +::: + +If order of execution is not maintained when resuming, one `interrupt` call may be matched with the wrong `resume` value, leading to incorrect results. + +Please read the section on [determinism](#determinism) for more details. + + + + In this example, the workflow uses the current time to determine which task to execute. This is non-deterministic because the result of the workflow depends on the time at which it is executed. + + :::python + ```python + from langgraph.func import entrypoint + + @entrypoint(checkpointer=checkpointer) + def my_workflow(inputs: dict) -> int: + t0 = inputs["t0"] + # highlight-next-line + t1 = time.time() + + delta_t = t1 - t0 + + if delta_t > 1: + result = slow_task(1).result() + value = interrupt("question") + else: + result = slow_task(2).result() + value = interrupt("question") + + return { + "result": result, + "value": value + } + ``` + ::: + + :::js + ```typescript + import { entrypoint, interrupt } from "@langchain/langgraph"; + + const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (inputs: { t0: number }) => { + const t1 = Date.now(); + + const deltaT = t1 - inputs.t0; + + if (deltaT > 1000) { + const result = await slowTask(1); + const value = interrupt("question"); + return { result, value }; + } else { + const result = await slowTask(2); + const value = interrupt("question"); + return { result, value }; + } + } + ); + ``` + ::: + + + :::python + In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input. + + ```python + import time + + from langgraph.func import task + + # highlight-next-line + @task + # highlight-next-line + def get_time() -> float: + return time.time() + + @entrypoint(checkpointer=checkpointer) + def my_workflow(inputs: dict) -> int: + t0 = inputs["t0"] + # highlight-next-line + t1 = get_time().result() + + delta_t = t1 - t0 + + if delta_t > 1: + result = slow_task(1).result() + value = interrupt("question") + else: + result = slow_task(2).result() + value = interrupt("question") + + return { + "result": result, + "value": value + } + ``` + ::: + + :::js + In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input. + + ```typescript + import { entrypoint, task, interrupt } from "@langchain/langgraph"; + + const getTime = task("getTime", () => Date.now()); + + const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (inputs: { t0: number }): Promise => { + const t1 = await getTime(); + + const deltaT = t1 - inputs.t0; + + if (deltaT > 1000) { + const result = await slowTask(1); + const value = interrupt("question"); + return { result, value }; + } else { + const result = await slowTask(2); + const value = interrupt("question"); + return { result, value }; + } + } + ); + ``` + ::: + + diff --git a/src/oss/graph-api.mdx b/src/oss/graph-api.mdx new file mode 100644 index 00000000..9a76ec62 --- /dev/null +++ b/src/oss/graph-api.mdx @@ -0,0 +1,1087 @@ +--- +title: Graph API concepts +sidebarTitle: Overview +--- + +## Graphs + +At its core, LangGraph models agent workflows as graphs. You define the behavior of your agents using three key components: + +1. [`State`](#state): A shared data structure that represents the current snapshot of your application. It can be any data type, but is typically defined using a shared state schema. + +2. [`Nodes`](#nodes): Functions that encode the logic of your agents. They receive the current state as input, perform some computation or side-effect, and return an updated state. + +3. [`Edges`](#edges): Functions that determine which `Node` to execute next based on the current state. They can be conditional branches or fixed transitions. + +By composing `Nodes` and `Edges`, you can create complex, looping workflows that evolve the state over time. The real power, though, comes from how LangGraph manages that state. To emphasize: `Nodes` and `Edges` are nothing more than functions - they can contain an LLM or just good ol' code. + +In short: _nodes do the work, edges tell what to do next_. + +LangGraph's underlying graph algorithm uses [message passing](https://en.wikipedia.org/wiki/Message_passing) to define a general program. When a Node completes its operation, it sends messages along one or more edges to other node(s). These recipient nodes then execute their functions, pass the resulting messages to the next set of nodes, and the process continues. Inspired by Google's [Pregel](https://research.google/pubs/pregel-a-system-for-large-scale-graph-processing/) system, the program proceeds in discrete "super-steps." + +A super-step can be considered a single iteration over the graph nodes. Nodes that run in parallel are part of the same super-step, while nodes that run sequentially belong to separate super-steps. At the start of graph execution, all nodes begin in an `inactive` state. A node becomes `active` when it receives a new message (state) on any of its incoming edges (or "channels"). The active node then runs its function and responds with updates. At the end of each super-step, nodes with no incoming messages vote to `halt` by marking themselves as `inactive`. The graph execution terminates when all nodes are `inactive` and no messages are in transit. + +### StateGraph + +The `StateGraph` class is the main graph class to use. This is parameterized by a user defined `State` object. + +### Compiling your graph + +To build your graph, you first define the [state](#state), you then add [nodes](#nodes) and [edges](#edges), and then you compile it. What exactly is compiling your graph and why is it needed? + +Compiling is a pretty simple step. It provides a few basic checks on the structure of your graph (no orphaned nodes, etc). It is also where you can specify runtime args like [checkpointers](/oss/persistence) and breakpoints. You compile your graph by just calling the `.compile` method: + +:::python + +```python +graph = graph_builder.compile(...) +``` + +::: + +:::js + +```typescript +const graph = new StateGraph(StateAnnotation) + .addNode("nodeA", nodeA) + .addEdge(START, "nodeA") + .addEdge("nodeA", END) + .compile(); +``` + +::: + +You **MUST** compile your graph before you can use it. + +## State + +:::python +The first thing you do when you define a graph is define the `State` of the graph. The `State` consists of the [schema of the graph](#schema) as well as [`reducer` functions](#reducers) which specify how to apply updates to the state. The schema of the `State` will be the input schema to all `Nodes` and `Edges` in the graph, and can be either a `TypedDict` or a `Pydantic` model. All `Nodes` will emit updates to the `State` which are then applied using the specified `reducer` function. +::: + +:::js +The first thing you do when you define a graph is define the `State` of the graph. The `State` consists of the [schema of the graph](#schema) as well as [`reducer` functions](#reducers) which specify how to apply updates to the state. The schema of the `State` will be the input schema to all `Nodes` and `Edges` in the graph, and can be either a Zod schema or a schema built using `Annotation.Root`. All `Nodes` will emit updates to the `State` which are then applied using the specified `reducer` function. +::: + +### Schema + +:::python +The main documented way to specify the schema of a graph is by using a [`TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict). If you want to provide default values in your state, use a [`dataclass`](https://docs.python.org/3/library/dataclasses.html). We also support using a Pydantic [BaseModel](/oss/graph-api.md#use-pydantic-models-for-graph-state) as your graph state if you want recursive data validation (though note that pydantic is less performant than a `TypedDict` or `dataclass`). + +By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [guide here](/oss/graph-api.md#define-input-and-output-schemas) for how to use. +::: + +:::js +The main documented way to specify the schema of a graph is by using Zod schemas. However, we also support using the `Annotation` API to define the schema of the graph. + +By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. +::: + +#### Multiple schemas + +Typically, all graph nodes communicate with a single schema. This means that they will read and write to the same state channels. But, there are cases where we want more control over this: + +- Internal nodes can pass information that is not required in the graph's input / output. +- We may also want to use different input / output schemas for the graph. The output might, for example, only contain a single relevant output key. + +It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, `PrivateState`. + +It is also possible to define explicit input and output schemas for a graph. In these cases, we define an "internal" schema that contains _all_ keys relevant to graph operations. But, we also define `input` and `output` schemas that are sub-sets of the "internal" schema to constrain the input and output of the graph. See [this guide](/oss/graph-api.md#define-input-and-output-schemas) for more detail. + +Let's look at an example: + +:::python + +```python +class InputState(TypedDict): + user_input: str + +class OutputState(TypedDict): + graph_output: str + +class OverallState(TypedDict): + foo: str + user_input: str + graph_output: str + +class PrivateState(TypedDict): + bar: str + +def node_1(state: InputState) -> OverallState: + # Write to OverallState + return {"foo": state["user_input"] + " name"} + +def node_2(state: OverallState) -> PrivateState: + # Read from OverallState, write to PrivateState + return {"bar": state["foo"] + " is"} + +def node_3(state: PrivateState) -> OutputState: + # Read from PrivateState, write to OutputState + return {"graph_output": state["bar"] + " Lance"} + +builder = StateGraph(OverallState,input_schema=InputState,output_schema=OutputState) +builder.add_node("node_1", node_1) +builder.add_node("node_2", node_2) +builder.add_node("node_3", node_3) +builder.add_edge(START, "node_1") +builder.add_edge("node_1", "node_2") +builder.add_edge("node_2", "node_3") +builder.add_edge("node_3", END) + +graph = builder.compile() +graph.invoke({"user_input":"My"}) +# {'graph_output': 'My name is Lance'} +``` + +::: + +:::js + +```typescript +const InputState = z.object({ + userInput: z.string(), +}); + +const OutputState = z.object({ + graphOutput: z.string(), +}); + +const OverallState = z.object({ + foo: z.string(), + userInput: z.string(), + graphOutput: z.string(), +}); + +const PrivateState = z.object({ + bar: z.string(), +}); + +const graph = new StateGraph({ + state: OverallState, + input: InputState, + output: OutputState, +}) + .addNode("node1", (state) => { + // Write to OverallState + return { foo: state.userInput + " name" }; + }) + .addNode("node2", (state) => { + // Read from OverallState, write to PrivateState + return { bar: state.foo + " is" }; + }) + .addNode( + "node3", + (state) => { + // Read from PrivateState, write to OutputState + return { graphOutput: state.bar + " Lance" }; + }, + { input: PrivateState } + ) + .addEdge(START, "node1") + .addEdge("node1", "node2") + .addEdge("node2", "node3") + .addEdge("node3", END) + .compile(); + +await graph.invoke({ userInput: "My" }); +// { graphOutput: 'My name is Lance' } +``` + +::: + +There are two subtle and important points to note here: + +:::python + +1. We pass `state: InputState` as the input schema to `node_1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`. + +2. We initialize the graph with `StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)`. So, how can we write to `PrivateState` in `node_2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it. +::: + +:::js + +1. We pass `state` as the input schema to `node1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`. + +2. We initialize the graph with `StateGraph({ state: OverallState, input: InputState, output: OutputState })`. So, how can we write to `PrivateState` in `node2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it. +::: + +### Reducers + +Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. There are a few different types of reducers, starting with the default type of reducer: + +#### Default Reducer + +These two examples show how to use the default reducer: + +**Example A:** + +:::python + +```python +from typing_extensions import TypedDict + +class State(TypedDict): + foo: int + bar: list[str] +``` + +::: + +:::js + +```typescript +const State = z.object({ + foo: z.number(), + bar: z.array(z.string()), +}); +``` + +::: + +In this example, no reducer functions are specified for any key. Let's assume the input to the graph is: + +:::python +`{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["bye"]}` +::: + +:::js +`{ foo: 1, bar: ["hi"] }`. Let's then assume the first `Node` returns `{ foo: 2 }`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{ foo: 2, bar: ["hi"] }`. If the second node returns `{ bar: ["bye"] }` then the `State` would then be `{ foo: 2, bar: ["bye"] }` +::: + +**Example B:** + +:::python + +```python +from typing import Annotated +from typing_extensions import TypedDict +from operator import add + +class State(TypedDict): + foo: int + bar: Annotated[list[str], add] +``` + +In this example, we've used the `Annotated` type to specify a reducer function (`operator.add`) for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["hi", "bye"]}`. Notice here that the `bar` key is updated by adding the two lists together. +::: + +:::js + +```typescript +import { z } from "zod"; +import { withLangGraph } from "@langchain/langgraph/zod"; + +const State = z.object({ + foo: z.number(), + bar: withLangGraph(z.array(z.string()), { + reducer: { + fn: (x, y) => x.concat(y), + }, + }), +}); +``` + +In this example, we've used the `withLangGraph` function to specify a reducer function for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{ foo: 1, bar: ["hi"] }`. Let's then assume the first `Node` returns `{ foo: 2 }`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{ foo: 2, bar: ["hi"] }`. If the second node returns `{ bar: ["bye"] }` then the `State` would then be `{ foo: 2, bar: ["hi", "bye"] }`. Notice here that the `bar` key is updated by adding the two arrays together. +::: + +### Working with Messages in Graph State + +#### Why use messages? + +:::python +Most modern LLM providers have a chat model interface that accepts a list of messages as input. LangChain's [`ChatModel`](https://python.langchain.com/docs/concepts/#chat-models) in particular accepts a list of `Message` objects as inputs. These messages come in a variety of forms such as `HumanMessage` (user input) or `AIMessage` (LLM response). To read more about what message objects are, please refer to [this](https://python.langchain.com/docs/concepts/#messages) conceptual guide. +::: + +:::js +Most modern LLM providers have a chat model interface that accepts a list of messages as input. LangChain's [`ChatModel`](https://js.langchain.com/docs/concepts/#chat-models) in particular accepts a list of `Message` objects as inputs. These messages come in a variety of forms such as `HumanMessage` (user input) or `AIMessage` (LLM response). To read more about what message objects are, please refer to [this](https://js.langchain.com/docs/concepts/#messages) conceptual guide. +::: + +#### Using Messages in your Graph + +:::python +In many cases, it is helpful to store prior conversation history as a list of messages in your graph state. To do so, we can add a key (channel) to the graph state that stores a list of `Message` objects and annotate it with a reducer function (see `messages` key in the example below). The reducer function is vital to telling the graph how to update the list of `Message` objects in the state with each state update (for example, when a node sends an update). If you don't specify a reducer, every state update will overwrite the list of messages with the most recently provided value. If you wanted to simply append messages to the existing list, you could use `operator.add` as a reducer. + +However, you might also want to manually update messages in your graph state (e.g. human-in-the-loop). If you were to use `operator.add`, the manual state updates you send to the graph would be appended to the existing list of messages, instead of updating existing messages. To avoid that, you need a reducer that can keep track of message IDs and overwrite existing messages, if updated. To achieve this, you can use the prebuilt `add_messages` function. For brand new messages, it will simply append to existing list, but it will also handle the updates for existing messages correctly. +::: + +:::js +In many cases, it is helpful to store prior conversation history as a list of messages in your graph state. To do so, we can add a key (channel) to the graph state that stores a list of `Message` objects and annotate it with a reducer function (see `messages` key in the example below). The reducer function is vital to telling the graph how to update the list of `Message` objects in the state with each state update (for example, when a node sends an update). If you don't specify a reducer, every state update will overwrite the list of messages with the most recently provided value. If you wanted to simply append messages to the existing list, you could use a function that concatenates arrays as a reducer. + +However, you might also want to manually update messages in your graph state (e.g. human-in-the-loop). If you were to use a simple concatenation function, the manual state updates you send to the graph would be appended to the existing list of messages, instead of updating existing messages. To avoid that, you need a reducer that can keep track of message IDs and overwrite existing messages, if updated. To achieve this, you can use the prebuilt `MessagesZodState` schema. For brand new messages, it will simply append to existing list, but it will also handle the updates for existing messages correctly. +::: + +#### Serialization + +:::python +In addition to keeping track of message IDs, the `add_messages` function will also try to deserialize messages into LangChain `Message` objects whenever a state update is received on the `messages` channel. See more information on LangChain serialization/deserialization [here](https://python.langchain.com/docs/how_to/serialization/). This allows sending graph inputs / state updates in the following format: + +```python +# this is supported +{"messages": [HumanMessage(content="message")]} + +# and this is also supported +{"messages": [{"type": "human", "content": "message"}]} +``` + +Since the state updates are always deserialized into LangChain `Messages` when using `add_messages`, you should use dot notation to access message attributes, like `state["messages"][-1].content`. Below is an example of a graph that uses `add_messages` as its reducer function. + +```python +from langchain_core.messages import AnyMessage +from langgraph.graph.message import add_messages +from typing import Annotated +from typing_extensions import TypedDict + +class GraphState(TypedDict): + messages: Annotated[list[AnyMessage], add_messages] +``` + +::: + +:::js +In addition to keeping track of message IDs, `MessagesZodState` will also try to deserialize messages into LangChain `Message` objects whenever a state update is received on the `messages` channel. This allows sending graph inputs / state updates in the following format: + +```typescript +// this is supported +{ + messages: [new HumanMessage("message")]; +} + +// and this is also supported +{ + messages: [{ role: "human", content: "message" }]; +} +``` + +Since the state updates are always deserialized into LangChain `Messages` when using `MessagesZodState`, you should use dot notation to access message attributes, like `state.messages[state.messages.length - 1].content`. Below is an example of a graph that uses `MessagesZodState`: + +```typescript +import { StateGraph, MessagesZodState } from "@langchain/langgraph"; + +const graph = new StateGraph(MessagesZodState) + ... +``` + +`MessagesZodState` is defined with a single `messages` key which is a list of `BaseMessage` objects and uses the appropriate reducer. Typically, there is more state to track than just messages, so we see people extend this state and add more fields, like: + +```typescript +const State = z.object({ + messages: MessagesZodState.shape.messages, + documents: z.array(z.string()), +}); +``` + +::: + +:::python + +#### MessagesState + +Since having a list of messages in your state is so common, there exists a prebuilt state called `MessagesState` which makes it easy to use messages. `MessagesState` is defined with a single `messages` key which is a list of `AnyMessage` objects and uses the `add_messages` reducer. Typically, there is more state to track than just messages, so we see people subclass this state and add more fields, like: + +```python +from langgraph.graph import MessagesState + +class State(MessagesState): + documents: list[str] +``` + +::: + +## Nodes + +:::python + +In LangGraph, nodes are Python functions (either synchronous or asynchronous) that accept the following arguments: + +1. `state`: The [state](#state) of the graph +2. `config`: A `RunnableConfig` object that contains configuration information like `thread_id` and tracing information like `tags` +3. `runtime`: A `Runtime` object that contains [runtime `context`](#runtime-context) and other information like `store` and `stream_writer` + +Similar to `NetworkX`, you add these nodes to a graph using the @[add_node][add_node] method: + +```python +from dataclasses import dataclass +from typing_extensions import TypedDict + +from langchain_core.runnables import RunnableConfig +from langgraph.graph import StateGraph +from langgraph.runtime import Runtime + +class State(TypedDict): + input: str + results: str + +@dataclass +class Context: + user_id: str + +builder = StateGraph(State) + +def plain_node(state: State): + return state + +def node_with_runtime(state: State, runtime: Runtime[Context]): + print("In node: ", runtime.context.user_id) + return {"results": f"Hello, {state['input']}!"} + +def node_with_config(state: State, config: RunnableConfig): + print("In node with thread_id: ", config["configurable"]["thread_id"]) + return {"results": f"Hello, {state['input']}!"} + + +builder.add_node("plain_node", plain_node) +builder.add_node("node_with_runtime", node_with_runtime) +builder.add_node("node_with_config", node_with_config) +... +``` + +::: + +:::js + +In LangGraph, nodes are typically functions (sync or async) that accept the following arguments: + +1. `state`: The [state](#state) of the graph +2. `config`: A `RunnableConfig` object that contains configuration information like `thread_id` and tracing information like `tags` + +You can add nodes to a graph using the `addNode` method. + +```typescript +import { StateGraph } from "@langchain/langgraph"; +import { RunnableConfig } from "@langchain/core/runnables"; +import { z } from "zod"; + +const State = z.object({ + input: z.string(), + results: z.string(), +}); + +const builder = new StateGraph(State); + .addNode("myNode", (state, config) => { + console.log("In node: ", config?.configurable?.user_id); + return { results: `Hello, ${state.input}!` }; + }) + addNode("otherNode", (state) => { + return state; + }) + ... +``` + +::: + +Behind the scenes, functions are converted to [RunnableLambda](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.base.RunnableLambda.html)s, which add batch and async support to your function, along with native tracing and debugging. + +If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name. + +:::python + +```python +builder.add_node(my_node) +# You can then create edges to/from this node by referencing it as `"my_node"` +``` + +::: + +:::js + +```typescript +builder.addNode(myNode); +// You can then create edges to/from this node by referencing it as `"myNode"` +``` + +::: + +### `START` Node + +The `START` Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first. + +:::python + +```python +from langgraph.graph import START + +graph.add_edge(START, "node_a") +``` + +::: + +:::js + +```typescript +import { START } from "@langchain/langgraph"; + +graph.addEdge(START, "nodeA"); +``` + +::: + +### `END` Node + +The `END` Node is a special node that represents a terminal node. This node is referenced when you want to denote which edges have no actions after they are done. + +:::python + +```python +from langgraph.graph import END + +graph.add_edge("node_a", END) +``` + +::: + +:::js + +```typescript +import { END } from "@langchain/langgraph"; + +graph.addEdge("nodeA", END); +``` + +::: + +### Node Caching + +:::python +LangGraph supports caching of tasks/nodes based on the input to the node. To use caching: + +- Specify a cache when compiling a graph (or specifying an entrypoint) +- Specify a cache policy for nodes. Each cache policy supports: + - `key_func` used to generate a cache key based on the input to a node, which defaults to a `hash` of the input with pickle. + - `ttl`, the time to live for the cache in seconds. If not specified, the cache will never expire. + +For example: + +```python +import time +from typing_extensions import TypedDict +from langgraph.graph import StateGraph +from langgraph.cache.memory import InMemoryCache +from langgraph.types import CachePolicy + + +class State(TypedDict): + x: int + result: int + + +builder = StateGraph(State) + + +def expensive_node(state: State) -> dict[str, int]: + # expensive computation + time.sleep(2) + return {"result": state["x"] * 2} + + +builder.add_node("expensive_node", expensive_node, cache_policy=CachePolicy(ttl=3)) +builder.set_entry_point("expensive_node") +builder.set_finish_point("expensive_node") + +graph = builder.compile(cache=InMemoryCache()) + +print(graph.invoke({"x": 5}, stream_mode='updates')) # (1)! +[{'expensive_node': {'result': 10}}] +print(graph.invoke({"x": 5}, stream_mode='updates')) # (2)! +[{'expensive_node': {'result': 10}, '__metadata__': {'cached': True}}] +``` + +1. First run takes two seconds to run (due to mocked expensive computation). +2. Second run utilizes cache and returns quickly. +::: + +:::js +LangGraph supports caching of tasks/nodes based on the input to the node. To use caching: + +- Specify a cache when compiling a graph (or specifying an entrypoint) +- Specify a cache policy for nodes. Each cache policy supports: + - `keyFunc`, which is used to generate a cache key based on the input to a node. + - `ttl`, the time to live for the cache in seconds. If not specified, the cache will never expire. + +```typescript +import { StateGraph, MessagesZodState } from "@langchain/langgraph"; +import { InMemoryCache } from "@langchain/langgraph-checkpoint"; + +const graph = new StateGraph(MessagesZodState) + .addNode( + "expensive_node", + async () => { + // Simulate an expensive operation + await new Promise((resolve) => setTimeout(resolve, 3000)); + return { result: 10 }; + }, + { cachePolicy: { ttl: 3 } } + ) + .addEdge(START, "expensive_node") + .compile({ cache: new InMemoryCache() }); + +await graph.invoke({ x: 5 }, { streamMode: "updates" }); // (1)! +// [{"expensive_node": {"result": 10}}] +await graph.invoke({ x: 5 }, { streamMode: "updates" }); // (2)! +// [{"expensive_node": {"result": 10}, "__metadata__": {"cached": true}}] +``` + +::: + +## Edges + +Edges define how the logic is routed and how the graph decides to stop. This is a big part of how your agents work and how different nodes communicate with each other. There are a few key types of edges: + +- Normal Edges: Go directly from one node to the next. +- Conditional Edges: Call a function to determine which node(s) to go to next. +- Entry Point: Which node to call first when user input arrives. +- Conditional Entry Point: Call a function to determine which node(s) to call first when user input arrives. + +A node can have MULTIPLE outgoing edges. If a node has multiple out-going edges, **all** of those destination nodes will be executed in parallel as a part of the next superstep. + +### Normal Edges + +:::python +If you **always** want to go from node A to node B, you can use the @[add_edge][add_edge] method directly. + +```python +graph.add_edge("node_a", "node_b") +``` + +::: + +:::js +If you **always** want to go from node A to node B, you can use the @[`addEdge`][add_edge] method directly. + +```typescript +graph.addEdge("nodeA", "nodeB"); +``` + +::: + +### Conditional Edges + +:::python +If you want to **optionally** route to 1 or more edges (or optionally terminate), you can use the @[add_conditional_edges][add_conditional_edges] method. This method accepts the name of a node and a "routing function" to call after that node is executed: + +```python +graph.add_conditional_edges("node_a", routing_function) +``` + +Similar to nodes, the `routing_function` accepts the current `state` of the graph and returns a value. + +By default, the return value `routing_function` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep. + +You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node. + +```python +graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) +``` + +::: + +:::js +If you want to **optionally** route to 1 or more edges (or optionally terminate), you can use the @[`addConditionalEdges`][add_conditional_edges] method. This method accepts the name of a node and a "routing function" to call after that node is executed: + +```typescript +graph.addConditionalEdges("nodeA", routingFunction); +``` + +Similar to nodes, the `routingFunction` accepts the current `state` of the graph and returns a value. + +By default, the return value `routingFunction` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep. + +You can optionally provide an object that maps the `routingFunction`'s output to the name of the next node. + +```typescript +graph.addConditionalEdges("nodeA", routingFunction, { + true: "nodeB", + false: "nodeC", +}); +``` + +::: + + + + Use [`Command`](#command) instead of conditional edges if you want to combine state updates and routing in a single function. + + + +### Entry Point + +:::python +The entry point is the first node(s) that are run when the graph starts. You can use the @[`add_edge`][add_edge] method from the virtual @[`START`][START] node to the first node to execute to specify where to enter the graph. + +```python +from langgraph.graph import START + +graph.add_edge(START, "node_a") +``` + +::: + +:::js +The entry point is the first node(s) that are run when the graph starts. You can use the @[`addEdge`][add_edge] method from the virtual @[`START`][START] node to the first node to execute to specify where to enter the graph. + +```typescript +import { START } from "@langchain/langgraph"; + +graph.addEdge(START, "nodeA"); +``` + +::: + +### Conditional Entry Point + +:::python +A conditional entry point lets you start at different nodes depending on custom logic. You can use @[`add_conditional_edges`][add_conditional_edges] from the virtual @[`START`][START] node to accomplish this. + +```python +from langgraph.graph import START + +graph.add_conditional_edges(START, routing_function) +``` + +You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node. + +```python +graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"}) +``` + +::: + +:::js +A conditional entry point lets you start at different nodes depending on custom logic. You can use @[`addConditionalEdges`][add_conditional_edges] from the virtual @[`START`][START] node to accomplish this. + +```typescript +import { START } from "@langchain/langgraph"; + +graph.addConditionalEdges(START, routingFunction); +``` + +You can optionally provide an object that maps the `routingFunction`'s output to the name of the next node. + +```typescript +graph.addConditionalEdges(START, routingFunction, { + true: "nodeB", + false: "nodeC", +}); +``` + +::: + +## `Send` + +:::python +By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with [map-reduce](/oss/graph-api#map-reduce-and-the-send-api) design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object). + +To support this design pattern, LangGraph supports returning @[`Send`][Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node. + +```python +def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state['subjects']] + +graph.add_conditional_edges("node_a", continue_to_jokes) +``` + +::: + +:::js +By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with map-reduce design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object). + +To support this design pattern, LangGraph supports returning @[`Send`][Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node. + +```typescript +import { Send } from "@langchain/langgraph"; + +graph.addConditionalEdges("nodeA", (state) => { + return state.subjects.map((subject) => new Send("generateJoke", { subject })); +}); +``` + +::: + +## `Command` + +:::python +It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a @[`Command`][Command] object from node functions: + +```python +def my_node(state: State) -> Command[Literal["my_other_node"]]: + return Command( + # state update + update={"foo": "bar"}, + # control flow + goto="my_other_node" + ) +``` + +With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)): + +```python +def my_node(state: State) -> Command[Literal["my_other_node"]]: + if state["foo"] == "bar": + return Command(update={"foo": "baz"}, goto="my_other_node") +``` + +::: + +:::js +It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a `Command` object from node functions: + +```typescript +import { Command } from "@langchain/langgraph"; + +graph.addNode("myNode", (state) => { + return new Command({ + update: { foo: "bar" }, + goto: "myOtherNode", + }); +}); +``` + +With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)): + +```typescript +import { Command } from "@langchain/langgraph"; + +graph.addNode("myNode", (state) => { + if (state.foo === "bar") { + return new Command({ + update: { foo: "baz" }, + goto: "myOtherNode", + }); + } +}); +``` + +When using `Command` in your node functions, you must add the `ends` parameter when adding the node to specify which nodes it can route to: + +```typescript +builder.addNode("myNode", myNode, { + ends: ["myOtherNode", END], +}); +``` + +::: + + + + When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["my_other_node"]]`. This is necessary for the graph rendering and tells LangGraph that `my_node` can navigate to `my_other_node`. + + + +Check out this [how-to guide](/oss/graph-api.md#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`. + +### When should I use Command instead of conditional edges? + +- Use `Command` when you need to **both** update the graph state **and** route to a different node. For example, when implementing [multi-agent handoffs](/oss/multi-agent#handoffs) where it's important to route to a different agent and pass some information to that agent. +- Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state. + +### Navigating to a node in a parent graph + +:::python +If you are using [subgraphs](/oss/subgraphs), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`: + +```python +def my_node(state: State) -> Command[Literal["other_subgraph"]]: + return Command( + update={"foo": "bar"}, + goto="other_subgraph", # where `other_subgraph` is a node in the parent graph + graph=Command.PARENT + ) +``` + + + + Setting `graph` to `Command.PARENT` will navigate to the closest parent graph. + + + When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. See this [example](/oss/graph-api.md#navigate-to-a-node-in-a-parent-graph). + + + +::: + +:::js +If you are using [subgraphs](/oss/subgraphs.md), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph: Command.PARENT` in `Command`: + +```typescript +import { Command } from "@langchain/langgraph"; + +graph.addNode("myNode", (state) => { + return new Command({ + update: { foo: "bar" }, + goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph + graph: Command.PARENT, + }); +}); +``` + + + + Setting `graph` to `Command.PARENT` will navigate to the closest parent graph. + + When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. + + + +::: + +:::js +If you are using [subgraphs](/oss/subgraphs.md), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph: Command.PARENT` in `Command`: + +```typescript +import { Command } from "@langchain/langgraph"; + +graph.addNode("myNode", (state) => { + return new Command({ + update: { foo: "bar" }, + goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph + graph: Command.PARENT, + }); +}); +``` + + + + Setting `graph` to `Command.PARENT` will navigate to the closest parent graph. + + When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. + + + +::: + +This is particularly useful when implementing [multi-agent handoffs](/oss/multi-agent#handoffs). + +Check out [this guide](/oss/graph-api.md#navigate-to-a-node-in-a-parent-graph) for detail. + +### Using inside tools + +A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. + +Refer to [this guide](/oss/graph-api.md#use-inside-tools) for detail. + +### Human-in-the-loop + +:::python +`Command` is an important part of human-in-the-loop workflows: when using `interrupt()` to collect user input, `Command` is then used to supply the input and resume execution via `Command(resume="User input")`. Check out [this conceptual guide](/oss/human-in-the-loop) for more information. +::: + +:::js +`Command` is an important part of human-in-the-loop workflows: when using `interrupt()` to collect user input, `Command` is then used to supply the input and resume execution via `new Command({ resume: "User input" })`. Check out the [human-in-the-loop conceptual guide](/oss/human-in-the-loop) for more information. +::: + +## Graph Migrations + +LangGraph can easily handle migrations of graph definitions (nodes, edges, and state) even when using a checkpointer to track state. + +- For threads at the end of the graph (i.e. not interrupted) you can change the entire topology of the graph (i.e. all nodes and edges, remove, add, rename, etc) +- For threads currently interrupted, we support all topology changes other than renaming / removing nodes (as that thread could now be about to enter a node that no longer exists) -- if this is a blocker please reach out and we can prioritize a solution. +- For modifying state, we have full backwards and forwards compatibility for adding and removing keys +- State keys that are renamed lose their saved state in existing threads +- State keys whose types change in incompatible ways could currently cause issues in threads with state from before the change -- if this is a blocker please reach out and we can prioritize a solution. + +:::python + +## Runtime Context + +When creating a graph, you can specify a `context_schema` for runtime context passed to nodes. This is useful for passing +information to nodes that is not part of the graph state. For example, you might want to pass dependencies such as model name or a database connection. + +```python +@dataclass +class ContextSchema: + llm_provider: str = "openai" + +graph = StateGraph(State, context_schema=ContextSchema) +``` + +::: + +:::js + +When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it. + +You can optionally specify a config schema when creating a graph. + +```typescript +import { z } from "zod"; + +const ConfigSchema = z.object({ + llm: z.string(), +}); + +const graph = new StateGraph(State, ConfigSchema); +``` + +::: + +:::python +You can then pass this context into the graph using the `context` parameter of the `invoke` method. + +```python +graph.invoke(inputs, context={"llm_provider": "anthropic"}) +``` + +::: + +:::js +You can then pass this configuration into the graph using the `configurable` config field. + +```typescript +const config = { configurable: { llm: "anthropic" } }; + +await graph.invoke(inputs, config); +``` + +::: + +You can then access and use this context inside a node or conditional edge: + +```python +from langgraph.runtime import Runtime + +def node_a(state: State, runtime: Runtime[ContextSchema]): + llm = get_llm(runtime.context.llm_provider) + ... +``` + +See [this guide](/oss/use-graph-api#add-runtime-configuration) for a full breakdown on configuration. +::: + +:::js + +```typescript +graph.addNode("myNode", (state, config) => { + const llmType = config?.configurable?.llm || "openai"; + const llm = getLlm(llmType); + return { results: `Hello, ${state.input}!` }; +}); +``` + +::: + +### Recursion Limit + +:::python +The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below: + +```python +graph.invoke(inputs, config={"recursion_limit": 5}, context={"llm": "anthropic"}) +``` + +Read [this how-to](/oss/graph-api#impose-a-recursion-limit) to learn more about how the recursion limit works. +::: + +:::js +The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config object. Importantly, `recursionLimit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below: + +```typescript +await graph.invoke(inputs, { + recursionLimit: 5, + configurable: { llm: "anthropic" }, +}); +``` + +::: + +## Visualization + +It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](/oss/graph-api.md#visualize-your-graph) for more info. diff --git a/src/oss/human-in-the-loop.mdx b/src/oss/human-in-the-loop.mdx new file mode 100644 index 00000000..972a205d --- /dev/null +++ b/src/oss/human-in-the-loop.mdx @@ -0,0 +1,33 @@ +--- +title: Human-in-the-loop overview +sidebarTitle: Overview +--- + +To review, edit, and approve tool calls in an agent or workflow, [use LangGraph's human-in-the-loop features](/oss/add-human-in-the-loop) to enable human intervention at any point in a workflow. This is especially useful in large language model (LLM)-driven applications where model output may require validation, correction, or additional context. + + +![image](/oss/images/tool-call-review.png) + + + For information on how to use human-in-the-loop, see [Enable human intervention](/oss/add-human-in-the-loop) and [Human-in-the-loop using Server API](/langgraph-platform/add-human-in-the-loop). + + +## Key capabilities + +* **Persistent execution state**: Interrupts use LangGraph's [persistence](/oss/persistence) layer, which saves the graph state, to indefinitely pause graph execution until you resume. This is possible because LangGraph checkpoints the graph state after each step, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. This supports asynchronous human review or input without time constraints. + There are two ways to pause a graph: + * [Dynamic interrupts](/oss/add-human-in-the-loop#pause-using-interrupt): Use `interrupt` to pause a graph from inside a specific node, based on the current state of the graph. + * [Static interrupts](/oss/add-human-in-the-loop#debug-with-interrupts): Use `interrupt_before` and `interrupt_after` to pause the graph at pre-defined points, either before or after a node executes. + + ![example graph consisting of 3 sequential steps with a breakpoint before step_3](/oss/images/breakpoints.png) + +* **Flexible integration points**: Human-in-the-loop logic can be introduced at any point in the workflow. This allows targeted human involvement, such as approving API calls, correcting outputs, or guiding conversations. + +## Patterns + +There are four typical design patterns that you can implement using `interrupt` and `Command`: + +* [Approve or reject](/oss/add-human-in-the-loop#approve-or-reject): Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action. This pattern often involves routing the graph based on the human's input. +* [Edit graph state](/oss/add-human-in-the-loop#review-and-edit-state): Pause the graph to review and edit the graph state. This is useful for correcting mistakes or updating the state with additional information. This pattern often involves updating the state with the human's input. +* [Review tool calls](/oss/add-human-in-the-loop#review-tool-calls): Pause the graph to review and edit tool calls requested by the LLM before tool execution. +* [Validate human input](/oss/add-human-in-the-loop#validate-human-input): Pause the graph to validate human input before proceeding with the next step. diff --git a/src/oss/images/0000.svg b/src/oss/images/0000.svg new file mode 100644 index 00000000..864b0d46 --- /dev/null +++ b/src/oss/images/0000.svg @@ -0,0 +1 @@ +

__start__

agent

__end__

\ No newline at end of file diff --git a/src/oss/images/0001.svg b/src/oss/images/0001.svg new file mode 100644 index 00000000..2643171d --- /dev/null +++ b/src/oss/images/0001.svg @@ -0,0 +1 @@ +

__start__

agent

tools

__end__

\ No newline at end of file diff --git a/src/oss/images/0010.svg b/src/oss/images/0010.svg new file mode 100644 index 00000000..c63d3a28 --- /dev/null +++ b/src/oss/images/0010.svg @@ -0,0 +1 @@ +

__start__

agent

pre_model_hook

__end__

\ No newline at end of file diff --git a/src/oss/images/0011.svg b/src/oss/images/0011.svg new file mode 100644 index 00000000..00618e1b --- /dev/null +++ b/src/oss/images/0011.svg @@ -0,0 +1 @@ +

__start__

agent

tools

pre_model_hook

__end__

\ No newline at end of file diff --git a/src/oss/images/0100.svg b/src/oss/images/0100.svg new file mode 100644 index 00000000..2d99f88a --- /dev/null +++ b/src/oss/images/0100.svg @@ -0,0 +1 @@ +

__start__

agent

post_model_hook

__end__

\ No newline at end of file diff --git a/src/oss/images/0101.svg b/src/oss/images/0101.svg new file mode 100644 index 00000000..605b5eae --- /dev/null +++ b/src/oss/images/0101.svg @@ -0,0 +1 @@ +

__start__

agent

tools

post_model_hook

__end__

\ No newline at end of file diff --git a/src/oss/images/0110.svg b/src/oss/images/0110.svg new file mode 100644 index 00000000..bf6e988e --- /dev/null +++ b/src/oss/images/0110.svg @@ -0,0 +1 @@ +

__start__

agent

pre_model_hook

post_model_hook

__end__

\ No newline at end of file diff --git a/src/oss/images/0111.svg b/src/oss/images/0111.svg new file mode 100644 index 00000000..eb4af67e --- /dev/null +++ b/src/oss/images/0111.svg @@ -0,0 +1 @@ +

__start__

agent

tools

pre_model_hook

post_model_hook

__end__

\ No newline at end of file diff --git a/src/oss/images/1000.svg b/src/oss/images/1000.svg new file mode 100644 index 00000000..2d72d4bc --- /dev/null +++ b/src/oss/images/1000.svg @@ -0,0 +1 @@ +

__start__

agent

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1001.svg b/src/oss/images/1001.svg new file mode 100644 index 00000000..f416f346 --- /dev/null +++ b/src/oss/images/1001.svg @@ -0,0 +1 @@ +

__start__

agent

tools

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1010.svg b/src/oss/images/1010.svg new file mode 100644 index 00000000..9308976c --- /dev/null +++ b/src/oss/images/1010.svg @@ -0,0 +1 @@ +

__start__

agent

pre_model_hook

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1011.svg b/src/oss/images/1011.svg new file mode 100644 index 00000000..21fe20c1 --- /dev/null +++ b/src/oss/images/1011.svg @@ -0,0 +1 @@ +

__start__

agent

tools

pre_model_hook

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1100.svg b/src/oss/images/1100.svg new file mode 100644 index 00000000..f36fd09a --- /dev/null +++ b/src/oss/images/1100.svg @@ -0,0 +1 @@ +

__start__

agent

post_model_hook

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1101.svg b/src/oss/images/1101.svg new file mode 100644 index 00000000..7af4855e --- /dev/null +++ b/src/oss/images/1101.svg @@ -0,0 +1 @@ +

__start__

agent

tools

post_model_hook

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1110.svg b/src/oss/images/1110.svg new file mode 100644 index 00000000..fea6c4e7 --- /dev/null +++ b/src/oss/images/1110.svg @@ -0,0 +1 @@ +

__start__

agent

pre_model_hook

post_model_hook

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/1111.svg b/src/oss/images/1111.svg new file mode 100644 index 00000000..ec9a02cd --- /dev/null +++ b/src/oss/images/1111.svg @@ -0,0 +1 @@ +

__start__

agent

tools

pre_model_hook

post_model_hook

generate_structured_response

__end__

\ No newline at end of file diff --git a/src/oss/images/410e0089-2ab8-46bb-a61a-827187fd46b3.png b/src/oss/images/410e0089-2ab8-46bb-a61a-827187fd46b3.png new file mode 100644 index 00000000..9bd66e56 Binary files /dev/null and b/src/oss/images/410e0089-2ab8-46bb-a61a-827187fd46b3.png differ diff --git a/src/oss/images/agent.png b/src/oss/images/agent.png new file mode 100644 index 00000000..35027af1 Binary files /dev/null and b/src/oss/images/agent.png differ diff --git a/src/oss/images/agent_types.png b/src/oss/images/agent_types.png new file mode 100644 index 00000000..3cefe033 Binary files /dev/null and b/src/oss/images/agent_types.png differ diff --git a/src/oss/images/agent_workflow.png b/src/oss/images/agent_workflow.png new file mode 100644 index 00000000..6f3987ed Binary files /dev/null and b/src/oss/images/agent_workflow.png differ diff --git a/src/oss/images/agentic-rag-output.png b/src/oss/images/agentic-rag-output.png new file mode 100644 index 00000000..2efa1353 Binary files /dev/null and b/src/oss/images/agentic-rag-output.png differ diff --git a/src/oss/images/approval.png b/src/oss/images/approval.png new file mode 100644 index 00000000..730336a7 Binary files /dev/null and b/src/oss/images/approval.png differ diff --git a/src/oss/images/approve-or-reject.png b/src/oss/images/approve-or-reject.png new file mode 100644 index 00000000..0ba06b1a Binary files /dev/null and b/src/oss/images/approve-or-reject.png differ diff --git a/src/oss/images/architectures.png b/src/oss/images/architectures.png new file mode 100644 index 00000000..9a45e838 Binary files /dev/null and b/src/oss/images/architectures.png differ diff --git a/src/oss/images/assistants.png b/src/oss/images/assistants.png new file mode 100644 index 00000000..0da78a03 Binary files /dev/null and b/src/oss/images/assistants.png differ diff --git a/src/oss/images/augmented_llm.png b/src/oss/images/augmented_llm.png new file mode 100644 index 00000000..f897a35c Binary files /dev/null and b/src/oss/images/augmented_llm.png differ diff --git a/src/oss/images/authentication.png b/src/oss/images/authentication.png new file mode 100644 index 00000000..e2edcbcc Binary files /dev/null and b/src/oss/images/authentication.png differ diff --git a/src/oss/images/authorization.png b/src/oss/images/authorization.png new file mode 100644 index 00000000..3783416c Binary files /dev/null and b/src/oss/images/authorization.png differ diff --git a/src/oss/images/autogen-output.png b/src/oss/images/autogen-output.png new file mode 100644 index 00000000..02106782 Binary files /dev/null and b/src/oss/images/autogen-output.png differ diff --git a/src/oss/images/basic-chatbot.png b/src/oss/images/basic-chatbot.png new file mode 100644 index 00000000..52539183 Binary files /dev/null and b/src/oss/images/basic-chatbot.png differ diff --git a/src/oss/images/benchmark.png b/src/oss/images/benchmark.png new file mode 100644 index 00000000..6b96e656 Binary files /dev/null and b/src/oss/images/benchmark.png differ diff --git a/src/oss/images/brave-shields.png b/src/oss/images/brave-shields.png new file mode 100644 index 00000000..133cd287 Binary files /dev/null and b/src/oss/images/brave-shields.png differ diff --git a/src/oss/images/breakpoints.png b/src/oss/images/breakpoints.png new file mode 100644 index 00000000..0fb5aabe Binary files /dev/null and b/src/oss/images/breakpoints.png differ diff --git a/src/oss/images/byoc_architecture.png b/src/oss/images/byoc_architecture.png new file mode 100644 index 00000000..97bb2db1 Binary files /dev/null and b/src/oss/images/byoc_architecture.png differ diff --git a/src/oss/images/challenge.png b/src/oss/images/challenge.png new file mode 100644 index 00000000..f131c2d0 Binary files /dev/null and b/src/oss/images/challenge.png differ diff --git a/src/oss/images/chatbot-with-tools.png b/src/oss/images/chatbot-with-tools.png new file mode 100644 index 00000000..6aff3501 Binary files /dev/null and b/src/oss/images/chatbot-with-tools.png differ diff --git a/src/oss/images/checkpoints.jpg b/src/oss/images/checkpoints.jpg new file mode 100644 index 00000000..61ccf554 Binary files /dev/null and b/src/oss/images/checkpoints.jpg differ diff --git a/src/oss/images/checkpoints_full_story.jpg b/src/oss/images/checkpoints_full_story.jpg new file mode 100644 index 00000000..691d17c1 Binary files /dev/null and b/src/oss/images/checkpoints_full_story.jpg differ diff --git a/src/oss/images/click_create_assistant.png b/src/oss/images/click_create_assistant.png new file mode 100644 index 00000000..74da8f16 Binary files /dev/null and b/src/oss/images/click_create_assistant.png differ diff --git a/src/oss/images/create_assistant.png b/src/oss/images/create_assistant.png new file mode 100644 index 00000000..e80f1473 Binary files /dev/null and b/src/oss/images/create_assistant.png differ diff --git a/src/oss/images/create_assistant_view.png b/src/oss/images/create_assistant_view.png new file mode 100644 index 00000000..5cdbc098 Binary files /dev/null and b/src/oss/images/create_assistant_view.png differ diff --git a/src/oss/images/create_new_version.png b/src/oss/images/create_new_version.png new file mode 100644 index 00000000..75be4b5f Binary files /dev/null and b/src/oss/images/create_new_version.png differ diff --git a/src/oss/images/customer-support-bot-4.png b/src/oss/images/customer-support-bot-4.png new file mode 100644 index 00000000..8b9da217 Binary files /dev/null and b/src/oss/images/customer-support-bot-4.png differ diff --git a/src/oss/images/d38d1f2b-0f4c-4707-b531-a3c749de987f.png b/src/oss/images/d38d1f2b-0f4c-4707-b531-a3c749de987f.png new file mode 100644 index 00000000..ce95409d Binary files /dev/null and b/src/oss/images/d38d1f2b-0f4c-4707-b531-a3c749de987f.png differ diff --git a/src/oss/images/diagram-part-1.png b/src/oss/images/diagram-part-1.png new file mode 100644 index 00000000..b9b49a80 Binary files /dev/null and b/src/oss/images/diagram-part-1.png differ diff --git a/src/oss/images/diagram-part-2.png b/src/oss/images/diagram-part-2.png new file mode 100644 index 00000000..502b9739 Binary files /dev/null and b/src/oss/images/diagram-part-2.png differ diff --git a/src/oss/images/diagram.png b/src/oss/images/diagram.png new file mode 100644 index 00000000..57c04d1e Binary files /dev/null and b/src/oss/images/diagram.png differ diff --git a/src/oss/images/double_texting.png b/src/oss/images/double_texting.png new file mode 100644 index 00000000..a16a58ce Binary files /dev/null and b/src/oss/images/double_texting.png differ diff --git a/src/oss/images/edit-graph-state-simple.png b/src/oss/images/edit-graph-state-simple.png new file mode 100644 index 00000000..4c4d4fac Binary files /dev/null and b/src/oss/images/edit-graph-state-simple.png differ diff --git a/src/oss/images/edit_created_assistant.png b/src/oss/images/edit_created_assistant.png new file mode 100644 index 00000000..75b18cdc Binary files /dev/null and b/src/oss/images/edit_created_assistant.png differ diff --git a/src/oss/images/edit_graph_state.png b/src/oss/images/edit_graph_state.png new file mode 100644 index 00000000..c1911f17 Binary files /dev/null and b/src/oss/images/edit_graph_state.png differ diff --git a/src/oss/images/evaluator_optimizer.png b/src/oss/images/evaluator_optimizer.png new file mode 100644 index 00000000..0d09f93d Binary files /dev/null and b/src/oss/images/evaluator_optimizer.png differ diff --git a/src/oss/images/fast_parrot.png b/src/oss/images/fast_parrot.png new file mode 100644 index 00000000..336cd6e9 Binary files /dev/null and b/src/oss/images/fast_parrot.png differ diff --git a/src/oss/images/favicon.png b/src/oss/images/favicon.png new file mode 100644 index 00000000..71269b74 Binary files /dev/null and b/src/oss/images/favicon.png differ diff --git a/src/oss/images/filter.png b/src/oss/images/filter.png new file mode 100644 index 00000000..ed1bef4f Binary files /dev/null and b/src/oss/images/filter.png differ diff --git a/src/oss/images/forking.png b/src/oss/images/forking.png new file mode 100644 index 00000000..70428cd5 Binary files /dev/null and b/src/oss/images/forking.png differ diff --git a/src/oss/images/generative_ui_sample.jpg b/src/oss/images/generative_ui_sample.jpg new file mode 100644 index 00000000..4d32a5e9 Binary files /dev/null and b/src/oss/images/generative_ui_sample.jpg differ diff --git a/src/oss/images/get_state.jpg b/src/oss/images/get_state.jpg new file mode 100644 index 00000000..7355a142 Binary files /dev/null and b/src/oss/images/get_state.jpg differ diff --git a/src/oss/images/graph_api_image_1.png b/src/oss/images/graph_api_image_1.png new file mode 100644 index 00000000..c1ac60da Binary files /dev/null and b/src/oss/images/graph_api_image_1.png differ diff --git a/src/oss/images/graph_api_image_10.png b/src/oss/images/graph_api_image_10.png new file mode 100644 index 00000000..18ea05fa Binary files /dev/null and b/src/oss/images/graph_api_image_10.png differ diff --git a/src/oss/images/graph_api_image_11.png b/src/oss/images/graph_api_image_11.png new file mode 100644 index 00000000..faec2d5f Binary files /dev/null and b/src/oss/images/graph_api_image_11.png differ diff --git a/src/oss/images/graph_api_image_2.png b/src/oss/images/graph_api_image_2.png new file mode 100644 index 00000000..4f890a3d Binary files /dev/null and b/src/oss/images/graph_api_image_2.png differ diff --git a/src/oss/images/graph_api_image_3.png b/src/oss/images/graph_api_image_3.png new file mode 100644 index 00000000..802dd648 Binary files /dev/null and b/src/oss/images/graph_api_image_3.png differ diff --git a/src/oss/images/graph_api_image_4.png b/src/oss/images/graph_api_image_4.png new file mode 100644 index 00000000..09bb71be Binary files /dev/null and b/src/oss/images/graph_api_image_4.png differ diff --git a/src/oss/images/graph_api_image_5.png b/src/oss/images/graph_api_image_5.png new file mode 100644 index 00000000..c1ebb138 Binary files /dev/null and b/src/oss/images/graph_api_image_5.png differ diff --git a/src/oss/images/graph_api_image_6.png b/src/oss/images/graph_api_image_6.png new file mode 100644 index 00000000..3d60cbfb Binary files /dev/null and b/src/oss/images/graph_api_image_6.png differ diff --git a/src/oss/images/graph_api_image_7.png b/src/oss/images/graph_api_image_7.png new file mode 100644 index 00000000..d607bdc4 Binary files /dev/null and b/src/oss/images/graph_api_image_7.png differ diff --git a/src/oss/images/graph_api_image_8.png b/src/oss/images/graph_api_image_8.png new file mode 100644 index 00000000..67049fb5 Binary files /dev/null and b/src/oss/images/graph_api_image_8.png differ diff --git a/src/oss/images/graph_api_image_9.png b/src/oss/images/graph_api_image_9.png new file mode 100644 index 00000000..2eb23cc1 Binary files /dev/null and b/src/oss/images/graph_api_image_9.png differ diff --git a/src/oss/images/graph_diagram.png b/src/oss/images/graph_diagram.png new file mode 100644 index 00000000..2105cde6 Binary files /dev/null and b/src/oss/images/graph_diagram.png differ diff --git a/src/oss/images/hot_path_vs_background.png b/src/oss/images/hot_path_vs_background.png new file mode 100644 index 00000000..4f4d66b3 Binary files /dev/null and b/src/oss/images/hot_path_vs_background.png differ diff --git a/src/oss/images/human_in_loop_parallel.png b/src/oss/images/human_in_loop_parallel.png new file mode 100644 index 00000000..0fdb9da9 Binary files /dev/null and b/src/oss/images/human_in_loop_parallel.png differ diff --git a/src/oss/images/langgraph.png b/src/oss/images/langgraph.png new file mode 100644 index 00000000..a821f8a1 Binary files /dev/null and b/src/oss/images/langgraph.png differ diff --git a/src/oss/images/langgraph_cloud_architecture.png b/src/oss/images/langgraph_cloud_architecture.png new file mode 100644 index 00000000..dc2d1b15 Binary files /dev/null and b/src/oss/images/langgraph_cloud_architecture.png differ diff --git a/src/oss/images/langgraph_platform_deployment_architecture.png b/src/oss/images/langgraph_platform_deployment_architecture.png new file mode 100644 index 00000000..313c0828 Binary files /dev/null and b/src/oss/images/langgraph_platform_deployment_architecture.png differ diff --git a/src/oss/images/langgraph_studio.png b/src/oss/images/langgraph_studio.png new file mode 100644 index 00000000..6f1d208d Binary files /dev/null and b/src/oss/images/langgraph_studio.png differ diff --git a/src/oss/images/lg_platform.png b/src/oss/images/lg_platform.png new file mode 100644 index 00000000..de54cedc Binary files /dev/null and b/src/oss/images/lg_platform.png differ diff --git a/src/oss/images/lg_studio.png b/src/oss/images/lg_studio.png new file mode 100644 index 00000000..91fcc447 Binary files /dev/null and b/src/oss/images/lg_studio.png differ diff --git a/src/oss/images/mcp.png b/src/oss/images/mcp.png new file mode 100644 index 00000000..988f821f Binary files /dev/null and b/src/oss/images/mcp.png differ diff --git a/src/oss/images/memory.png b/src/oss/images/memory.png new file mode 100644 index 00000000..3dbd8fee Binary files /dev/null and b/src/oss/images/memory.png differ diff --git a/src/oss/images/multi-output.png b/src/oss/images/multi-output.png new file mode 100644 index 00000000..99afd27f Binary files /dev/null and b/src/oss/images/multi-output.png differ diff --git a/src/oss/images/multi-turn-conversation.png b/src/oss/images/multi-turn-conversation.png new file mode 100644 index 00000000..f6541803 Binary files /dev/null and b/src/oss/images/multi-turn-conversation.png differ diff --git a/src/oss/images/no_auth.png b/src/oss/images/no_auth.png new file mode 100644 index 00000000..8a6f8bc5 Binary files /dev/null and b/src/oss/images/no_auth.png differ diff --git a/src/oss/images/output.png b/src/oss/images/output.png new file mode 100644 index 00000000..006d100a Binary files /dev/null and b/src/oss/images/output.png differ diff --git a/src/oss/images/parallelization.png b/src/oss/images/parallelization.png new file mode 100644 index 00000000..b3bf0641 Binary files /dev/null and b/src/oss/images/parallelization.png differ diff --git a/src/oss/images/part-1-diagram.png b/src/oss/images/part-1-diagram.png new file mode 100644 index 00000000..9f8bddf3 Binary files /dev/null and b/src/oss/images/part-1-diagram.png differ diff --git a/src/oss/images/part-2-diagram.png b/src/oss/images/part-2-diagram.png new file mode 100644 index 00000000..62eeeed6 Binary files /dev/null and b/src/oss/images/part-2-diagram.png differ diff --git a/src/oss/images/part-3-diagram.png b/src/oss/images/part-3-diagram.png new file mode 100644 index 00000000..7a8a1e94 Binary files /dev/null and b/src/oss/images/part-3-diagram.png differ diff --git a/src/oss/images/part-4-diagram.png b/src/oss/images/part-4-diagram.png new file mode 100644 index 00000000..874f5bd9 Binary files /dev/null and b/src/oss/images/part-4-diagram.png differ diff --git a/src/oss/images/prompt_chain.png b/src/oss/images/prompt_chain.png new file mode 100644 index 00000000..e6b33efd Binary files /dev/null and b/src/oss/images/prompt_chain.png differ diff --git a/src/oss/images/quick_start_studio.png b/src/oss/images/quick_start_studio.png new file mode 100644 index 00000000..54160b40 Binary files /dev/null and b/src/oss/images/quick_start_studio.png differ diff --git a/src/oss/images/re_play.png b/src/oss/images/re_play.png new file mode 100644 index 00000000..d32a0812 Binary files /dev/null and b/src/oss/images/re_play.png differ diff --git a/src/oss/images/react_diagrams.png b/src/oss/images/react_diagrams.png new file mode 100644 index 00000000..9d755884 Binary files /dev/null and b/src/oss/images/react_diagrams.png differ diff --git a/src/oss/images/replay.png b/src/oss/images/replay.png new file mode 100644 index 00000000..d32a0812 Binary files /dev/null and b/src/oss/images/replay.png differ diff --git a/src/oss/images/request.png b/src/oss/images/request.png new file mode 100644 index 00000000..8a5f5a16 Binary files /dev/null and b/src/oss/images/request.png differ diff --git a/src/oss/images/response.png b/src/oss/images/response.png new file mode 100644 index 00000000..5f2c0844 Binary files /dev/null and b/src/oss/images/response.png differ diff --git a/src/oss/images/routing.png b/src/oss/images/routing.png new file mode 100644 index 00000000..800d0564 Binary files /dev/null and b/src/oss/images/routing.png differ diff --git a/src/oss/images/run_in_studio.png b/src/oss/images/run_in_studio.png new file mode 100644 index 00000000..a74d362d Binary files /dev/null and b/src/oss/images/run_in_studio.png differ diff --git a/src/oss/images/screenshot_2024_02_14_3_43_58_pm.png b/src/oss/images/screenshot_2024_02_14_3_43_58_pm.png new file mode 100644 index 00000000..65402f1e Binary files /dev/null and b/src/oss/images/screenshot_2024_02_14_3_43_58_pm.png differ diff --git a/src/oss/images/see_new_version.png b/src/oss/images/see_new_version.png new file mode 100644 index 00000000..3a759691 Binary files /dev/null and b/src/oss/images/see_new_version.png differ diff --git a/src/oss/images/see_version_history.png b/src/oss/images/see_version_history.png new file mode 100644 index 00000000..b956de5d Binary files /dev/null and b/src/oss/images/see_version_history.png differ diff --git a/src/oss/images/select_different_version.png b/src/oss/images/select_different_version.png new file mode 100644 index 00000000..284d3345 Binary files /dev/null and b/src/oss/images/select_different_version.png differ diff --git a/src/oss/images/self_hosted_control_plane_architecture.png b/src/oss/images/self_hosted_control_plane_architecture.png new file mode 100644 index 00000000..e13e2cb0 Binary files /dev/null and b/src/oss/images/self_hosted_control_plane_architecture.png differ diff --git a/src/oss/images/self_hosted_data_plane_architecture.png b/src/oss/images/self_hosted_data_plane_architecture.png new file mode 100644 index 00000000..07358f73 Binary files /dev/null and b/src/oss/images/self_hosted_data_plane_architecture.png differ diff --git a/src/oss/images/shared_state.png b/src/oss/images/shared_state.png new file mode 100644 index 00000000..b48b0baa Binary files /dev/null and b/src/oss/images/shared_state.png differ diff --git a/src/oss/images/short-vs-long.png b/src/oss/images/short-vs-long.png new file mode 100644 index 00000000..3dbd8fee Binary files /dev/null and b/src/oss/images/short-vs-long.png differ diff --git a/src/oss/images/static-interrupt.png b/src/oss/images/static-interrupt.png new file mode 100644 index 00000000..d095dd2d Binary files /dev/null and b/src/oss/images/static-interrupt.png differ diff --git a/src/oss/images/studio_datasets.jpg b/src/oss/images/studio_datasets.jpg new file mode 100644 index 00000000..5205238c Binary files /dev/null and b/src/oss/images/studio_datasets.jpg differ diff --git a/src/oss/images/studio_forks_poster.png b/src/oss/images/studio_forks_poster.png new file mode 100644 index 00000000..14825c28 Binary files /dev/null and b/src/oss/images/studio_forks_poster.png differ diff --git a/src/oss/images/studio_graph_with_configuration.png b/src/oss/images/studio_graph_with_configuration.png new file mode 100644 index 00000000..91166691 Binary files /dev/null and b/src/oss/images/studio_graph_with_configuration.png differ diff --git a/src/oss/images/studio_input_poster.png b/src/oss/images/studio_input_poster.png new file mode 100644 index 00000000..6c23c452 Binary files /dev/null and b/src/oss/images/studio_input_poster.png differ diff --git a/src/oss/images/studio_node_configuration.png b/src/oss/images/studio_node_configuration.png new file mode 100644 index 00000000..1511f48e Binary files /dev/null and b/src/oss/images/studio_node_configuration.png differ diff --git a/src/oss/images/studio_playground.png b/src/oss/images/studio_playground.png new file mode 100644 index 00000000..daa4117d Binary files /dev/null and b/src/oss/images/studio_playground.png differ diff --git a/src/oss/images/studio_screenshot.png b/src/oss/images/studio_screenshot.png new file mode 100644 index 00000000..02592a7a Binary files /dev/null and b/src/oss/images/studio_screenshot.png differ diff --git a/src/oss/images/studio_threads_poster.png b/src/oss/images/studio_threads_poster.png new file mode 100644 index 00000000..5f222278 Binary files /dev/null and b/src/oss/images/studio_threads_poster.png differ diff --git a/src/oss/images/studio_usage_poster.png b/src/oss/images/studio_usage_poster.png new file mode 100644 index 00000000..83c5027b Binary files /dev/null and b/src/oss/images/studio_usage_poster.png differ diff --git a/src/oss/images/subgraph.png b/src/oss/images/subgraph.png new file mode 100644 index 00000000..29401933 Binary files /dev/null and b/src/oss/images/subgraph.png differ diff --git a/src/oss/images/summary.png b/src/oss/images/summary.png new file mode 100644 index 00000000..80da9af3 Binary files /dev/null and b/src/oss/images/summary.png differ diff --git a/src/oss/images/supervisor.png b/src/oss/images/supervisor.png new file mode 100644 index 00000000..a76312e2 Binary files /dev/null and b/src/oss/images/supervisor.png differ diff --git a/src/oss/images/swarm.png b/src/oss/images/swarm.png new file mode 100644 index 00000000..6143a794 Binary files /dev/null and b/src/oss/images/swarm.png differ diff --git a/src/oss/images/thread_diagram.png b/src/oss/images/thread_diagram.png new file mode 100644 index 00000000..ec938ac6 Binary files /dev/null and b/src/oss/images/thread_diagram.png differ diff --git a/src/oss/images/tnt_llm.png b/src/oss/images/tnt_llm.png new file mode 100644 index 00000000..400d083e Binary files /dev/null and b/src/oss/images/tnt_llm.png differ diff --git a/src/oss/images/tool-call-review.png b/src/oss/images/tool-call-review.png new file mode 100644 index 00000000..1299e79c Binary files /dev/null and b/src/oss/images/tool-call-review.png differ diff --git a/src/oss/images/tool_call.png b/src/oss/images/tool_call.png new file mode 100644 index 00000000..9301ee60 Binary files /dev/null and b/src/oss/images/tool_call.png differ diff --git a/src/oss/images/tot.png b/src/oss/images/tot.png new file mode 100644 index 00000000..519937d1 Binary files /dev/null and b/src/oss/images/tot.png differ diff --git a/src/oss/images/update-instructions.png b/src/oss/images/update-instructions.png new file mode 100644 index 00000000..6724e4ee Binary files /dev/null and b/src/oss/images/update-instructions.png differ diff --git a/src/oss/images/update-list.png b/src/oss/images/update-list.png new file mode 100644 index 00000000..7a28853f Binary files /dev/null and b/src/oss/images/update-list.png differ diff --git a/src/oss/images/update-profile.png b/src/oss/images/update-profile.png new file mode 100644 index 00000000..5248a4af Binary files /dev/null and b/src/oss/images/update-profile.png differ diff --git a/src/oss/images/usaco.png b/src/oss/images/usaco.png new file mode 100644 index 00000000..fdba6c2b Binary files /dev/null and b/src/oss/images/usaco.png differ diff --git a/src/oss/images/values_vs_updates.png b/src/oss/images/values_vs_updates.png new file mode 100644 index 00000000..dba7a4dc Binary files /dev/null and b/src/oss/images/values_vs_updates.png differ diff --git a/src/oss/images/wait_for_input.png b/src/oss/images/wait_for_input.png new file mode 100644 index 00000000..a6ebdaf0 Binary files /dev/null and b/src/oss/images/wait_for_input.png differ diff --git a/src/oss/images/web-voyager.excalidraw.jpg b/src/oss/images/web-voyager.excalidraw.jpg new file mode 100644 index 00000000..e6f826a6 Binary files /dev/null and b/src/oss/images/web-voyager.excalidraw.jpg differ diff --git a/src/oss/images/wordmark_dark.svg b/src/oss/images/wordmark_dark.svg new file mode 100644 index 00000000..99adc6f2 --- /dev/null +++ b/src/oss/images/wordmark_dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/oss/images/wordmark_light.svg b/src/oss/images/wordmark_light.svg new file mode 100644 index 00000000..d60eae6f --- /dev/null +++ b/src/oss/images/wordmark_light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/oss/images/worker.png b/src/oss/images/worker.png new file mode 100644 index 00000000..201db4f4 Binary files /dev/null and b/src/oss/images/worker.png differ diff --git a/src/oss/langgraph-academy.mdx b/src/oss/langgraph-academy.mdx new file mode 100644 index 00000000..370a8223 --- /dev/null +++ b/src/oss/langgraph-academy.mdx @@ -0,0 +1,4 @@ +--- +title: LangGraph Academy +url: https://academy.langchain.com/ +--- diff --git a/src/oss/local-server.mdx b/src/oss/local-server.mdx new file mode 100644 index 00000000..ada1d558 --- /dev/null +++ b/src/oss/local-server.mdx @@ -0,0 +1,278 @@ +--- +title: Run a local server +--- + +This guide shows you how to run a LangGraph application locally. + +## Prerequisites + +Before you begin, ensure you have the following: + +* An API key for [LangSmith](https://smith.langchain.com/settings) - free to sign up + +## 1. Install the LangGraph CLI + +:::python +```shell +# Python >= 3.11 is required. + +pip install --upgrade "langgraph-cli[inmem]" +``` +::: + +:::js +```shell +npx @langchain/langgraph-cli +``` +::: + +## 2. Create a LangGraph app 🌱 + +:::python +Create a new app from the [`new-langgraph-project-python` template](https://github.com/langchain-ai/new-langgraph-project). This template demonstrates a single-node application you can extend with your own logic. + +```shell +langgraph new path/to/your/app --template new-langgraph-project-python +``` + + + **Additional templates** + If you use `langgraph new` without specifying a template, you will be presented with an interactive menu that will allow you to choose from a list of available templates. + +::: + +:::js +Create a new app from the [`new-langgraph-project-js` template](https://github.com/langchain-ai/new-langgraphjs-project). This template demonstrates a single-node application you can extend with your own logic. + +```shell +npm create langgraph +``` +::: + +## 3. Install dependencies + +In the root of your new LangGraph app, install the dependencies in `edit` mode so your local changes are used by the server: + +:::python +```shell +cd path/to/your/app +pip install -e . +``` +::: + +:::js +```shell +cd path/to/your/app +npm install +``` +::: + +## 4. Create a `.env` file + +You will find a `.env.example` in the root of your new LangGraph app. Create a `.env` file in the root of your new LangGraph app and copy the contents of the `.env.example` file into it, filling in the necessary API keys: + +```bash +LANGSMITH_API_KEY=lsv2... +``` + +## 5. Launch LangGraph Server 🚀 + +Start the LangGraph API server locally: + +:::python +```shell +langgraph dev +``` +::: + +:::js +```shell +npx @langchain/langgraph-cli dev +``` +::: + +Sample output: + +``` +> Ready! +> +> - API: [http://localhost:2024](http://localhost:2024/) +> +> - Docs: http://localhost:2024/docs +> +> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024 +``` + +The `langgraph dev` command starts LangGraph Server in an in-memory mode. This mode is suitable for development and testing purposes. For production use, deploy LangGraph Server with access to a persistent storage backend. For more information, see [Deployment options](/langgraph-platform/deployment-options). + +## 6. Test your application in LangGraph Studio + +[LangGraph Studio](/langgraph-platform/langgraph-studio) is a specialized UI that you can connect to LangGraph API server to visualize, interact with, and debug your application locally. Test your graph in LangGraph Studio by visiting the URL provided in the output of the `langgraph dev` command: + +``` +> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024 +``` + +For a LangGraph Server running on a custom host/port, update the baseURL parameter. + + + Use the `--tunnel` flag with your command to create a secure tunnel, as Safari has limitations when connecting to localhost servers: + + ```shell + langgraph dev --tunnel + ``` + + +## 7. Test the API + +:::python + + + 1. Install the LangGraph Python SDK: + ```shell + pip install langgraph-sdk + ``` + 2. Send a message to the assistant (threadless run): + ```python + from langgraph_sdk import get_client + import asyncio + + client = get_client(url="http://localhost:2024") + + async def main(): + async for chunk in client.runs.stream( + None, # Threadless run + "agent", # Name of assistant. Defined in langgraph.json. + input={ + "messages": [{ + "role": "human", + "content": "What is LangGraph?", + }], + }, + ): + print(f"Receiving new event of type: {chunk.event}...") + print(chunk.data) + print("\n\n") + + asyncio.run(main()) + ``` + + + 1. Install the LangGraph Python SDK: + ```shell + pip install langgraph-sdk + ``` + 2. Send a message to the assistant (threadless run): + ```python + from langgraph_sdk import get_sync_client + + client = get_sync_client(url="http://localhost:2024") + + for chunk in client.runs.stream( + None, # Threadless run + "agent", # Name of assistant. Defined in langgraph.json. + input={ + "messages": [{ + "role": "human", + "content": "What is LangGraph?", + }], + }, + stream_mode="messages-tuple", + ): + print(f"Receiving new event of type: {chunk.event}...") + print(chunk.data) + print("\n\n") + ``` + + + ```bash + curl -s --request POST \ + --url "http://localhost:2024/runs/stream" \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": { + \"messages\": [ + { + \"role\": \"human\", + \"content\": \"What is LangGraph?\" + } + ] + }, + \"stream_mode\": \"messages-tuple\" + }" + ``` + + +::: + +:::js + + + 1. Install the LangGraph JS SDK: + ```shell + npm install @langchain/langgraph-sdk + ``` + 2. Send a message to the assistant (threadless run): + ```js + const { Client } = await import("@langchain/langgraph-sdk"); + + // only set the apiUrl if you changed the default port when calling langgraph dev + const client = new Client({ apiUrl: "http://localhost:2024"}); + + const streamResponse = client.runs.stream( + null, // Threadless run + "agent", // Assistant ID + { + input: { + "messages": [ + { "role": "user", "content": "What is LangGraph?"} + ] + }, + streamMode: "messages-tuple", + } + ); + + for await (const chunk of streamResponse) { + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(JSON.stringify(chunk.data)); + console.log("\n\n"); + } + ``` + + + ```bash + curl -s --request POST \ + --url "http://localhost:2024/runs/stream" \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": { + \"messages\": [ + { + \"role\": \"human\", + \"content\": \"What is LangGraph?\" + } + ] + }, + \"stream_mode\": \"messages-tuple\" + }" + ``` + + +::: + +## Next steps + +Now that you have a LangGraph app running locally, take your journey further by exploring deployment and advanced features: + +* [Deployment quickstart](/langgraph-platform/deployment-quickstart): Deploy your LangGraph app using LangGraph Platform. +* [LangGraph Platform](/langgraph-platform): Learn about foundational LangGraph Platform concepts. + +:::python +* [Python SDK Reference](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/python_sdk_ref/): Explore the Python SDK API Reference. +::: + +:::js +* [JS/TS SDK Reference](https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html): Explore the JS/TS SDK API Reference. +::: diff --git a/src/oss/mcp.mdx b/src/oss/mcp.mdx new file mode 100644 index 00000000..d911dab9 --- /dev/null +++ b/src/oss/mcp.mdx @@ -0,0 +1,14 @@ +--- +title: MCP overview +sidebarTitle: Overview +--- + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide tools and context to language models. LangGraph agents can use tools defined on MCP servers through the `langchain-mcp-adapters` library. + +![MCP](/oss/images/mcp.png) + +Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph: + +```bash +pip install langchain-mcp-adapters +``` diff --git a/src/oss/memory.mdx b/src/oss/memory.mdx new file mode 100644 index 00000000..50517b0a --- /dev/null +++ b/src/oss/memory.mdx @@ -0,0 +1,274 @@ +--- +title: Memory overview +--- + +[Memory](/oss/add-memory) is a system that remembers information about previous interactions. For AI agents, memory is crucial because it lets them remember previous interactions, learn from feedback, and adapt to user preferences. As agents tackle more complex tasks with numerous user interactions, this capability becomes essential for both efficiency and user satisfaction. + +This conceptual guide covers two types of memory, based on their recall scope: + +* [Short-term memory](#short-term-memory), or [thread](/oss/persistence#threads)-scoped memory, tracks the ongoing conversation by maintaining message history within a session. LangGraph manages short-term memory as a part of your agent's [state](/oss/graph-api#state). State is persisted to a database using a [checkpointer](/oss/persistence#checkpoints) so the thread can be resumed at any time. Short-term memory updates when the graph is invoked or a step is completed, and the State is read at the start of each step. +* [Long-term memory](#long-term-memory) stores user-specific or application-level data across sessions and is shared _across_ conversational threads. It can be recalled _at any time_ and _in any thread_. Memories are scoped to any custom namespace, not just within a single thread ID. LangGraph provides [stores](/oss/persistence#memory-store) ([reference doc](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore)) to let you save and recall long-term memories. + +![](/oss/images/short-vs-long.png) + +## Short-term memory + +[Short-term memory](/oss/add-memory#add-short-term-memory) lets your application remember previous interactions within a single [thread](/oss/persistence#threads) or conversation. A [thread](/oss/persistence#threads) organizes multiple interactions in a session, similar to the way email groups messages in a single conversation. + +LangGraph manages short-term memory as part of the agent's state, persisted via thread-scoped checkpoints. This state can normally include the conversation history along with other stateful data, such as uploaded files, retrieved documents, or generated artifacts. By storing these in the graph's state, the bot can access the full context for a given conversation while maintaining separation between different threads. + +### Manage short-term memory + +Conversation history is the most common form of short-term memory, and long conversations pose a challenge to today's LLMs. A full history may not fit inside an LLM's context window, resulting in an irrecoverable error. Even if your LLM supports the full context length, most LLMs still perform poorly over long contexts. They get "distracted" by stale or off-topic content, all while suffering from slower response times and higher costs. + +Chat models accept context using messages, which include developer provided instructions (a system message) and user inputs (human messages). In chat applications, messages alternate between human inputs and model responses, resulting in a list of messages that grows longer over time. Because context windows are limited and token-rich message lists can be costly, many applications can benefit from using techniques to manually remove or forget stale information. + +![](/oss/images/filter.png) + +For more information on common techniques for managing messages, see the [Add and manage memory](/oss/add-memory#manage-short-term-memory) guide. + +## Long-term memory + +[Long-term memory](/oss/add-memory#add-long-term-memory) in LangGraph allows systems to retain information across different conversations or sessions. Unlike short-term memory, which is **thread-scoped**, long-term memory is saved within custom "namespaces." + +Long-term memory is a complex challenge without a one-size-fits-all solution. However, the following questions provide a framework to help you navigate the different techniques: + +* [What is the type of memory?](#memory-types) Humans use memories to remember facts ([semantic memory](#semantic-memory)), experiences ([episodic memory](#episodic-memory)), and rules ([procedural memory](#procedural-memory)). AI agents can use memory in the same ways. For example, AI agents can use memory to remember specific facts about a user to accomplish a task. +* [When do you want to update memories?](#writing-memories) Memory can be updated as part of an agent's application logic (e.g., "on the hot path"). In this case, the agent typically decides to remember facts before responding to a user. Alternatively, memory can be updated as a background task (logic that runs in the background / asynchronously and generates memories). We explain the tradeoffs between these approaches in the [section below](#writing-memories). + +### Memory types + +Different applications require various types of memory. Although the analogy isn't perfect, examining [human memory types](https://www.psychologytoday.com/us/basics/memory/types-of-memory?ref=blog.langchain.dev) can be insightful. Some research (e.g., the [CoALA paper](https://arxiv.org/pdf/2309.02427)) have even mapped these human memory types to those used in AI agents. + +| Memory Type | What is Stored | Human Example | Agent Example | +|-------------|----------------|---------------|---------------| +| [Semantic](#semantic-memory) | Facts | Things I learned in school | Facts about a user | +| [Episodic](#episodic-memory) | Experiences | Things I did | Past agent actions | +| [Procedural](#procedural-memory) | Instructions | Instincts or motor skills | Agent system prompt | + +#### Semantic memory + +[Semantic memory](https://en.wikipedia.org/wiki/Semantic_memory), both in humans and AI agents, involves the retention of specific facts and concepts. In humans, it can include information learned in school and the understanding of concepts and their relationships. For AI agents, semantic memory is often used to personalize applications by remembering facts or concepts from past interactions. + + + Semantic memory is different from "semantic search," which is a technique for finding similar content using "meaning" (usually as embeddings). Semantic memory is a term from psychology, referring to storing facts and knowledge, while semantic search is a method for retrieving information based on meaning rather than exact matches. + + +##### Profile + +Semantic memories can be managed in different ways. For example, memories can be a single, continuously updated "profile" of well-scoped and specific information about a user, organization, or other entity (including the agent itself). A profile is generally just a JSON document with various key-value pairs you've selected to represent your domain. + +When remembering a profile, you will want to make sure that you are **updating** the profile each time. As a result, you will want to pass in the previous profile and [ask the model to generate a new profile](https://github.com/langchain-ai/memory-template) (or some [JSON patch](https://github.com/hinthornw/trustcall) to apply to the old profile). This can be become error-prone as the profile gets larger, and may benefit from splitting a profile into multiple documents or **strict** decoding when generating documents to ensure the memory schemas remains valid. + +![](/oss/images/update-profile.png) + +##### Collection + +Alternatively, memories can be a collection of documents that are continuously updated and extended over time. Each individual memory can be more narrowly scoped and easier to generate, which means that you're less likely to **lose** information over time. It's easier for an LLM to generate _new_ objects for new information than reconcile new information with an existing profile. As a result, a document collection tends to lead to [higher recall downstream](https://en.wikipedia.org/wiki/Precision_and_recall). + +However, this shifts some complexity memory updating. The model must now _delete_ or _update_ existing items in the list, which can be tricky. In addition, some models may default to over-inserting and others may default to over-updating. See the [Trustcall](https://github.com/hinthornw/trustcall) package for one way to manage this and consider evaluation (e.g., with a tool like [LangSmith](https://docs.smith.langchain.com/tutorials/Developers/evaluation)) to help you tune the behavior. + +Working with document collections also shifts complexity to memory **search** over the list. The `Store` currently supports both [semantic search](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.SearchOp.query) and [filtering by content](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.SearchOp.filter). + +Finally, using a collection of memories can make it challenging to provide comprehensive context to the model. While individual memories may follow a specific schema, this structure might not capture the full context or relationships between memories. As a result, when using these memories to generate responses, the model may lack important contextual information that would be more readily available in a unified profile approach. + +![](/oss/images/update-list.png) + +Regardless of memory management approach, the central point is that the agent will use the semantic memories to [ground its responses](https://python.langchain.com/docs/concepts/rag/), which often leads to more personalized and relevant interactions. + +#### Episodic memory + +[Episodic memory](https://en.wikipedia.org/wiki/Episodic_memory), in both humans and AI agents, involves recalling past events or actions. The [CoALA paper](https://arxiv.org/pdf/2309.02427) frames this well: facts can be written to semantic memory, whereas *experiences* can be written to episodic memory. For AI agents, episodic memory is often used to help an agent remember how to accomplish a task. + +:::python +In practice, episodic memories are often implemented through [few-shot example prompting](https://python.langchain.com/docs/concepts/few_shot_prompting/), where agents learn from past sequences to perform tasks correctly. Sometimes it's easier to "show" than "tell" and LLMs learn well from examples. Few-shot learning lets you ["program"](https://x.com/karpathy/status/1627366413840322562) your LLM by updating the prompt with input-output examples to illustrate the intended behavior. While various [best-practices](https://python.langchain.com/docs/concepts/#1-generating-examples) can be used to generate few-shot examples, often the challenge lies in selecting the most relevant examples based on user input. +::: + +:::js +In practice, episodic memories are often implemented through few-shot example prompting, where agents learn from past sequences to perform tasks correctly. Sometimes it's easier to "show" than "tell" and LLMs learn well from examples. Few-shot learning lets you ["program"](https://x.com/karpathy/status/1627366413840322562) your LLM by updating the prompt with input-output examples to illustrate the intended behavior. While various best-practices can be used to generate few-shot examples, often the challenge lies in selecting the most relevant examples based on user input. +::: + +:::python +Note that the memory [store](/oss/persistence#memory-store) is just one way to store data as few-shot examples. If you want to have more developer involvement, or tie few-shots more closely to your evaluation harness, you can also use a [LangSmith Dataset](https://docs.smith.langchain.com/evaluation/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) to store your data. Then dynamic few-shot example selectors can be used out-of-the box to achieve this same goal. LangSmith will index the dataset for you and enable retrieval of few shot examples that are most relevant to the user input based upon keyword similarity ([using a BM25-like algorithm](https://docs.smith.langchain.com/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) for keyword based similarity). + +See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example usage of dynamic few-shot example selection in LangSmith. Also, see this [blog post](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/) showcasing few-shot prompting to improve tool calling performance and this [blog post](https://blog.langchain.dev/aligning-llm-as-a-judge-with-human-preferences/) using few-shot example to align an LLMs to human preferences. +::: + +:::js +Note that the memory [store](/oss/persistence#memory-store) is just one way to store data as few-shot examples. If you want to have more developer involvement, or tie few-shots more closely to your evaluation harness, you can also use a LangSmith Dataset to store your data. Then dynamic few-shot example selectors can be used out-of-the box to achieve this same goal. LangSmith will index the dataset for you and enable retrieval of few shot examples that are most relevant to the user input based upon keyword similarity. + +See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example usage of dynamic few-shot example selection in LangSmith. Also, see this [blog post](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/) showcasing few-shot prompting to improve tool calling performance and this [blog post](https://blog.langchain.dev/aligning-llm-as-a-judge-with-human-preferences/) using few-shot example to align an LLMs to human preferences. +::: + +#### Procedural memory + +[Procedural memory](https://en.wikipedia.org/wiki/Procedural_memory), in both humans and AI agents, involves remembering the rules used to perform tasks. In humans, procedural memory is like the internalized knowledge of how to perform tasks, such as riding a bike via basic motor skills and balance. Episodic memory, on the other hand, involves recalling specific experiences, such as the first time you successfully rode a bike without training wheels or a memorable bike ride through a scenic route. For AI agents, procedural memory is a combination of model weights, agent code, and agent's prompt that collectively determine the agent's functionality. + +In practice, it is fairly uncommon for agents to modify their model weights or rewrite their code. However, it is more common for agents to modify their own prompts. + +One effective approach to refining an agent's instructions is through ["Reflection"](https://blog.langchain.dev/reflection-agents/) or meta-prompting. This involves prompting the agent with its current instructions (e.g., the system prompt) along with recent conversations or explicit user feedback. The agent then refines its own instructions based on this input. This method is particularly useful for tasks where instructions are challenging to specify upfront, as it allows the agent to learn and adapt from its interactions. + +For example, we built a [Tweet generator](https://www.youtube.com/watch?v=Vn8A3BxfplE) using external feedback and prompt re-writing to produce high-quality paper summaries for Twitter. In this case, the specific summarization prompt was difficult to specify *a priori*, but it was fairly easy for a user to critique the generated Tweets and provide feedback on how to improve the summarization process. + +The below pseudo-code shows how you might implement this with the LangGraph memory [store](/oss/persistence#memory-store), using the store to save a prompt, the `update_instructions` node to get the current prompt (as well as feedback from the conversation with the user captured in `state["messages"]`), update the prompt, and save the new prompt back to the store. Then, the `call_model` get the updated prompt from the store and uses it to generate a response. + +:::python +```python +# Node that *uses* the instructions +def call_model(state: State, store: BaseStore): + namespace = ("agent_instructions", ) + instructions = store.get(namespace, key="agent_a")[0] + # Application logic + prompt = prompt_template.format(instructions=instructions.value["instructions"]) + ... + +# Node that updates instructions +def update_instructions(state: State, store: BaseStore): + namespace = ("instructions",) + current_instructions = store.search(namespace)[0] + # Memory logic + prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"]) + output = llm.invoke(prompt) + new_instructions = output['new_instructions'] + store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions}) + ... +``` +::: + +:::js +```typescript +// Node that *uses* the instructions +const callModel = async (state: State, store: BaseStore) => { + const namespace = ["agent_instructions"]; + const instructions = await store.get(namespace, "agent_a"); + // Application logic + const prompt = promptTemplate.format({ + instructions: instructions[0].value.instructions + }); + // ... +}; + +// Node that updates instructions +const updateInstructions = async (state: State, store: BaseStore) => { + const namespace = ["instructions"]; + const currentInstructions = await store.search(namespace); + // Memory logic + const prompt = promptTemplate.format({ + instructions: currentInstructions[0].value.instructions, + conversation: state.messages + }); + const output = await llm.invoke(prompt); + const newInstructions = output.new_instructions; + await store.put(["agent_instructions"], "agent_a", { + instructions: newInstructions + }); + // ... +}; +``` +::: + +![](/oss/images/update-instructions.png) + +### Writing memories + +There are two primary methods for agents to write memories: ["in the hot path"](#in-the-hot-path) and ["in the background"](#in-the-background). + +![](/oss/images/hot_path_vs_background.png) + +#### In the hot path + +Creating memories during runtime offers both advantages and challenges. On the positive side, this approach allows for real-time updates, making new memories immediately available for use in subsequent interactions. It also enables transparency, as users can be notified when memories are created and stored. + +However, this method also presents challenges. It may increase complexity if the agent requires a new tool to decide what to commit to memory. In addition, the process of reasoning about what to save to memory can impact agent latency. Finally, the agent must multitask between memory creation and its other responsibilities, potentially affecting the quantity and quality of memories created. + +As an example, ChatGPT uses a [save_memories](https://openai.com/index/memory-and-new-controls-for-chatgpt/) tool to upsert memories as content strings, deciding whether and how to use this tool with each user message. See our [memory-agent](https://github.com/langchain-ai/memory-agent) template as an reference implementation. + +#### In the background + +Creating memories as a separate background task offers several advantages. It eliminates latency in the primary application, separates application logic from memory management, and allows for more focused task completion by the agent. This approach also provides flexibility in timing memory creation to avoid redundant work. + +However, this method has its own challenges. Determining the frequency of memory writing becomes crucial, as infrequent updates may leave other threads without new context. Deciding when to trigger memory formation is also important. Common strategies include scheduling after a set time period (with rescheduling if new events occur), using a cron schedule, or allowing manual triggers by users or the application logic. + +See our [memory-service](https://github.com/langchain-ai/memory-template) template as an reference implementation. + +### Memory storage + +LangGraph stores long-term memories as JSON documents in a [store](/oss/persistence#memory-store). Each memory is organized under a custom `namespace` (similar to a folder) and a distinct `key` (like a file name). Namespaces often include user or org IDs or other labels that makes it easier to organize information. This structure enables hierarchical organization of memories. Cross-namespace searching is then supported through content filters. + +:::python +```python +from langgraph.store.memory import InMemoryStore + + +def embed(texts: list[str]) -> list[list[float]]: + # Replace with an actual embedding function or LangChain embeddings object + return [[1.0, 2.0] * len(texts)] + + +# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use. +store = InMemoryStore(index={"embed": embed, "dims": 2}) +user_id = "my-user" +application_context = "chitchat" +namespace = (user_id, application_context) +store.put( + namespace, + "a-memory", + { + "rules": [ + "User likes short, direct language", + "User only speaks English & python", + ], + "my-key": "my-value", + }, +) +# get the "memory" by ID +item = store.get(namespace, "a-memory") +# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity +items = store.search( + namespace, filter={"my-key": "my-value"}, query="language preferences" +) +``` +::: + +:::js +```typescript +import { InMemoryStore } from "@langchain/langgraph"; + +const embed = (texts: string[]): number[][] => { + // Replace with an actual embedding function or LangChain embeddings object + return texts.map(() => [1.0, 2.0]); +}; + +// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use. +const store = new InMemoryStore({ index: { embed, dims: 2 } }); +const userId = "my-user"; +const applicationContext = "chitchat"; +const namespace = [userId, applicationContext]; + +await store.put( + namespace, + "a-memory", + { + rules: [ + "User likes short, direct language", + "User only speaks English & TypeScript", + ], + "my-key": "my-value", + } +); + +// get the "memory" by ID +const item = await store.get(namespace, "a-memory"); + +// search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity +const items = await store.search( + namespace, + { + filter: { "my-key": "my-value" }, + query: "language preferences" + } +); +``` +::: + +For more information about the memory store, see the [Persistence](/oss/persistence#memory-store) guide. diff --git a/src/oss/models.mdx b/src/oss/models.mdx new file mode 100644 index 00000000..49088a85 --- /dev/null +++ b/src/oss/models.mdx @@ -0,0 +1,381 @@ +--- +title: Models +--- + +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; + +LangGraph provides built-in support for [LLMs (language models)](https://python.langchain.com/docs/concepts/chat_models/) via the LangChain library. This makes it easy to integrate various LLMs into your agents and workflows. + +## Initialize a model + +:::python +Use [`init_chat_model`](https://python.langchain.com/docs/how_to/chat_models_universal_init/) to initialize models: + + + +::: + +:::js +Use model provider classes to initialize models: + + + + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + + const model = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, + }); + ``` + + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + + const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-20240620", + temperature: 0, + maxTokens: 2048, + }); + ``` + + + ```typescript + import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; + + const model = new ChatGoogleGenerativeAI({ + model: "gemini-1.5-pro", + temperature: 0, + }); + ``` + + + ```typescript + import { ChatGroq } from "@langchain/groq"; + + const model = new ChatGroq({ + model: "llama-3.1-70b-versatile", + temperature: 0, + }); + ``` + + +::: + +:::python +### Instantiate a model directly + +If a model provider is not available via `init_chat_model`, you can instantiate the provider's model class directly. The model must implement the [BaseChatModel interface](https://python.langchain.com/api_reference/core/language_models/langchain_core.language_models.chat_models.BaseChatModel.html) and support tool calling: + +```python +# Anthropic is already supported by `init_chat_model`, +# but you can also instantiate it directly. +from langchain_anthropic import ChatAnthropic + +model = ChatAnthropic( + model="claude-3-7-sonnet-latest", + temperature=0, + max_tokens=2048 +) +``` +::: + + + **Tool calling support** + If you are building an agent or workflow that requires the model to call external tools, ensure that the underlying + language model supports [tool calling](/oss/tools). Compatible models can be found in the [LangChain integrations directory](https://python.langchain.com/docs/integrations/chat/). + + +## Use in an agent + +:::python +When using `create_react_agent` you can specify the model by its name string, which is a shorthand for initializing the model using `init_chat_model`. This allows you to use the model without needing to import or instantiate it directly. + + + + ```python + from langgraph.prebuilt import create_react_agent + + create_react_agent( + # highlight-next-line + model="anthropic:claude-3-7-sonnet-latest", + # other parameters + ) + ``` + + + ```python + from langchain_anthropic import ChatAnthropic + from langgraph.prebuilt import create_react_agent + + model = ChatAnthropic( + model="claude-3-7-sonnet-latest", + temperature=0, + max_tokens=2048 + ) + # Alternatively + # model = init_chat_model("anthropic:claude-3-7-sonnet-latest") + + agent = create_react_agent( + # highlight-next-line + model=model, + # other parameters + ) + ``` + + +::: + +:::js +When using `createReactAgent` you can pass the model instance directly: + +```typescript +import { ChatOpenAI } from "@langchain/openai"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const model = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, +}); + +const agent = createReactAgent({ + llm: model, + tools: tools, +}); +``` +::: + +:::python +### Dynamic model selection + +Pass a callable function to `create_react_agent` to dynamically select the model at runtime. This is useful for scenarios where you want to choose a model based on user input, configuration settings, or other runtime conditions. + +The selector function must return a chat model. If you're using tools, you must bind the tools to the model within the selector function. + +```python +from dataclasses import dataclass +from typing import Literal +from langchain.chat_models import init_chat_model +from langchain_core.language_models import BaseChatModel +from langchain_core.tools import tool +from langgraph.prebuilt import create_react_agent +from langgraph.prebuilt.chat_agent_executor import AgentState +from langgraph.runtime import Runtime + +@tool +def weather() -> str: + """Returns the current weather conditions.""" + return "It's nice and sunny." + + +# Define the runtime context +@dataclass +class CustomContext: + provider: Literal["anthropic", "openai"] + +# Initialize models +openai_model = init_chat_model("openai:gpt-4o") +anthropic_model = init_chat_model("anthropic:claude-sonnet-4-20250514") + + +# Selector function for model choice +def select_model(state: AgentState, runtime: Runtime[CustomContext]) -> BaseChatModel: + if runtime.context.provider == "anthropic": + model = anthropic_model + elif runtime.context.provider == "openai": + model = openai_model + else: + raise ValueError(f"Unsupported provider: {runtime.context.provider}") + + # With dynamic model selection, you must bind tools explicitly + return model.bind_tools([weather]) + + +# Create agent with dynamic model selection +agent = create_react_agent(select_model, tools=[weather]) + +# Invoke with context to select model +output = agent.invoke( + { + "messages": [ + { + "role": "user", + "content": "Which model is handling this?", + } + ] + }, + context=CustomContext(provider="openai"), +) + +print(output["messages"][-1].text()) +``` + + + **New in LangGraph v0.6** + +::: + +## Advanced model configuration + +### Disable streaming + +:::python +To disable streaming of the individual LLM tokens, set `disable_streaming=True` when initializing the model: + + + + ```python + from langchain.chat_models import init_chat_model + + model = init_chat_model( + "anthropic:claude-3-7-sonnet-latest", + # highlight-next-line + disable_streaming=True + ) + ``` + + + ```python + from langchain_anthropic import ChatAnthropic + + model = ChatAnthropic( + model="claude-3-7-sonnet-latest", + # highlight-next-line + disable_streaming=True + ) + ``` + + + +Refer to the [API reference](https://python.langchain.com/api_reference/core/language_models/langchain_core.language_models.chat_models.BaseChatModel.html#langchain_core.language_models.chat_models.BaseChatModel.disable_streaming) for more information on `disable_streaming` +::: + +:::js +To disable streaming of the individual LLM tokens, set `streaming: false` when initializing the model: + +```typescript +import { ChatOpenAI } from "@langchain/openai"; + +const model = new ChatOpenAI({ + model: "gpt-4o", + streaming: false, +}); +``` +::: + +### Add model fallbacks + +:::python +You can add a fallback to a different model or a different LLM provider using `model.with_fallbacks([...])`: + + + + ```python + from langchain.chat_models import init_chat_model + + model_with_fallbacks = ( + init_chat_model("anthropic:claude-3-5-haiku-latest") + # highlight-next-line + .with_fallbacks([ + init_chat_model("openai:gpt-4.1-mini"), + ]) + ) + ``` + + + ```python + from langchain_anthropic import ChatAnthropic + from langchain_openai import ChatOpenAI + + model_with_fallbacks = ( + ChatAnthropic(model="claude-3-5-haiku-latest") + # highlight-next-line + .with_fallbacks([ + ChatOpenAI(model="gpt-4.1-mini"), + ]) + ) + ``` + + + +See this [guide](https://python.langchain.com/docs/how_to/fallbacks/#fallback-to-better-model) for more information on model fallbacks. +::: + +:::js +You can add a fallback to a different model or a different LLM provider using `model.withFallbacks([...])`: + +```typescript +import { ChatOpenAI } from "@langchain/openai"; +import { ChatAnthropic } from "@langchain/anthropic"; + +const modelWithFallbacks = new ChatOpenAI({ + model: "gpt-4o", +}).withFallbacks([ + new ChatAnthropic({ + model: "claude-3-5-sonnet-20240620", + }), +]); +``` + +See this [guide](https://js.langchain.com/docs/how_to/fallbacks/#fallback-to-better-model) for more information on model fallbacks. +::: + +:::python +### Use the built-in rate limiter + +Langchain includes a built-in in-memory rate limiter. This rate limiter is thread safe and can be shared by multiple threads in the same process. + +```python +from langchain_core.rate_limiters import InMemoryRateLimiter +from langchain_anthropic import ChatAnthropic + +rate_limiter = InMemoryRateLimiter( + requests_per_second=0.1, # <-- Super slow! We can only make a request once every 10 seconds!! + check_every_n_seconds=0.1, # Wake up every 100 ms to check whether allowed to make a request, + max_bucket_size=10, # Controls the maximum burst size. +) + +model = ChatAnthropic( + model_name="claude-3-opus-20240229", + rate_limiter=rate_limiter +) +``` + +See the LangChain docs for more information on how to [handle rate limiting](https://python.langchain.com/docs/how_to/chat_model_rate_limiting/). +::: + +## Bring your own model + +If your desired LLM isn't officially supported by LangChain, consider these options: + +:::python +1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://python.langchain.com/docs/how_to/custom_chat_model/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework. +::: + +:::js +1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://js.langchain.com/docs/how_to/custom_chat/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework. +::: + +1. **Direct invocation with custom streaming**: Use your model directly by [adding custom streaming logic](/oss/streaming#use-with-any-llm) with `StreamWriter`. + Refer to the [custom streaming documentation](/oss/streaming#use-with-any-llm) for guidance. This approach suits custom workflows where prebuilt agent integration is not necessary. + +## Additional resources + +:::python +* [Multimodal inputs](https://python.langchain.com/docs/how_to/multimodal_inputs/) +* [Structured outputs](https://python.langchain.com/docs/how_to/structured_output/) +* [Model integration directory](https://python.langchain.com/docs/integrations/chat/) +* [Force model to call a specific tool](https://python.langchain.com/docs/how_to/tool_choice/) +* [All chat model how-to guides](https://python.langchain.com/docs/how_to/#chat-models) +* [Chat model integrations](https://python.langchain.com/docs/integrations/chat/) +::: + +:::js +* [Multimodal inputs](https://js.langchain.com/docs/how_to/multimodal_inputs/) +* [Structured outputs](https://js.langchain.com/docs/how_to/structured_output/) +* [Model integration directory](https://js.langchain.com/docs/integrations/chat/) +* [Force model to call a specific tool](https://js.langchain.com/docs/how_to/tool_choice/) +* [All chat model how-to guides](https://js.langchain.com/docs/how_to/#chat-models) +* [Chat model integrations](https://js.langchain.com/docs/integrations/chat/) +::: diff --git a/src/oss/multi-agent-custom.mdx b/src/oss/multi-agent-custom.mdx new file mode 100644 index 00000000..87b650fa --- /dev/null +++ b/src/oss/multi-agent-custom.mdx @@ -0,0 +1,581 @@ +--- +title: Build custom multi-agent systems +sidebarTitle: Build custom +--- +A single agent might struggle if it needs to specialize in multiple domains or manage many tools. To tackle this, you can break your agent into smaller, independent agents and composing them into a [multi-agent system](/oss/multi-agent). + +In multi-agent systems, agents need to communicate between each other. They do so via [handoffs](#handoffs) — a primitive that describes which agent to hand control to and the payload to send to that agent. + +This guide covers the following: + +* implementing [handoffs](#handoffs) between agents +* using handoffs and the prebuilt [agent](/oss/prebuilts) to [build a custom multi-agent system](#build-a-multi-agent-system) + +To get started with building multi-agent systems, check out LangGraph [prebuilt implementations](#prebuilt-implementations) of two of the most popular multi-agent architectures — [supervisor](/oss/multi-agent-prebuilts#supervisor) and [swarm](/oss/multi-agent-prebuilts#swarm). + +## Handoffs + +To set up communication between the agents in a multi-agent system you can use [**handoffs**](/oss/multi-agent#handoffs) — a pattern where one agent *hands off* control to another. Handoffs allow you to specify: + +* **destination**: target agent to navigate to (e.g., name of the LangGraph node to go to) +* **payload**: information to pass to that agent (e.g., state update) + +### Create handoffs + +To implement handoffs, you can return `Command` objects from your agent nodes or tools: + +```python +from typing import Annotated +from langchain_core.tools import tool, InjectedToolCallId +from langgraph.prebuilt import create_react_agent, InjectedState +from langgraph.graph import StateGraph, START, MessagesState +from langgraph.types import Command + +def create_handoff_tool(*, agent_name: str, description: str | None = None): + name = f"transfer_to_{agent_name}" + description = description or f"Transfer to {agent_name}" + + @tool(name, description=description) + def handoff_tool( + # highlight-next-line + state: Annotated[MessagesState, InjectedState], # (1)! + # highlight-next-line + tool_call_id: Annotated[str, InjectedToolCallId], + ) -> Command: + tool_message = { + "role": "tool", + "content": f"Successfully transferred to {agent_name}", + "name": name, + "tool_call_id": tool_call_id, + } + return Command( # (2)! + # highlight-next-line + goto=agent_name, # (3)! + # highlight-next-line + update={"messages": state["messages"] + [tool_message]}, # (4)! + # highlight-next-line + graph=Command.PARENT, # (5)! + ) + return handoff_tool +``` + +1. Access the [state](/oss/graph-api#state) of the agent that is calling the handoff tool using the @[InjectedState] annotation. +2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs. +3. Name of the agent or node to hand off to. +4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state. +5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph. + + + If you want to use tools that return `Command`, you can either use prebuilt @[`create_react_agent`][create_react_agent] / @[`ToolNode`][ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.: + + ```python + def call_tools(state): + ... + commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls] + return commands + ``` + + + + This handoff implementation assumes that: + + * each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs) + * each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function: + ```python + def call_hotel_assistant(state): + # return agent's final response, + # excluding inner monologue + response = hotel_assistant.invoke(state) + # highlight-next-line + return {"messages": response["messages"][-1]} + ``` + + +### Control agent inputs + +You can use the @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent: + +```python + +from typing import Annotated +from langchain_core.tools import tool, InjectedToolCallId +from langgraph.prebuilt import InjectedState +from langgraph.graph import StateGraph, START, MessagesState +# highlight-next-line +from langgraph.types import Command, Send + +def create_task_description_handoff_tool( + *, agent_name: str, description: str | None = None +): + name = f"transfer_to_{agent_name}" + description = description or f"Ask {agent_name} for help." + + @tool(name, description=description) + def handoff_tool( + # this is populated by the calling agent + task_description: Annotated[ + str, + "Description of what the next agent should do, including all of the relevant context.", + ], + # these parameters are ignored by the LLM + state: Annotated[MessagesState, InjectedState], + ) -> Command: + task_description_message = {"role": "user", "content": task_description} + agent_input = {**state, "messages": [task_description_message]} + return Command( + # highlight-next-line + goto=[Send(agent_name, agent_input)], + graph=Command.PARENT, + ) + + return handoff_tool +``` + +See the multi-agent [supervisor](/oss/agent-supervisor#4-create-delegation-tasks) example for a full example of using @[`Send()`][Send] in handoffs. + +## Build a multi-agent system + +You can use handoffs in any agents built with LangGraph. We recommend using the prebuilt [agent](/oss/agentic-architectures) or [`ToolNode`](/oss/call-tools#toolnode), as they natively support handoffs tools returning `Command`. Below is an example of how you can implement a multi-agent system for booking travel using handoffs: + +```python +from langgraph.prebuilt import create_react_agent +from langgraph.graph import StateGraph, START, MessagesState + +def create_handoff_tool(*, agent_name: str, description: str | None = None): + # same implementation as above + ... + return Command(...) + +# Handoffs +transfer_to_hotel_assistant = create_handoff_tool(agent_name="hotel_assistant") +transfer_to_flight_assistant = create_handoff_tool(agent_name="flight_assistant") + +# Define agents +flight_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[..., transfer_to_hotel_assistant], + # highlight-next-line + name="flight_assistant" +) +hotel_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[..., transfer_to_flight_assistant], + # highlight-next-line + name="hotel_assistant" +) + +# Define multi-agent graph +multi_agent_graph = ( + StateGraph(MessagesState) + # highlight-next-line + .add_node(flight_assistant) + # highlight-next-line + .add_node(hotel_assistant) + .add_edge(START, "flight_assistant") + .compile() +) +``` + + + ```python + from typing import Annotated + from langchain_core.messages import convert_to_messages + from langchain_core.tools import tool, InjectedToolCallId + from langgraph.prebuilt import create_react_agent, InjectedState + from langgraph.graph import StateGraph, START, MessagesState + from langgraph.types import Command + + # We'll use `pretty_print_messages` helper to render the streamed agent outputs nicely + + def pretty_print_message(message, indent=False): + pretty_message = message.pretty_repr(html=True) + if not indent: + print(pretty_message) + return + + indented = "\n".join("\t" + c for c in pretty_message.split("\n")) + print(indented) + + + def pretty_print_messages(update, last_message=False): + is_subgraph = False + if isinstance(update, tuple): + ns, update = update + # skip parent graph updates in the printouts + if len(ns) == 0: + return + + graph_id = ns[-1].split(":")[0] + print(f"Update from subgraph {graph_id}:") + print("\n") + is_subgraph = True + + for node_name, node_update in update.items(): + update_label = f"Update from node {node_name}:" + if is_subgraph: + update_label = "\t" + update_label + + print(update_label) + print("\n") + + messages = convert_to_messages(node_update["messages"]) + if last_message: + messages = messages[-1:] + + for m in messages: + pretty_print_message(m, indent=is_subgraph) + print("\n") + + + def create_handoff_tool(*, agent_name: str, description: str | None = None): + name = f"transfer_to_{agent_name}" + description = description or f"Transfer to {agent_name}" + + @tool(name, description=description) + def handoff_tool( + # highlight-next-line + state: Annotated[MessagesState, InjectedState], # (1)! + # highlight-next-line + tool_call_id: Annotated[str, InjectedToolCallId], + ) -> Command: + tool_message = { + "role": "tool", + "content": f"Successfully transferred to {agent_name}", + "name": name, + "tool_call_id": tool_call_id, + } + return Command( # (2)! + # highlight-next-line + goto=agent_name, # (3)! + # highlight-next-line + update={"messages": state["messages"] + [tool_message]}, # (4)! + # highlight-next-line + graph=Command.PARENT, # (5)! + ) + return handoff_tool + + # Handoffs + transfer_to_hotel_assistant = create_handoff_tool( + agent_name="hotel_assistant", + description="Transfer user to the hotel-booking assistant.", + ) + transfer_to_flight_assistant = create_handoff_tool( + agent_name="flight_assistant", + description="Transfer user to the flight-booking assistant.", + ) + + # Simple agent tools + def book_hotel(hotel_name: str): + """Book a hotel""" + return f"Successfully booked a stay at {hotel_name}." + + def book_flight(from_airport: str, to_airport: str): + """Book a flight""" + return f"Successfully booked a flight from {from_airport} to {to_airport}." + + # Define agents + flight_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[book_flight, transfer_to_hotel_assistant], + prompt="You are a flight booking assistant", + # highlight-next-line + name="flight_assistant" + ) + hotel_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[book_hotel, transfer_to_flight_assistant], + prompt="You are a hotel booking assistant", + # highlight-next-line + name="hotel_assistant" + ) + + # Define multi-agent graph + multi_agent_graph = ( + StateGraph(MessagesState) + .add_node(flight_assistant) + .add_node(hotel_assistant) + .add_edge(START, "flight_assistant") + .compile() + ) + + # Run the multi-agent graph + for chunk in multi_agent_graph.stream( + { + "messages": [ + { + "role": "user", + "content": "book a flight from BOS to JFK and a stay at McKittrick Hotel" + } + ] + }, + # highlight-next-line + subgraphs=True + ): + pretty_print_messages(chunk) + ``` + + 1. Access agent's state + 2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs. + 3. Name of the agent or node to hand off to. + 4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state. + 5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph. + + +## Multi-turn conversation + +Users might want to engage in a *multi-turn conversation* with one or more agents. To build a system that can handle this, you can create a node that uses an @[`interrupt`][interrupt] to collect user input and routes back to the **active** agent. + +The agents can then be implemented as nodes in a graph that executes agent steps and determines the next action: + +1. **Wait for user input** to continue the conversation, or +2. **Route to another agent** (or back to itself, such as in a loop) via a [handoff](#handoffs) + +```python +def human(state) -> Command[Literal["agent", "another_agent"]]: + """A node for collecting user input.""" + user_input = interrupt(value="Ready for user input.") + + # Determine the active agent. + active_agent = ... + + ... + return Command( + update={ + "messages": [{ + "role": "human", + "content": user_input, + }] + }, + goto=active_agent + ) + +def agent(state) -> Command[Literal["agent", "another_agent", "human"]]: + # The condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc. + goto = get_next_agent(...) # 'agent' / 'another_agent' + if goto: + return Command(goto=goto, update={"my_state_key": "my_state_value"}) + else: + return Command(goto="human") # Go to human node +``` + + + In this example, we will build a team of travel assistant agents that can communicate with each other via handoffs. + + We will create 2 agents: + + * travel_advisor: can help with travel destination recommendations. Can ask hotel_advisor for help. + * hotel_advisor: can help with hotel recommendations. Can ask travel_advisor for help. + + ```python + from langchain_anthropic import ChatAnthropic + from langgraph.graph import MessagesState, StateGraph, START + from langgraph.prebuilt import create_react_agent, InjectedState + from langgraph.types import Command, interrupt + from langgraph.checkpoint.memory import InMemorySaver + + + model = ChatAnthropic(model="claude-3-5-sonnet-latest") + + class MultiAgentState(MessagesState): + last_active_agent: str + + + # Define travel advisor tools and ReAct agent + travel_advisor_tools = [ + get_travel_recommendations, + make_handoff_tool(agent_name="hotel_advisor"), + ] + travel_advisor = create_react_agent( + model, + travel_advisor_tools, + prompt=( + "You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). " + "If you need hotel recommendations, ask 'hotel_advisor' for help. " + "You MUST include human-readable response before transferring to another agent." + ), + ) + + + def call_travel_advisor( + state: MultiAgentState, + ) -> Command[Literal["hotel_advisor", "human"]]: + # You can also add additional logic like changing the input to the agent / output from the agent, etc. + # NOTE: we're invoking the ReAct agent with the full history of messages in the state + response = travel_advisor.invoke(state) + update = {**response, "last_active_agent": "travel_advisor"} + return Command(update=update, goto="human") + + + # Define hotel advisor tools and ReAct agent + hotel_advisor_tools = [ + get_hotel_recommendations, + make_handoff_tool(agent_name="travel_advisor"), + ] + hotel_advisor = create_react_agent( + model, + hotel_advisor_tools, + prompt=( + "You are a hotel expert that can provide hotel recommendations for a given destination. " + "If you need help picking travel destinations, ask 'travel_advisor' for help." + "You MUST include human-readable response before transferring to another agent." + ), + ) + + + def call_hotel_advisor( + state: MultiAgentState, + ) -> Command[Literal["travel_advisor", "human"]]: + response = hotel_advisor.invoke(state) + update = {**response, "last_active_agent": "hotel_advisor"} + return Command(update=update, goto="human") + + + def human_node( + state: MultiAgentState, config + ) -> Command[Literal["hotel_advisor", "travel_advisor", "human"]]: + """A node for collecting user input.""" + + user_input = interrupt(value="Ready for user input.") + active_agent = state["last_active_agent"] + + return Command( + update={ + "messages": [ + { + "role": "human", + "content": user_input, + } + ] + }, + goto=active_agent, + ) + + + builder = StateGraph(MultiAgentState) + builder.add_node("travel_advisor", call_travel_advisor) + builder.add_node("hotel_advisor", call_hotel_advisor) + + # This adds a node to collect human input, which will route + # back to the active agent. + builder.add_node("human", human_node) + + # We'll always start with a general travel advisor. + builder.add_edge(START, "travel_advisor") + + + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + ``` + + Let's test a multi turn conversation with this application. + + ```python + import uuid + + thread_config = {"configurable": {"thread_id": str(uuid.uuid4())}} + + inputs = [ + # 1st round of conversation, + { + "messages": [ + {"role": "user", "content": "i wanna go somewhere warm in the caribbean"} + ] + }, + # Since we're using `interrupt`, we'll need to resume using the Command primitive. + # 2nd round of conversation, + Command( + resume="could you recommend a nice hotel in one of the areas and tell me which area it is." + ), + # 3rd round of conversation, + Command( + resume="i like the first one. could you recommend something to do near the hotel?" + ), + ] + + for idx, user_input in enumerate(inputs): + print() + print(f"--- Conversation Turn {idx + 1} ---") + print() + print(f"User: {user_input}") + print() + for update in graph.stream( + user_input, + config=thread_config, + stream_mode="updates", + ): + for node_id, value in update.items(): + if isinstance(value, dict) and value.get("messages", []): + last_message = value["messages"][-1] + if isinstance(last_message, dict) or last_message.type != "ai": + continue + print(f"{node_id}: {last_message.content}") + ``` + + ``` + --- Conversation Turn 1 --- + + User: {'messages': [{'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}]} + + travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known as "One Happy Island" and offers: + - Year-round warm weather with consistent temperatures around 82°F (28°C) + - Beautiful white sand beaches like Eagle Beach and Palm Beach + - Clear turquoise waters perfect for swimming and snorkeling + - Minimal rainfall and location outside the hurricane belt + - A blend of Caribbean and Dutch culture + - Great dining options and nightlife + - Various water sports and activities + + Would you like me to get some specific hotel recommendations in Aruba for your stay? I can transfer you to our hotel advisor who can help with accommodations. + + --- Conversation Turn 2 --- + + User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.') + + hotel_advisor: Based on the recommendations, I can suggest two excellent options: + + 1. The Ritz-Carlton, Aruba - Located in Palm Beach + - This luxury resort is situated in the vibrant Palm Beach area + - Known for its exceptional service and amenities + - Perfect if you want to be close to dining, shopping, and entertainment + - Features multiple restaurants, a casino, and a world-class spa + - Located on a pristine stretch of Palm Beach + + 2. Bucuti & Tara Beach Resort - Located in Eagle Beach + - An adults-only boutique resort on Eagle Beach + - Known for being more intimate and peaceful + - Award-winning for its sustainability practices + - Perfect for a romantic getaway or peaceful vacation + - Located on one of the most beautiful beaches in the Caribbean + + Would you like more specific information about either of these properties or their locations? + + --- Conversation Turn 3 --- + + User: Command(resume='i like the first one. could you recommend something to do near the hotel?') + + travel_advisor: Near the Ritz-Carlton in Palm Beach, here are some highly recommended activities: + + 1. Visit the Palm Beach Plaza Mall - Just a short walk from the hotel, featuring shopping, dining, and entertainment + 2. Try your luck at the Stellaris Casino - It's right in the Ritz-Carlton + 3. Take a sunset sailing cruise - Many depart from the nearby pier + 4. Visit the California Lighthouse - A scenic landmark just north of Palm Beach + 5. Enjoy water sports at Palm Beach: + - Jet skiing + - Parasailing + - Snorkeling + - Stand-up paddleboarding + + Would you like more specific information about any of these activities or would you like to know about other options in the area? + ``` + + +## Prebuilt implementations + +LangGraph comes with prebuilt implementations of two of the most popular multi-agent architectures: + +* [supervisor](/oss/multi-agent-prebuilts#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent systems. +* [swarm](/oss/multi-agent-prebuilts#swarm) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems. diff --git a/src/oss/multi-agent-prebuilts.mdx b/src/oss/multi-agent-prebuilts.mdx new file mode 100644 index 00000000..82eca6f7 --- /dev/null +++ b/src/oss/multi-agent-prebuilts.mdx @@ -0,0 +1,613 @@ +--- +title: Use prebuilt multi-agent systems +sidebarTitle: Use prebuilts +--- + +A single agent might struggle if it needs to specialize in multiple domains or manage many tools. To tackle this, you can break your agent into smaller, independent agents and compose them into a [multi-agent system](/oss/multi-agent). + +In multi-agent systems, agents need to communicate between each other. They do so via [handoffs](#handoffs) — a primitive that describes which agent to hand control to and the payload to send to that agent. + +Two of the most popular multi-agent architectures are: + +* [supervisor](#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. +* [swarm](#swarm) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. + +## Supervisor + +![Supervisor](/oss/images/supervisor.png) + +:::python +Use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent system: + +```bash +pip install langgraph-supervisor +``` + +```python +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import create_react_agent +# highlight-next-line +from langgraph_supervisor import create_supervisor + +def book_hotel(hotel_name: str): + """Book a hotel""" + return f"Successfully booked a stay at {hotel_name}." + +def book_flight(from_airport: str, to_airport: str): + """Book a flight""" + return f"Successfully booked a flight from {from_airport} to {to_airport}." + +flight_assistant = create_react_agent( + model="openai:gpt-4o", + tools=[book_flight], + prompt="You are a flight booking assistant", + # highlight-next-line + name="flight_assistant" +) + +hotel_assistant = create_react_agent( + model="openai:gpt-4o", + tools=[book_hotel], + prompt="You are a hotel booking assistant", + # highlight-next-line + name="hotel_assistant" +) + +# highlight-next-line +supervisor = create_supervisor( + agents=[flight_assistant, hotel_assistant], + model=ChatOpenAI(model="gpt-4o"), + prompt=( + "You manage a hotel booking assistant and a" + "flight booking assistant. Assign work to them." + ) +).compile() + +for chunk in supervisor.stream( + { + "messages": [ + { + "role": "user", + "content": "book a flight from BOS to JFK and a stay at McKittrick Hotel" + } + ] + } +): + print(chunk) + print("\n") +``` +::: + +:::js +Use [`@langchain/langgraph-supervisor`](https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-supervisor) library to create a supervisor multi-agent system: + +```bash +npm install @langchain/langgraph-supervisor +``` + +```typescript +import { ChatOpenAI } from "@langchain/openai"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +// highlight-next-line +import { createSupervisor } from "langgraph-supervisor"; + +function bookHotel(hotelName: string) { + /**Book a hotel*/ + return `Successfully booked a stay at ${hotelName}.`; +} + +function bookFlight(fromAirport: string, toAirport: string) { + /**Book a flight*/ + return `Successfully booked a flight from ${fromAirport} to ${toAirport}.`; +} + +const flightAssistant = createReactAgent({ + llm: "openai:gpt-4o", + tools: [bookFlight], + stateModifier: "You are a flight booking assistant", + // highlight-next-line + name: "flight_assistant", +}); + +const hotelAssistant = createReactAgent({ + llm: "openai:gpt-4o", + tools: [bookHotel], + stateModifier: "You are a hotel booking assistant", + // highlight-next-line + name: "hotel_assistant", +}); + +// highlight-next-line +const supervisor = createSupervisor({ + agents: [flightAssistant, hotelAssistant], + llm: new ChatOpenAI({ model: "gpt-4o" }), + systemPrompt: + "You manage a hotel booking assistant and a " + + "flight booking assistant. Assign work to them.", +}); + +for await (const chunk of supervisor.stream({ + messages: [ + { + role: "user", + content: "book a flight from BOS to JFK and a stay at McKittrick Hotel", + }, + ], +})) { + console.log(chunk); + console.log("\n"); +} +``` +::: + +## Swarm + +![Swarm](/oss/images/swarm.png) + +:::python +Use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent system: + +```bash +pip install langgraph-swarm +``` + +```python +from langgraph.prebuilt import create_react_agent +# highlight-next-line +from langgraph_swarm import create_swarm, create_handoff_tool + +transfer_to_hotel_assistant = create_handoff_tool( + agent_name="hotel_assistant", + description="Transfer user to the hotel-booking assistant.", +) +transfer_to_flight_assistant = create_handoff_tool( + agent_name="flight_assistant", + description="Transfer user to the flight-booking assistant.", +) + +flight_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[book_flight, transfer_to_hotel_assistant], + prompt="You are a flight booking assistant", + # highlight-next-line + name="flight_assistant" +) +hotel_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[book_hotel, transfer_to_flight_assistant], + prompt="You are a hotel booking assistant", + # highlight-next-line + name="hotel_assistant" +) + +# highlight-next-line +swarm = create_swarm( + agents=[flight_assistant, hotel_assistant], + default_active_agent="flight_assistant" +).compile() + +for chunk in swarm.stream( + { + "messages": [ + { + "role": "user", + "content": "book a flight from BOS to JFK and a stay at McKittrick Hotel" + } + ] + } +): + print(chunk) + print("\n") +``` +::: + +:::js +Use [`@langchain/langgraph-swarm`](https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-swarm) library to create a swarm multi-agent system: + +```bash +npm install @langchain/langgraph-swarm +``` + +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +// highlight-next-line +import { createSwarm, createHandoffTool } from "@langchain/langgraph-swarm"; + +const transferToHotelAssistant = createHandoffTool({ + agentName: "hotel_assistant", + description: "Transfer user to the hotel-booking assistant.", +}); + +const transferToFlightAssistant = createHandoffTool({ + agentName: "flight_assistant", + description: "Transfer user to the flight-booking assistant.", +}); + +const flightAssistant = createReactAgent({ + llm: "anthropic:claude-3-5-sonnet-latest", + // highlight-next-line + tools: [bookFlight, transferToHotelAssistant], + stateModifier: "You are a flight booking assistant", + // highlight-next-line + name: "flight_assistant", +}); + +const hotelAssistant = createReactAgent({ + llm: "anthropic:claude-3-5-sonnet-latest", + // highlight-next-line + tools: [bookHotel, transferToFlightAssistant], + stateModifier: "You are a hotel booking assistant", + // highlight-next-line + name: "hotel_assistant", +}); + +// highlight-next-line +const swarm = createSwarm({ + agents: [flightAssistant, hotelAssistant], + defaultActiveAgent: "flight_assistant", +}); + +for await (const chunk of swarm.stream({ + messages: [ + { + role: "user", + content: "book a flight from BOS to JFK and a stay at McKittrick Hotel", + }, + ], +})) { + console.log(chunk); + console.log("\n"); +} +``` +::: + +## Handoffs + +A common pattern in multi-agent interactions is **handoffs**, where one agent _hands off_ control to another. Handoffs allow you to specify: + +* **destination**: target agent to navigate to +* **payload**: information to pass to that agent + +:::python +This is used both by `langgraph-supervisor` (supervisor hands off to individual agents) and `langgraph-swarm` (an individual agent can hand off to other agents). + +To implement handoffs with `create_react_agent`, you need to: + +1. Create a special tool that can transfer control to a different agent + ```python + def transfer_to_bob(): + """Transfer to bob.""" + return Command( + # name of the agent (node) to go to + # highlight-next-line + goto="bob", + # data to send to the agent + # highlight-next-line + update={"messages": [...]}, + # indicate to LangGraph that we need to navigate to + # agent node in a parent graph + # highlight-next-line + graph=Command.PARENT, + ) + ``` +2. Create individual agents that have access to handoff tools: + ```python + flight_assistant = create_react_agent( + ..., tools=[book_flight, transfer_to_hotel_assistant] + ) + hotel_assistant = create_react_agent( + ..., tools=[book_hotel, transfer_to_flight_assistant] + ) + ``` +3. Define a parent graph that contains individual agents as nodes: + ```python + from langgraph.graph import StateGraph, MessagesState + multi_agent_graph = ( + StateGraph(MessagesState) + .add_node(flight_assistant) + .add_node(hotel_assistant) + ... + ) + ``` +::: + +:::js +This is used both by `@langchain/langgraph-supervisor` (supervisor hands off to individual agents) and `@langchain/langgraph-swarm` (an individual agent can hand off to other agents). + +To implement handoffs with `createReactAgent`, you need to: + +1. Create a special tool that can transfer control to a different agent + ```typescript + function transferToBob() { + /**Transfer to bob.*/ + return new Command({ + // name of the agent (node) to go to + // highlight-next-line + goto: "bob", + // data to send to the agent + // highlight-next-line + update: { messages: [...] }, + // indicate to LangGraph that we need to navigate to + // agent node in a parent graph + // highlight-next-line + graph: Command.PARENT, + }); + } + ``` +2. Create individual agents that have access to handoff tools: + ```typescript + const flightAssistant = createReactAgent({ + ..., tools: [bookFlight, transferToHotelAssistant] + }); + const hotelAssistant = createReactAgent({ + ..., tools: [bookHotel, transferToFlightAssistant] + }); + ``` +3. Define a parent graph that contains individual agents as nodes: + ```typescript + import { StateGraph, MessagesZodState } from "@langchain/langgraph"; + const multiAgentGraph = new StateGraph(MessagesZodState) + .addNode("flight_assistant", flightAssistant) + .addNode("hotel_assistant", hotelAssistant) + // ... + ``` +::: + +Putting this together, here is how you can implement a simple multi-agent system with two agents — a flight booking assistant and a hotel booking assistant: + +:::python +```python +from typing import Annotated +from langchain_core.tools import tool, InjectedToolCallId +from langgraph.prebuilt import create_react_agent, InjectedState +from langgraph.graph import StateGraph, START, MessagesState +from langgraph.types import Command + +def create_handoff_tool(*, agent_name: str, description: str | None = None): + name = f"transfer_to_{agent_name}" + description = description or f"Transfer to {agent_name}" + + @tool(name, description=description) + def handoff_tool( + # highlight-next-line + state: Annotated[MessagesState, InjectedState], # (1)! + # highlight-next-line + tool_call_id: Annotated[str, InjectedToolCallId], + ) -> Command: + tool_message = { + "role": "tool", + "content": f"Successfully transferred to {agent_name}", + "name": name, + "tool_call_id": tool_call_id, + } + return Command( # (2)! + # highlight-next-line + goto=agent_name, # (3)! + # highlight-next-line + update={"messages": state["messages"] + [tool_message]}, # (4)! + # highlight-next-line + graph=Command.PARENT, # (5)! + ) + return handoff_tool + +# Handoffs +transfer_to_hotel_assistant = create_handoff_tool( + agent_name="hotel_assistant", + description="Transfer user to the hotel-booking assistant.", +) +transfer_to_flight_assistant = create_handoff_tool( + agent_name="flight_assistant", + description="Transfer user to the flight-booking assistant.", +) + +# Simple agent tools +def book_hotel(hotel_name: str): + """Book a hotel""" + return f"Successfully booked a stay at {hotel_name}." + +def book_flight(from_airport: str, to_airport: str): + """Book a flight""" + return f"Successfully booked a flight from {from_airport} to {to_airport}." + +# Define agents +flight_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[book_flight, transfer_to_hotel_assistant], + prompt="You are a flight booking assistant", + # highlight-next-line + name="flight_assistant" +) +hotel_assistant = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + # highlight-next-line + tools=[book_hotel, transfer_to_flight_assistant], + prompt="You are a hotel booking assistant", + # highlight-next-line + name="hotel_assistant" +) + +# Define multi-agent graph +multi_agent_graph = ( + StateGraph(MessagesState) + .add_node(flight_assistant) + .add_node(hotel_assistant) + .add_edge(START, "flight_assistant") + .compile() +) + +# Run the multi-agent graph +for chunk in multi_agent_graph.stream( + { + "messages": [ + { + "role": "user", + "content": "book a flight from BOS to JFK and a stay at McKittrick Hotel" + } + ] + } +): + print(chunk) + print("\n") +``` + +1. Access agent's state +2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs. +3. Name of the agent or node to hand off to. +4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state. +5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph. +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { ChatAnthropic } from "@langchain/anthropic"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { + StateGraph, + START, + MessagesZodState, + Command, +} from "@langchain/langgraph"; +import { z } from "zod"; + +function createHandoffTool({ + agentName, + description, +}: { + agentName: string; + description?: string; +}) { + const name = `transfer_to_${agentName}`; + const toolDescription = description || `Transfer to ${agentName}`; + + return tool( + async (_, config) => { + const toolMessage = { + role: "tool" as const, + content: `Successfully transferred to ${agentName}`, + name: name, + tool_call_id: config.toolCall?.id!, + }; + return new Command({ + // (2)! + // highlight-next-line + goto: agentName, // (3)! + // highlight-next-line + update: { messages: [toolMessage] }, // (4)! + // highlight-next-line + graph: Command.PARENT, // (5)! + }); + }, + { + name, + description: toolDescription, + schema: z.object({}), + } + ); +} + +// Handoffs +const transferToHotelAssistant = createHandoffTool({ + agentName: "hotel_assistant", + description: "Transfer user to the hotel-booking assistant.", +}); + +const transferToFlightAssistant = createHandoffTool({ + agentName: "flight_assistant", + description: "Transfer user to the flight-booking assistant.", +}); + +// Simple agent tools +const bookHotel = tool( + async ({ hotelName }) => { + /**Book a hotel*/ + return `Successfully booked a stay at ${hotelName}.`; + }, + { + name: "book_hotel", + description: "Book a hotel", + schema: z.object({ + hotelName: z.string().describe("Name of the hotel to book"), + }), + } +); + +const bookFlight = tool( + async ({ fromAirport, toAirport }) => { + /**Book a flight*/ + return `Successfully booked a flight from ${fromAirport} to ${toAirport}.`; + }, + { + name: "book_flight", + description: "Book a flight", + schema: z.object({ + fromAirport: z.string().describe("Departure airport code"), + toAirport: z.string().describe("Arrival airport code"), + }), + } +); + +// Define agents +const flightAssistant = createReactAgent({ + llm: new ChatAnthropic({ model: "anthropic:claude-3-5-sonnet-latest" }), + // highlight-next-line + tools: [bookFlight, transferToHotelAssistant], + stateModifier: "You are a flight booking assistant", + // highlight-next-line + name: "flight_assistant", +}); + +const hotelAssistant = createReactAgent({ + llm: new ChatAnthropic({ model: "anthropic:claude-3-5-sonnet-latest" }), + // highlight-next-line + tools: [bookHotel, transferToFlightAssistant], + stateModifier: "You are a hotel booking assistant", + // highlight-next-line + name: "hotel_assistant", +}); + +// Define multi-agent graph +const multiAgentGraph = new StateGraph(MessagesZodState) + .addNode("flight_assistant", flightAssistant) + .addNode("hotel_assistant", hotelAssistant) + .addEdge(START, "flight_assistant") + .compile(); + +// Run the multi-agent graph +for await (const chunk of multiAgentGraph.stream({ + messages: [ + { + role: "user", + content: "book a flight from BOS to JFK and a stay at McKittrick Hotel", + }, + ], +})) { + console.log(chunk); + console.log("\n"); +} +``` + +1. Access agent's state +2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs. +3. Name of the agent or node to hand off to. +4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state. +5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph. +::: + + + This handoff implementation assumes that: + + * each agent receives overall message history (across all agents) in the multi-agent system as its input + * each agent outputs its internal messages history to the overall message history of the multi-agent system + + +:::python +Check out LangGraph [supervisor](https://github.com/langchain-ai/langgraph-supervisor-py#customizing-handoff-tools) and [swarm](https://github.com/langchain-ai/langgraph-swarm-py#customizing-handoff-tools) documentation to learn how to customize handoffs. +::: + +:::js +Check out LangGraph [supervisor](https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-supervisor#customizing-handoff-tools) and [swarm](https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-swarm#customizing-handoff-tools) documentation to learn how to customize handoffs. +::: diff --git a/src/oss/multi-agent.mdx b/src/oss/multi-agent.mdx new file mode 100644 index 00000000..5338f47b --- /dev/null +++ b/src/oss/multi-agent.mdx @@ -0,0 +1,865 @@ +--- +title: Multi-agent systems overview +sidebarTitle: Overview +--- + +An [agent](/oss/agentic-architectures#agent-architectures) is _a system that uses an LLM to decide the control flow of an application_. As you develop these systems, they might grow more complex over time, making them harder to manage and scale. For example, you might run into the following problems: + +* agent has too many tools at its disposal and makes poor decisions about which tool to call next +* context grows too complex for a single agent to keep track of +* there is a need for multiple specialization areas in the system (e.g. planner, researcher, math expert, etc.) + +To tackle these, you might consider breaking your application into multiple smaller, independent agents and composing them into a **multi-agent system**. These independent agents can be as simple as a prompt and an LLM call, or as complex as a [ReAct](/oss/agentic-architectures#tool-calling-agent) agent (and more!). + +The primary benefits of using multi-agent systems are: + +* **Modularity**: Separate agents make it easier to develop, test, and maintain agentic systems. +* **Specialization**: You can create expert agents focused on specific domains, which helps with the overall system performance. +* **Control**: You can explicitly control how agents communicate (as opposed to relying on function calling). + +## Multi-agent architectures + +![](/oss/images/architectures.png) + +There are several ways to connect agents in a multi-agent system: + +* **Network**: each agent can communicate with [every other agent](/oss/multi-agent). Any agent can decide which other agent to call next. +* **Supervisor**: each agent communicates with a single [supervisor](/oss/agent-supervisor) agent. Supervisor agent makes decisions on which agent should be called next. +* **Supervisor (tool-calling)**: this is a special case of supervisor architecture. Individual agents can be represented as tools. In this case, a supervisor agent uses a tool-calling LLM to decide which of the agent tools to call, as well as the arguments to pass to those agents. +* **Hierarchical**: you can define a multi-agent system with [a supervisor of supervisors](/oss/multi-agent#hierarchical). This is a generalization of the supervisor architecture and allows for more complex control flows. +* **Custom multi-agent workflow**: each agent communicates with only a subset of agents. Parts of the flow are deterministic, and only some agents can decide which other agents to call next. + +### Handoffs + +In multi-agent architectures, agents can be represented as graph nodes. Each agent node executes its step(s) and decides whether to finish execution or route to another agent, including potentially routing to itself (e.g., running in a loop). A common pattern in multi-agent interactions is **handoffs**, where one agent _hands off_ control to another. Handoffs allow you to specify: + +* **destination**: target agent to navigate to (e.g., name of the node to go to) +* **payload**: [information to pass to that agent](#communication-and-state-management) (e.g., state update) + +To implement handoffs in LangGraph, agent nodes can return [`Command`](/oss/graph-api#command) object that allows you to combine both control flow and state updates: + +:::python +```python +def agent(state) -> Command[Literal["agent", "another_agent"]]: + # the condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc. + goto = get_next_agent(...) # 'agent' / 'another_agent' + return Command( + # Specify which agent to call next + goto=goto, + # Update the graph state + update={"my_state_key": "my_state_value"} + ) +``` +::: + +:::js +```typescript +graph.addNode((state) => { + // the condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc. + const goto = getNextAgent(...); // 'agent' / 'another_agent' + return new Command({ + // Specify which agent to call next + goto, + // Update the graph state + update: { myStateKey: "myStateValue" } + }); +}) +``` +::: + +:::python +In a more complex scenario where each agent node is itself a graph (i.e., a [subgraph](/oss/subgraphs)), a node in one of the agent subgraphs might want to navigate to a different agent. For example, if you have two agents, `alice` and `bob` (subgraph nodes in a parent graph), and `alice` needs to navigate to `bob`, you can set `graph=Command.PARENT` in the `Command` object: + +```python +def some_node_inside_alice(state): + return Command( + goto="bob", + update={"my_state_key": "my_state_value"}, + # specify which graph to navigate to (defaults to the current graph) + graph=Command.PARENT, + ) +``` +::: + +:::js +In a more complex scenario where each agent node is itself a graph (i.e., a [subgraph](/oss/subgraphs)), a node in one of the agent subgraphs might want to navigate to a different agent. For example, if you have two agents, `alice` and `bob` (subgraph nodes in a parent graph), and `alice` needs to navigate to `bob`, you can set `graph: Command.PARNT` in the `Command` object: + +```typescript +alice.addNode((state) => { + return new Command({ + goto: "bob", + update: { myStateKey: "myStateValue" }, + // specify which graph to navigate to (defaults to the current graph) + graph: Command.PARENT, + }); +}); +``` +::: + + + :::python + If you need to support visualization for subgraphs communicating using `Command(graph=Command.PARENT)` you would need to wrap them in a node function with `Command` annotation: + Instead of this: + + ```python + builder.add_node(alice) + ``` + + you would need to do this: + + ```python + def call_alice(state) -> Command[Literal["bob"]]: + return alice.invoke(state) + + builder.add_node("alice", call_alice) + ``` + ::: + + :::js + If you need to support visualization for subgraphs communicating using/ `Command({ graph: Command.PARENT })` you would need to wrap them in a node function with `Command` annotation: + + Instead of this: + + ```typescript + builder.addNode("alice", alice); + ``` + + you would need to do this: + + ```typescript + builder.addNode("alice", (state) => alice.invoke(state), { ends: ["bob"] }); + ``` + ::: + + +#### Handoffs as tools + +One of the most common agent types is a [tool-calling agent](/oss/agentic-architectures). For those types of agents, a common pattern is wrapping a handoff in a tool call: + +:::python +```python +from langchain_core.tools import tool + +@tool +def transfer_to_bob(): + """Transfer to bob.""" + return Command( + # name of the agent (node) to go to + goto="bob", + # data to send to the agent + update={"my_state_key": "my_state_value"}, + # indicate to LangGraph that we need to navigate to + # agent node in a parent graph + graph=Command.PARENT, + ) +``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { Command } from "@langchain/langgraph"; +import { z } from "zod"; + +const transferToBob = tool( + async () => { + return new Command({ + // name of the agent (node) to go to + goto: "bob", + // data to send to the agent + update: { myStateKey: "myStateValue" }, + // indicate to LangGraph that we need to navigate to + // agent node in a parent graph + graph: Command.PARENT, + }); + }, + { + name: "transfer_to_bob", + description: "Transfer to bob.", + schema: z.object({}), + } +); +``` +::: + +This is a special case of updating the graph state from tools where, in addition to the state update, the control flow is included as well. + + + :::python + If you want to use tools that return `Command`, you can use the prebuilt @[`create_react_agent`][create_react_agent] / @[`ToolNode`][ToolNode] components, or else implement your own logic: + + ```python + def call_tools(state): + ... + commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls] + return commands + ``` + ::: + + :::js + If you want to use tools that return `Command`, you can use the prebuilt @[`createReactAgent`][create_react_agent] / @[ToolNode] components, or else implement your own logic: + + ```typescript + graph.addNode("call_tools", async (state) => { + // ... tool execution logic + const commands = toolCalls.map((toolCall) => + toolsByName[toolCall.name].invoke(toolCall) + ); + return commands; + }); + ``` + ::: + + +Let's now take a closer look at the different multi-agent architectures. + +### Network + +In this architecture, agents are defined as graph nodes. Each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. This architecture is good for problems that do not have a clear hierarchy of agents or a specific sequence in which agents should be called. + +:::python +```python +from typing import Literal +from langchain_openai import ChatOpenAI +from langgraph.types import Command +from langgraph.graph import StateGraph, MessagesState, START, END + +model = ChatOpenAI() + +def agent_1(state: MessagesState) -> Command[Literal["agent_2", "agent_3", END]]: + # you can pass relevant parts of the state to the LLM (e.g., state["messages"]) + # to determine which agent to call next. a common pattern is to call the model + # with a structured output (e.g. force it to return an output with a "next_agent" field) + response = model.invoke(...) + # route to one of the agents or exit based on the LLM's decision + # if the LLM returns "__end__", the graph will finish execution + return Command( + goto=response["next_agent"], + update={"messages": [response["content"]]}, + ) + +def agent_2(state: MessagesState) -> Command[Literal["agent_1", "agent_3", END]]: + response = model.invoke(...) + return Command( + goto=response["next_agent"], + update={"messages": [response["content"]]}, + ) + +def agent_3(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]: + ... + return Command( + goto=response["next_agent"], + update={"messages": [response["content"]]}, + ) + +builder = StateGraph(MessagesState) +builder.add_node(agent_1) +builder.add_node(agent_2) +builder.add_node(agent_3) + +builder.add_edge(START, "agent_1") +network = builder.compile() +``` +::: + +:::js +```typescript +import { StateGraph, MessagesZodState, START, END } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { Command } from "@langchain/langgraph"; +import { z } from "zod"; + +const model = new ChatOpenAI(); + +const agent1 = async (state: z.infer) => { + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // to determine which agent to call next. a common pattern is to call the model + // with a structured output (e.g. force it to return an output with a "next_agent" field) + const response = await model.invoke(...); + // route to one of the agents or exit based on the LLM's decision + // if the LLM returns "__end__", the graph will finish execution + return new Command({ + goto: response.nextAgent, + update: { messages: [response.content] }, + }); +}; + +const agent2 = async (state: z.infer) => { + const response = await model.invoke(...); + return new Command({ + goto: response.nextAgent, + update: { messages: [response.content] }, + }); +}; + +const agent3 = async (state: z.infer) => { + // ... + return new Command({ + goto: response.nextAgent, + update: { messages: [response.content] }, + }); +}; + +const builder = new StateGraph(MessagesZodState) + .addNode("agent1", agent1, { + ends: ["agent2", "agent3", END] + }) + .addNode("agent2", agent2, { + ends: ["agent1", "agent3", END] + }) + .addNode("agent3", agent3, { + ends: ["agent1", "agent2", END] + }) + .addEdge(START, "agent1"); + +const network = builder.compile(); +``` +::: + +### Supervisor + +In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [`Command`](/oss/graph-api#command) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](/oss/graph-api#map-reduce-and-the-send-api) pattern. + +:::python +```python +from typing import Literal +from langchain_openai import ChatOpenAI +from langgraph.types import Command +from langgraph.graph import StateGraph, MessagesState, START, END + +model = ChatOpenAI() + +def supervisor(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]: + # you can pass relevant parts of the state to the LLM (e.g., state["messages"]) + # to determine which agent to call next. a common pattern is to call the model + # with a structured output (e.g. force it to return an output with a "next_agent" field) + response = model.invoke(...) + # route to one of the agents or exit based on the supervisor's decision + # if the supervisor returns "__end__", the graph will finish execution + return Command(goto=response["next_agent"]) + +def agent_1(state: MessagesState) -> Command[Literal["supervisor"]]: + # you can pass relevant parts of the state to the LLM (e.g., state["messages"]) + # and add any additional logic (different models, custom prompts, structured output, etc.) + response = model.invoke(...) + return Command( + goto="supervisor", + update={"messages": [response]}, + ) + +def agent_2(state: MessagesState) -> Command[Literal["supervisor"]]: + response = model.invoke(...) + return Command( + goto="supervisor", + update={"messages": [response]}, + ) + +builder = StateGraph(MessagesState) +builder.add_node(supervisor) +builder.add_node(agent_1) +builder.add_node(agent_2) + +builder.add_edge(START, "supervisor") + +supervisor = builder.compile() +``` +::: + +:::js +```typescript +import { StateGraph, MessagesZodState, Command, START, END } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; + +const model = new ChatOpenAI(); + +const supervisor = async (state: z.infer) => { + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // to determine which agent to call next. a common pattern is to call the model + // with a structured output (e.g. force it to return an output with a "next_agent" field) + const response = await model.invoke(...); + // route to one of the agents or exit based on the supervisor's decision + // if the supervisor returns "__end__", the graph will finish execution + return new Command({ goto: response.nextAgent }); +}; + +const agent1 = async (state: z.infer) => { + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // and add any additional logic (different models, custom prompts, structured output, etc.) + const response = await model.invoke(...); + return new Command({ + goto: "supervisor", + update: { messages: [response] }, + }); +}; + +const agent2 = async (state: z.infer) => { + const response = await model.invoke(...); + return new Command({ + goto: "supervisor", + update: { messages: [response] }, + }); +}; + +const builder = new StateGraph(MessagesZodState) + .addNode("supervisor", supervisor, { + ends: ["agent1", "agent2", END] + }) + .addNode("agent1", agent1, { + ends: ["supervisor"] + }) + .addNode("agent2", agent2, { + ends: ["supervisor"] + }) + .addEdge(START, "supervisor"); + +const supervisorGraph = builder.compile(); +``` +::: + +:::js +```typescript +import { StateGraph, MessagesZodState, Command, START, END } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; + +const model = new ChatOpenAI(); + +const supervisor = async (state: z.infer) => { + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // to determine which agent to call next. a common pattern is to call the model + // with a structured output (e.g. force it to return an output with a "next_agent" field) + const response = await model.invoke(...); + // route to one of the agents or exit based on the supervisor's decision + // if the supervisor returns "__end__", the graph will finish execution + return new Command({ goto: response.nextAgent }); +}; + +const agent1 = async (state: z.infer) => { + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // and add any additional logic (different models, custom prompts, structured output, etc.) + const response = await model.invoke(...); + return new Command({ + goto: "supervisor", + update: { messages: [response] }, + }); +}; + +const agent2 = async (state: z.infer) => { + const response = await model.invoke(...); + return new Command({ + goto: "supervisor", + update: { messages: [response] }, + }); +}; + +const builder = new StateGraph(MessagesZodState) + .addNode("supervisor", supervisor, { + ends: ["agent1", "agent2", END] + }) + .addNode("agent1", agent1, { + ends: ["supervisor"] + }) + .addNode("agent2", agent2, { + ends: ["supervisor"] + }) + .addEdge(START, "supervisor"); + +const supervisorGraph = builder.compile(); +``` +::: + +Check out this [tutorial](/oss/agent-supervisor) for an example of supervisor multi-agent architecture. + + +### Supervisor + +In this variant of the [supervisor](#supervisor) architecture, we define a supervisor [agent](/oss/agentic-architectures#agent-architectures) which is responsible for calling sub-agents. The sub-agents are exposed to the supervisor as tools, and the supervisor agent decides which tool to call next. The supervisor agent follows a [standard implementation](/oss/agentic-architectures#tool-calling-agent) as an LLM running in a while loop calling tools until it decides to stop. + +:::python +```python +from typing import Annotated +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import InjectedState, create_react_agent + +model = ChatOpenAI() + +# this is the agent function that will be called as tool +# notice that you can pass the state to the tool via InjectedState annotation +def agent_1(state: Annotated[dict, InjectedState]): + # you can pass relevant parts of the state to the LLM (e.g., state["messages"]) + # and add any additional logic (different models, custom prompts, structured output, etc.) + response = model.invoke(...) + # return the LLM response as a string (expected tool response format) + # this will be automatically turned to ToolMessage + # by the prebuilt create_react_agent (supervisor) + return response.content + +def agent_2(state: Annotated[dict, InjectedState]): + response = model.invoke(...) + return response.content + +tools = [agent_1, agent_2] +# the simplest way to build a supervisor w/ tool-calling is to use prebuilt ReAct agent graph +# that consists of a tool-calling LLM node (i.e. supervisor) and a tool-executing node +supervisor = create_react_agent(model, tools) +``` +::: + +:::js +```typescript +import { ChatOpenAI } from "@langchain/openai"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const model = new ChatOpenAI(); + +// this is the agent function that will be called as tool +// notice that you can pass the state to the tool via config parameter +const agent1 = tool( + async (_, config) => { + const state = config.configurable?.state; + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // and add any additional logic (different models, custom prompts, structured output, etc.) + const response = await model.invoke(...); + // return the LLM response as a string (expected tool response format) + // this will be automatically turned to ToolMessage + // by the prebuilt createReactAgent (supervisor) + return response.content; + }, + { + name: "agent1", + description: "Agent 1 description", + schema: z.object({}), + } +); + +const agent2 = tool( + async (_, config) => { + const state = config.configurable?.state; + const response = await model.invoke(...); + return response.content; + }, + { + name: "agent2", + description: "Agent 2 description", + schema: z.object({}), + } +); + +const tools = [agent1, agent2]; +// the simplest way to build a supervisor w/ tool-calling is to use prebuilt ReAct agent graph +// that consists of a tool-calling LLM node (i.e. supervisor) and a tool-executing node +const supervisor = createReactAgent({ llm: model, tools }); +``` +::: + +### Hierarchical + +As you add more agents to your system, it might become too hard for the supervisor to manage all of them. The supervisor might start making poor decisions about which agent to call next, or the context might become too complex for a single supervisor to keep track of. In other words, you end up with the same problems that motivated the multi-agent architecture in the first place. + +To address this, you can design your system _hierarchically_. For example, you can create separate, specialized teams of agents managed by individual supervisors, and a top-level supervisor to manage the teams. + +:::python +```python +from typing import Literal +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, MessagesState, START, END +from langgraph.types import Command +model = ChatOpenAI() + +# define team 1 (same as the single supervisor example above) + +def team_1_supervisor(state: MessagesState) -> Command[Literal["team_1_agent_1", "team_1_agent_2", END]]: + response = model.invoke(...) + return Command(goto=response["next_agent"]) + +def team_1_agent_1(state: MessagesState) -> Command[Literal["team_1_supervisor"]]: + response = model.invoke(...) + return Command(goto="team_1_supervisor", update={"messages": [response]}) + +def team_1_agent_2(state: MessagesState) -> Command[Literal["team_1_supervisor"]]: + response = model.invoke(...) + return Command(goto="team_1_supervisor", update={"messages": [response]}) + +team_1_builder = StateGraph(Team1State) +team_1_builder.add_node(team_1_supervisor) +team_1_builder.add_node(team_1_agent_1) +team_1_builder.add_node(team_1_agent_2) +team_1_builder.add_edge(START, "team_1_supervisor") +team_1_graph = team_1_builder.compile() + +# define team 2 (same as the single supervisor example above) +class Team2State(MessagesState): + next: Literal["team_2_agent_1", "team_2_agent_2", "__end__"] + +def team_2_supervisor(state: Team2State): + ... + +def team_2_agent_1(state: Team2State): + ... + +def team_2_agent_2(state: Team2State): + ... + +team_2_builder = StateGraph(Team2State) +... +team_2_graph = team_2_builder.compile() + + +# define top-level supervisor + +builder = StateGraph(MessagesState) +def top_level_supervisor(state: MessagesState) -> Command[Literal["team_1_graph", "team_2_graph", END]]: + # you can pass relevant parts of the state to the LLM (e.g., state["messages"]) + # to determine which team to call next. a common pattern is to call the model + # with a structured output (e.g. force it to return an output with a "next_team" field) + response = model.invoke(...) + # route to one of the teams or exit based on the supervisor's decision + # if the supervisor returns "__end__", the graph will finish execution + return Command(goto=response["next_team"]) + +builder = StateGraph(MessagesState) +builder.add_node(top_level_supervisor) +builder.add_node("team_1_graph", team_1_graph) +builder.add_node("team_2_graph", team_2_graph) +builder.add_edge(START, "top_level_supervisor") +builder.add_edge("team_1_graph", "top_level_supervisor") +builder.add_edge("team_2_graph", "top_level_supervisor") +graph = builder.compile() +``` +::: + +:::js +```typescript +import { StateGraph, MessagesZodState, Command, START, END } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; + +const model = new ChatOpenAI(); + +// define team 1 (same as the single supervisor example above) + +const team1Supervisor = async (state: z.infer) => { + const response = await model.invoke(...); + return new Command({ goto: response.nextAgent }); +}; + +const team1Agent1 = async (state: z.infer) => { + const response = await model.invoke(...); + return new Command({ + goto: "team1Supervisor", + update: { messages: [response] } + }); +}; + +const team1Agent2 = async (state: z.infer) => { + const response = await model.invoke(...); + return new Command({ + goto: "team1Supervisor", + update: { messages: [response] } + }); +}; + +const team1Builder = new StateGraph(MessagesZodState) + .addNode("team1Supervisor", team1Supervisor, { + ends: ["team1Agent1", "team1Agent2", END] + }) + .addNode("team1Agent1", team1Agent1, { + ends: ["team1Supervisor"] + }) + .addNode("team1Agent2", team1Agent2, { + ends: ["team1Supervisor"] + }) + .addEdge(START, "team1Supervisor"); +const team1Graph = team1Builder.compile(); + +// define team 2 (same as the single supervisor example above) +const team2Supervisor = async (state: z.infer) => { + // ... +}; + +const team2Agent1 = async (state: z.infer) => { + // ... +}; + +const team2Agent2 = async (state: z.infer) => { + // ... +}; + +const team2Builder = new StateGraph(MessagesZodState); +// ... build team2Graph +const team2Graph = team2Builder.compile(); + +// define top-level supervisor + +const topLevelSupervisor = async (state: z.infer) => { + // you can pass relevant parts of the state to the LLM (e.g., state.messages) + // to determine which team to call next. a common pattern is to call the model + // with a structured output (e.g. force it to return an output with a "next_team" field) + const response = await model.invoke(...); + // route to one of the teams or exit based on the supervisor's decision + // if the supervisor returns "__end__", the graph will finish execution + return new Command({ goto: response.nextTeam }); +}; + +const builder = new StateGraph(MessagesZodState) + .addNode("topLevelSupervisor", topLevelSupervisor, { + ends: ["team1Graph", "team2Graph", END] + }) + .addNode("team1Graph", team1Graph) + .addNode("team2Graph", team2Graph) + .addEdge(START, "topLevelSupervisor") + .addEdge("team1Graph", "topLevelSupervisor") + .addEdge("team2Graph", "topLevelSupervisor"); + +const graph = builder.compile(); +``` +::: + +### Custom multi-agent workflow + +In this architecture we add individual agents as graph nodes and define the order in which agents are called ahead of time, in a custom workflow. In LangGraph the workflow can be defined in two ways: + +* **Explicit control flow (normal edges)**: LangGraph allows you to explicitly define the control flow of your application (i.e. the sequence of how agents communicate) explicitly, via [normal graph edges](/oss/graph-api#normal-edges). This is the most deterministic variant of this architecture above — we always know which agent will be called next ahead of time. +* **Dynamic control flow (Command)**: in LangGraph you can allow LLMs to decide parts of your application control flow. This can be achieved by using [`Command`](/oss/graph-api#command). A special case of this is a [supervisor tool-calling](#supervisor-tool-calling) architecture. In that case, the tool-calling LLM powering the supervisor agent will make decisions about the order in which the tools (agents) are being called. + +:::python +```python +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, MessagesState, START + +model = ChatOpenAI() + +def agent_1(state: MessagesState): + response = model.invoke(...) + return {"messages": [response]} + +def agent_2(state: MessagesState): + response = model.invoke(...) + return {"messages": [response]} + +builder = StateGraph(MessagesState) +builder.add_node(agent_1) +builder.add_node(agent_2) +# define the flow explicitly +builder.add_edge(START, "agent_1") +builder.add_edge("agent_1", "agent_2") +``` +::: + +:::js +```typescript +import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; + +const model = new ChatOpenAI(); + +const agent1 = async (state: z.infer) => { + const response = await model.invoke(...); + return { messages: [response] }; +}; + +const agent2 = async (state: z.infer) => { + const response = await model.invoke(...); + return { messages: [response] }; +}; + +const builder = new StateGraph(MessagesZodState) + .addNode("agent1", agent1) + .addNode("agent2", agent2) + // define the flow explicitly + .addEdge(START, "agent1") + .addEdge("agent1", "agent2"); +``` +::: + +## Communication and state management + +The most important thing when building multi-agent systems is figuring out how the agents communicate. + +A common, generic way for agents to communicate is via a list of messages. This opens up the following questions: + +* Do agents communicate [**via handoffs or via tool calls**](#handoffs-vs-tool-calls)? +* What messages are [**passed from one agent to the next**](#message-passing-between-agents)? +* How are [**handoffs represented in the list of messages**](#representing-handoffs-in-message-history)? +* How do you [**manage state for subagents**](#state-management-for-subagents)? + +Additionally, if you are dealing with more complex agents or wish to keep individual agent state separate from the multi-agent system state, you may need to use [**different state schemas**](#using-different-state-schemas). + +### Handoffs vs tool calls + +What is the "payload" that is being passed around between agents? In most of the architectures discussed above, the agents communicate via [handoffs](#handoffs) and pass the [graph state](/oss/graph-api#state) as part of the handoff payload. Specifically, agents pass around lists of messages as part of the graph state. In the case of the [supervisor with tool-calling](#supervisor-tool-calling), the payloads are tool call arguments. + +![](/oss/images/request.png) + +### Message passing between agents + +The most common way for agents to communicate is via a shared state channel, typically a list of messages. This assumes that there is always at least a single channel (key) in the state that is shared by the agents (e.g., `messages`). When communicating via a shared message list, there is an additional consideration: should the agents [share the full history](#sharing-full-thought-process) of their thought process or only [the final result](#sharing-only-final-results)? + +![](/oss/images/response.png) + +#### Sharing full thought process + +Agents can **share the full history** of their thought process (i.e., "scratchpad") with all other agents. This "scratchpad" would typically look like a [list of messages](/oss/graph-api#why-use-messages). The benefit of sharing the full thought process is that it might help other agents make better decisions and improve reasoning ability for the system as a whole. The downside is that as the number of agents and their complexity grows, the "scratchpad" will grow quickly and might require additional strategies for [memory management](/oss/add-memory). + +#### Sharing only final results + +Agents can have their own private "scratchpad" and only **share the final result** with the rest of the agents. This approach might work better for systems with many agents or agents that are more complex. In this case, you would need to define agents with [different state schemas](#using-different-state-schemas). + +For agents called as tools, the supervisor determines the inputs based on the tool schema. Additionally, LangGraph allows [passing state](/oss/call-tools#short-term-memory) to individual tools at runtime, so subordinate agents can access parent state, if needed. + +#### Indicating agent name in messages + +It can be helpful to indicate which agent a particular AI message is from, especially for long message histories. Some LLM providers (like OpenAI) support adding a `name` parameter to messages — you can use that to attach the agent name to the message. If that is not supported, you can consider manually injecting the agent name into the message content, e.g., `alicemessage from alice`. + +### Representing handoffs in message history + +:::python +Handoffs are typically done via the LLM calling a dedicated [handoff tool](#handoffs-as-tools). This is represented as an [AI message](https://python.langchain.com/docs/concepts/messages/#aimessage) with tool calls that is passed to the next agent (LLM). Most LLM providers don't support receiving AI messages with tool calls **without** corresponding tool messages. +::: + +:::js +Handoffs are typically done via the LLM calling a dedicated [handoff tool](#handoffs-as-tools). This is represented as an [AI message](https://js.langchain.com/docs/concepts/messages/#aimessage) with tool calls that is passed to the next agent (LLM). Most LLM providers don't support receiving AI messages with tool calls **without** corresponding tool messages. +::: + +You therefore have two options: + +:::python +1. Add an extra [tool message](https://python.langchain.com/docs/concepts/messages/#toolmessage) to the message list, e.g., "Successfully transferred to agent X" +2. Remove the AI message with the tool calls +::: + +:::js +1. Add an extra [tool message](https://js.langchain.com/docs/concepts/messages/#toolmessage) to the message list, e.g., "Successfully transferred to agent X" +2. Remove the AI message with the tool calls +::: + +In practice, we see that most developers opt for option (1). + +### State management for subagents + +A common practice is to have multiple agents communicating on a shared message list, but only [adding their final messages to the list](#sharing-only-final-results). This means that any intermediate messages (e.g., tool calls) are not saved in this list. + +What if you **do** want to save these messages so that if this particular subagent is invoked in the future you can pass those back in? + +There are two high-level approaches to achieve that: + +:::python +1. Store these messages in the shared message list, but filter the list before passing it to the subagent LLM. For example, you can choose to filter out all tool calls from **other** agents. +2. Store a separate message list for each agent (e.g., `alice_messages`) in the subagent's graph state. This would be their "view" of what the message history looks like. +::: + +:::js +1. Store these messages in the shared message list, but filter the list before passing it to the subagent LLM. For example, you can choose to filter out all tool calls from **other** agents. +2. Store a separate message list for each agent (e.g., `aliceMessages`) in the subagent's graph state. This would be their "view" of what the message history looks like. +::: + +### Using different state schemas + +An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph: + +* Define [subgraph](/oss/subgraphs) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](/oss/subgraphs#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs. +* Define agent node functions with a [private input state schema](/oss/graph-api#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent. diff --git a/src/oss/overview.mdx b/src/oss/overview.mdx new file mode 100644 index 00000000..fee228c5 --- /dev/null +++ b/src/oss/overview.mdx @@ -0,0 +1,66 @@ +--- +title: Overview +--- + +Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents. + +Install LangGraph: + +``` +pip install -U langgraph +``` + +Then, create an agent [using prebuilt components](/oss/prebuilts): + +```python +# pip install -qU "langchain[anthropic]" to call the model + +from langgraph.prebuilt import create_react_agent + +def get_weather(city: str) -> str: + """Get weather for a given city.""" + return f"It's always sunny in {city}!" + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + prompt="You are a helpful assistant" +) + +# Run the agent +agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]} +) +``` + +For more information, see the [Quickstart](/oss/quickstart). Or, to learn how to build an [agent workflow](/oss/graph-api) with a customizable architecture, long-term memory, and other complex task handling, see the [LangGraph basics tutorials](/oss/1-build-basic-chatbot). + +## Core benefits + +LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits: + +* [Durable execution](/oss/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off. +* [Human-in-the-loop](/oss/human-in-the-loop): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution. +* [Comprehensive memory](/oss/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions. +* [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics. +* [Production-ready deployment](/langgraph-platform/deployment-options): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows. + +## LangGraph’s ecosystem + +While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with: + +* [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time. +* [LangGraph Platform](/langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](/langgraph-platform/langgraph-studio). +* [LangChain](https://python.langchain.com/docs/introduction/) – Provides integrations and composable components to streamline LLM application development. + + +## Additional resources + +* [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback. +* [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course. +* [Templates](/oss/template-applications): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted. +* [Case studies](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship AI applications at scale. + +## Acknowledgements + +LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain. diff --git a/src/oss/persistence.mdx b/src/oss/persistence.mdx new file mode 100644 index 00000000..1b630b55 --- /dev/null +++ b/src/oss/persistence.mdx @@ -0,0 +1,1172 @@ +--- +title: Persistence +--- + +LangGraph has a built-in persistence layer, implemented through checkpointers. When you compile a graph with a checkpointer, the checkpointer saves a `checkpoint` of the graph state at every super-step. Those checkpoints are saved to a `thread`, which can be accessed after graph execution. Because `threads` allow access to graph's state after execution, several powerful capabilities including human-in-the-loop, memory, time travel, and fault-tolerance are all possible. Below, we'll discuss each of these concepts in more detail. + +![Checkpoints](/oss/images/checkpoints.jpg) + + + **LangGraph API handles checkpointing automatically** + When using the LangGraph API, you don't need to implement or configure checkpointers manually. The API handles all persistence infrastructure for you behind the scenes. + + +## Threads + +A thread is a unique ID or thread identifier assigned to each checkpoint saved by a checkpointer. It contains the accumulated state of a sequence of [runs](/langgraph-platform/assistants#execution). When a run is executed, the [state](/oss/graph-api#state) of the underlying graph of the assistant will be persisted to the thread. + +When invoking a graph with a checkpointer, you **must** specify a `thread_id` as part of the `configurable` portion of the config: + +:::python +```python +{"configurable": {"thread_id": "1"}} +``` +::: + +:::js +```typescript +{ + configurable: { + thread_id: "1"; + } +} +``` +::: + +A thread's current and historical state can be retrieved. To persist state, a thread must be created prior to executing a run. The LangGraph Platform API provides several endpoints for creating and managing threads and thread state. See the [API reference](https://langchain-ai.github.io/langgraph/cloud/reference/api/) for more details. + +## Checkpoints + +The state of a thread at a particular point in time is called a checkpoint. Checkpoint is a snapshot of the graph state saved at each super-step and is represented by `StateSnapshot` object with the following key properties: + +* `config`: Config associated with this checkpoint. +* `metadata`: Metadata associated with this checkpoint. +* `values`: Values of the state channels at this point in time. +* `next` A tuple of the node names to execute next in the graph. +* `tasks`: A tuple of `PregelTask` objects that contain information about next tasks to be executed. If the step was previously attempted, it will include error information. If a graph was interrupted [dynamically](/oss/add-human-in-the-loop#pause-using-interrupt) from within a node, tasks will contain additional data associated with interrupts. + +Checkpoints are persisted and can be used to restore the state of a thread at a later time. + +Let's see what checkpoints are saved when a simple graph is invoked as follows: + +:::python +```python +from langgraph.graph import StateGraph, START, END +from langgraph.checkpoint.memory import InMemorySaver +from typing import Annotated +from typing_extensions import TypedDict +from operator import add + +class State(TypedDict): + foo: str + bar: Annotated[list[str], add] + +def node_a(state: State): + return {"foo": "a", "bar": ["a"]} + +def node_b(state: State): + return {"foo": "b", "bar": ["b"]} + + +workflow = StateGraph(State) +workflow.add_node(node_a) +workflow.add_node(node_b) +workflow.add_edge(START, "node_a") +workflow.add_edge("node_a", "node_b") +workflow.add_edge("node_b", END) + +checkpointer = InMemorySaver() +graph = workflow.compile(checkpointer=checkpointer) + +config = {"configurable": {"thread_id": "1"}} +graph.invoke({"foo": ""}, config) +``` +::: + +:::js +```typescript +import { StateGraph, START, END, MemoryServer } from "@langchain/langgraph"; +import { withLangGraph } from "@langchain/langgraph/zod"; +import { z } from "zod"; + +const State = z.object({ + foo: z.string(), + bar: withLangGraph(z.array(z.string()), { + reducer: { + fn: (x, y) => x.concat(y), + }, + default: () => [], + }), +}); + +const workflow = new StateGraph(State) + .addNode("nodeA", (state) => { + return { foo: "a", bar: ["a"] }; + }) + .addNode("nodeB", (state) => { + return { foo: "b", bar: ["b"] }; + }) + .addEdge(START, "nodeA") + .addEdge("nodeA", "nodeB") + .addEdge("nodeB", END); + +const checkpointer = new MemorySaver(); +const graph = workflow.compile({ checkpointer }); + +const config = { configurable: { thread_id: "1" } }; +await graph.invoke({ foo: "" }, config); +``` +::: + +:::js +```typescript +import { StateGraph, START, END, MemoryServer } from "@langchain/langgraph"; +import { withLangGraph } from "@langchain/langgraph/zod"; +import { z } from "zod"; + +const State = z.object({ + foo: z.string(), + bar: withLangGraph(z.array(z.string()), { + reducer: { + fn: (x, y) => x.concat(y), + }, + default: () => [], + }), +}); + +const workflow = new StateGraph(State) + .addNode("nodeA", (state) => { + return { foo: "a", bar: ["a"] }; + }) + .addNode("nodeB", (state) => { + return { foo: "b", bar: ["b"] }; + }) + .addEdge(START, "nodeA") + .addEdge("nodeA", "nodeB") + .addEdge("nodeB", END); + +const checkpointer = new MemorySaver(); +const graph = workflow.compile({ checkpointer }); + +const config = { configurable: { thread_id: "1" } }; +await graph.invoke({ foo: "" }, config); +``` +::: + +:::python +After we run the graph, we expect to see exactly 4 checkpoints: + +* empty checkpoint with `START` as the next node to be executed +* checkpoint with the user input `{'foo': '', 'bar': []}` and `node_a` as the next node to be executed +* checkpoint with the outputs of `node_a` `{'foo': 'a', 'bar': ['a']}` and `node_b` as the next node to be executed +* checkpoint with the outputs of `node_b` `{'foo': 'b', 'bar': ['a', 'b']}` and no next nodes to be executed + +Note that we `bar` channel values contain outputs from both nodes as we have a reducer for `bar` channel. +::: + +:::js +After we run the graph, we expect to see exactly 4 checkpoints: + +* empty checkpoint with `START` as the next node to be executed +* checkpoint with the user input `{'foo': '', 'bar': []}` and `nodeA` as the next node to be executed +* checkpoint with the outputs of `nodeA` `{'foo': 'a', 'bar': ['a']}` and `nodeB` as the next node to be executed +* checkpoint with the outputs of `nodeB` `{'foo': 'b', 'bar': ['a', 'b']}` and no next nodes to be executed + +Note that the `bar` channel values contain outputs from both nodes as we have a reducer for the `bar` channel. +::: + +### Get state + +:::python +When interacting with the saved graph state, you **must** specify a [thread identifier](#threads). You can view the _latest_ state of the graph by calling `graph.get_state(config)`. This will return a `StateSnapshot` object that corresponds to the latest checkpoint associated with the thread ID provided in the config or a checkpoint associated with a checkpoint ID for the thread, if provided. + +```python +# get the latest state snapshot +config = {"configurable": {"thread_id": "1"}} +graph.get_state(config) + +# get a state snapshot for a specific checkpoint_id +config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}} +graph.get_state(config) +``` +::: + +:::js +When interacting with the saved graph state, you **must** specify a [thread identifier](#threads). You can view the _latest_ state of the graph by calling `graph.getState(config)`. This will return a `StateSnapshot` object that corresponds to the latest checkpoint associated with the thread ID provided in the config or a checkpoint associated with a checkpoint ID for the thread, if provided. + +```typescript +// get the latest state snapshot +const config = { configurable: { thread_id: "1" } }; +await graph.getState(config); + +// get a state snapshot for a specific checkpoint_id +const config = { + configurable: { + thread_id: "1", + checkpoint_id: "1ef663ba-28fe-6528-8002-5a559208592c", + }, +}; +await graph.getState(config); +``` +::: + +:::python +In our example, the output of `get_state` will look like this: + +``` +StateSnapshot( + values={'foo': 'b', 'bar': ['a', 'b']}, + next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}}, + metadata={'source': 'loop', 'writes': {'node_b': {'foo': 'b', 'bar': ['b']}}, 'step': 2}, + created_at='2024-08-29T19:19:38.821749+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, tasks=() +) +``` +::: + +:::js +In our example, the output of `getState` will look like this: + +``` +StateSnapshot { + values: { foo: 'b', bar: ['a', 'b'] }, + next: [], + config: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' + } + }, + metadata: { + source: 'loop', + writes: { nodeB: { foo: 'b', bar: ['b'] } }, + step: 2 + }, + createdAt: '2024-08-29T19:19:38.821749+00:00', + parentConfig: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' + } + }, + tasks: [] +} +``` +::: + +### Get state history + +:::python +You can get the full history of the graph execution for a given thread by calling `graph.get_state_history(config)`. This will return a list of `StateSnapshot` objects associated with the thread ID provided in the config. Importantly, the checkpoints will be ordered chronologically with the most recent checkpoint / `StateSnapshot` being the first in the list. + +```python +config = {"configurable": {"thread_id": "1"}} +list(graph.get_state_history(config)) +``` +::: + +:::js +You can get the full history of the graph execution for a given thread by calling `graph.getStateHistory(config)`. This will return a list of `StateSnapshot` objects associated with the thread ID provided in the config. Importantly, the checkpoints will be ordered chronologically with the most recent checkpoint / `StateSnapshot` being the first in the list. + +```typescript +const config = { configurable: { thread_id: "1" } }; +for await (const state of graph.getStateHistory(config)) { + console.log(state); +} +``` +::: + +:::python +In our example, the output of `get_state_history` will look like this: + +``` +[ + StateSnapshot( + values={'foo': 'b', 'bar': ['a', 'b']}, + next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}}, + metadata={'source': 'loop', 'writes': {'node_b': {'foo': 'b', 'bar': ['b']}}, 'step': 2}, + created_at='2024-08-29T19:19:38.821749+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, + tasks=(), + ), + StateSnapshot( + values={'foo': 'a', 'bar': ['a']}, next=('node_b',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, + metadata={'source': 'loop', 'writes': {'node_a': {'foo': 'a', 'bar': ['a']}}, 'step': 1}, + created_at='2024-08-29T19:19:38.819946+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f4-6b4a-8000-ca575a13d36a'}}, + tasks=(PregelTask(id='6fb7314f-f114-5413-a1f3-d37dfe98ff44', name='node_b', error=None, interrupts=()),), + ), + StateSnapshot( + values={'foo': '', 'bar': []}, + next=('node_a',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f4-6b4a-8000-ca575a13d36a'}}, + metadata={'source': 'loop', 'writes': None, 'step': 0}, + created_at='2024-08-29T19:19:38.817813+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f0-6c66-bfff-6723431e8481'}}, + tasks=(PregelTask(id='f1b14528-5ee5-579c-949b-23ef9bfbed58', name='node_a', error=None, interrupts=()),), + ), + StateSnapshot( + values={'bar': []}, + next=('__start__',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f0-6c66-bfff-6723431e8481'}}, + metadata={'source': 'input', 'writes': {'foo': ''}, 'step': -1}, + created_at='2024-08-29T19:19:38.816205+00:00', + parent_config=None, + tasks=(PregelTask(id='6d27aa2e-d72b-5504-a36f-8620e54a76dd', name='__start__', error=None, interrupts=()),), + ) +] +``` +::: + +:::js +In our example, the output of `getStateHistory` will look like this: + +``` +[ + StateSnapshot { + values: { foo: 'b', bar: ['a', 'b'] }, + next: [], + config: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' + } + }, + metadata: { + source: 'loop', + writes: { nodeB: { foo: 'b', bar: ['b'] } }, + step: 2 + }, + createdAt: '2024-08-29T19:19:38.821749+00:00', + parentConfig: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' + } + }, + tasks: [] + }, + StateSnapshot { + values: { foo: 'a', bar: ['a'] }, + next: ['nodeB'], + config: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' + } + }, + metadata: { + source: 'loop', + writes: { nodeA: { foo: 'a', bar: ['a'] } }, + step: 1 + }, + createdAt: '2024-08-29T19:19:38.819946+00:00', + parentConfig: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f4-6b4a-8000-ca575a13d36a' + } + }, + tasks: [ + PregelTask { + id: '6fb7314f-f114-5413-a1f3-d37dfe98ff44', + name: 'nodeB', + error: null, + interrupts: [] + } + ] + }, + StateSnapshot { + values: { foo: '', bar: [] }, + next: ['node_a'], + config: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f4-6b4a-8000-ca575a13d36a' + } + }, + metadata: { + source: 'loop', + writes: null, + step: 0 + }, + createdAt: '2024-08-29T19:19:38.817813+00:00', + parentConfig: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f0-6c66-bfff-6723431e8481' + } + }, + tasks: [ + PregelTask { + id: 'f1b14528-5ee5-579c-949b-23ef9bfbed58', + name: 'node_a', + error: null, + interrupts: [] + } + ] + }, + StateSnapshot { + values: { bar: [] }, + next: ['__start__'], + config: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f0-6c66-bfff-6723431e8481' + } + }, + metadata: { + source: 'input', + writes: { foo: '' }, + step: -1 + }, + createdAt: '2024-08-29T19:19:38.816205+00:00', + parentConfig: null, + tasks: [ + PregelTask { + id: '6d27aa2e-d72b-5504-a36f-8620e54a76dd', + name: '__start__', + error: null, + interrupts: [] + } + ] + } +] +``` +::: + +![State](/oss/images/get_state.jpg) + +### Replay + +It's also possible to play-back a prior graph execution. If we `invoke` a graph with a `thread_id` and a `checkpoint_id`, then we will _re-play_ the previously executed steps _before_ a checkpoint that corresponds to the `checkpoint_id`, and only execute the steps _after_ the checkpoint. + +* `thread_id` is the ID of a thread. +* `checkpoint_id` is an identifier that refers to a specific checkpoint within a thread. + +You must pass these when invoking the graph as part of the `configurable` portion of the config: + +:::python +```python +config = {"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}} +graph.invoke(None, config=config) +``` +::: + +:::js +```typescript +const config = { + configurable: { + thread_id: "1", + checkpoint_id: "0c62ca34-ac19-445d-bbb0-5b4984975b2a", + }, +}; +await graph.invoke(null, config); +``` +::: + +Importantly, LangGraph knows whether a particular step has been executed previously. If it has, LangGraph simply _re-plays_ that particular step in the graph and does not re-execute the step, but only for the steps _before_ the provided `checkpoint_id`. All of the steps _after_ `checkpoint_id` will be executed (i.e., a new fork), even if they have been executed previously. See this [how to guide on time-travel to learn more about replaying](/oss/time-travel). + +![Replay](/oss/images/re_play.png) + +### Update state + +:::python +In addition to re-playing the graph from specific `checkpoints`, we can also _edit_ the graph state. We do this using `graph.update_state()`. This method accepts three different arguments: +::: + +:::js +In addition to re-playing the graph from specific `checkpoints`, we can also _edit_ the graph state. We do this using `graph.updateState()`. This method accepts three different arguments: +::: + +#### `config` + +The config should contain `thread_id` specifying which thread to update. When only the `thread_id` is passed, we update (or fork) the current state. Optionally, if we include `checkpoint_id` field, then we fork that selected checkpoint. + +#### `values` + +These are the values that will be used to update the state. Note that this update is treated exactly as any update from a node is treated. This means that these values will be passed to the [reducer](/oss/graph-api#reducers) functions, if they are defined for some of the channels in the graph state. This means that `update_state` does NOT automatically overwrite the channel values for every channel, but only for the channels without reducers. Let's walk through an example. + +Let's assume you have defined the state of your graph with the following schema (see full example above): + +:::python +```python +from typing import Annotated +from typing_extensions import TypedDict +from operator import add + +class State(TypedDict): + foo: int + bar: Annotated[list[str], add] +``` +::: + +:::js +```typescript +import { withLangGraph } from "@langchain/langgraph/zod"; +import { z } from "zod"; + +const State = z.object({ + foo: z.number(), + bar: withLangGraph(z.array(z.string()), { + reducer: { + fn: (x, y) => x.concat(y), + }, + default: () => [], + }), +}); +``` +::: + +Let's now assume the current state of the graph is + +:::python +``` +{"foo": 1, "bar": ["a"]} +``` +::: + +:::js +```typescript +{ foo: 1, bar: ["a"] } +``` +::: + +If you update the state as below: + +:::python +```python +graph.update_state(config, {"foo": 2, "bar": ["b"]}) +``` +::: + +:::js +```typescript +await graph.updateState(config, { foo: 2, bar: ["b"] }); +``` +::: + +Then the new state of the graph will be: + +:::python +``` +{"foo": 2, "bar": ["a", "b"]} +``` + +The `foo` key (channel) is completely changed (because there is no reducer specified for that channel, so `update_state` overwrites it). However, there is a reducer specified for the `bar` key, and so it appends `"b"` to the state of `bar`. +::: + +:::js +```typescript +{ foo: 2, bar: ["a", "b"] } +``` + +The `foo` key (channel) is completely changed (because there is no reducer specified for that channel, so `updateState` overwrites it). However, there is a reducer specified for the `bar` key, and so it appends `"b"` to the state of `bar`. +::: + +#### `as_node` + +:::python +The final thing you can optionally specify when calling `update_state` is `as_node`. If you provided it, the update will be applied as if it came from node `as_node`. If `as_node` is not provided, it will be set to the last node that updated the state, if not ambiguous. The reason this matters is that the next steps to execute depend on the last node to have given an update, so this can be used to control which node executes next. See this [how to guide on time-travel to learn more about forking state](/oss/time-travel). +::: + +:::js +The final thing you can optionally specify when calling `updateState` is `asNode`. If you provide it, the update will be applied as if it came from node `asNode`. If `asNode` is not provided, it will be set to the last node that updated the state, if not ambiguous. The reason this matters is that the next steps to execute depend on the last node to have given an update, so this can be used to control which node executes next. See this [how to guide on time-travel to learn more about forking state](/oss/time-travel). +::: + +![Update](/oss/images/checkpoints_full_story.jpg) + +## Memory Store + +![Model of shared state](/oss/images/shared_state.png) + +A [state schema](/oss/graph-api#schema) specifies a set of keys that are populated as a graph is executed. As discussed above, state can be written by a checkpointer to a thread at each graph step, enabling state persistence. + +But, what if we want to retain some information _across threads_? Consider the case of a chatbot where we want to retain specific information about the user across _all_ chat conversations (e.g., threads) with that user! + +With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](https://python.langchain.com/api_reference/langgraph/index.html#module-langgraph.store) interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable. + + + **LangGraph API handles stores automatically** + When using the LangGraph API, you don't need to implement or configure stores manually. The API handles all storage infrastructure for you behind the scenes. + + +### Basic Usage + +First, let's showcase this in isolation without using LangGraph. + +:::python +```python +from langgraph.store.memory import InMemoryStore +in_memory_store = InMemoryStore() +``` +::: + +:::js +```typescript +import { MemoryStore } from "@langchain/langgraph"; + +const memoryStore = new MemoryStore(); +``` +::: + +Memories are namespaced by a `tuple`, which in this specific example will be `(, "memories")`. The namespace can be any length and represent anything, does not have to be user specific. + +:::python +```python +user_id = "1" +namespace_for_memory = (user_id, "memories") +``` +::: + +:::js +```typescript +const userId = "1"; +const namespaceForMemory = [userId, "memories"]; +``` +::: + +We use the `store.put` method to save memories to our namespace in the store. When we do this, we specify the namespace, as defined above, and a key-value pair for the memory: the key is simply a unique identifier for the memory (`memory_id`) and the value (a dictionary) is the memory itself. + +:::python +```python +memory_id = str(uuid.uuid4()) +memory = {"food_preference" : "I like pizza"} +in_memory_store.put(namespace_for_memory, memory_id, memory) +``` +::: + +:::js +```typescript +import { v4 as uuidv4 } from "uuid"; + +const memoryId = uuidv4(); +const memory = { food_preference: "I like pizza" }; +await memoryStore.put(namespaceForMemory, memoryId, memory); +``` +::: + +We can read out memories in our namespace using the `store.search` method, which will return all memories for a given user as a list. The most recent memory is the last in the list. + +:::python +```python +memories = in_memory_store.search(namespace_for_memory) +memories[-1].dict() +{'value': {'food_preference': 'I like pizza'}, + 'key': '07e0caf4-1631-47b7-b15f-65515d4c1843', + 'namespace': ['1', 'memories'], + 'created_at': '2024-10-02T17:22:31.590602+00:00', + 'updated_at': '2024-10-02T17:22:31.590605+00:00'} +``` + +Each memory type is a Python class ([`Item`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.Item)) with certain attributes. We can access it as a dictionary by converting via `.dict` as above. + +The attributes it has are: + +* `value`: The value (itself a dictionary) of this memory +* `key`: A unique key for this memory in this namespace +* `namespace`: A list of strings, the namespace of this memory type +* `created_at`: Timestamp for when this memory was created +* `updated_at`: Timestamp for when this memory was updated +::: + +:::js +```typescript +const memories = await memoryStore.search(namespaceForMemory); +memories[memories.length - 1]; + +// { +// value: { food_preference: 'I like pizza' }, +// key: '07e0caf4-1631-47b7-b15f-65515d4c1843', +// namespace: ['1', 'memories'], +// createdAt: '2024-10-02T17:22:31.590602+00:00', +// updatedAt: '2024-10-02T17:22:31.590605+00:00' +// } +``` + +The attributes it has are: + +* `value`: The value of this memory +* `key`: A unique key for this memory in this namespace +* `namespace`: A list of strings, the namespace of this memory type +* `createdAt`: Timestamp for when this memory was created +* `updatedAt`: Timestamp for when this memory was updated +::: + +### Semantic Search + +Beyond simple retrieval, the store also supports semantic search, allowing you to find memories based on meaning rather than exact matches. To enable this, configure the store with an embedding model: + +:::python +```python +from langchain.embeddings import init_embeddings + +store = InMemoryStore( + index={ + "embed": init_embeddings("openai:text-embedding-3-small"), # Embedding provider + "dims": 1536, # Embedding dimensions + "fields": ["food_preference", "$"] # Fields to embed + } +) +``` +::: + +:::js +```typescript +import { OpenAIEmbeddings } from "@langchain/openai"; + +const store = new InMemoryStore({ + index: { + embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }), + dims: 1536, + fields: ["food_preference", "$"], // Fields to embed + }, +}); +``` +::: + +Now when searching, you can use natural language queries to find relevant memories: + +:::python +```python +# Find memories about food preferences +# (This can be done after putting memories into the store) +memories = store.search( + namespace_for_memory, + query="What does the user like to eat?", + limit=3 # Return top 3 matches +) +``` +::: + +:::js +```typescript +// Find memories about food preferences +// (This can be done after putting memories into the store) +const memories = await store.search(namespaceForMemory, { + query: "What does the user like to eat?", + limit: 3, // Return top 3 matches +}); +``` +::: + +You can control which parts of your memories get embedded by configuring the `fields` parameter or by specifying the `index` parameter when storing memories: + +:::python +```python +# Store with specific fields to embed +store.put( + namespace_for_memory, + str(uuid.uuid4()), + { + "food_preference": "I love Italian cuisine", + "context": "Discussing dinner plans" + }, + index=["food_preference"] # Only embed "food_preferences" field +) + +# Store without embedding (still retrievable, but not searchable) +store.put( + namespace_for_memory, + str(uuid.uuid4()), + {"system_info": "Last updated: 2024-01-01"}, + index=False +) +``` +::: + +:::js +```typescript +// Store with specific fields to embed +await store.put( + namespaceForMemory, + uuidv4(), + { + food_preference: "I love Italian cuisine", + context: "Discussing dinner plans", + }, + { index: ["food_preference"] } // Only embed "food_preferences" field +); + +// Store without embedding (still retrievable, but not searchable) +await store.put( + namespaceForMemory, + uuidv4(), + { system_info: "Last updated: 2024-01-01" }, + { index: false } +); +``` +::: + +### Using in LangGraph + +:::python +With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `in_memory_store` allows us to store arbitrary information for access _across_ threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows. + +```python +from langgraph.checkpoint.memory import InMemorySaver + +# We need this because we want to enable threads (conversations) +checkpointer = InMemorySaver() + +# ... Define the graph ... + +# Compile the graph with the checkpointer and store +graph = graph.compile(checkpointer=checkpointer, store=in_memory_store) +``` +::: + +:::js +With this all in place, we use the `memoryStore` in LangGraph. The `memoryStore` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `memoryStore` allows us to store arbitrary information for access _across_ threads. We compile the graph with both the checkpointer and the `memoryStore` as follows. + +```typescript +import { MemorySaver } from "@langchain/langgraph"; + +// We need this because we want to enable threads (conversations) +const checkpointer = new MemorySaver(); + +// ... Define the graph ... + +// Compile the graph with the checkpointer and store +const graph = workflow.compile({ checkpointer, store: memoryStore }); +``` +::: + +We invoke the graph with a `thread_id`, as before, and also with a `user_id`, which we'll use to namespace our memories to this particular user as we showed above. + +:::python +```python +# Invoke the graph +user_id = "1" +config = {"configurable": {"thread_id": "1", "user_id": user_id}} + +# First let's just say hi to the AI +for update in graph.stream( + {"messages": [{"role": "user", "content": "hi"}]}, config, stream_mode="updates" +): + print(update) +``` +::: + +:::js +```typescript +// Invoke the graph +const userId = "1"; +const config = { configurable: { thread_id: "1", user_id: userId } }; + +// First let's just say hi to the AI +for await (const update of await graph.stream( + { messages: [{ role: "user", content: "hi" }] }, + { ...config, streamMode: "updates" } +)) { + console.log(update); +} +``` +::: + +:::python +We can access the `in_memory_store` and the `user_id` in _any node_ by passing `store: BaseStore` and `config: RunnableConfig` as node arguments. Here's how we might use semantic search in a node to find relevant memories: + +```python +def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseStore): + + # Get the user id from the config + user_id = config["configurable"]["user_id"] + + # Namespace the memory + namespace = (user_id, "memories") + + # ... Analyze conversation and create a new memory + + # Create a new memory ID + memory_id = str(uuid.uuid4()) + + # We create a new memory + store.put(namespace, memory_id, {"memory": memory}) + +``` +::: + +:::js +We can access the `memoryStore` and the `user_id` in _any node_ by accessing `config` and `store` as node arguments. Here's how we might use semantic search in a node to find relevant memories: + +```typescript +import { + LangGraphRunnableConfig, + BaseStore, + MessagesZodState, +} from "@langchain/langgraph"; +import { z } from "zod"; + +const updateMemory = async ( + state: z.infer, + config: LangGraphRunnableConfig, + store: BaseStore +) => { + // Get the user id from the config + const userId = config.configurable?.user_id; + + // Namespace the memory + const namespace = [userId, "memories"]; + + // ... Analyze conversation and create a new memory + + // Create a new memory ID + const memoryId = uuidv4(); + + // We create a new memory + await store.put(namespace, memoryId, { memory }); +}; +``` +::: + +As we showed above, we can also access the store in any node and use the `store.search` method to get memories. Recall the memories are returned as a list of objects that can be converted to a dictionary. + +:::python +```python +memories[-1].dict() +{'value': {'food_preference': 'I like pizza'}, + 'key': '07e0caf4-1631-47b7-b15f-65515d4c1843', + 'namespace': ['1', 'memories'], + 'created_at': '2024-10-02T17:22:31.590602+00:00', + 'updated_at': '2024-10-02T17:22:31.590605+00:00'} +``` +::: + +:::js +```typescript +memories[memories.length - 1]; +// { +// value: { food_preference: 'I like pizza' }, +// key: '07e0caf4-1631-47b7-b15f-65515d4c1843', +// namespace: ['1', 'memories'], +// createdAt: '2024-10-02T17:22:31.590602+00:00', +// updatedAt: '2024-10-02T17:22:31.590605+00:00' +// } +``` +::: + +We can access the memories and use them in our model call. + +:::python +```python +def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore): + # Get the user id from the config + user_id = config["configurable"]["user_id"] + + # Namespace the memory + namespace = (user_id, "memories") + + # Search based on the most recent message + memories = store.search( + namespace, + query=state["messages"][-1].content, + limit=3 + ) + info = "\n".join([d.value["memory"] for d in memories]) + + # ... Use memories in the model call +``` +::: + +:::js +```typescript +const callModel = async ( + state: z.infer, + config: LangGraphRunnableConfig, + store: BaseStore +) => { + // Get the user id from the config + const userId = config.configurable?.user_id; + + // Namespace the memory + const namespace = [userId, "memories"]; + + // Search based on the most recent message + const memories = await store.search(namespace, { + query: state.messages[state.messages.length - 1].content, + limit: 3, + }); + const info = memories.map((d) => d.value.memory).join("\n"); + + // ... Use memories in the model call +}; +``` +::: + +If we create a new thread, we can still access the same memories so long as the `user_id` is the same. + +:::python +```python +# Invoke the graph +config = {"configurable": {"thread_id": "2", "user_id": "1"}} + +# Let's say hi again +for update in graph.stream( + {"messages": [{"role": "user", "content": "hi, tell me about my memories"}]}, config, stream_mode="updates" +): + print(update) +``` +::: + +:::js +```typescript +// Invoke the graph +const config = { configurable: { thread_id: "2", user_id: "1" } }; + +// Let's say hi again +for await (const update of await graph.stream( + { messages: [{ role: "user", content: "hi, tell me about my memories" }] }, + { ...config, streamMode: "updates" } +)) { + console.log(update); +} +``` +::: + +When we use the LangGraph Platform, either locally (e.g., in LangGraph Studio) or with LangGraph Platform, the base store is available to use by default and does not need to be specified during graph compilation. To enable semantic search, however, you **do** need to configure the indexing settings in your `langgraph.json` file. For example: + +```json +{ + ... + "store": { + "index": { + "embed": "openai:text-embeddings-3-small", + "dims": 1536, + "fields": ["$"] + } + } +} +``` + +See the [deployment guide](/langgraph-platform/semantic-search) for more details and configuration options. + +## Checkpointer libraries + +Under the hood, checkpointing is powered by checkpointer objects that conform to @[BaseCheckpointSaver] interface. LangGraph provides several checkpointer implementations, all implemented via standalone, installable libraries: + +:::python +* `langgraph-checkpoint`: The base interface for checkpointer savers (@[BaseCheckpointSaver]) and serialization/deserialization interface (@[SerializerProtocol][SerializerProtocol]). Includes in-memory checkpointer implementation (@[InMemorySaver][InMemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included. +* `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database (@[SqliteSaver][SqliteSaver] / @[AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately. +* `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database (@[PostgresSaver][PostgresSaver] / @[AsyncPostgresSaver]), used in LangGraph Platform. Ideal for using in production. Needs to be installed separately. +::: + +:::js +* `@langchain/langgraph-checkpoint`: The base interface for checkpointer savers (@[BaseCheckpointSaver][BaseCheckpointSaver]) and serialization/deserialization interface (@[SerializerProtocol][SerializerProtocol]). Includes in-memory checkpointer implementation (@[MemorySaver]) for experimentation. LangGraph comes with `@langchain/langgraph-checkpoint` included. +* `@langchain/langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database (@[SqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately. +* `@langchain/langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database (@[PostgresSaver]), used in LangGraph Platform. Ideal for using in production. Needs to be installed separately. +::: + +### Checkpointer interface + +:::python +Each checkpointer conforms to @[BaseCheckpointSaver] interface and implements the following methods: + +* `.put` - Store a checkpoint with its configuration and metadata. +* `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. [pending writes](#pending-writes)). +* `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`). This is used to populate `StateSnapshot` in `graph.get_state()`. +* `.list` - List checkpoints that match a given configuration and filter criteria. This is used to populate state history in `graph.get_state_history()` + +If the checkpointer is used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), asynchronous versions of the above methods will be used (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). + + + For running your graph asynchronously, you can use `InMemorySaver`, or async versions of Sqlite/Postgres checkpointers -- `AsyncSqliteSaver` / `AsyncPostgresSaver` checkpointers. + +::: + +:::js +Each checkpointer conforms to the @[BaseCheckpointSaver][BaseCheckpointSaver] interface and implements the following methods: + +* `.put` - Store a checkpoint with its configuration and metadata. +* `.putWrites` - Store intermediate writes linked to a checkpoint (i.e. [pending writes](#pending-writes)). +* `.getTuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`). This is used to populate `StateSnapshot` in `graph.getState()`. +* `.list` - List checkpoints that match a given configuration and filter criteria. This is used to populate state history in `graph.getStateHistory()` +::: + +### Serializer + +When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects. + +:::python +`langgraph_checkpoint` defines @[protocol][SerializerProtocol] for implementing serializers provides a default implementation (@[JsonPlusSerializer][JsonPlusSerializer]) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more. + +#### Serialization with `pickle` + +The default serializer, @[`JsonPlusSerializer`][JsonPlusSerializer], uses ormsgpack and JSON under the hood, which is not suitable for all types of objects. + +If you want to fallback to pickle for objects not currently supported by our msgpack encoder (such as Pandas dataframes), +you can use the `pickle_fallback` argument of the `JsonPlusSerializer`: + +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer + +# ... Define the graph ... +graph.compile( + checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True)) +) +``` + +#### Encryption + +Checkpointers can optionally encrypt all persisted state. To enable this, pass an instance of @[`EncryptedSerializer`][EncryptedSerializer] to the `serde` argument of any `BaseCheckpointSaver` implementation. The easiest way to create an encrypted serializer is via @[`from_pycryptodome_aes`][from_pycryptodome_aes], which reads the AES key from the `LANGGRAPH_AES_KEY` environment variable (or accepts a `key` argument): + +```python +import sqlite3 + +from langgraph.checkpoint.serde.encrypted import EncryptedSerializer +from langgraph.checkpoint.sqlite import SqliteSaver + +serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY +checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde) +``` + +```python +from langgraph.checkpoint.serde.encrypted import EncryptedSerializer +from langgraph.checkpoint.postgres import PostgresSaver + +serde = EncryptedSerializer.from_pycryptodome_aes() +checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde) +checkpointer.setup() +``` + +When running on LangGraph Platform, encryption is automatically enabled whenever `LANGGRAPH_AES_KEY` is present, so you only need to provide the environment variable. Other encryption schemes can be used by implementing @[`CipherProtocol`][CipherProtocol] and supplying it to `EncryptedSerializer`. +::: + +:::js +`@langchain/langgraph-checkpoint` defines protocol for implementing serializers and provides a default implementation that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more. +::: + +## Capabilities + +### Human-in-the-loop + +First, checkpointers facilitate [human-in-the-loop workflows](/oss/human-in-the-loop) workflows by allowing humans to inspect, interrupt, and approve graph steps. Checkpointers are needed for these workflows as the human has to be able to view the state of a graph at any point in time, and the graph has to be to resume execution after the human has made any updates to the state. See [the how-to guides](/oss/add-human-in-the-loop) for examples. + +### Memory + +Second, checkpointers allow for ["memory"](/oss/memory) between interactions. In the case of repeated human interactions (like conversations) any follow up messages can be sent to that thread, which will retain its memory of previous ones. See [Add memory](/oss/add-memory) for information on how to add and manage conversation memory using checkpointers. + +### Time Travel + +Third, checkpointers allow for ["time travel"](/oss/time-travel), allowing users to replay prior graph executions to review and / or debug specific graph steps. In addition, checkpointers make it possible to fork the graph state at arbitrary checkpoints to explore alternative trajectories. + +### Fault-tolerance + +Lastly, checkpointing also provides fault-tolerance and error recovery: if one or more nodes fail at a given superstep, you can restart your graph from the last successful step. Additionally, when a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes. + +#### Pending writes + +Additionally, when a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes. diff --git a/src/oss/prebuilt-vs-low-level.mdx b/src/oss/prebuilt-vs-low-level.mdx new file mode 100644 index 00000000..6aec0451 --- /dev/null +++ b/src/oss/prebuilt-vs-low-level.mdx @@ -0,0 +1,4 @@ +--- +title: Prebuilt components vs. low-level orchestration +sidebarTitle: Prebuilt vs. low-level +--- \ No newline at end of file diff --git a/src/oss/prebuilts.mdx b/src/oss/prebuilts.mdx new file mode 100644 index 00000000..fe7f50c4 --- /dev/null +++ b/src/oss/prebuilts.mdx @@ -0,0 +1,339 @@ +--- +title: Agent development using prebuilt components +sidebarTitle: Use prebuilt components +--- + +LangGraph provides both low-level primitives and high-level prebuilt components for building agent-based applications. This section focuses on the prebuilt, ready-to-use components designed to help you construct agentic systems quickly and reliably—without the need to implement orchestration, memory, or human feedback handling from scratch. + +## What is an agent? + +An _agent_ consists of three components: a **large language model (LLM)**, a set of **tools** it can use, and a **prompt** that provides instructions. + +The LLM operates in a loop. In each iteration, it selects a tool to invoke, provides input, receives the result (an observation), and uses that observation to inform the next action. The loop continues until a stopping condition is met — typically when the agent has gathered enough information to respond to the user. + +![Agent loop: the LLM selects tools and uses their outputs to fulfill a user request](/oss/images/agent.png) + +## Key features + +LangGraph includes several capabilities essential for building robust, production-ready agentic systems: + +* [**Memory integration**](/oss/add-memory): Native support for _short-term_ (session-based) and _long-term_ (persistent across sessions) memory, enabling stateful behaviors in chatbots and assistants. +* [**Human-in-the-loop control**](/oss/human-in-the-loop): Execution can pause _indefinitely_ to await human feedback—unlike websocket-based solutions limited to real-time interaction. This enables asynchronous approval, correction, or intervention at any point in the workflow. +* [**Streaming support**](/oss/streaming): Real-time streaming of agent state, model tokens, tool outputs, or combined streams. +* [**Deployment tooling**](/oss/local-server): Includes infrastructure-free deployment tools. [**LangGraph Platform**](/langgraph-platform) supports testing, debugging, and deployment. + * **[Studio](/langgraph-platform/langgraph-studio)**: A visual IDE for inspecting and debugging workflows. + * Supports multiple [**deployment options**](/langgraph-platform/deployment-options) for production. + +## High-level building blocks + +LangGraph comes with a set of prebuilt components that implement common agent behaviors and workflows. These abstractions are built on top of the LangGraph framework, offering a faster path to production while remaining flexible for advanced customization. + +Using LangGraph for agent development allows you to focus on your application's logic and behavior, instead of building and maintaining the supporting infrastructure for state, memory, and human feedback. + +:::python +## Package ecosystem + +The high-level components are organized into several packages, each with a specific focus. + +| Package | Description | Installation | +| ------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------- | +| `langgraph-prebuilt` (part of `langgraph`) | Prebuilt components to [**create agents**](/oss/quickstart) | `pip install -U langgraph langchain` | +| `langgraph-supervisor` | Tools for building [**supervisor**](/oss/multi-agent#supervisor) agents | `pip install -U langgraph-supervisor` | +| `langgraph-swarm` | Tools for building a [**swarm**](/oss/multi-agent#swarm) multi-agent system | `pip install -U langgraph-swarm` | +| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](/oss/mcp) for tool and resource integration | `pip install -U langchain-mcp-adapters` | +| `langmem` | Agent memory management: [**short-term and long-term**](/oss/add-memory) | `pip install -U langmem` | +| `agentevals` | Utilities to [**evaluate agent performance**](/oss/evals) | `pip install -U agentevals` | + +/* +## Visualize an agent graph + +Use the following tool to visualize the graph generated by +@[`create_react_agent`][create_react_agent] +and to view an outline of the corresponding code. +It allows you to explore the infrastructure of the agent as defined by the presence of: + +* [`tools`](/oss/call-tools): A list of tools (functions, APIs, or other callable objects) that the agent can use to perform tasks. +* `pre_model_hook`: A function that is called before the model is invoked. It can be used to condense messages or perform other preprocessing tasks. +* `post_model_hook`: A function that is called after the model is invoked. It can be used to implement guardrails, human-in-the-loop flows, or other postprocessing tasks. +* [`response_format`](/oss/prebuilts#6-configure-structured-output): A data structure used to constrain the type of the final output, e.g., a `pydantic` `BaseModel`. + +
+
+
+

Features

+ + + + +
+
+ +
+

Graph

+ graph image +
+
+ +The following code snippet shows how to create the above agent (and underlying graph) with +@[`create_react_agent`][create_react_agent]: + +
+
+
+ + +*/ + +::: + +:::js +## Package ecosystem + +The high-level components are organized into several packages, each with a specific focus. + +| Package | Description | Installation | +| ------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------- | +| `langgraph` | Prebuilt components to [**create agents**](/oss/quickstart) | `npm install @langchain/langgraph @langchain/core` | +| `langgraph-supervisor` | Tools for building [**supervisor**](/oss/multi-agent#supervisor) agents | `npm install @langchain/langgraph-supervisor` | +| `langgraph-swarm` | Tools for building a [**swarm**](/oss/multi-agent#swarm) multi-agent system | `npm install @langchain/langgraph-swarm` | +| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](/oss/mcp) for tool and resource integration | `npm install @langchain/mcp-adapters` | +| `agentevals` | Utilities to [**evaluate agent performance**](/oss/evals) | `npm install agentevals` | + +/* ## Visualize an agent graph + +Use the following tool to visualize the graph generated by @[`createReactAgent`][create_react_agent] and to view an outline of the corresponding code. It allows you to explore the infrastructure of the agent as defined by the presence of: + +* [`tools`](/oss/tools): A list of tools (functions, APIs, or other callable objects) that the agent can use to perform tasks. +* `preModelHook`: A function that is called before the model is invoked. It can be used to condense messages or perform other preprocessing tasks. +* `postModelHook`: A function that is called after the model is invoked. It can be used to implement guardrails, human-in-the-loop flows, or other postprocessing tasks. +* [`responseFormat`](/oss/quickstart#6-configure-structured-output): A data structure used to constrain the type of the final output (via Zod schemas). + +
+
+
+

Features

+ + + + +
+
+ +
+

Graph

+ graph image +
+
+ +The following code snippet shows how to create the above agent (and underlying graph) with @[`createReactAgent`][create_react_agent]: + +
+
+
+ + +*/ +::: diff --git a/src/oss/pregel.mdx b/src/oss/pregel.mdx new file mode 100644 index 00000000..dfaeddc0 --- /dev/null +++ b/src/oss/pregel.mdx @@ -0,0 +1,659 @@ +--- +title: LangGraph runtime +--- + +:::python +@[Pregel] implements LangGraph's runtime, managing the execution of LangGraph applications. + +Compiling a @[StateGraph][StateGraph] or creating an @[entrypoint][entrypoint] produces a @[Pregel] instance that can be invoked with input. +::: + +:::js +@[Pregel] implements LangGraph's runtime, managing the execution of LangGraph applications. + +Compiling a @[StateGraph][StateGraph] or creating an @[entrypoint][entrypoint] produces a @[Pregel] instance that can be invoked with input. +::: + +This guide explains the runtime at a high level and provides instructions for directly implementing applications with Pregel. + +:::python +> **Note:** The @[Pregel] runtime is named after [Google's Pregel algorithm](https://research.google/pubs/pub37252/), which describes an efficient method for large-scale parallel computation using graphs. +::: + +:::js +> **Note:** The @[Pregel] runtime is named after [Google's Pregel algorithm](https://research.google/pubs/pub37252/), which describes an efficient method for large-scale parallel computation using graphs. +::: + +## Overview + +In LangGraph, Pregel combines [**actors**](https://en.wikipedia.org/wiki/Actor_model) and **channels** into a single application. **Actors** read data from channels and write data to channels. Pregel organizes the execution of the application into multiple steps, following the **Pregel Algorithm**/**Bulk Synchronous Parallel** model. + +Each step consists of three phases: + +* **Plan**: Determine which **actors** to execute in this step. For example, in the first step, select the **actors** that subscribe to the special **input** channels; in subsequent steps, select the **actors** that subscribe to channels updated in the previous step. +* **Execution**: Execute all selected **actors** in parallel, until all complete, or one fails, or a timeout is reached. During this phase, channel updates are invisible to actors until the next step. +* **Update**: Update the channels with the values written by the **actors** in this step. + +Repeat until no **actors** are selected for execution, or a maximum number of steps is reached. + +## Actors + +An **actor** is a `PregelNode`. It subscribes to channels, reads data from them, and writes data to them. It can be thought of as an **actor** in the Pregel algorithm. `PregelNodes` implement LangChain's Runnable interface. + +## Channels + +Channels are used to communicate between actors (PregelNodes). Each channel has a value type, an update type, and an update function – which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. LangGraph provides a number of built-in channels: + +:::python +* @[LastValue][LastValue]: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next. +* @[Topic][Topic]: A configurable PubSub Topic, useful for sending multiple values between **actors**, or for accumulating output. Can be configured to deduplicate values or to accumulate values over the course of multiple steps. +* @[BinaryOperatorAggregate][BinaryOperatorAggregate]: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps; e.g.,`total = BinaryOperatorAggregate(int, operator.add)` +::: + +:::js +* @[LastValue]: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next. +* @[Topic]: A configurable PubSub Topic, useful for sending multiple values between **actors**, or for accumulating output. Can be configured to deduplicate values or to accumulate values over the course of multiple steps. +* @[BinaryOperatorAggregate]: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps; e.g.,`total = BinaryOperatorAggregate(int, operator.add)` +::: + +## Examples + +:::python +While most users will interact with Pregel through the @[StateGraph][StateGraph] API or the @[entrypoint][entrypoint] decorator, it is possible to interact with Pregel directly. +::: + +:::js +While most users will interact with Pregel through the @[StateGraph] API or the @[entrypoint] decorator, it is possible to interact with Pregel directly. +::: + +Below are a few different examples to give you a sense of the Pregel API. + + + + :::python + ```python + from langgraph.channels import EphemeralValue + from langgraph.pregel import Pregel, NodeBuilder + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b") + ) + + app = Pregel( + nodes={"node1": node1}, + channels={ + "a": EphemeralValue(str), + "b": EphemeralValue(str), + }, + input_channels=["a"], + output_channels=["b"], + ) + + app.invoke({"a": "foo"}) + ``` + + ```con + {'b': 'foofoo'} + ``` + ::: + + :::js + ```typescript + import { EphemeralValue } from "@langchain/langgraph/channels"; + import { Pregel, NodeBuilder } from "@langchain/langgraph/pregel"; + + const node1 = new NodeBuilder() + .subscribeOnly("a") + .do((x: string) => x + x) + .writeTo("b"); + + const app = new Pregel({ + nodes: { node1 }, + channels: { + a: new EphemeralValue(), + b: new EphemeralValue(), + }, + inputChannels: ["a"], + outputChannels: ["b"], + }); + + await app.invoke({ a: "foo" }); + ``` + + ```console + { b: 'foofoo' } + ``` + ::: + + + :::python + ```python + from langgraph.channels import LastValue, EphemeralValue + from langgraph.pregel import Pregel, NodeBuilder + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b") + ) + + node2 = ( + NodeBuilder().subscribe_only("b") + .do(lambda x: x + x) + .write_to("c") + ) + + + app = Pregel( + nodes={"node1": node1, "node2": node2}, + channels={ + "a": EphemeralValue(str), + "b": LastValue(str), + "c": EphemeralValue(str), + }, + input_channels=["a"], + output_channels=["b", "c"], + ) + + app.invoke({"a": "foo"}) + ``` + + ```con + {'b': 'foofoo', 'c': 'foofoofoofoo'} + ``` + ::: + + :::js + ```typescript + import { LastValue, EphemeralValue } from "@langchain/langgraph/channels"; + import { Pregel, NodeBuilder } from "@langchain/langgraph/pregel"; + + const node1 = new NodeBuilder() + .subscribeOnly("a") + .do((x: string) => x + x) + .writeTo("b"); + + const node2 = new NodeBuilder() + .subscribeOnly("b") + .do((x: string) => x + x) + .writeTo("c"); + + const app = new Pregel({ + nodes: { node1, node2 }, + channels: { + a: new EphemeralValue(), + b: new LastValue(), + c: new EphemeralValue(), + }, + inputChannels: ["a"], + outputChannels: ["b", "c"], + }); + + await app.invoke({ a: "foo" }); + ``` + + ```console + { b: 'foofoo', c: 'foofoofoofoo' } + ``` + ::: + + + :::python + ```python + from langgraph.channels import EphemeralValue, Topic + from langgraph.pregel import Pregel, NodeBuilder + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b", "c") + ) + + node2 = ( + NodeBuilder().subscribe_to("b") + .do(lambda x: x["b"] + x["b"]) + .write_to("c") + ) + + app = Pregel( + nodes={"node1": node1, "node2": node2}, + channels={ + "a": EphemeralValue(str), + "b": EphemeralValue(str), + "c": Topic(str, accumulate=True), + }, + input_channels=["a"], + output_channels=["c"], + ) + + app.invoke({"a": "foo"}) + ``` + + ```pycon + {'c': ['foofoo', 'foofoofoofoo']} + ``` + ::: + + :::js + ```typescript + import { EphemeralValue, Topic } from "@langchain/langgraph/channels"; + import { Pregel, NodeBuilder } from "@langchain/langgraph/pregel"; + + const node1 = new NodeBuilder() + .subscribeOnly("a") + .do((x: string) => x + x) + .writeTo("b", "c"); + + const node2 = new NodeBuilder() + .subscribeTo("b") + .do((x: { b: string }) => x.b + x.b) + .writeTo("c"); + + const app = new Pregel({ + nodes: { node1, node2 }, + channels: { + a: new EphemeralValue(), + b: new EphemeralValue(), + c: new Topic({ accumulate: true }), + }, + inputChannels: ["a"], + outputChannels: ["c"], + }); + + await app.invoke({ a: "foo" }); + ``` + + ```console + { c: ['foofoo', 'foofoofoofoo'] } + ``` + ::: + + + This examples demonstrates how to use the BinaryOperatorAggregate channel to implement a reducer. + + :::python + ```python + from langgraph.channels import EphemeralValue, BinaryOperatorAggregate + from langgraph.pregel import Pregel, NodeBuilder + + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b", "c") + ) + + node2 = ( + NodeBuilder().subscribe_only("b") + .do(lambda x: x + x) + .write_to("c") + ) + + def reducer(current, update): + if current: + return current + " | " + update + else: + return update + + app = Pregel( + nodes={"node1": node1, "node2": node2}, + channels={ + "a": EphemeralValue(str), + "b": EphemeralValue(str), + "c": BinaryOperatorAggregate(str, operator=reducer), + }, + input_channels=["a"], + output_channels=["c"], + ) + + app.invoke({"a": "foo"}) + ``` + ::: + + :::js + ```typescript + import { EphemeralValue, BinaryOperatorAggregate } from "@langchain/langgraph/channels"; + import { Pregel, NodeBuilder } from "@langchain/langgraph/pregel"; + + const node1 = new NodeBuilder() + .subscribeOnly("a") + .do((x: string) => x + x) + .writeTo("b", "c"); + + const node2 = new NodeBuilder() + .subscribeOnly("b") + .do((x: string) => x + x) + .writeTo("c"); + + const reducer = (current: string, update: string) => { + if (current) { + return current + " | " + update; + } else { + return update; + } + }; + + const app = new Pregel({ + nodes: { node1, node2 }, + channels: { + a: new EphemeralValue(), + b: new EphemeralValue(), + c: new BinaryOperatorAggregate({ operator: reducer }), + }, + inputChannels: ["a"], + outputChannels: ["c"], + }); + + await app.invoke({ a: "foo" }); + ``` + ::: + + + :::python + This example demonstrates how to introduce a cycle in the graph, by having + a chain write to a channel it subscribes to. Execution will continue + until a `None` value is written to the channel. + + ```python + from langgraph.channels import EphemeralValue + from langgraph.pregel import Pregel, NodeBuilder, ChannelWriteEntry + + example_node = ( + NodeBuilder().subscribe_only("value") + .do(lambda x: x + x if len(x) < 10 else None) + .write_to(ChannelWriteEntry("value", skip_none=True)) + ) + + app = Pregel( + nodes={"example_node": example_node}, + channels={ + "value": EphemeralValue(str), + }, + input_channels=["value"], + output_channels=["value"], + ) + + app.invoke({"value": "a"}) + ``` + + ```pycon + {'value': 'aaaaaaaaaaaaaaaa'} + ``` + ::: + + :::js + This example demonstrates how to introduce a cycle in the graph, by having + a chain write to a channel it subscribes to. Execution will continue + until a `null` value is written to the channel. + + ```typescript + import { EphemeralValue } from "@langchain/langgraph/channels"; + import { Pregel, NodeBuilder, ChannelWriteEntry } from "@langchain/langgraph/pregel"; + + const exampleNode = new NodeBuilder() + .subscribeOnly("value") + .do((x: string) => x.length < 10 ? x + x : null) + .writeTo(new ChannelWriteEntry("value", { skipNone: true })); + + const app = new Pregel({ + nodes: { exampleNode }, + channels: { + value: new EphemeralValue(), + }, + inputChannels: ["value"], + outputChannels: ["value"], + }); + + await app.invoke({ value: "a" }); + ``` + + ```console + { value: 'aaaaaaaaaaaaaaaa' } + ``` + ::: + + + +## High-level API + +LangGraph provides two high-level APIs for creating a Pregel application: the [StateGraph (Graph API)](/oss/graph-api) and the [Functional API](/oss/functional-api). + + + + :::python + The @[StateGraph (Graph API)][StateGraph] is a higher-level abstraction that simplifies the creation of Pregel applications. It allows you to define a graph of nodes and edges. When you compile the graph, the StateGraph API automatically creates the Pregel application for you. + + ```python + from typing import TypedDict, Optional + + from langgraph.constants import START + from langgraph.graph import StateGraph + + class Essay(TypedDict): + topic: str + content: Optional[str] + score: Optional[float] + + def write_essay(essay: Essay): + return { + "content": f"Essay about {essay['topic']}", + } + + def score_essay(essay: Essay): + return { + "score": 10 + } + + builder = StateGraph(Essay) + builder.add_node(write_essay) + builder.add_node(score_essay) + builder.add_edge(START, "write_essay") + + # Compile the graph. + # This will return a Pregel instance. + graph = builder.compile() + ``` + ::: + + :::js + The @[StateGraph (Graph API)][StateGraph] is a higher-level abstraction that simplifies the creation of Pregel applications. It allows you to define a graph of nodes and edges. When you compile the graph, the StateGraph API automatically creates the Pregel application for you. + + ```typescript + import { START, StateGraph } from "@langchain/langgraph"; + + interface Essay { + topic: string; + content?: string; + score?: number; + } + + const writeEssay = (essay: Essay) => { + return { + content: `Essay about ${essay.topic}`, + }; + }; + + const scoreEssay = (essay: Essay) => { + return { + score: 10 + }; + }; + + const builder = new StateGraph({ + channels: { + topic: null, + content: null, + score: null, + } + }) + .addNode("writeEssay", writeEssay) + .addNode("scoreEssay", scoreEssay) + .addEdge(START, "writeEssay"); + + // Compile the graph. + // This will return a Pregel instance. + const graph = builder.compile(); + ``` + ::: + + The compiled Pregel instance will be associated with a list of nodes and channels. You can inspect the nodes and channels by printing them. + + :::python + ```python + print(graph.nodes) + ``` + + You will see something like this: + + ```pycon + {'__start__': , + 'write_essay': , + 'score_essay': } + ``` + + ```python + print(graph.channels) + ``` + + You should see something like this + + ```pycon + {'topic': , + 'content': , + 'score': , + '__start__': , + 'write_essay': , + 'score_essay': , + 'branch:__start__:__self__:write_essay': , + 'branch:__start__:__self__:score_essay': , + 'branch:write_essay:__self__:write_essay': , + 'branch:write_essay:__self__:score_essay': , + 'branch:score_essay:__self__:write_essay': , + 'branch:score_essay:__self__:score_essay': , + 'start:write_essay': } + ``` + ::: + + :::js + ```typescript + console.log(graph.nodes); + ``` + + You will see something like this: + + ```console + { + __start__: PregelNode { ... }, + writeEssay: PregelNode { ... }, + scoreEssay: PregelNode { ... } + } + ``` + + ```typescript + console.log(graph.channels); + ``` + + You should see something like this + + ```console + { + topic: LastValue { ... }, + content: LastValue { ... }, + score: LastValue { ... }, + __start__: EphemeralValue { ... }, + writeEssay: EphemeralValue { ... }, + scoreEssay: EphemeralValue { ... }, + 'branch:__start__:__self__:writeEssay': EphemeralValue { ... }, + 'branch:__start__:__self__:scoreEssay': EphemeralValue { ... }, + 'branch:writeEssay:__self__:writeEssay': EphemeralValue { ... }, + 'branch:writeEssay:__self__:scoreEssay': EphemeralValue { ... }, + 'branch:scoreEssay:__self__:writeEssay': EphemeralValue { ... }, + 'branch:scoreEssay:__self__:scoreEssay': EphemeralValue { ... }, + 'start:writeEssay': EphemeralValue { ... } + } + ``` + ::: + + + :::python + In the [Functional API](/oss/functional-api), you can use an @[`entrypoint`][entrypoint] to create a Pregel application. The `entrypoint` decorator allows you to define a function that takes input and returns output. + + ```python + from typing import TypedDict, Optional + + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.func import entrypoint + + class Essay(TypedDict): + topic: str + content: Optional[str] + score: Optional[float] + + + checkpointer = InMemorySaver() + + @entrypoint(checkpointer=checkpointer) + def write_essay(essay: Essay): + return { + "content": f"Essay about {essay['topic']}", + } + + print("Nodes: ") + print(write_essay.nodes) + print("Channels: ") + print(write_essay.channels) + ``` + + ```pycon + Nodes: + {'write_essay': } + Channels: + {'__start__': , '__end__': , '__previous__': } + ``` + ::: + + :::js + In the [Functional API](/oss/functional-api), you can use an @[`entrypoint`][entrypoint] to create a Pregel application. The `entrypoint` decorator allows you to define a function that takes input and returns output. + + ```typescript + import { MemorySaver } from "@langchain/langgraph"; + import { entrypoint } from "@langchain/langgraph/func"; + + interface Essay { + topic: string; + content?: string; + score?: number; + } + + const checkpointer = new MemorySaver(); + + const writeEssay = entrypoint( + { checkpointer, name: "writeEssay" }, + async (essay: Essay) => { + return { + content: `Essay about ${essay.topic}`, + }; + } + ); + + console.log("Nodes: "); + console.log(writeEssay.nodes); + console.log("Channels: "); + console.log(writeEssay.channels); + ``` + + ```console + Nodes: + { writeEssay: PregelNode { ... } } + Channels: + { + __start__: EphemeralValue { ... }, + __end__: LastValue { ... }, + __previous__: LastValue { ... } + } + ``` + ::: + + diff --git a/src/oss/quickstart.mdx b/src/oss/quickstart.mdx new file mode 100644 index 00000000..62b8af2b --- /dev/null +++ b/src/oss/quickstart.mdx @@ -0,0 +1,434 @@ +--- +title: LangGraph quickstart +sidebarTitle: Quickstart +--- + +This guide shows you how to set up and use LangGraph's **prebuilt**, **reusable** components, which are designed to help you construct agentic systems quickly and reliably. + +## Prerequisites + +Before you start this tutorial, ensure you have the following: + +* An [Anthropic](https://console.anthropic.com/settings/keys) API key + +## 1. Install dependencies + +If you haven't already, install LangGraph and LangChain: + +:::python +``` +pip install -U langgraph "langchain[anthropic]" +``` + + + LangChain is installed so the agent can call the [model](https://python.langchain.com/docs/integrations/chat/). + +::: + +:::js +```bash +npm install @langchain/langgraph @langchain/core @langchain/anthropic +``` + + + LangChain is installed so the agent can call the [model](https://js.langchain.com/docs/integrations/chat/). + +::: + +## 2. Create an agent + +:::python +To create an agent, use @[`create_react_agent`][create_react_agent]: + +```python +from langgraph.prebuilt import create_react_agent + +def get_weather(city: str) -> str: # (1)! + """Get weather for a given city.""" + return f"It's always sunny in {city}!" + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", # (2)! + tools=[get_weather], # (3)! + prompt="You are a helpful assistant" # (4)! +) + +# Run the agent +agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]} +) +``` + +1. Define a tool for the agent to use. Tools can be defined as vanilla Python functions. For more advanced tool usage and customization, check the [tools](/oss/call-tools) page. +2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](/oss/models) page. +3. Provide a list of tools for the model to use. +4. Provide a system prompt (instructions) to the language model used by the agent. +::: + +:::js +To create an agent, use [`createReactAgent`](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html): + +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const getWeather = tool( + // (1)! + async ({ city }) => { + return `It's always sunny in ${city}!`; + }, + { + name: "get_weather", + description: "Get weather for a given city.", + schema: z.object({ + city: z.string().describe("The city to get weather for"), + }), + } +); + +const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "anthropic:claude-3-5-sonnet-latest" }), // (2)! + tools: [getWeather], // (3)! + stateModifier: "You are a helpful assistant", // (4)! +}); + +// Run the agent +await agent.invoke({ + messages: [{ role: "user", content: "what is the weather in sf" }], +}); +``` + +1. Define a tool for the agent to use. Tools can be defined using the `tool` function. For more advanced tool usage and customization, check the [tools](/oss/tools) page. +2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](/oss/models) page. +3. Provide a list of tools for the model to use. +4. Provide a system prompt (instructions) to the language model used by the agent. +::: + +## 3. Configure an LLM + +:::python +To configure an LLM with specific parameters, such as temperature, use [init_chat_model](https://python.langchain.com/api_reference/langchain/chat_models/langchain.chat_models.base.init_chat_model.html): + +```python +from langchain.chat_models import init_chat_model +from langgraph.prebuilt import create_react_agent + +# highlight-next-line +model = init_chat_model( + "anthropic:claude-3-7-sonnet-latest", + # highlight-next-line + temperature=0 +) + +agent = create_react_agent( + # highlight-next-line + model=model, + tools=[get_weather], +) +``` +::: + +:::js +To configure an LLM with specific parameters, such as temperature, use a model instance: + +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +// highlight-next-line +const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", + // highlight-next-line + temperature: 0, +}); + +const agent = createReactAgent({ + // highlight-next-line + llm: model, + tools: [getWeather], +}); +``` +::: + +For more information on how to configure LLMs, see [Models](/oss/models). + +## 4. Add a custom prompt + +Prompts instruct the LLM how to behave. Add one of the following types of prompts: + +* **Static**: A string is interpreted as a **system message**. +* **Dynamic**: A list of messages generated at **runtime**, based on input or configuration. + + + + Define a fixed prompt string or list of messages: + + :::python + ```python + from langgraph.prebuilt import create_react_agent + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + # A static prompt that never changes + # highlight-next-line + prompt="Never answer questions about the weather." + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]} + ) + ``` + ::: + + :::js + ```typescript + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { ChatAnthropic } from "@langchain/anthropic"; + + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "anthropic:claude-3-5-sonnet-latest" }), + tools: [getWeather], + // A static prompt that never changes + // highlight-next-line + stateModifier: "Never answer questions about the weather." + }); + + await agent.invoke({ + messages: [{ role: "user", content: "what is the weather in sf" }] + }); + ``` + ::: + + + :::python + Define a function that returns a message list based on the agent's state and configuration: + + ```python + from langchain_core.messages import AnyMessage + from langchain_core.runnables import RunnableConfig + from langgraph.prebuilt.chat_agent_executor import AgentState + from langgraph.prebuilt import create_react_agent + + # highlight-next-line + def prompt(state: AgentState, config: RunnableConfig) -> list[AnyMessage]: # (1)! + user_name = config["configurable"].get("user_name") + system_msg = f"You are a helpful assistant. Address the user as {user_name}." + return [{"role": "system", "content": system_msg}] + state["messages"] + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + # highlight-next-line + prompt=prompt + ) + + agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + config={"configurable": {"user_name": "John Smith"}} + ) + ``` + + 1. Dynamic prompts allow including non-message [context](/oss/context) when constructing an input to the LLM, such as: + * Information passed at runtime, like a `user_id` or API credentials (using `config`). + * Internal agent state updated during a multi-step reasoning process (using `state`). + Dynamic prompts can be defined as functions that take `state` and `config` and return a list of messages to send to the LLM. + ::: + + :::js + Define a function that returns messages based on the agent's state and configuration: + + ```typescript + import { type BaseMessageLike } from "@langchain/core/messages"; + import { type RunnableConfig } from "@langchain/core/runnables"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + // highlight-next-line + const dynamicPrompt = (state: { messages: BaseMessageLike[] }, config: RunnableConfig): BaseMessageLike[] => { // (1)! + const userName = config.configurable?.user_name; + const systemMsg = `You are a helpful assistant. Address the user as ${userName}.`; + return [{ role: "system", content: systemMsg }, ...state.messages]; + }; + + const agent = createReactAgent({ + llm: "anthropic:claude-3-5-sonnet-latest", + tools: [getWeather], + // highlight-next-line + stateModifier: dynamicPrompt + }); + + await agent.invoke( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + // highlight-next-line + { configurable: { user_name: "John Smith" } } + ); + ``` + + 1. Dynamic prompts allow including non-message [context](/oss/context) when constructing an input to the LLM, such as: + * Information passed at runtime, like a `user_id` or API credentials (using `config`). + * Internal agent state updated during a multi-step reasoning process (using `state`). + Dynamic prompts can be defined as functions that take `state` and `config` and return a list of messages to send to the LLM. + ::: + + + +For more information, see [Context](/oss/context). + +## 5. Add memory + +To allow multi-turn conversations with an agent, you need to enable [persistence](/oss/persistence) by providing a checkpointer when creating an agent. At runtime, you need to provide a config containing `thread_id` — a unique identifier for the conversation (session): + +:::python +```python +from langgraph.prebuilt import create_react_agent +from langgraph.checkpoint.memory import InMemorySaver + +# highlight-next-line +checkpointer = InMemorySaver() + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + # highlight-next-line + checkpointer=checkpointer # (1)! +) + +# Run the agent +# highlight-next-line +config = {"configurable": {"thread_id": "1"}} +sf_response = agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + config # (2)! +) +ny_response = agent.invoke( + {"messages": [{"role": "user", "content": "what about new york?"}]}, + # highlight-next-line + config +) +``` + +1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](/oss/add-memory#add-short-term-memory) and [human-in-the-loop](/oss/human-in-the-loop) capabilities. +2. Pass configuration with `thread_id` to be able to resume the same conversation on future agent invocations. +::: + +:::js +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { MemorySaver } from "@langchain/langgraph"; + +// highlight-next-line +const checkpointer = new MemorySaver(); + +const agent = createReactAgent({ + llm: "anthropic:claude-3-5-sonnet-latest", + tools: [getWeather], + // highlight-next-line + checkpointSaver: checkpointer, // (1)! +}); + +// Run the agent +// highlight-next-line +const config = { configurable: { thread_id: "1" } }; +const sfResponse = await agent.invoke( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + // highlight-next-line + config // (2)! +); +const nyResponse = await agent.invoke( + { messages: [{ role: "user", content: "what about new york?" }] }, + // highlight-next-line + config +); +``` + +1. `checkpointSaver` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](/oss/add-memory#add-short-term-memory) and [human-in-the-loop](/oss/human-in-the-loop) capabilities. +2. Pass configuration with `thread_id` to be able to resume the same conversation on future agent invocations. +::: + +:::python +When you enable the checkpointer, it stores agent state at every step in the provided checkpointer database (or in memory, if using `InMemorySaver`). +::: + +:::js +When you enable the checkpointer, it stores agent state at every step in the provided checkpointer database (or in memory, if using `MemorySaver`). +::: + +Note that in the above example, when the agent is invoked the second time with the same `thread_id`, the original message history from the first conversation is automatically included, together with the new user input. + +For more information, see [Memory](/oss/add-memory). + +## 6. Configure structured output + +:::python +To produce structured responses conforming to a schema, use the `response_format` parameter. The schema can be defined with a `Pydantic` model or `TypedDict`. The result will be accessible via the `structured_response` field. + +```python +from pydantic import BaseModel +from langgraph.prebuilt import create_react_agent + +class WeatherResponse(BaseModel): + conditions: str + +agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + # highlight-next-line + response_format=WeatherResponse # (1)! +) + +response = agent.invoke( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]} +) + +# highlight-next-line +response["structured_response"] +``` + +1. When `response_format` is provided, a separate step is added at the end of the agent loop: agent message history is passed to an LLM with structured output to generate a structured response. + To provide a system prompt to this LLM, use a tuple `(prompt, schema)`, e.g., `response_format=(prompt, WeatherResponse)`. +::: + +:::js +To produce structured responses conforming to a schema, use the `responseFormat` parameter. The schema can be defined with a `Zod` schema. The result will be accessible via the `structuredResponse` field. + +```typescript +import { z } from "zod"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const WeatherResponse = z.object({ + conditions: z.string(), +}); + +const agent = createReactAgent({ + llm: "anthropic:claude-3-5-sonnet-latest", + tools: [getWeather], + // highlight-next-line + responseFormat: WeatherResponse, // (1)! +}); + +const response = await agent.invoke({ + messages: [{ role: "user", content: "what is the weather in sf" }], +}); + +// highlight-next-line +response.structuredResponse; +``` + +1. When `responseFormat` is provided, a separate step is added at the end of the agent loop: agent message history is passed to an LLM with structured output to generate a structured response. + To provide a system prompt to this LLM, use an object `{ prompt, schema }`, e.g., `responseFormat: { prompt, schema: WeatherResponse }`. +::: + + + **LLM post-processing** + Structured output requires an additional call to the LLM to format the response according to the schema. + + +## Next steps + +* [Deploy your agent locally](/oss/local-server) +* [Learn more about prebuilt agents](/oss/agentic-architectures) +* [LangGraph Platform quickstart](/langgraph-platform/quick-start-studio) diff --git a/src/oss/reference/agents.mdx b/src/oss/reference/agents.mdx new file mode 100644 index 00000000..ff93276b --- /dev/null +++ b/src/oss/reference/agents.mdx @@ -0,0 +1,4 @@ +--- +title: Agents +url: https://langchain-ai.github.io/langgraph/reference/agents/ +--- \ No newline at end of file diff --git a/src/oss/reference/caching.mdx b/src/oss/reference/caching.mdx new file mode 100644 index 00000000..1e919264 --- /dev/null +++ b/src/oss/reference/caching.mdx @@ -0,0 +1,4 @@ +--- +title: Caching +url: https://langchain-ai.github.io/langgraph/reference/cache/ +--- \ No newline at end of file diff --git a/src/oss/reference/channels.mdx b/src/oss/reference/channels.mdx new file mode 100644 index 00000000..db3d6c7e --- /dev/null +++ b/src/oss/reference/channels.mdx @@ -0,0 +1,4 @@ +--- +title: Channels +url: https://langchain-ai.github.io/langgraph/reference/channels/ +--- \ No newline at end of file diff --git a/src/oss/reference/checkpointers.mdx b/src/oss/reference/checkpointers.mdx new file mode 100644 index 00000000..0dcee015 --- /dev/null +++ b/src/oss/reference/checkpointers.mdx @@ -0,0 +1,4 @@ +--- +title: Checkpointers +url: https://langchain-ai.github.io/langgraph/reference/checkpoints/ +--- \ No newline at end of file diff --git a/src/oss/reference/config.mdx b/src/oss/reference/config.mdx new file mode 100644 index 00000000..fc65b1ad --- /dev/null +++ b/src/oss/reference/config.mdx @@ -0,0 +1,4 @@ +--- +title: Config +url: https://langchain-ai.github.io/langgraph/reference/config/ +--- diff --git a/src/oss/reference/constants.mdx b/src/oss/reference/constants.mdx new file mode 100644 index 00000000..bf9ce0ce --- /dev/null +++ b/src/oss/reference/constants.mdx @@ -0,0 +1,4 @@ +--- +title: Constants +url: https://langchain-ai.github.io/langgraph/reference/constants/ +--- \ No newline at end of file diff --git a/src/oss/reference/errors.mdx b/src/oss/reference/errors.mdx new file mode 100644 index 00000000..08d43169 --- /dev/null +++ b/src/oss/reference/errors.mdx @@ -0,0 +1,4 @@ +--- +title: Errors +url: https://langchain-ai.github.io/langgraph/reference/errors/ +--- \ No newline at end of file diff --git a/src/oss/reference/functional-api.mdx b/src/oss/reference/functional-api.mdx new file mode 100644 index 00000000..8dfffa05 --- /dev/null +++ b/src/oss/reference/functional-api.mdx @@ -0,0 +1,4 @@ +--- +title: Functional API +url: https://langchain-ai.github.io/langgraph/reference/func/ +--- \ No newline at end of file diff --git a/src/oss/reference/graphs.mdx b/src/oss/reference/graphs.mdx new file mode 100644 index 00000000..8b1eddf1 --- /dev/null +++ b/src/oss/reference/graphs.mdx @@ -0,0 +1,4 @@ +--- +title: Graphs +url: https://langchain-ai.github.io/langgraph/reference/graphs/ +--- diff --git a/src/oss/reference/mcp.mdx b/src/oss/reference/mcp.mdx new file mode 100644 index 00000000..d325b899 --- /dev/null +++ b/src/oss/reference/mcp.mdx @@ -0,0 +1,4 @@ +--- +title: MCP +url: https://langchain-ai.github.io/langgraph/reference/mcp/ +--- \ No newline at end of file diff --git a/src/oss/reference/overview.mdx b/src/oss/reference/overview.mdx new file mode 100644 index 00000000..374f1046 --- /dev/null +++ b/src/oss/reference/overview.mdx @@ -0,0 +1,35 @@ +--- +title: Reference +sidebarTitle: Overview +--- + +These pages detail the core interfaces you will use when building with LangGraph. Each section covers a different part of the ecosystem. + + +If you are just getting started, see the [Quickstart](/oss/quickstart) for an introduction to the main concepts and usage patterns. + + +## LangGraph + +The core APIs for the LangGraph open source library. + +- [Graphs](https://langchain-ai.github.io/langgraph/reference/graphs/): Main graph abstraction and usage. +- [Functional API](https://langchain-ai.github.io/langgraph/reference/func/): Functional programming interface for graphs. +- [Pregel](https://langchain-ai.github.io/langgraph/reference/pregel/): Pregel-inspired computation model. +- [Checkpointers](https://langchain-ai.github.io/langgraph/reference/checkpoints/): Saving and restoring graph state. +- [Storage](https://langchain-ai.github.io/langgraph/reference/store/): Storage backends and options. +- [Caching](https://langchain-ai.github.io/langgraph/reference/cache/): Caching mechanisms for performance. +- [Types](https://langchain-ai.github.io/langgraph/reference/types/): Type definitions for graph components. +- [Config](https://langchain-ai.github.io/langgraph/reference/config/): Configuration options. +- [Errors](https://langchain-ai.github.io/langgraph/reference/errors/): Error types and handling. +- [Constants](https://langchain-ai.github.io/langgraph/reference/constants/): Global constants. +- [Channels](https://langchain-ai.github.io/langgraph/reference/channels/): Message passing and channels. + +## Prebuilt components + +Higher-level abstractions for common workflows, agents, and other patterns. + +- [Agents](https://langchain-ai.github.io/langgraph/reference/agents/): Built-in agent patterns. +- [Supervisor](https://langchain-ai.github.io/langgraph/reference/supervisor/): Orchestration and delegation. +- [Swarm](https://langchain-ai.github.io/langgraph/reference/swarm/): Multi-agent collaboration. +- [MCP adapters](https://langchain-ai.github.io/langgraph/reference/mcp/): Integrations with external systems. \ No newline at end of file diff --git a/src/oss/reference/pregel.mdx b/src/oss/reference/pregel.mdx new file mode 100644 index 00000000..23b3a618 --- /dev/null +++ b/src/oss/reference/pregel.mdx @@ -0,0 +1,4 @@ +--- +title: Pregel +url: https://langchain-ai.github.io/langgraph/reference/pregel/ +--- \ No newline at end of file diff --git a/src/oss/reference/runtime.mdx b/src/oss/reference/runtime.mdx new file mode 100644 index 00000000..962b7a17 --- /dev/null +++ b/src/oss/reference/runtime.mdx @@ -0,0 +1,4 @@ +--- +title: Runtime +url: https://langchain-ai.github.io/langgraph/reference/runtime/ +--- \ No newline at end of file diff --git a/src/oss/reference/storage.mdx b/src/oss/reference/storage.mdx new file mode 100644 index 00000000..fa66692f --- /dev/null +++ b/src/oss/reference/storage.mdx @@ -0,0 +1,4 @@ +--- +title: Storage +url: https://langchain-ai.github.io/langgraph/reference/store/ +--- \ No newline at end of file diff --git a/src/oss/reference/supervisor.mdx b/src/oss/reference/supervisor.mdx new file mode 100644 index 00000000..a242d4f9 --- /dev/null +++ b/src/oss/reference/supervisor.mdx @@ -0,0 +1,4 @@ +--- +title: Supervisor +url: https://langchain-ai.github.io/langgraph/reference/supervisor/ +--- \ No newline at end of file diff --git a/src/oss/reference/swarm.mdx b/src/oss/reference/swarm.mdx new file mode 100644 index 00000000..a521f6dc --- /dev/null +++ b/src/oss/reference/swarm.mdx @@ -0,0 +1,4 @@ +--- +title: Swarm +url: https://langchain-ai.github.io/langgraph/reference/swarm/ +--- \ No newline at end of file diff --git a/src/oss/reference/types.mdx b/src/oss/reference/types.mdx new file mode 100644 index 00000000..b187c402 --- /dev/null +++ b/src/oss/reference/types.mdx @@ -0,0 +1,4 @@ +--- +title: Types +url: https://langchain-ai.github.io/langgraph/reference/types/ +--- \ No newline at end of file diff --git a/src/oss/run-an-agent.mdx b/src/oss/run-an-agent.mdx new file mode 100644 index 00000000..97ad1c22 --- /dev/null +++ b/src/oss/run-an-agent.mdx @@ -0,0 +1,310 @@ +--- +title: Run an agent +--- + +Agents support both synchronous and asynchronous execution using either `.invoke()` / `await .ainvoke()` for full responses, or `.stream()` / `.astream()` for **incremental** [streaming](/oss/streaming) output. This section explains how to provide input, interpret output, enable streaming, and control execution limits. + +## Basic usage + +Agents can be executed in two primary modes: + +:::python +* **Synchronous** using `.invoke()` or `.stream()` +* **Asynchronous** using `await .ainvoke()` or `async for` with `.astream()` +::: + +:::js +* **Synchronous** using `.invoke()` or `.stream()` +* **Asynchronous** using `await .invoke()` or `for await` with `.stream()` +::: + +:::python + + + ```python + from langgraph.prebuilt import create_react_agent + + agent = create_react_agent(...) + + # highlight-next-line + response = agent.invoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]}) + ``` + + + ```python + from langgraph.prebuilt import create_react_agent + + agent = create_react_agent(...) + # highlight-next-line + response = await agent.ainvoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]}) + ``` + + +::: + +:::js +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const agent = createReactAgent(...); +// highlight-next-line +const response = await agent.invoke({ + "messages": [ + { "role": "user", "content": "what is the weather in sf" } + ] +}); +``` +::: + +## Inputs and outputs + +Agents use a language model that expects a list of `messages` as an input. Therefore, agent inputs and outputs are stored as a list of `messages` under the `messages` key in the agent [state](/oss/graph-api#working-with-messages-in-graph-state). + +## Input format + +Agent input must be a dictionary with a `messages` key. Supported formats are: + +:::python +| Format | Example | +|--------------------|-------------------------------------------------------------------------------------------------------------------------------| +| String | `{"messages": "Hello"}` — Interpreted as a [HumanMessage](https://python.langchain.com/docs/concepts/messages/#humanmessage) | +| Message dictionary | `{"messages": {"role": "user", "content": "Hello"}}` | +| List of messages | `{"messages": [{"role": "user", "content": "Hello"}]}` | +| With custom state | `{"messages": [{"role": "user", "content": "Hello"}], "user_name": "Alice"}` — If using a custom `state_schema` | +::: + +:::js +| Format | Example | +|--------------------|-------------------------------------------------------------------------------------------------------------------------------| +| String | `{"messages": "Hello"}` — Interpreted as a [HumanMessage](https://js.langchain.com/docs/concepts/messages/#humanmessage) | +| Message dictionary | `{"messages": {"role": "user", "content": "Hello"}}` | +| List of messages | `{"messages": [{"role": "user", "content": "Hello"}]}` | +| With custom state | `{"messages": [{"role": "user", "content": "Hello"}], "user_name": "Alice"}` — If using a custom state definition | +::: + +:::python +Messages are automatically converted into LangChain's internal message format. You can read +more about [LangChain messages](https://python.langchain.com/docs/concepts/messages/#langchain-messages) in the LangChain documentation. +::: + +:::js +Messages are automatically converted into LangChain's internal message format. You can read +more about [LangChain messages](https://js.langchain.com/docs/concepts/messages/#langchain-messages) in the LangChain documentation. +::: + + + **Using custom agent state** + :::python + You can provide additional fields defined in your agent's state schema directly in the input dictionary. This allows dynamic behavior based on runtime data or prior tool outputs. + See the [context guide](/oss/context) for full details. + ::: + + :::js + You can provide additional fields defined in your agent's state directly in the state definition. This allows dynamic behavior based on runtime data or prior tool outputs. + See the [context guide](/oss/context) for full details. + ::: + + + + :::python + A string input for `messages` is converted to a [HumanMessage](https://python.langchain.com/docs/concepts/messages/#humanmessage). This behavior differs from the `prompt` parameter in `create_react_agent`, which is interpreted as a [SystemMessage](https://python.langchain.com/docs/concepts/messages/#systemmessage) when passed as a string. + ::: + + :::js + A string input for `messages` is converted to a [HumanMessage](https://js.langchain.com/docs/concepts/messages/#humanmessage). This behavior differs from the `prompt` parameter in `createReactAgent`, which is interpreted as a [SystemMessage](https://js.langchain.com/docs/concepts/messages/#systemmessage) when passed as a string. + ::: + + +## Output format + +:::python +Agent output is a dictionary containing: + +* `messages`: A list of all messages exchanged during execution (user input, assistant replies, tool invocations). +* Optionally, `structured_response` if [structured output](/oss/quickstart#6-configure-structured-output) is configured. +* If using a custom `state_schema`, additional keys corresponding to your defined fields may also be present in the output. These can hold updated state values from tool execution or prompt logic. +::: + +:::js +Agent output is a dictionary containing: + +* `messages`: A list of all messages exchanged during execution (user input, assistant replies, tool invocations). +* Optionally, `structuredResponse` if [structured output](/oss/quickstart#6-configure-structured-output) is configured. +* If using a custom state definition, additional keys corresponding to your defined fields may also be present in the output. These can hold updated state values from tool execution or prompt logic. +::: + +See the [context guide](/oss/context) for more details on working with custom state schemas and accessing context. + +## Streaming output + +Agents support streaming responses for more responsive applications. This includes: + +* **Progress updates** after each step +* **LLM tokens** as they're generated +* **Custom tool messages** during execution + +Streaming is available in both sync and async modes: + +:::python + + + ```python + for chunk in agent.stream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + stream_mode="updates" + ): + print(chunk) + ``` + + + ```python + async for chunk in agent.astream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + stream_mode="updates" + ): + print(chunk) + ``` + + +::: + +:::js +```typescript +for await (const chunk of agent.stream( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + { streamMode: "updates" } +)) { + console.log(chunk); +} +``` +::: + + + For full details, see the [streaming guide](/oss/streaming). + + +## Max iterations + +:::python +To control agent execution and avoid infinite loops, set a recursion limit. This defines the maximum number of steps the agent can take before raising a `GraphRecursionError`. You can configure `recursion_limit` at runtime or when defining agent via `.with_config()`: +::: + +:::js +To control agent execution and avoid infinite loops, set a recursion limit. This defines the maximum number of steps the agent can take before raising a `GraphRecursionError`. You can configure `recursionLimit` at runtime or when defining agent via `.withConfig()`: +::: + +:::python + + + ```python + from langgraph.errors import GraphRecursionError + from langgraph.prebuilt import create_react_agent + + max_iterations = 3 + # highlight-next-line + recursion_limit = 2 * max_iterations + 1 + agent = create_react_agent( + model="anthropic:claude-3-5-haiku-latest", + tools=[get_weather] + ) + + try: + response = agent.invoke( + {"messages": [{"role": "user", "content": "what's the weather in sf"}]}, + # highlight-next-line + {"recursion_limit": recursion_limit}, + ) + except GraphRecursionError: + print("Agent stopped due to max iterations.") + ``` + + + ```python + from langgraph.errors import GraphRecursionError + from langgraph.prebuilt import create_react_agent + + max_iterations = 3 + # highlight-next-line + recursion_limit = 2 * max_iterations + 1 + agent = create_react_agent( + model="anthropic:claude-3-5-haiku-latest", + tools=[get_weather] + ) + # highlight-next-line + agent_with_recursion_limit = agent.with_config(recursion_limit=recursion_limit) + + try: + response = agent_with_recursion_limit.invoke( + {"messages": [{"role": "user", "content": "what's the weather in sf"}]}, + ) + except GraphRecursionError: + print("Agent stopped due to max iterations.") + ``` + + +::: + +:::js + + + ```typescript + import { GraphRecursionError } from "@langchain/langgraph"; + import { ChatAnthropic } from "@langchain/langgraph/prebuilt"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + const maxIterations = 3; + // highlight-next-line + const recursionLimit = 2 * maxIterations + 1; + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-haiku-latest" }), + tools: [getWeather] + }); + + try { + const response = await agent.invoke( + {"messages": [{"role": "user", "content": "what's the weather in sf"}]}, + // highlight-next-line + { recursionLimit } + ); + } catch (error) { + if (error instanceof GraphRecursionError) { + console.log("Agent stopped due to max iterations."); + } + } + ``` + + + ```typescript + import { GraphRecursionError } from "@langchain/langgraph"; + import { ChatAnthropic } from "@langchain/langgraph/prebuilt"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + const maxIterations = 3; + // highlight-next-line + const recursionLimit = 2 * maxIterations + 1; + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-5-haiku-latest" }), + tools: [getWeather] + }); + // highlight-next-line + const agentWithRecursionLimit = agent.withConfig({ recursionLimit }); + + try { + const response = await agentWithRecursionLimit.invoke( + {"messages": [{"role": "user", "content": "what's the weather in sf"}]}, + ); + } catch (error) { + if (error instanceof GraphRecursionError) { + console.log("Agent stopped due to max iterations."); + } + } + ``` + + +::: + +:::python +## Additional Resources + +* [Async programming in LangChain](https://python.langchain.com/docs/concepts/async) +::: diff --git a/src/oss/run-id-langsmith.mdx b/src/oss/run-id-langsmith.mdx new file mode 100644 index 00000000..260a1cf8 --- /dev/null +++ b/src/oss/run-id-langsmith.mdx @@ -0,0 +1,160 @@ +--- +title: How to pass custom run ID or set tags and metadata for graph runs in LangSmith +--- + + **Prerequisites** + This guide assumes familiarity with the following: + + * [LangSmith Documentation](https://docs.smith.langchain.com) + * [LangSmith Platform](https://smith.langchain.com) + * [RunnableConfig](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig) + * [Add metadata and tags to traces](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain#add-metadata-and-tags-to-traces) + * [Customize run name](https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain#customize-run-name) + + +Debugging graph runs can sometimes be difficult to do in an IDE or terminal. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read the [LangSmith documentation](https://docs.smith.langchain.com) for more information on how to get started. + +To make it easier to identify and analyzed traces generated during graph invocation, you can set additional configuration at run time (see [RunnableConfig](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig)): + +| **Field** | **Type** | **Description** | +|-------------|---------------------|--------------------------------------------------------------------------------------------------------------------| +| run_name | `str` | Name for the tracer run for this call. Defaults to the name of the class. | +| run_id | `UUID` | Unique identifier for the tracer run for this call. If not provided, a new UUID will be generated. | +| tags | `List[str]` | Tags for this call and any sub-calls (e.g., a Chain calling an LLM). You can use these to filter calls. | +| metadata | `Dict[str, Any]` | Metadata for this call and any sub-calls (e.g., a Chain calling an LLM). Keys should be strings, values should be JSON-serializable. | + +LangGraph graphs implement the [LangChain Runnable Interface](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.base.Runnable.html) and accept a second argument (`RunnableConfig`) in methods like `invoke`, `ainvoke`, `stream` etc. + +The LangSmith platform will allow you to search and filter traces based on `run_name`, `run_id`, `tags` and `metadata`. + +## TLDR + +```python +import uuid +# Generate a random UUID -- it must be a UUID +config = {"run_id": uuid.uuid4()}, "tags": ["my_tag1"], "metadata": {"a": 5}} +# Works with all standard Runnable methods +# like invoke, batch, ainvoke, astream_events etc +graph.stream(inputs, config, stream_mode="values") +``` + +The rest of the how to guide will show a full agent. + +## Setup + +First, let's install the required packages and set our API keys + +```python +%%capture --no-stderr +%pip install --quiet -U langgraph langchain_openai +``` + +```python +import getpass +import os + + +def _set_env(var: str): + if not os.environ.get(var): + os.environ[var] = getpass.getpass(f"{var}: ") + + +_set_env("OPENAI_API_KEY") +_set_env("LANGSMITH_API_KEY") +``` + + + Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com). + + +## Define the graph + +For this example we will use the [prebuilt ReAct agent](/oss/agentic-architectures). + +```python +from langchain_openai import ChatOpenAI +from typing import Literal +from langgraph.prebuilt import create_react_agent +from langchain_core.tools import tool + +# First we initialize the model we want to use. +model = ChatOpenAI(model="gpt-4o", temperature=0) + + +# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF) +@tool +def get_weather(city: Literal["nyc", "sf"]): + """Use this to get weather information.""" + if city == "nyc": + return "It might be cloudy in nyc" + elif city == "sf": + return "It's always sunny in sf" + else: + raise AssertionError("Unknown city") + + +tools = [get_weather] + + +# Define the graph +graph = create_react_agent(model, tools=tools) +``` + +## Run your graph + +Now that we've defined our graph let's run it once and view the trace in LangSmith. In order for our trace to be easily accessible in LangSmith, we will pass in a custom `run_id` in the config. + +This assumes that you have set your `LANGSMITH_API_KEY` environment variable. + +Note that you can also configure what project to trace to by setting the `LANGCHAIN_PROJECT` environment variable, by default runs will be traced to the `default` project. + +```python +import uuid + + +def print_stream(stream): + for s in stream: + message = s["messages"][-1] + if isinstance(message, tuple): + print(message) + else: + message.pretty_print() + + +inputs = {"messages": [("user", "what is the weather in sf")]} + +config = {"run_name": "agent_007", "tags": ["cats are awesome"]} + +print_stream(graph.stream(inputs, config, stream_mode="values")) +``` + +**Output:** + +``` +================================ Human Message ================================== + +what is the weather in sf +================================== Ai Message =================================== +Tool Calls: + get_weather (call_9ZudXyMAdlUjptq9oMGtQo8o) + Call ID: call_9ZudXyMAdlUjptq9oMGtQo8o + Args: + city: sf +================================= Tool Message ================================== +Name: get_weather + +It's always sunny in sf +================================== Ai Message =================================== + +The weather in San Francisco is currently sunny. +``` + +## View the trace in LangSmith + +Now that we've ran our graph, let's head over to LangSmith and view our trace. First click into the project that you traced to (in our case the default project). You should see a run with the custom run name "agent_007". + +![LangSmith Trace View](/oss/images/d38d1f2b-0f4c-4707-b531-a3c749de987f.png) + +In addition, you will be able to filter traces after the fact using the tags or metadata provided. For example, + +![LangSmith Filter View](/oss/images/410e0089-2ab8-46bb-a61a-827187fd46b3.png) diff --git a/src/oss/sql-agent.mdx b/src/oss/sql-agent.mdx new file mode 100644 index 00000000..64622755 --- /dev/null +++ b/src/oss/sql-agent.mdx @@ -0,0 +1,561 @@ +--- +title: Build a SQL agent +sidebarTitle: SQL agent +--- +In this tutorial, we will walk through how to build an agent that can answer questions about a SQL database. + +At a high level, the agent will: + +1. Fetch the available tables from the database +2. Decide which tables are relevant to the question +3. Fetch the schemas for the relevant tables +4. Generate a query based on the question and information from the schemas +5. Double-check the query for common mistakes using an LLM +6. Execute the query and return the results +7. Correct mistakes surfaced by the database engine until the query is successful +8. Formulate a response based on the results + + + **Security note** + Building Q&A systems of SQL databases requires executing model-generated SQL queries. There are inherent risks in doing this. Make sure that your database connection permissions are always scoped as narrowly as possible for your agent's needs. This will mitigate though not eliminate the risks of building a model-driven system. + + +## 1. Setup + +Let's first install some dependencies. This tutorial uses SQL database and tool abstractions from [langchain-community](https://python.langchain.com/docs/concepts/architecture/#langchain-community). We will also require a LangChain [chat model](https://python.langchain.com/docs/concepts/chat_models/). + +```python +%%capture --no-stderr +%pip install -U langgraph langchain_community "langchain[openai]" +``` + + + Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. [LangSmith](https://docs.smith.langchain.com) lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph. + + +### Select a LLM + +First we [initialize our LLM](https://python.langchain.com/docs/how_to/chat_models_universal_init/). Any model supporting [tool-calling](https://python.langchain.com/docs/integrations/chat/#featured-providers) should work. We use OpenAI below. + +```python +from langchain.chat_models import init_chat_model + +llm = init_chat_model("openai:gpt-4.1") +``` + +### Configure the database + +We will be creating a SQLite database for this tutorial. SQLite is a lightweight database that is easy to set up and use. We will be loading the `chinook` database, which is a sample database that represents a digital media store. +Find more information about the database [here](https://www.sqlitetutorial.net/sqlite-sample-database/). + +For convenience, we have hosted the database (`Chinook.db`) on a public GCS bucket. + +```python +import requests + +url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db" + +response = requests.get(url) + +if response.status_code == 200: + # Open a local file in binary write mode + with open("Chinook.db", "wb") as file: + # Write the content of the response (the file) to the local file + file.write(response.content) + print("File downloaded and saved as Chinook.db") +else: + print(f"Failed to download the file. Status code: {response.status_code}") +``` + +We will use a handy SQL database wrapper available in the `langchain_community` package to interact with the database. The wrapper provides a simple interface to execute SQL queries and fetch results: + +```python +from langchain_community.utilities import SQLDatabase + +db = SQLDatabase.from_uri("sqlite:///Chinook.db") + +print(f"Dialect: {db.dialect}") +print(f"Available tables: {db.get_usable_table_names()}") +print(f'Sample output: {db.run("SELECT * FROM Artist LIMIT 5;")}') +``` + +**Output:** + +``` +Dialect: sqlite +Available tables: ['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track'] +Sample output: [(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains')] +``` + +### Tools for database interactions + +`langchain-community` implements some built-in tools for interacting with our `SQLDatabase`, including tools for listing tables, reading table schemas, and checking and running queries: + +```python +from langchain_community.agent_toolkits import SQLDatabaseToolkit + +toolkit = SQLDatabaseToolkit(db=db, llm=llm) + +tools = toolkit.get_tools() + +for tool in tools: + print(f"{tool.name}: {tool.description}\n") +``` + +**Output:** + +``` +sql_db_query: Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields. + +sql_db_schema: Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3 + +sql_db_list_tables: Input is an empty string, output is a comma-separated list of tables in the database. + +sql_db_query_checker: Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query! + +``` + +## 2. Using a prebuilt agent + +Given these tools, we can initialize a pre-built agent in a single line. To customize our agents behavior, we write a descriptive system prompt. + +```python +from langgraph.prebuilt import create_react_agent + +system_prompt = """ +You are an agent designed to interact with a SQL database. +Given an input question, create a syntactically correct {dialect} query to run, +then look at the results of the query and return the answer. Unless the user +specifies a specific number of examples they wish to obtain, always limit your +query to at most {top_k} results. + +You can order the results by a relevant column to return the most interesting +examples in the database. Never query for all the columns from a specific table, +only ask for the relevant columns given the question. + +You MUST double check your query before executing it. If you get an error while +executing a query, rewrite the query and try again. + +DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the +database. + +To start you should ALWAYS look at the tables in the database to see what you +can query. Do NOT skip this step. + +Then you should query the schema of the most relevant tables. +""".format( + dialect=db.dialect, + top_k=5, +) + +agent = create_react_agent( + llm, + tools, + prompt=system_prompt, +) +``` + + + This system prompt includes a number of instructions, such as always running specific tools before or after others. In the [next section](#3-customizing-the-agent), we will enforce these behaviors through the graph's structure, providing us a greater degree of control and allowing us to simplify the prompt. + + +Let's run this agent on a sample query and observe its behavior: + +```python +question = "Which genre on average has the longest tracks?" + +for step in agent.stream( + {"messages": [{"role": "user", "content": question}]}, + stream_mode="values", +): + step["messages"][-1].pretty_print() +``` + +**Output:** + +``` +================================ Human Message ================================= + +Which genre on average has the longest tracks? +================================== Ai Message ================================== +Tool Calls: + sql_db_list_tables (call_d8lCgywSroCgpVl558nmXKwA) + Call ID: call_d8lCgywSroCgpVl558nmXKwA + Args: +================================= Tool Message ================================= +Name: sql_db_list_tables + +Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track +================================== Ai Message ================================== +Tool Calls: + sql_db_schema (call_nNf6IIUcwMYLIkE0l6uWkZHe) + Call ID: call_nNf6IIUcwMYLIkE0l6uWkZHe + Args: + table_names: Genre, Track +================================= Tool Message ================================= +Name: sql_db_schema + + +CREATE TABLE "Genre" ( + "GenreId" INTEGER NOT NULL, + "Name" NVARCHAR(120), + PRIMARY KEY ("GenreId") +) + +/* +3 rows from Genre table: +GenreId Name +1 Rock +2 Jazz +3 Metal +*/ + + +CREATE TABLE "Track" ( + "TrackId" INTEGER NOT NULL, + "Name" NVARCHAR(200) NOT NULL, + "AlbumId" INTEGER, + "MediaTypeId" INTEGER NOT NULL, + "GenreId" INTEGER, + "Composer" NVARCHAR(220), + "Milliseconds" INTEGER NOT NULL, + "Bytes" INTEGER, + "UnitPrice" NUMERIC(10, 2) NOT NULL, + PRIMARY KEY ("TrackId"), + FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"), + FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"), + FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId") +) + +/* +3 rows from Track table: +TrackId Name AlbumId MediaTypeId GenreId Composer Milliseconds Bytes UnitPrice +1 For Those About To Rock (We Salute You) 1 1 1 Angus Young, Malcolm Young, Brian Johnson 343719 11170334 0.99 +2 Balls to the Wall 2 2 1 None 342562 5510424 0.99 +3 Fast As a Shark 3 2 1 F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman 230619 3990994 0.99 +*/ +================================== Ai Message ================================== +Tool Calls: + sql_db_query_checker (call_urTRmtiGtTxkwHtscec7Fd2K) + Call ID: call_urTRmtiGtTxkwHtscec7Fd2K + Args: + query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgMilliseconds +FROM Track +JOIN Genre ON Track.GenreId = Genre.GenreId +GROUP BY Genre.Name +ORDER BY AvgMilliseconds DESC +LIMIT 1; +================================= Tool Message ================================= +Name: sql_db_query_checker + +\`\`\`sql +SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgMilliseconds +FROM Track +JOIN Genre ON Track.GenreId = Genre.GenreId +GROUP BY Genre.Name +ORDER BY AvgMilliseconds DESC +LIMIT 1; +\`\`\` +================================== Ai Message ================================== +Tool Calls: + sql_db_query (call_RNMqyUEMv0rvy0UxSwrXY2AV) + Call ID: call_RNMqyUEMv0rvy0UxSwrXY2AV + Args: + query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgMilliseconds +FROM Track +JOIN Genre ON Track.GenreId = Genre.GenreId +GROUP BY Genre.Name +ORDER BY AvgMilliseconds DESC +LIMIT 1; +================================= Tool Message ================================= +Name: sql_db_query + +[('Sci Fi & Fantasy', 2911783.0384615385)] +================================== Ai Message ================================== + +The genre with the longest average track length is "Sci Fi & Fantasy," with an average duration of about 2,911,783 milliseconds (approximately 48.5 minutes) per track. +``` + +This worked well enough: the agent correctly listed the tables, obtained the schemas, wrote a query, checked the query, and ran it to inform its final response. + + + You can inspect all aspects of the above run, including steps taken, tools invoked, what prompts were seen by the LLM, and more in the [LangSmith trace](https://smith.langchain.com/public/bd594960-73e3-474b-b6f2-db039d7c713a/r). + + +## 3. Customizing the agent + +The prebuilt agent lets us get started quickly, but at each step the agent has access to the full set of tools. Above, we relied on the system prompt to constrain its behavior— for example, we instructed the agent to always start with the "list tables" tool, and to always run a query-checker tool before executing the query. + +We can enforce a higher degree of control in LangGraph by customizing the agent. Below, we implement a simple ReAct-agent setup, with dedicated nodes for specific tool-calls. We will use the same [state](/oss/graph-api#state) as the pre-built agent. + +We construct dedicated nodes for the following steps: + +* Listing DB tables +* Calling the "get schema" tool +* Generating a query +* Checking the query + +Putting these steps in dedicated nodes lets us (1) force tool-calls when needed, and (2) customize the prompts associated with each step. + +```python +from typing import Literal +from langchain_core.messages import AIMessage +from langchain_core.runnables import RunnableConfig +from langgraph.graph import END, START, MessagesState, StateGraph +from langgraph.prebuilt import ToolNode + + +get_schema_tool = next(tool for tool in tools if tool.name == "sql_db_schema") +get_schema_node = ToolNode([get_schema_tool], name="get_schema") + +run_query_tool = next(tool for tool in tools if tool.name == "sql_db_query") +run_query_node = ToolNode([run_query_tool], name="run_query") + + +# Example: create a predetermined tool call +def list_tables(state: MessagesState): + tool_call = { + "name": "sql_db_list_tables", + "args": {}, + "id": "abc123", + "type": "tool_call", + } + tool_call_message = AIMessage(content="", tool_calls=[tool_call]) + + list_tables_tool = next(tool for tool in tools if tool.name == "sql_db_list_tables") + tool_message = list_tables_tool.invoke(tool_call) + response = AIMessage(f"Available tables: {tool_message.content}") + + return {"messages": [tool_call_message, tool_message, response]} + + +# Example: force a model to create a tool call +def call_get_schema(state: MessagesState): + # Note that LangChain enforces that all models accept `tool_choice="any"` + # as well as `tool_choice=`. + llm_with_tools = llm.bind_tools([get_schema_tool], tool_choice="any") + response = llm_with_tools.invoke(state["messages"]) + + return {"messages": [response]} + + +generate_query_system_prompt = """ +You are an agent designed to interact with a SQL database. +Given an input question, create a syntactically correct {dialect} query to run, +then look at the results of the query and return the answer. Unless the user +specifies a specific number of examples they wish to obtain, always limit your +query to at most {top_k} results. + +You can order the results by a relevant column to return the most interesting +examples in the database. Never query for all the columns from a specific table, +only ask for the relevant columns given the question. + +DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database. +""".format( + dialect=db.dialect, + top_k=5, +) + + +def generate_query(state: MessagesState): + system_message = { + "role": "system", + "content": generate_query_system_prompt, + } + # We do not force a tool call here, to allow the model to + # respond naturally when it obtains the solution. + llm_with_tools = llm.bind_tools([run_query_tool]) + response = llm_with_tools.invoke([system_message] + state["messages"]) + + return {"messages": [response]} + + +check_query_system_prompt = """ +You are a SQL expert with a strong attention to detail. +Double check the {dialect} query for common mistakes, including: +- Using NOT IN with NULL values +- Using UNION when UNION ALL should have been used +- Using BETWEEN for exclusive ranges +- Data type mismatch in predicates +- Properly quoting identifiers +- Using the correct number of arguments for functions +- Casting to the correct data type +- Using the proper columns for joins + +If there are any of the above mistakes, rewrite the query. If there are no mistakes, +just reproduce the original query. + +You will call the appropriate tool to execute the query after running this check. +""".format(dialect=db.dialect) + + +def check_query(state: MessagesState): + system_message = { + "role": "system", + "content": check_query_system_prompt, + } + + # Generate an artificial user message to check + tool_call = state["messages"][-1].tool_calls[0] + user_message = {"role": "user", "content": tool_call["args"]["query"]} + llm_with_tools = llm.bind_tools([run_query_tool], tool_choice="any") + response = llm_with_tools.invoke([system_message, user_message]) + response.id = state["messages"][-1].id + + return {"messages": [response]} +``` + +Finally, we assemble these steps into a workflow using the Graph API. We define a [conditional edge](/oss/graph-api#conditional-edges) at the query generation step that will route to the query checker if a query is generated, or end if there are no tool calls present, such that the LLM has delivered a response to the query. + +```python +def should_continue(state: MessagesState) -> Literal[END, "check_query"]: + messages = state["messages"] + last_message = messages[-1] + if not last_message.tool_calls: + return END + else: + return "check_query" + + +builder = StateGraph(MessagesState) +builder.add_node(list_tables) +builder.add_node(call_get_schema) +builder.add_node(get_schema_node, "get_schema") +builder.add_node(generate_query) +builder.add_node(check_query) +builder.add_node(run_query_node, "run_query") + +builder.add_edge(START, "list_tables") +builder.add_edge("list_tables", "call_get_schema") +builder.add_edge("call_get_schema", "get_schema") +builder.add_edge("get_schema", "generate_query") +builder.add_conditional_edges( + "generate_query", + should_continue, +) +builder.add_edge("check_query", "run_query") +builder.add_edge("run_query", "generate_query") + +agent = builder.compile() +``` + +We visualize the application below: + +```python +from IPython.display import Image, display +from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles + +display(Image(agent.get_graph().draw_mermaid_png())) +``` + +![Graph](/oss/images/output.png) + +**Note:** When you run this code, it will generate and display a visual representation of the SQL agent graph showing the flow between the different nodes (list_tables → call_get_schema → get_schema → generate_query → check_query → run_query). + +We can now invoke the graph exactly as before: + +```python +question = "Which genre on average has the longest tracks?" + +for step in agent.stream( + {"messages": [{"role": "user", "content": question}]}, + stream_mode="values", +): + step["messages"][-1].pretty_print() +``` + +**Output:** + +``` +================================ Human Message ================================= + +Which genre on average has the longest tracks? +================================== Ai Message ================================== + +Available tables: Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track +================================== Ai Message ================================== +Tool Calls: + sql_db_schema (call_qxKtYiHgf93AiTDin9ez5wFp) + Call ID: call_qxKtYiHgf93AiTDin9ez5wFp + Args: + table_names: Genre,Track +================================= Tool Message ================================= +Name: sql_db_schema + + +CREATE TABLE "Genre" ( + "GenreId" INTEGER NOT NULL, + "Name" NVARCHAR(120), + PRIMARY KEY ("GenreId") +) + +/* +3 rows from Genre table: +GenreId Name +1 Rock +2 Jazz +3 Metal +*/ + + +CREATE TABLE "Track" ( + "TrackId" INTEGER NOT NULL, + "Name" NVARCHAR(200) NOT NULL, + "AlbumId" INTEGER, + "MediaTypeId" INTEGER NOT NULL, + "GenreId" INTEGER, + "Composer" NVARCHAR(220), + "Milliseconds" INTEGER NOT NULL, + "Bytes" INTEGER, + "UnitPrice" NUMERIC(10, 2) NOT NULL, + PRIMARY KEY ("TrackId"), + FOREIGN KEY("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId"), + FOREIGN KEY("GenreId") REFERENCES "Genre" ("GenreId"), + FOREIGN KEY("AlbumId") REFERENCES "Album" ("AlbumId") +) + +/* +3 rows from Track table: +TrackId Name AlbumId MediaTypeId GenreId Composer Milliseconds Bytes UnitPrice +1 For Those About To Rock (We Salute You) 1 1 1 Angus Young, Malcolm Young, Brian Johnson 343719 11170334 0.99 +2 Balls to the Wall 2 2 1 None 342562 5510424 0.99 +3 Fast As a Shark 3 2 1 F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman 230619 3990994 0.99 +*/ +================================== Ai Message ================================== +Tool Calls: + sql_db_query (call_RPN3GABMfb6DTaFTLlwnZxVN) + Call ID: call_RPN3GABMfb6DTaFTLlwnZxVN + Args: + query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgTrackLength +FROM Track +JOIN Genre ON Track.GenreId = Genre.GenreId +GROUP BY Genre.GenreId +ORDER BY AvgTrackLength DESC +LIMIT 1; +================================== Ai Message ================================== +Tool Calls: + sql_db_query (call_PR4s8ymiF3ZQLaoZADXtdqcl) + Call ID: call_PR4s8ymiF3ZQLaoZADXtdqcl + Args: + query: SELECT Genre.Name, AVG(Track.Milliseconds) AS AvgTrackLength +FROM Track +JOIN Genre ON Track.GenreId = Genre.GenreId +GROUP BY Genre.GenreId +ORDER BY AvgTrackLength DESC +LIMIT 1; +================================= Tool Message ================================= +Name: sql_db_query + +[('Sci Fi & Fantasy', 2911783.0384615385)] +================================== Ai Message ================================== + +The genre with the longest tracks on average is "Sci Fi & Fantasy," with an average track length of approximately 2,911,783 milliseconds. +``` + + + See [LangSmith trace](https://smith.langchain.com/public/94b8c9ac-12f7-4692-8706-836a1f30f1ea/r) for the above run. + + +## Next steps + +Check out [this guide](https://docs.smith.langchain.com/evaluation/how_to_guides/langgraph) for evaluating LangGraph applications, including SQL agents like this one, using LangSmith. diff --git a/src/oss/streaming.mdx b/src/oss/streaming.mdx new file mode 100644 index 00000000..8e218e65 --- /dev/null +++ b/src/oss/streaming.mdx @@ -0,0 +1,21 @@ +--- +title: Streaming overview +sidebarTitle: Overview +--- + +LangGraph implements a streaming system to surface real-time updates, allowing for responsive and transparent user experiences. + +LangGraph’s streaming system lets you surface live feedback from graph runs to your app. +There are three main categories of data you can stream: + +1. **Workflow progress** — get state updates after each graph node is executed. +2. **LLM tokens** — stream language model tokens as they’re generated. +3. **Custom updates** — emit user-defined signals (e.g., “Fetched 10/100 records”). + +## What’s possible with LangGraph streaming + +* [**Stream LLM tokens**](/oss/streaming#messages) — capture token streams from anywhere: inside nodes, subgraphs, or tools. +* [**Emit progress notifications from tools**](/oss/streaming#stream-custom-data) — send custom updates or progress signals directly from tool functions. +* [**Stream from subgraphs**](/oss/streaming#stream-subgraph-outputs) — include outputs from both the parent graph and any nested subgraphs. +* [**Use any LLM**](/oss/streaming#use-with-any-llm) — stream tokens from any LLM, even if it's not a LangChain model using the `custom` streaming mode. +* [**Use multiple streaming modes**](/oss/streaming#stream-multiple-modes) — choose from `values` (full state), `updates` (state deltas), `messages` (LLM tokens + metadata), `custom` (arbitrary user data), or `debug` (detailed traces). diff --git a/src/oss/subgraphs.mdx b/src/oss/subgraphs.mdx new file mode 100644 index 00000000..2f96baac --- /dev/null +++ b/src/oss/subgraphs.mdx @@ -0,0 +1,152 @@ +--- +title: Subgraphs overview +sidebarTitle: Overview +--- +A subgraph is a [graph](/oss/graph-api#graphs) that is used as a [node](/oss/graph-api#nodes) in another graph — this is the concept of encapsulation applied to LangGraph. Subgraphs allow you to build complex systems with multiple components that are themselves graphs. + +![Subgraph](/oss/images/subgraph.png) + +Some reasons for using subgraphs are: + +* building [multi-agent systems](/oss/multi-agent) +* when you want to reuse a set of nodes in multiple graphs +* when you want different teams to work on different parts of the graph independently, you can define each part as a subgraph, and as long as the subgraph interface (the input and output schemas) is respected, the parent graph can be built without knowing any details of the subgraph + +The main question when adding subgraphs is how the parent graph and subgraph communicate, i.e. how they pass the [state](/oss/graph-api#state) between each other during the graph execution. There are two scenarios: + +* parent and subgraph have **shared state keys** in their state [schemas](/oss/graph-api#state). In this case, you can [include the subgraph as a node in the parent graph](/oss/use-subgraphs#shared-state-schemas) + :::python + ```python + from langgraph.graph import StateGraph, MessagesState, START + + # Subgraph + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + subgraph_builder = StateGraph(State) + subgraph_builder.add_node(call_model) + ... + # highlight-next-line + subgraph = subgraph_builder.compile() + + # Parent graph + + builder = StateGraph(State) + # highlight-next-line + builder.add_node("subgraph_node", subgraph) + builder.add_edge(START, "subgraph_node") + graph = builder.compile() + ... + graph.invoke({"messages": [{"role": "user", "content": "hi!"}]}) + ``` + ::: + :::js + ```typescript + import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; + + // Subgraph + + const subgraphBuilder = new StateGraph(MessagesZodState).addNode( + "callModel", + async (state) => { + const response = await model.invoke(state.messages); + return { messages: response }; + } + ); + // ... other nodes and edges + // highlight-next-line + const subgraph = subgraphBuilder.compile(); + + // Parent graph + + const builder = new StateGraph(MessagesZodState) + // highlight-next-line + .addNode("subgraphNode", subgraph) + .addEdge(START, "subgraphNode"); + const graph = builder.compile(); + // ... + await graph.invoke({ messages: [{ role: "user", content: "hi!" }] }); + ``` + ::: +* parent graph and subgraph have **different schemas** (no shared state keys in their state [schemas](/oss/graph-api#state)). In this case, you have to [call the subgraph from inside a node in the parent graph](/oss/use-subgraphs#different-state-schemas): this is useful when the parent graph and the subgraph have different state schemas and you need to transform state before or after calling the subgraph + :::python + ```python + from typing_extensions import TypedDict, Annotated + from langchain_core.messages import AnyMessage + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.graph.message import add_messages + + class SubgraphMessagesState(TypedDict): + # highlight-next-line + subgraph_messages: Annotated[list[AnyMessage], add_messages] + + # Subgraph + + # highlight-next-line + def call_model(state: SubgraphMessagesState): + response = model.invoke(state["subgraph_messages"]) + return {"subgraph_messages": response} + + subgraph_builder = StateGraph(SubgraphMessagesState) + subgraph_builder.add_node("call_model_from_subgraph", call_model) + subgraph_builder.add_edge(START, "call_model_from_subgraph") + ... + # highlight-next-line + subgraph = subgraph_builder.compile() + + # Parent graph + + def call_subgraph(state: MessagesState): + response = subgraph.invoke({"subgraph_messages": state["messages"]}) + return {"messages": response["subgraph_messages"]} + + builder = StateGraph(State) + # highlight-next-line + builder.add_node("subgraph_node", call_subgraph) + builder.add_edge(START, "subgraph_node") + graph = builder.compile() + ... + graph.invoke({"messages": [{"role": "user", "content": "hi!"}]}) + ``` + ::: + :::js + ```typescript + import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; + import { z } from "zod"; + + const SubgraphState = z.object({ + // highlight-next-line + subgraphMessages: MessagesZodState.shape.messages, + }); + + // Subgraph + + const subgraphBuilder = new StateGraph(SubgraphState) + // highlight-next-line + .addNode("callModelFromSubgraph", async (state) => { + const response = await model.invoke(state.subgraphMessages); + return { subgraphMessages: response }; + }) + .addEdge(START, "callModelFromSubgraph"); + // ... + // highlight-next-line + const subgraph = subgraphBuilder.compile(); + + // Parent graph + + const builder = new StateGraph(MessagesZodState) + // highlight-next-line + .addNode("subgraphNode", async (state) => { + const response = await subgraph.invoke({ + subgraphMessages: state.messages, + }); + return { messages: response.subgraphMessages }; + }) + .addEdge(START, "subgraphNode"); + const graph = builder.compile(); + // ... + await graph.invoke({ messages: [{ role: "user", content: "hi!" }] }); + ``` + ::: diff --git a/src/oss/template-applications.mdx b/src/oss/template-applications.mdx new file mode 100644 index 00000000..134069e5 --- /dev/null +++ b/src/oss/template-applications.mdx @@ -0,0 +1,111 @@ +--- +title: Template Applications +--- + +Templates are open source reference applications designed to help you get started quickly when building with LangGraph. They provide working examples of common agentic workflows that can be customized to your needs. + +You can create an application from a template using the LangGraph CLI. + +:::python + + **Requirements** + * Python >= 3.11 + * [LangGraph CLI](/langgraph-platform/cli): Requires langchain-cli[inmem] >= 0.1.58 + + +## Install the LangGraph CLI + +```bash +pip install "langgraph-cli[inmem]" --upgrade +``` + +Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): + +```bash +uvx --from "langgraph-cli[inmem]" langgraph dev --help +``` +::: + +:::js +```bash +npx @langchain/langgraph-cli --help +``` +::: + +## Available Templates + +:::python +| Template | Description | Link | +| -------- | ----------- | ------ | +| **New LangGraph Project** | A simple, minimal chatbot with memory. | [Repo](https://github.com/langchain-ai/new-langgraph-project) | +| **ReAct Agent** | A simple agent that can be flexibly extended to many tools. | [Repo](https://github.com/langchain-ai/react-agent) | +| **Memory Agent** | A ReAct-style agent with an additional tool to store memories for use across threads. | [Repo](https://github.com/langchain-ai/memory-agent) | +| **Retrieval Agent** | An agent that includes a retrieval-based question-answering system. | [Repo](https://github.com/langchain-ai/retrieval-agent-template) | +| **Data-Enrichment Agent** | An agent that performs web searches and organizes its findings into a structured format. | [Repo](https://github.com/langchain-ai/data-enrichment) | +::: + +:::js +| Template | Description | Link | +| -------- | ----------- | ------ | +| **New LangGraph Project** | A simple, minimal chatbot with memory. | [Repo](https://github.com/langchain-ai/new-langgraphjs-project) | +| **ReAct Agent** | A simple agent that can be flexibly extended to many tools. | [Repo](https://github.com/langchain-ai/react-agent-js) | +| **Memory Agent** | A ReAct-style agent with an additional tool to store memories for use across threads. | [Repo](https://github.com/langchain-ai/memory-agent-js) | +| **Retrieval Agent** | An agent that includes a retrieval-based question-answering system. | [Repo](https://github.com/langchain-ai/retrieval-agent-template-js) | +| **Data-Enrichment Agent** | An agent that performs web searches and organizes its findings into a structured format. | [Repo](https://github.com/langchain-ai/data-enrichment-js) | +::: + +## 🌱 Create a LangGraph App + +To create a new app from a template, use the `langgraph new` command. + +:::python +```bash +langgraph new +``` + +Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): + +```bash +uvx --from "langgraph-cli[inmem]" langgraph new +``` +::: + +:::js +```bash +npm create langgraph +``` +::: + +## Next Steps + +Review the `README.md` file in the root of your new LangGraph app for more information about the template and how to customize it. + +After configuring the app properly and adding your API keys, you can start the app using the LangGraph CLI: + +:::python +```bash +langgraph dev +``` + +Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): + +```bash +uvx --from "langgraph-cli[inmem]" --with-editable . langgraph dev +``` + + + **Missing Local Package?** + If you are not using `uv` and run into a "`ModuleNotFoundError`" or "`ImportError`", even after installing the local package (`pip install -e .`), it is likely the case that you need to install the CLI into your local virtual environment to make the CLI "aware" of the local package. You can do this by running `python -m pip install "langgraph-cli[inmem]"` and re-activating your virtual environment before running `langgraph dev`. + +::: + +:::js +```bash +npx @langchain/langgraph-cli dev +``` +::: + +See the following guides for more information on how to deploy your app: + +* **[Launch Local LangGraph Server](/oss/local-server)**: This quick start guide shows how to start a LangGraph Server locally for the **ReAct Agent** template. The steps are similar for other templates. +* **[Deploy to LangGraph Platform](/langgraph-platform/quick-start-studio)**: Deploy your LangGraph app using LangGraph Platform. diff --git a/src/oss/time-travel.mdx b/src/oss/time-travel.mdx new file mode 100644 index 00000000..5c6063de --- /dev/null +++ b/src/oss/time-travel.mdx @@ -0,0 +1,16 @@ +--- +title: Time travel overview +sidebarTitle: Overview +--- + +When working with non-deterministic systems that make model-based decisions (e.g., agents powered by LLMs), it can be useful to examine their decision-making process in detail: + +1. 🤔 **Understand reasoning**: Analyze the steps that led to a successful result. +2. 🐞 **Debug mistakes**: Identify where and why errors occurred. +3. 🔍 **Explore alternatives**: Test different paths to uncover better solutions. + +LangGraph provides [time travel functionality](/oss/time-travel) to support these use cases. Specifically, you can resume execution from a prior checkpoint — either replaying the same state or modifying it to explore alternatives. In all cases, resuming past execution produces a new fork in the history. + + + For information on how to use time travel, see [Use time travel](/oss/time-travel) and [Time travel using Server API](/langgraph-platform/human-in-the-loop-time-travel). + diff --git a/src/oss/tools.mdx b/src/oss/tools.mdx new file mode 100644 index 00000000..12284b63 --- /dev/null +++ b/src/oss/tools.mdx @@ -0,0 +1,149 @@ +--- +title: Tools overview +sidebarTitle: Overview +--- + +Many AI applications interact with users via natural language. However, some use cases require models to interface directly with external systems—such as APIs, databases, or file systems—using structured input. In these scenarios, [tool calling](/oss/call-tools) enables models to generate requests that conform to a specified input schema. + +:::python +**Tools** encapsulate a callable function and its input schema. These can be passed to compatible [chat models](https://python.langchain.com/docs/concepts/chat_models), allowing the model to decide whether to invoke a tool and with what arguments. +::: + +:::js +**Tools** encapsulate a callable function and its input schema. These can be passed to compatible [chat models](https://js.langchain.com/docs/concepts/chat_models), allowing the model to decide whether to invoke a tool and with what arguments. +::: + +## Tool calling + +![Diagram of a tool call by a model](/oss/images/tool_call.png) + +Tool calling is typically **conditional**. Based on the user input and available tools, the model may choose to issue a tool call request. This request is returned in an `AIMessage` object, which includes a `tool_calls` field that specifies the tool name and input arguments: + +:::python +```python +llm_with_tools.invoke("What is 2 multiplied by 3?") +# -> AIMessage(tool_calls=[{'name': 'multiply', 'args': {'a': 2, 'b': 3}, ...}]) +``` + +``` +AIMessage( + tool_calls=[ + ToolCall(name="multiply", args={"a": 2, "b": 3}), + ... + ] +) +``` +::: + +:::js +```typescript +await llmWithTools.invoke("What is 2 multiplied by 3?"); +``` + +``` +AIMessage { + tool_calls: [ + ToolCall { + name: "multiply", + args: { a: 2, b: 3 }, + ... + }, + ... + ] +} +``` +::: + +If the input is unrelated to any tool, the model returns only a natural language message: + +:::python +```python +llm_with_tools.invoke("Hello world!") # -> AIMessage(content="Hello!") +``` +::: + +:::js +```typescript +await llmWithTools.invoke("Hello world!"); // { content: "Hello!" } +``` +::: + +Importantly, the model does not execute the tool—it only generates a request. A separate executor (such as a runtime or agent) is responsible for handling the tool call and returning the result. + +See the [tool calling guide](/oss/call-tools) for more details. + +## Prebuilt tools + +LangChain provides prebuilt tool integrations for common external systems including APIs, databases, file systems, and web data. + +:::python +Browse the [integrations directory](https://python.langchain.com/docs/integrations/tools/) for available tools. +::: + +:::js +Browse the [integrations directory](https://js.langchain.com/docs/integrations/tools/) for available tools. +::: + +Common categories: + +* **Search**: Bing, SerpAPI, Tavily +* **Code execution**: Python REPL, Node.js REPL +* **Databases**: SQL, MongoDB, Redis +* **Web data**: Scraping and browsing +* **APIs**: OpenWeatherMap, NewsAPI, etc. + +## Custom tools + +:::python +You can define custom tools using the `@tool` decorator or plain Python functions. For example: + +```python +from langchain_core.tools import tool + +@tool +def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b +``` +::: + +:::js +You can define custom tools using the `tool` function. For example: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const multiply = tool( + (input) => { + return input.a * input.b; + }, + { + name: "multiply", + description: "Multiply two numbers.", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + } +); +``` +::: + +See the [tool calling guide](/oss/call-tools) for more details. + +## Tool execution + +While the model determines when to call a tool, execution of the tool call must be handled by a runtime component. + +LangGraph provides prebuilt components for this: + +:::python +* @[`ToolNode`][ToolNode]: A prebuilt node that executes tools. +* @[`create_react_agent`][create_react_agent]: Constructs a full agent that manages tool calling automatically. +::: + +:::js +* @[ToolNode]: A prebuilt node that executes tools. +* @[`createReactAgent`][create_react_agent]: Constructs a full agent that manages tool calling automatically. +::: diff --git a/src/oss/trace-agent.mdx b/src/oss/trace-agent.mdx new file mode 100644 index 00000000..2d19b6f9 --- /dev/null +++ b/src/oss/trace-agent.mdx @@ -0,0 +1,28 @@ +--- +title: Trace an agent +--- + +Traces are a series of steps that your application takes to go from input to output. Each of these individual steps is represented by a run. You can use [LangSmith](https://smith.langchain.com/) to visualize these execution steps. To use it, [enable tracing for your application](/oss/trace-agent). This enables you to do the following: + +* [Debug a locally running application](/langgraph-platform/clone-traces-studio). +* [Evaluate the application performance](/oss/evals). +* [Monitor the application](https://docs.smith.langchain.com/observability/how_to_guides/dashboards). + +To get started, sign up for a free account at [LangSmith](https://smith.langchain.com/). + +## Enable tracing + +To enable [tracing](/oss/trace-agent) for your application, set the following environment variables: + +```python +export LANGSMITH_TRACING=true +export LANGSMITH_API_KEY= +``` + +For more information, see [Trace with LangGraph](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_langgraph). + +## Learn more + +* [LangSmith Observability quickstart](https://docs.smith.langchain.com/observability) +* [Trace with LangGraph](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_langgraph) +* [Tracing conceptual guide](https://docs.smith.langchain.com/observability/concepts#traces) diff --git a/src/oss/ui.mdx b/src/oss/ui.mdx new file mode 100644 index 00000000..57e28590 --- /dev/null +++ b/src/oss/ui.mdx @@ -0,0 +1,33 @@ +--- +title: UI +--- + +You can use a prebuilt chat UI for interacting with any LangGraph agent through the [Agent Chat UI](https://github.com/langchain-ai/agent-chat-ui). Using the [deployed version](https://agentchat.vercel.app) is the quickest way to get started, and allows you to interact with both local and deployed graphs. + +## Run agent in UI + +First, set up LangGraph API server [locally](/oss/local-server) or deploy your agent on [LangGraph Platform](/langgraph-platform/quick-start-studio). + +Then, navigate to [Agent Chat UI](https://agentchat.vercel.app), or clone the repository and [run the dev server locally](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#setup): + + + + + UI has out-of-box support for rendering tool calls, and tool result messages. To customize what messages are shown, see the [Hiding Messages in the Chat](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#hiding-messages-in-the-chat) section in the Agent Chat UI documentation. + + +## Add human-in-the-loop + +Agent Chat UI has full support for [human-in-the-loop](/oss/human-in-the-loop) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](/oss/local-server) guide) with this [agent implementation](/oss/add-human-in-the-loop#add-interrupts-to-any-tool): + + + + + Agent Chat UI works best if your LangGraph agent interrupts using the @[`HumanInterrupt` schema][HumanInterrupt]. If you do not use that schema, the Agent Chat UI will be able to render the input passed to the `interrupt` function, but it will not have full support for resuming your graph. + + +## Generative UI + +You can also use generative UI in the Agent Chat UI. + +Generative UI allows you to define [React](https://react.dev/) components, and push them to the UI from the LangGraph server. For more documentation on building generative UI LangGraph agents, read [these docs](/langgraph-platform/generative-ui-react). diff --git a/src/oss/use-functional-api.mdx b/src/oss/use-functional-api.mdx new file mode 100644 index 00000000..a0edf22c --- /dev/null +++ b/src/oss/use-functional-api.mdx @@ -0,0 +1,1684 @@ +--- +title: Use the functional API +--- +The [**Functional API**](/oss/functional-api) allows you to add LangGraph's key features — [persistence](/oss/persistence), [memory](/oss/add-memory), [human-in-the-loop](/oss/human-in-the-loop), and [streaming](/oss/streaming) — to your applications with minimal changes to your existing code. + + + For conceptual information on the functional API, see [Functional API](/oss/functional-api). + + +## Creating a simple workflow + +When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary. + +:::python +```python +@entrypoint(checkpointer=checkpointer) +def my_workflow(inputs: dict) -> int: + value = inputs["value"] + another_value = inputs["another_value"] + ... + +my_workflow.invoke({"value": 1, "another_value": 2}) +``` +::: + +:::js +```typescript +const checkpointer = new MemorySaver(); + +const myWorkflow = entrypoint( + { checkpointer, name: "myWorkflow" }, + async (inputs: { value: number; anotherValue: number }) => { + const value = inputs.value; + const anotherValue = inputs.anotherValue; + // ... + } +); + +await myWorkflow.invoke({ value: 1, anotherValue: 2 }); +``` +::: + + + :::python + ```python + import uuid + from langgraph.func import entrypoint, task + from langgraph.checkpoint.memory import InMemorySaver + + # Task that checks if a number is even + @task + def is_even(number: int) -> bool: + return number % 2 == 0 + + # Task that formats a message + @task + def format_message(is_even: bool) -> str: + return "The number is even." if is_even else "The number is odd." + + # Create a checkpointer for persistence + checkpointer = InMemorySaver() + + @entrypoint(checkpointer=checkpointer) + def workflow(inputs: dict) -> str: + """Simple workflow to classify a number.""" + even = is_even(inputs["number"]).result() + return format_message(even).result() + + # Run the workflow with a unique thread ID + config = {"configurable": {"thread_id": str(uuid.uuid4())}} + result = workflow.invoke({"number": 7}, config=config) + print(result) + ``` + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { entrypoint, task, MemorySaver } from "@langchain/langgraph"; + + // Task that checks if a number is even + const isEven = task("isEven", async (number: number) => { + return number % 2 === 0; + }); + + // Task that formats a message + const formatMessage = task("formatMessage", async (isEven: boolean) => { + return isEven ? "The number is even." : "The number is odd."; + }); + + // Create a checkpointer for persistence + const checkpointer = new MemorySaver(); + + const workflow = entrypoint( + { checkpointer, name: "workflow" }, + async (inputs: { number: number }) => { + // Simple workflow to classify a number + const even = await isEven(inputs.number); + return await formatMessage(even); + } + ); + + // Run the workflow with a unique thread ID + const config = { configurable: { thread_id: uuidv4() } }; + const result = await workflow.invoke({ number: 7 }, config); + console.log(result); + ``` + ::: + + + + This example demonstrates how to use the `@task` and `@entrypoint` decorators + syntactically. Given that a checkpointer is provided, the workflow results will + be persisted in the checkpointer. + + :::python + ```python + import uuid + from langchain.chat_models import init_chat_model + from langgraph.func import entrypoint, task + from langgraph.checkpoint.memory import InMemorySaver + + llm = init_chat_model('openai:gpt-3.5-turbo') + + # Task: generate essay using an LLM + @task + def compose_essay(topic: str) -> str: + """Generate an essay about the given topic.""" + return llm.invoke([ + {"role": "system", "content": "You are a helpful assistant that writes essays."}, + {"role": "user", "content": f"Write an essay about {topic}."} + ]).content + + # Create a checkpointer for persistence + checkpointer = InMemorySaver() + + @entrypoint(checkpointer=checkpointer) + def workflow(topic: str) -> str: + """Simple workflow that generates an essay with an LLM.""" + return compose_essay(topic).result() + + # Execute the workflow + config = {"configurable": {"thread_id": str(uuid.uuid4())}} + result = workflow.invoke("the history of flight", config=config) + print(result) + ``` + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { ChatOpenAI } from "@langchain/openai"; + import { entrypoint, task, MemorySaver } from "@langchain/langgraph"; + + const llm = new ChatOpenAI({ model: "gpt-3.5-turbo" }); + + // Task: generate essay using an LLM + const composeEssay = task("composeEssay", async (topic: string) => { + // Generate an essay about the given topic + const response = await llm.invoke([ + { role: "system", content: "You are a helpful assistant that writes essays." }, + { role: "user", content: `Write an essay about ${topic}.` } + ]); + return response.content as string; + }); + + // Create a checkpointer for persistence + const checkpointer = new MemorySaver(); + + const workflow = entrypoint( + { checkpointer, name: "workflow" }, + async (topic: string) => { + // Simple workflow that generates an essay with an LLM + return await composeEssay(topic); + } + ); + + // Execute the workflow + const config = { configurable: { thread_id: uuidv4() } }; + const result = await workflow.invoke("the history of flight", config); + console.log(result); + ``` + ::: + + +## Parallel execution + +Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs). + +:::python +```python +@task +def add_one(number: int) -> int: + return number + 1 + +@entrypoint(checkpointer=checkpointer) +def graph(numbers: list[int]) -> list[str]: + futures = [add_one(i) for i in numbers] + return [f.result() for f in futures] +``` +::: + +:::js +```typescript +const addOne = task("addOne", async (number: number) => { + return number + 1; +}); + +const graph = entrypoint( + { checkpointer, name: "graph" }, + async (numbers: number[]) => { + return await Promise.all(numbers.map(addOne)); + } +); +``` +::: + + + This example demonstrates how to run multiple LLM calls in parallel using `@task`. Each call generates a paragraph on a different topic, and results are joined into a single text output. + + :::python + ```python + import uuid + from langchain.chat_models import init_chat_model + from langgraph.func import entrypoint, task + from langgraph.checkpoint.memory import InMemorySaver + + # Initialize the LLM model + llm = init_chat_model("openai:gpt-3.5-turbo") + + # Task that generates a paragraph about a given topic + @task + def generate_paragraph(topic: str) -> str: + response = llm.invoke([ + {"role": "system", "content": "You are a helpful assistant that writes educational paragraphs."}, + {"role": "user", "content": f"Write a paragraph about {topic}."} + ]) + return response.content + + # Create a checkpointer for persistence + checkpointer = InMemorySaver() + + @entrypoint(checkpointer=checkpointer) + def workflow(topics: list[str]) -> str: + """Generates multiple paragraphs in parallel and combines them.""" + futures = [generate_paragraph(topic) for topic in topics] + paragraphs = [f.result() for f in futures] + return "\n\n".join(paragraphs) + + # Run the workflow + config = {"configurable": {"thread_id": str(uuid.uuid4())}} + result = workflow.invoke(["quantum computing", "climate change", "history of aviation"], config=config) + print(result) + ``` + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { ChatOpenAI } from "@langchain/openai"; + import { entrypoint, task, MemorySaver } from "@langchain/langgraph"; + + // Initialize the LLM model + const llm = new ChatOpenAI({ model: "gpt-3.5-turbo" }); + + // Task that generates a paragraph about a given topic + const generateParagraph = task("generateParagraph", async (topic: string) => { + const response = await llm.invoke([ + { role: "system", content: "You are a helpful assistant that writes educational paragraphs." }, + { role: "user", content: `Write a paragraph about ${topic}.` } + ]); + return response.content as string; + }); + + // Create a checkpointer for persistence + const checkpointer = new MemorySaver(); + + const workflow = entrypoint( + { checkpointer, name: "workflow" }, + async (topics: string[]) => { + // Generates multiple paragraphs in parallel and combines them + const paragraphs = await Promise.all(topics.map(generateParagraph)); + return paragraphs.join("\n\n"); + } + ); + + // Run the workflow + const config = { configurable: { thread_id: uuidv4() } }; + const result = await workflow.invoke(["quantum computing", "climate change", "history of aviation"], config); + console.log(result); + ``` + ::: + + This example uses LangGraph's concurrency model to improve execution time, especially when tasks involve I/O like LLM completions. + + +## Calling graphs + +The **Functional API** and the [**Graph API**](/oss/graph-api) can be used together in the same application as they share the same underlying runtime. + +:::python +```python +from langgraph.func import entrypoint +from langgraph.graph import StateGraph + +builder = StateGraph() +... +some_graph = builder.compile() + +@entrypoint() +def some_workflow(some_input: dict) -> int: + # Call a graph defined using the graph API + result_1 = some_graph.invoke(...) + # Call another graph defined using the graph API + result_2 = another_graph.invoke(...) + return { + "result_1": result_1, + "result_2": result_2 + } +``` +::: + +:::js +```typescript +import { entrypoint } from "@langchain/langgraph"; +import { StateGraph } from "@langchain/langgraph"; + +const builder = new StateGraph(/* ... */); +// ... +const someGraph = builder.compile(); + +const someWorkflow = entrypoint( + { name: "someWorkflow" }, + async (someInput: Record) => { + // Call a graph defined using the graph API + const result1 = await someGraph.invoke(/* ... */); + // Call another graph defined using the graph API + const result2 = await anotherGraph.invoke(/* ... */); + return { + result1, + result2, + }; + } +); +``` +::: + + + :::python + ```python + import uuid + from typing import TypedDict + from langgraph.func import entrypoint + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.graph import StateGraph + + # Define the shared state type + class State(TypedDict): + foo: int + + # Define a simple transformation node + def double(state: State) -> State: + return {"foo": state["foo"] * 2} + + # Build the graph using the Graph API + builder = StateGraph(State) + builder.add_node("double", double) + builder.set_entry_point("double") + graph = builder.compile() + + # Define the functional API workflow + checkpointer = InMemorySaver() + + @entrypoint(checkpointer=checkpointer) + def workflow(x: int) -> dict: + result = graph.invoke({"foo": x}) + return {"bar": result["foo"]} + + # Execute the workflow + config = {"configurable": {"thread_id": str(uuid.uuid4())}} + print(workflow.invoke(5, config=config)) # Output: {'bar': 10} + ``` + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { entrypoint, MemorySaver } from "@langchain/langgraph"; + import { StateGraph } from "@langchain/langgraph"; + import { z } from "zod"; + + // Define the shared state type + const State = z.object({ + foo: z.number(), + }); + + // Build the graph using the Graph API + const builder = new StateGraph(State) + .addNode("double", (state) => { + return { foo: state.foo * 2 }; + }) + .addEdge("__start__", "double"); + const graph = builder.compile(); + + // Define the functional API workflow + const checkpointer = new MemorySaver(); + + const workflow = entrypoint( + { checkpointer, name: "workflow" }, + async (x: number) => { + const result = await graph.invoke({ foo: x }); + return { bar: result.foo }; + } + ); + + // Execute the workflow + const config = { configurable: { thread_id: uuidv4() } }; + console.log(await workflow.invoke(5, config)); // Output: { bar: 10 } + ``` + ::: + + +## Call other entrypoints + +You can call other **entrypoints** from within an **entrypoint** or a **task**. + +:::python +```python +@entrypoint() # Will automatically use the checkpointer from the parent entrypoint +def some_other_workflow(inputs: dict) -> int: + return inputs["value"] + +@entrypoint(checkpointer=checkpointer) +def my_workflow(inputs: dict) -> int: + value = some_other_workflow.invoke({"value": 1}) + return value +``` +::: + +:::js +```typescript +// Will automatically use the checkpointer from the parent entrypoint +const someOtherWorkflow = entrypoint( + { name: "someOtherWorkflow" }, + async (inputs: { value: number }) => { + return inputs.value; + } +); + +const myWorkflow = entrypoint( + { checkpointer, name: "myWorkflow" }, + async (inputs: { value: number }) => { + const value = await someOtherWorkflow.invoke({ value: 1 }); + return value; + } +); +``` +::: + + + :::python + ```python + import uuid + from langgraph.func import entrypoint + from langgraph.checkpoint.memory import InMemorySaver + + # Initialize a checkpointer + checkpointer = InMemorySaver() + + # A reusable sub-workflow that multiplies a number + @entrypoint() + def multiply(inputs: dict) -> int: + return inputs["a"] * inputs["b"] + + # Main workflow that invokes the sub-workflow + @entrypoint(checkpointer=checkpointer) + def main(inputs: dict) -> dict: + result = multiply.invoke({"a": inputs["x"], "b": inputs["y"]}) + return {"product": result} + + # Execute the main workflow + config = {"configurable": {"thread_id": str(uuid.uuid4())}} + print(main.invoke({"x": 6, "y": 7}, config=config)) # Output: {'product': 42} + ``` + ::: + + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { entrypoint, MemorySaver } from "@langchain/langgraph"; + + // Initialize a checkpointer + const checkpointer = new MemorySaver(); + + // A reusable sub-workflow that multiplies a number + const multiply = entrypoint( + { name: "multiply" }, + async (inputs: { a: number; b: number }) => { + return inputs.a * inputs.b; + } + ); + + // Main workflow that invokes the sub-workflow + const main = entrypoint( + { checkpointer, name: "main" }, + async (inputs: { x: number; y: number }) => { + const result = await multiply.invoke({ a: inputs.x, b: inputs.y }); + return { product: result }; + } + ); + + // Execute the main workflow + const config = { configurable: { thread_id: uuidv4() } }; + console.log(await main.invoke({ x: 6, y: 7 }, config)); // Output: { product: 42 } + ``` + ::: + + +## Streaming + +The **Functional API** uses the same streaming mechanism as the **Graph API**. Please +read the [**streaming guide**](/oss/streaming) section for more details. + +Example of using the streaming API to stream both updates and custom data. + +:::python +```python +from langgraph.func import entrypoint +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.config import get_stream_writer # (1)! + +checkpointer = InMemorySaver() + +@entrypoint(checkpointer=checkpointer) +def main(inputs: dict) -> int: + writer = get_stream_writer() # (2)! + writer("Started processing") # (3)! + result = inputs["x"] * 2 + writer(f"Result is {result}") # (4)! + return result + +config = {"configurable": {"thread_id": "abc"}} + +# highlight-next-line +for mode, chunk in main.stream( # (5)! + {"x": 5}, + stream_mode=["custom", "updates"], # (6)! + config=config +): + print(f"{mode}: {chunk}") +``` + +1. Import `get_stream_writer` from `langgraph.config`. +2. Obtain a stream writer instance within the entrypoint. +3. Emit custom data before computation begins. +4. Emit another custom message after computing the result. +5. Use `.stream()` to process streamed output. +6. Specify which streaming modes to use. + +```pycon +('updates', {'add_one': 2}) +('updates', {'add_two': 3}) +('custom', 'hello') +('custom', 'world') +('updates', {'main': 5}) +``` + + + **Async with Python < 3.11** + If using Python < 3.11 and writing async code, using `get_stream_writer()` will not work. Instead please + use the `StreamWriter` class directly. See [Async with Python < 3.11](/oss/streaming#async) for more details. + + ```python + from langgraph.types import StreamWriter + + @entrypoint(checkpointer=checkpointer) + # highlight-next-line + async def main(inputs: dict, writer: StreamWriter) -> int: + ... + ``` + +::: + +:::js +```typescript +import { + entrypoint, + MemorySaver, + LangGraphRunnableConfig, +} from "@langchain/langgraph"; + +const checkpointer = new MemorySaver(); + +const main = entrypoint( + { checkpointer, name: "main" }, + async ( + inputs: { x: number }, + config: LangGraphRunnableConfig + ): Promise => { + config.writer?.("Started processing"); // (1)! + const result = inputs.x * 2; + config.writer?.(`Result is ${result}`); // (2)! + return result; + } +); + +const config = { configurable: { thread_id: "abc" } }; + +// (3)! +for await (const [mode, chunk] of await main.stream( + { x: 5 }, + { streamMode: ["custom", "updates"], ...config } // (4)! +)) { + console.log(`${mode}: ${JSON.stringify(chunk)}`); +} +``` + +1. Emit custom data before computation begins. +2. Emit another custom message after computing the result. +3. Use `.stream()` to process streamed output. +4. Specify which streaming modes to use. + +``` +updates: {"addOne": 2} +updates: {"addTwo": 3} +custom: "hello" +custom: "world" +updates: {"main": 5} +``` +::: + +## Retry policy + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.func import entrypoint, task +from langgraph.types import RetryPolicy + +# This variable is just used for demonstration purposes to simulate a network failure. +# It's not something you will have in your actual code. +attempts = 0 + +# Let's configure the RetryPolicy to retry on ValueError. +# The default RetryPolicy is optimized for retrying specific network errors. +retry_policy = RetryPolicy(retry_on=ValueError) + +@task(retry_policy=retry_policy) +def get_info(): + global attempts + attempts += 1 + + if attempts < 2: + raise ValueError('Failure') + return "OK" + +checkpointer = InMemorySaver() + +@entrypoint(checkpointer=checkpointer) +def main(inputs, writer): + return get_info().result() + +config = { + "configurable": { + "thread_id": "1" + } +} + +main.invoke({'any_input': 'foobar'}, config=config) +``` + +```pycon +'OK' +``` +::: + +:::js +```typescript +import { + MemorySaver, + entrypoint, + task, + RetryPolicy, +} from "@langchain/langgraph"; + +// This variable is just used for demonstration purposes to simulate a network failure. +// It's not something you will have in your actual code. +let attempts = 0; + +// Let's configure the RetryPolicy to retry on ValueError. +// The default RetryPolicy is optimized for retrying specific network errors. +const retryPolicy: RetryPolicy = { retryOn: (error) => error instanceof Error }; + +const getInfo = task( + { + name: "getInfo", + retry: retryPolicy, + }, + () => { + attempts += 1; + + if (attempts < 2) { + throw new Error("Failure"); + } + return "OK"; + } +); + +const checkpointer = new MemorySaver(); + +const main = entrypoint( + { checkpointer, name: "main" }, + async (inputs: Record) => { + return await getInfo(); + } +); + +const config = { + configurable: { + thread_id: "1", + }, +}; + +await main.invoke({ any_input: "foobar" }, config); +``` + +``` +'OK' +``` +::: + +## Caching Tasks + +:::python +```python +import time +from langgraph.cache.memory import InMemoryCache +from langgraph.func import entrypoint, task +from langgraph.types import CachePolicy + + +@task(cache_policy=CachePolicy(ttl=120)) # (1)! +def slow_add(x: int) -> int: + time.sleep(1) + return x * 2 + + +@entrypoint(cache=InMemoryCache()) +def main(inputs: dict) -> dict[str, int]: + result1 = slow_add(inputs["x"]).result() + result2 = slow_add(inputs["x"]).result() + return {"result1": result1, "result2": result2} + + +for chunk in main.stream({"x": 5}, stream_mode="updates"): + print(chunk) + +#> {'slow_add': 10} +#> {'slow_add': 10, '__metadata__': {'cached': True}} +#> {'main': {'result1': 10, 'result2': 10}} +``` + +1. `ttl` is specified in seconds. The cache will be invalidated after this time. +::: + +:::js +```typescript +import { + InMemoryCache, + entrypoint, + task, + CachePolicy, +} from "@langchain/langgraph"; + +const slowAdd = task( + { + name: "slowAdd", + cache: { ttl: 120 }, // (1)! + }, + async (x: number) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return x * 2; + } +); + +const main = entrypoint( + { cache: new InMemoryCache(), name: "main" }, + async (inputs: { x: number }) => { + const result1 = await slowAdd(inputs.x); + const result2 = await slowAdd(inputs.x); + return { result1, result2 }; + } +); + +for await (const chunk of await main.stream( + { x: 5 }, + { streamMode: "updates" } +)) { + console.log(chunk); +} + +//> { slowAdd: 10 } +//> { slowAdd: 10, '__metadata__': { cached: true } } +//> { main: { result1: 10, result2: 10 } } +``` + +1. `ttl` is specified in seconds. The cache will be invalidated after this time. +::: + +## Resuming after an error + +:::python +```python +import time +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.func import entrypoint, task +from langgraph.types import StreamWriter + +# This variable is just used for demonstration purposes to simulate a network failure. +# It's not something you will have in your actual code. +attempts = 0 + +@task() +def get_info(): + """ + Simulates a task that fails once before succeeding. + Raises an exception on the first attempt, then returns "OK" on subsequent tries. + """ + global attempts + attempts += 1 + + if attempts < 2: + raise ValueError("Failure") # Simulate a failure on the first attempt + return "OK" + +# Initialize an in-memory checkpointer for persistence +checkpointer = InMemorySaver() + +@task +def slow_task(): + """ + Simulates a slow-running task by introducing a 1-second delay. + """ + time.sleep(1) + return "Ran slow task." + +@entrypoint(checkpointer=checkpointer) +def main(inputs, writer: StreamWriter): + """ + Main workflow function that runs the slow_task and get_info tasks sequentially. + + Parameters: + - inputs: Dictionary containing workflow input values. + - writer: StreamWriter for streaming custom data. + + The workflow first executes `slow_task` and then attempts to execute `get_info`, + which will fail on the first invocation. + """ + slow_task_result = slow_task().result() # Blocking call to slow_task + get_info().result() # Exception will be raised here on the first attempt + return slow_task_result + +# Workflow execution configuration with a unique thread identifier +config = { + "configurable": { + "thread_id": "1" # Unique identifier to track workflow execution + } +} + +# This invocation will take ~1 second due to the slow_task execution +try: + # First invocation will raise an exception due to the `get_info` task failing + main.invoke({'any_input': 'foobar'}, config=config) +except ValueError: + pass # Handle the failure gracefully +``` + +When we resume execution, we won't need to re-run the `slow_task` as its result is already saved in the checkpoint. + +```python +main.invoke(None, config=config) +``` + +```pycon +'Ran slow task.' +``` +::: + +:::js +```typescript +import { entrypoint, task, MemorySaver } from "@langchain/langgraph"; + +// This variable is just used for demonstration purposes to simulate a network failure. +// It's not something you will have in your actual code. +let attempts = 0; + +const getInfo = task("getInfo", async () => { + /** + * Simulates a task that fails once before succeeding. + * Throws an exception on the first attempt, then returns "OK" on subsequent tries. + */ + attempts += 1; + + if (attempts < 2) { + throw new Error("Failure"); // Simulate a failure on the first attempt + } + return "OK"; +}); + +// Initialize an in-memory checkpointer for persistence +const checkpointer = new MemorySaver(); + +const slowTask = task("slowTask", async () => { + /** + * Simulates a slow-running task by introducing a 1-second delay. + */ + await new Promise((resolve) => setTimeout(resolve, 1000)); + return "Ran slow task."; +}); + +const main = entrypoint( + { checkpointer, name: "main" }, + async (inputs: Record) => { + /** + * Main workflow function that runs the slowTask and getInfo tasks sequentially. + * + * Parameters: + * - inputs: Record containing workflow input values. + * + * The workflow first executes `slowTask` and then attempts to execute `getInfo`, + * which will fail on the first invocation. + */ + const slowTaskResult = await slowTask(); // Blocking call to slowTask + await getInfo(); // Exception will be raised here on the first attempt + return slowTaskResult; + } +); + +// Workflow execution configuration with a unique thread identifier +const config = { + configurable: { + thread_id: "1", // Unique identifier to track workflow execution + }, +}; + +// This invocation will take ~1 second due to the slowTask execution +try { + // First invocation will raise an exception due to the `getInfo` task failing + await main.invoke({ any_input: "foobar" }, config); +} catch (err) { + // Handle the failure gracefully +} +``` + +When we resume execution, we won't need to re-run the `slowTask` as its result is already saved in the checkpoint. + +```typescript +await main.invoke(null, config); +``` + +``` +'Ran slow task.' +``` +::: + +## Human-in-the-loop + +The functional API supports [human-in-the-loop](/oss/human-in-the-loop) workflows using the `interrupt` function and the `Command` primitive. + +### Basic human-in-the-loop workflow + +We will create three [tasks](/oss/functional-api#task): + +1. Append `"bar"`. +2. Pause for human input. When resuming, append human input. +3. Append `"qux"`. + +:::python +```python +from langgraph.func import entrypoint, task +from langgraph.types import Command, interrupt + + +@task +def step_1(input_query): + """Append bar.""" + return f"{input_query} bar" + + +@task +def human_feedback(input_query): + """Append user input.""" + feedback = interrupt(f"Please provide feedback: {input_query}") + return f"{input_query} {feedback}" + + +@task +def step_3(input_query): + """Append qux.""" + return f"{input_query} qux" +``` +::: + +:::js +```typescript +import { entrypoint, task, interrupt, Command } from "@langchain/langgraph"; + +const step1 = task("step1", async (inputQuery: string) => { + // Append bar + return `${inputQuery} bar`; +}); + +const humanFeedback = task("humanFeedback", async (inputQuery: string) => { + // Append user input + const feedback = interrupt(`Please provide feedback: ${inputQuery}`); + return `${inputQuery} ${feedback}`; +}); + +const step3 = task("step3", async (inputQuery: string) => { + // Append qux + return `${inputQuery} qux`; +}); +``` +::: + +We can now compose these tasks in an [entrypoint](/oss/functional-api#entrypoint): + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver + +checkpointer = InMemorySaver() + + +@entrypoint(checkpointer=checkpointer) +def graph(input_query): + result_1 = step_1(input_query).result() + result_2 = human_feedback(result_1).result() + result_3 = step_3(result_2).result() + + return result_3 +``` +::: + +:::js +```typescript +import { MemorySaver } from "@langchain/langgraph"; + +const checkpointer = new MemorySaver(); + +const graph = entrypoint( + { checkpointer, name: "graph" }, + async (inputQuery: string) => { + const result1 = await step1(inputQuery); + const result2 = await humanFeedback(result1); + const result3 = await step3(result2); + + return result3; + } +); +``` +::: + +[interrupt()](/oss/add-human-in-the-loop#pause-using-interrupt) is called inside a task, enabling a human to review and edit the output of the previous task. The results of prior tasks-- in this case `step_1`-- are persisted, so that they are not run again following the `interrupt`. + +Let's send in a query string: + +:::python +```python +config = {"configurable": {"thread_id": "1"}} + +for event in graph.stream("foo", config): + print(event) + print("\n") +``` +::: + +:::js +```typescript +const config = { configurable: { thread_id: "1" } }; + +for await (const event of await graph.stream("foo", config)) { + console.log(event); + console.log("\n"); +} +``` +::: + +Note that we've paused with an `interrupt` after `step_1`. The interrupt provides instructions to resume the run. To resume, we issue a [Command](/oss/add-human-in-the-loop#resume-using-the-command-primitive) containing the data expected by the `human_feedback` task. + +:::python +```python +# Continue execution +for event in graph.stream(Command(resume="baz"), config): + print(event) + print("\n") +``` +::: + +:::js +```typescript +// Continue execution +for await (const event of await graph.stream( + new Command({ resume: "baz" }), + config +)) { + console.log(event); + console.log("\n"); +} +``` +::: + +After resuming, the run proceeds through the remaining step and terminates as expected. + +### Review tool calls + +To review tool calls before execution, we add a `review_tool_call` function that calls [`interrupt`](/oss/add-human-in-the-loop#pause-using-interrupt). When this function is called, execution will be paused until we issue a command to resume it. + +Given a tool call, our function will `interrupt` for human review. At that point we can either: + +* Accept the tool call +* Revise the tool call and continue +* Generate a custom tool message (e.g., instructing the model to re-format its tool call) + +:::python +```python +from typing import Union + +def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]: + """Review a tool call, returning a validated version.""" + human_review = interrupt( + { + "question": "Is this correct?", + "tool_call": tool_call, + } + ) + review_action = human_review["action"] + review_data = human_review.get("data") + if review_action == "continue": + return tool_call + elif review_action == "update": + updated_tool_call = {**tool_call, **{"args": review_data}} + return updated_tool_call + elif review_action == "feedback": + return ToolMessage( + content=review_data, name=tool_call["name"], tool_call_id=tool_call["id"] + ) +``` +::: + +:::js +```typescript +import { ToolCall } from "@langchain/core/messages/tool"; +import { ToolMessage } from "@langchain/core/messages"; + +function reviewToolCall(toolCall: ToolCall): ToolCall | ToolMessage { + // Review a tool call, returning a validated version + const humanReview = interrupt({ + question: "Is this correct?", + tool_call: toolCall, + }); + + const reviewAction = humanReview.action; + const reviewData = humanReview.data; + + if (reviewAction === "continue") { + return toolCall; + } else if (reviewAction === "update") { + const updatedToolCall = { ...toolCall, args: reviewData }; + return updatedToolCall; + } else if (reviewAction === "feedback") { + return new ToolMessage({ + content: reviewData, + name: toolCall.name, + tool_call_id: toolCall.id, + }); + } + + throw new Error(`Unknown review action: ${reviewAction}`); +} +``` +::: + +We can now update our [entrypoint](/oss/functional-api#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human. The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`. + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph.message import add_messages +from langgraph.types import Command, interrupt + + +checkpointer = InMemorySaver() + + +@entrypoint(checkpointer=checkpointer) +def agent(messages, previous): + if previous is not None: + messages = add_messages(previous, messages) + + llm_response = call_model(messages).result() + while True: + if not llm_response.tool_calls: + break + + # Review tool calls + tool_results = [] + tool_calls = [] + for i, tool_call in enumerate(llm_response.tool_calls): + review = review_tool_call(tool_call) + if isinstance(review, ToolMessage): + tool_results.append(review) + else: # is a validated tool call + tool_calls.append(review) + if review != tool_call: + llm_response.tool_calls[i] = review # update message + + # Execute remaining tool calls + tool_result_futures = [call_tool(tool_call) for tool_call in tool_calls] + remaining_tool_results = [fut.result() for fut in tool_result_futures] + + # Append to message list + messages = add_messages( + messages, + [llm_response, *tool_results, *remaining_tool_results], + ) + + # Call model again + llm_response = call_model(messages).result() + + # Generate final response + messages = add_messages(messages, llm_response) + return entrypoint.final(value=llm_response, save=messages) +``` +::: + +:::js +```typescript +import { + MemorySaver, + entrypoint, + interrupt, + Command, + addMessages, +} from "@langchain/langgraph"; +import { ToolMessage, AIMessage, BaseMessage } from "@langchain/core/messages"; + +const checkpointer = new MemorySaver(); + +const agent = entrypoint( + { checkpointer, name: "agent" }, + async ( + messages: BaseMessage[], + previous?: BaseMessage[] + ): Promise => { + if (previous !== undefined) { + messages = addMessages(previous, messages); + } + + let llmResponse = await callModel(messages); + while (true) { + if (!llmResponse.tool_calls?.length) { + break; + } + + // Review tool calls + const toolResults: ToolMessage[] = []; + const toolCalls: ToolCall[] = []; + + for (let i = 0; i < llmResponse.tool_calls.length; i++) { + const review = reviewToolCall(llmResponse.tool_calls[i]); + if (review instanceof ToolMessage) { + toolResults.push(review); + } else { + // is a validated tool call + toolCalls.push(review); + if (review !== llmResponse.tool_calls[i]) { + llmResponse.tool_calls[i] = review; // update message + } + } + } + + // Execute remaining tool calls + const remainingToolResults = await Promise.all( + toolCalls.map((toolCall) => callTool(toolCall)) + ); + + // Append to message list + messages = addMessages(messages, [ + llmResponse, + ...toolResults, + ...remainingToolResults, + ]); + + // Call model again + llmResponse = await callModel(messages); + } + + // Generate final response + messages = addMessages(messages, llmResponse); + return entrypoint.final({ value: llmResponse, save: messages }); + } +); +``` +::: + +## Short-term memory + +Short-term memory allows storing information across different **invocations** of the same **thread id**. See [short-term memory](/oss/functional-api#short-term-memory) for more details. + +### Manage checkpoints + +You can view and delete the information stored by the checkpointer. + + +#### View thread state + +:::python +```python +config = { + "configurable": { + # highlight-next-line + "thread_id": "1", + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # highlight-next-line + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" + + } +} +# highlight-next-line +graph.get_state(config) +``` + +``` +StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() +) +``` +::: + +:::js +```typescript +const config = { + configurable: { + // highlight-next-line + thread_id: "1", + // optionally provide an ID for a specific checkpoint, + // otherwise the latest checkpoint is shown + // highlight-next-line + // checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a" + }, +}; +// highlight-next-line +await graph.getState(config); +``` + +``` +StateSnapshot { + values: { + messages: [ + HumanMessage { content: "hi! I'm bob" }, + AIMessage { content: "Hi Bob! How are you doing today?" }, + HumanMessage { content: "what's my name?" }, + AIMessage { content: "Your name is Bob." } + ] + }, + next: [], + config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } }, + metadata: { + source: 'loop', + writes: { call_model: { messages: AIMessage { content: "Your name is Bob." } } }, + step: 4, + parents: {}, + thread_id: '1' + }, + createdAt: '2025-05-05T16:01:24.680462+00:00', + parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } }, + tasks: [], + interrupts: [] +} +``` +::: + + +#### View the history of the thread + +:::python +```python +config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } +} +# highlight-next-line +list(graph.get_state_history(config)) +``` + +``` +[ + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")]}, + next=('call_model',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.863421+00:00', + parent_config={...} + tasks=(PregelTask(id='8ab4155e-6b15-b885-9ce5-bed69a2c305c', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Your name is Bob.')}),), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, + next=('__start__',), + config={...}, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.863173+00:00', + parent_config={...} + tasks=(PregelTask(id='24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "what's my name?"}]}),), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, + next=(), + config={...}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.862295+00:00', + parent_config={...} + tasks=(), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob")]}, + next=('call_model',), + config={...}, + metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:22.278960+00:00', + parent_config={...} + tasks=(PregelTask(id='8cbd75e0-3720-b056-04f7-71ac805140a0', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}),), + interrupts=() + ), + StateSnapshot( + values={'messages': []}, + next=('__start__',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:22.277497+00:00', + parent_config=None, + tasks=(PregelTask(id='d458367b-8265-812c-18e2-33001d199ce6', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}),), + interrupts=() + ) +] +``` +::: + +:::js +```typescript +const config = { + configurable: { + // highlight-next-line + thread_id: "1", + }, +}; +// highlight-next-line +const history = []; +for await (const state of graph.getStateHistory(config)) { + history.push(state); +} +``` + +``` +[ + StateSnapshot { + values: { + messages: [ + HumanMessage { content: "hi! I'm bob" }, + AIMessage { content: "Hi Bob! How are you doing today? Is there anything I can help you with?" }, + HumanMessage { content: "what's my name?" }, + AIMessage { content: "Your name is Bob." } + ] + }, + next: [], + config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } }, + metadata: { source: 'loop', writes: { call_model: { messages: AIMessage { content: "Your name is Bob." } } }, step: 4, parents: {}, thread_id: '1' }, + createdAt: '2025-05-05T16:01:24.680462+00:00', + parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } }, + tasks: [], + interrupts: [] + }, + // ... more state snapshots +] +``` +::: + +### Decouple return value from saved value + +Use `entrypoint.final` to decouple what is returned to the caller from what is persisted in the checkpoint. This is useful when: + +* You want to return a computed result (e.g., a summary or status), but save a different internal value for use on the next invocation. +* You need to control what gets passed to the previous parameter on the next run. + +:::python +```python +from typing import Optional +from langgraph.func import entrypoint +from langgraph.checkpoint.memory import InMemorySaver + +checkpointer = InMemorySaver() + +@entrypoint(checkpointer=checkpointer) +def accumulate(n: int, *, previous: Optional[int]) -> entrypoint.final[int, int]: + previous = previous or 0 + total = previous + n + # Return the *previous* value to the caller but save the *new* total to the checkpoint. + return entrypoint.final(value=previous, save=total) + +config = {"configurable": {"thread_id": "my-thread"}} + +print(accumulate.invoke(1, config=config)) # 0 +print(accumulate.invoke(2, config=config)) # 1 +print(accumulate.invoke(3, config=config)) # 3 +``` +::: + +:::js +```typescript +import { entrypoint, MemorySaver } from "@langchain/langgraph"; + +const checkpointer = new MemorySaver(); + +const accumulate = entrypoint( + { checkpointer, name: "accumulate" }, + async (n: number, previous?: number) => { + const prev = previous || 0; + const total = prev + n; + // Return the *previous* value to the caller but save the *new* total to the checkpoint. + return entrypoint.final({ value: prev, save: total }); + } +); + +const config = { configurable: { thread_id: "my-thread" } }; + +console.log(await accumulate.invoke(1, config)); // 0 +console.log(await accumulate.invoke(2, config)); // 1 +console.log(await accumulate.invoke(3, config)); // 3 +``` +::: + +### Chatbot example + +An example of a simple chatbot using the functional API and the `InMemorySaver` checkpointer. +The bot is able to remember the previous conversation and continue from where it left off. + +:::python +```python +from langchain_core.messages import BaseMessage +from langgraph.graph import add_messages +from langgraph.func import entrypoint, task +from langgraph.checkpoint.memory import InMemorySaver +from langchain_anthropic import ChatAnthropic + +model = ChatAnthropic(model="claude-3-5-sonnet-latest") + +@task +def call_model(messages: list[BaseMessage]): + response = model.invoke(messages) + return response + +checkpointer = InMemorySaver() + +@entrypoint(checkpointer=checkpointer) +def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]): + if previous: + inputs = add_messages(previous, inputs) + + response = call_model(inputs).result() + return entrypoint.final(value=response, save=add_messages(inputs, response)) + +config = {"configurable": {"thread_id": "1"}} +input_message = {"role": "user", "content": "hi! I'm bob"} +for chunk in workflow.stream([input_message], config, stream_mode="values"): + chunk.pretty_print() + +input_message = {"role": "user", "content": "what's my name?"} +for chunk in workflow.stream([input_message], config, stream_mode="values"): + chunk.pretty_print() +``` +::: + +:::js +```typescript +import { BaseMessage } from "@langchain/core/messages"; +import { + addMessages, + entrypoint, + task, + MemorySaver, +} from "@langchain/langgraph"; +import { ChatAnthropic } from "@langchain/anthropic"; + +const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); + +const callModel = task( + "callModel", + async (messages: BaseMessage[]): Promise => { + const response = await model.invoke(messages); + return response; + } +); + +const checkpointer = new MemorySaver(); + +const workflow = entrypoint( + { checkpointer, name: "workflow" }, + async ( + inputs: BaseMessage[], + previous?: BaseMessage[] + ): Promise => { + let messages = inputs; + if (previous) { + messages = addMessages(previous, inputs); + } + + const response = await callModel(messages); + return entrypoint.final({ + value: response, + save: addMessages(messages, response), + }); + } +); + +const config = { configurable: { thread_id: "1" } }; +const inputMessage = { role: "user", content: "hi! I'm bob" }; + +for await (const chunk of await workflow.stream([inputMessage], { + ...config, + streamMode: "values", +})) { + console.log(chunk.content); +} + +const inputMessage2 = { role: "user", content: "what's my name?" }; +for await (const chunk of await workflow.stream([inputMessage2], { + ...config, + streamMode: "values", +})) { + console.log(chunk.content); +} +``` +::: + +## Long-term memory + +[long-term memory](/oss/memory#long-term-memory) allows storing information across different **thread ids**. This could be useful for learning information about a given user in one conversation and using it in another. + +## Workflows + +* [Workflows and agent](/oss/workflows-and-agents) guide for more examples of how to build workflows using the Functional API. + +## Integrate with other libraries + +* [Add LangGraph's features to other frameworks using the functional API](/langgraph-platform/autogen-integration): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box. diff --git a/src/oss/use-graph-api.mdx b/src/oss/use-graph-api.mdx new file mode 100644 index 00000000..9ae277df --- /dev/null +++ b/src/oss/use-graph-api.mdx @@ -0,0 +1,1940 @@ +--- +title: How to use the graph API +--- + +import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; + +This guide demonstrates the basics of LangGraph's Graph API. It walks through [state](#define-and-update-state), as well as composing common graph structures such as [sequences](#create-a-sequence-of-steps), [branches](#create-branches), and [loops](#create-and-control-loops). It also covers LangGraph's control features, including the [Send API](#map-reduce-and-the-send-api) for map-reduce workflows and the [Command API](#combine-control-flow-and-state-updates-with-command) for combining state updates with "hops" across nodes. + +## Setup + +Install `langgraph`: + +```bash +pip install -U langgraph +``` + + + **Set up LangSmith for better debugging** + Sign up for [LangSmith](https://smith.langchain.com) 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 in the [docs](https://docs.smith.langchain.com). + + +## Define and update state + +Here we show how to define and update [state](/oss/graph-api#state) in LangGraph. We will demonstrate: + +1. How to use state to define a graph's [schema](/oss/graph-api#schema) +2. How to use [reducers](/oss/graph-api#reducers) to control how state updates are processed. + +### Define state + +[State](/oss/graph-api#state) in LangGraph can be a `TypedDict`, `Pydantic` model, or dataclass. Below we will use `TypedDict`. See [this section](#use-pydantic-models-for-graph-state) for detail on using Pydantic. + +By default, graphs will have the same input and output schema, and the state determines that schema. See [this section](#define-input-and-output-schemas) for how to define distinct input and output schemas. + +:::python +Let's consider a simple example using [messages](/oss/graph-api#messagesstate). This represents a versatile formulation of state for many LLM applications. See our [concepts page](/oss/graph-api#working-with-messages-in-graph-state) for more detail. +::: + +```python +from langchain_core.messages import AnyMessage +from typing_extensions import TypedDict + +class State(TypedDict): + messages: list[AnyMessage] + extra_field: int +``` + +This state tracks a list of [message](https://python.langchain.com/docs/concepts/messages/) objects, as well as an extra integer field. + +### Update state + +Let's build an example graph with a single node. Our [node](/oss/graph-api#nodes) is just a Python function that reads our graph's state and makes updates to it. The first argument to this function will always be the state: + +```python +from langchain_core.messages import AIMessage + +def node(state: State): + messages = state["messages"] + new_message = AIMessage("Hello!") + return {"messages": messages + [new_message], "extra_field": 10} +``` + +This node simply appends a message to our message list, and populates an extra field. + + + Nodes should return updates to the state directly, instead of mutating the state. + + +Let's next define a simple graph containing this node. We use [StateGraph](/oss/graph-api#stategraph) to define a graph that operates on this state. We then use [add_node](/oss/graph-api#nodes) populate our graph. + +```python +from langgraph.graph import StateGraph + +builder = StateGraph(State) +builder.add_node(node) +builder.set_entry_point("node") +graph = builder.compile() +``` + +LangGraph provides built-in utilities for visualizing your graph. Let's inspect our graph. See [this section](#visualize-your-graph) for detail on visualization. + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Simple graph with single node](/oss/images/graph_api_image_1.png) + +In this case, our graph just executes a single node. Let's proceed with a simple invocation: + +```python +from langchain_core.messages import HumanMessage + +result = graph.invoke({"messages": [HumanMessage("Hi")]}) +result +``` + +``` +{'messages': [HumanMessage(content='Hi'), AIMessage(content='Hello!')], 'extra_field': 10} +``` + +Note that: + +* We kicked off invocation by updating a single key of the state. +* We receive the entire state in the invocation result. + +For convenience, we frequently inspect the content of [message objects](https://python.langchain.com/docs/concepts/messages/) via pretty-print: + +```python +for message in result["messages"]: + message.pretty_print() +``` + +``` +================================ Human Message ================================ + +Hi +================================== Ai Message ================================== + +Hello! +``` + +### Process state updates with reducers + +Each key in the state can have its own independent [reducer](/oss/graph-api#reducers) function, which controls how updates from nodes are applied. If no reducer function is explicitly specified then it is assumed that all updates to the key should override it. + +For `TypedDict` state schemas, we can define reducers by annotating the corresponding field of the state with a reducer function. + +In the earlier example, our node updated the `"messages"` key in the state by appending a message to it. Below, we add a reducer to this key, such that updates are automatically appended: + +```python +from typing_extensions import Annotated + +def add(left, right): + """Can also import `add` from the `operator` built-in.""" + return left + right + +class State(TypedDict): + # highlight-next-line + messages: Annotated[list[AnyMessage], add] + extra_field: int +``` + +Now our node can be simplified: + +```python +def node(state: State): + new_message = AIMessage("Hello!") + # highlight-next-line + return {"messages": [new_message], "extra_field": 10} +``` + +```python +from langgraph.graph import START + +graph = StateGraph(State).add_node(node).add_edge(START, "node").compile() + +result = graph.invoke({"messages": [HumanMessage("Hi")]}) + +for message in result["messages"]: + message.pretty_print() +``` + +``` +================================ Human Message ================================ + +Hi +================================== Ai Message ================================== + +Hello! +``` + +#### MessagesState + +In practice, there are additional considerations for updating lists of messages: + +* We may wish to update an existing message in the state. +* We may want to accept short-hands for [message formats](/oss/graph-api#using-messages-in-your-graph), such as [OpenAI format](https://python.langchain.com/docs/concepts/messages/#openai-format). + +LangGraph includes a built-in reducer `add_messages` that handles these considerations: + +```python +from langgraph.graph.message import add_messages + +class State(TypedDict): + # highlight-next-line + messages: Annotated[list[AnyMessage], add_messages] + extra_field: int + +def node(state: State): + new_message = AIMessage("Hello!") + return {"messages": [new_message], "extra_field": 10} + +graph = StateGraph(State).add_node(node).set_entry_point("node").compile() +``` + +```python +# highlight-next-line +input_message = {"role": "user", "content": "Hi"} + +result = graph.invoke({"messages": [input_message]}) + +for message in result["messages"]: + message.pretty_print() +``` + +``` +================================ Human Message ================================ + +Hi +================================== Ai Message ================================== + +Hello! +``` + +This is a versatile representation of state for applications involving [chat models](https://python.langchain.com/docs/concepts/chat_models/). LangGraph includes a pre-built `MessagesState` for convenience, so that we can have: + +```python +from langgraph.graph import MessagesState + +class State(MessagesState): + extra_field: int +``` + +### Define input and output schemas + +By default, `StateGraph` operates with a single schema, and all nodes are expected to communicate using that schema. However, it's also possible to define distinct input and output schemas for a graph. + +When distinct schemas are specified, an internal schema will still be used for communication between nodes. The input schema ensures that the provided input matches the expected structure, while the output schema filters the internal data to return only the relevant information according to the defined output schema. + +Below, we'll see how to define distinct input and output schema. + +```python +from langgraph.graph import StateGraph, START, END +from typing_extensions import TypedDict + +# Define the schema for the input +class InputState(TypedDict): + question: str + +# Define the schema for the output +class OutputState(TypedDict): + answer: str + +# Define the overall schema, combining both input and output +class OverallState(InputState, OutputState): + pass + +# Define the node that processes the input and generates an answer +def answer_node(state: InputState): + # Example answer and an extra key + return {"answer": "bye", "question": state["question"]} + +# Build the graph with input and output schemas specified +builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState) +builder.add_node(answer_node) # Add the answer node +builder.add_edge(START, "answer_node") # Define the starting edge +builder.add_edge("answer_node", END) # Define the ending edge +graph = builder.compile() # Compile the graph + +# Invoke the graph with an input and print the result +print(graph.invoke({"question": "hi"})) +``` + +``` +{'answer': 'bye'} +``` + +Notice that the output of invoke only includes the output schema. + +### Pass private state between nodes + +In some cases, you may want nodes to exchange information that is crucial for intermediate logic but doesn't need to be part of the main schema of the graph. This private data is not relevant to the overall input/output of the graph and should only be shared between certain nodes. + +Below, we'll create an example sequential graph consisting of three nodes (node_1, node_2 and node_3), where private data is passed between the first two steps (node_1 and node_2), while the third step (node_3) only has access to the public overall state. + +```python +from langgraph.graph import StateGraph, START, END +from typing_extensions import TypedDict + +# The overall state of the graph (this is the public state shared across nodes) +class OverallState(TypedDict): + a: str + +# Output from node_1 contains private data that is not part of the overall state +class Node1Output(TypedDict): + private_data: str + +# The private data is only shared between node_1 and node_2 +def node_1(state: OverallState) -> Node1Output: + output = {"private_data": "set by node_1"} + print(f"Entered node `node_1`:\n\tInput: {state}.\n\tReturned: {output}") + return output + +# Node 2 input only requests the private data available after node_1 +class Node2Input(TypedDict): + private_data: str + +def node_2(state: Node2Input) -> OverallState: + output = {"a": "set by node_2"} + print(f"Entered node `node_2`:\n\tInput: {state}.\n\tReturned: {output}") + return output + +# Node 3 only has access to the overall state (no access to private data from node_1) +def node_3(state: OverallState) -> OverallState: + output = {"a": "set by node_3"} + print(f"Entered node `node_3`:\n\tInput: {state}.\n\tReturned: {output}") + return output + +# Connect nodes in a sequence +# node_2 accepts private data from node_1, whereas +# node_3 does not see the private data. +builder = StateGraph(OverallState).add_sequence([node_1, node_2, node_3]) +builder.add_edge(START, "node_1") +graph = builder.compile() + +# Invoke the graph with the initial state +response = graph.invoke( + { + "a": "set at start", + } +) + +print() +print(f"Output of graph invocation: {response}") +``` + +``` +Entered node `node_1`: + ut: {'a': 'set at start'}. + urned: {'private_data': 'set by node_1'} +Entered node `node_2`: + ut: {'private_data': 'set by node_1'}. + urned: {'a': 'set by node_2'} +Entered node `node_3`: + ut: {'a': 'set by node_2'}. + urned: {'a': 'set by node_3'} + +Output of graph invocation: {'a': 'set by node_3'} +``` + +### Use Pydantic models for graph state + +A [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.StateGraph) accepts a `state_schema` argument on initialization that specifies the "shape" of the state that the nodes in the graph can access and update. + +In our examples, we typically use a python-native `TypedDict` or [`dataclass`](https://docs.python.org/3/library/dataclasses.html) for `state_schema`, but `state_schema` can be any [type](https://docs.python.org/3/library/stdtypes.html#type-objects). + +Here, we'll see how a [Pydantic BaseModel](https://docs.pydantic.dev/latest/api/base_model/) can be used for `state_schema` to add run-time validation on **inputs**. + + + **Known Limitations** + * Currently, the output of the graph will **NOT** be an instance of a pydantic model. + * Run-time validation only occurs on inputs into nodes, not on the outputs. + * The validation error trace from pydantic does not show which node the error arises in. + * Pydantic's recursive validation can be slow. For performance-sensitive applications, you may want to consider using a `dataclass` instead. + + +```python +from langgraph.graph import StateGraph, START, END +from typing_extensions import TypedDict +from pydantic import BaseModel + +# The overall state of the graph (this is the public state shared across nodes) +class OverallState(BaseModel): + a: str + +def node(state: OverallState): + return {"a": "goodbye"} + +# Build the state graph +builder = StateGraph(OverallState) +builder.add_node(node) # node_1 is the first node +builder.add_edge(START, "node") # Start the graph with node_1 +builder.add_edge("node", END) # End the graph after node_1 +graph = builder.compile() + +# Test the graph with a valid input +graph.invoke({"a": "hello"}) +``` + +Invoke the graph with an **invalid** input + +```python +try: + graph.invoke({"a": 123}) # Should be a string +except Exception as e: + print("An exception was raised because `a` is an integer rather than a string.") + print(e) +``` + +``` +An exception was raised because `a` is an integer rather than a string. +1 validation error for OverallState +a + Input should be a valid string [type=string_type, input_value=123, input_type=int] + For further information visit https://errors.pydantic.dev/2.9/v/string_type +``` + +See below for additional features of Pydantic model state: + + + When using Pydantic models as state schemas, it's important to understand how serialization works, especially when: + + * Passing Pydantic objects as inputs + * Receiving outputs from the graph + * Working with nested Pydantic models + + Let's see these behaviors in action. + + ```python + from langgraph.graph import StateGraph, START, END + from pydantic import BaseModel + + class NestedModel(BaseModel): + value: str + + class ComplexState(BaseModel): + text: str + count: int + nested: NestedModel + + def process_node(state: ComplexState): + # Node receives a validated Pydantic object + print(f"Input state type: {type(state)}") + print(f"Nested type: {type(state.nested)}") + # Return a dictionary update + return {"text": state.text + " processed", "count": state.count + 1} + + # Build the graph + builder = StateGraph(ComplexState) + builder.add_node("process", process_node) + builder.add_edge(START, "process") + builder.add_edge("process", END) + graph = builder.compile() + + # Create a Pydantic instance for input + input_state = ComplexState(text="hello", count=0, nested=NestedModel(value="test")) + print(f"Input object type: {type(input_state)}") + + # Invoke graph with a Pydantic instance + result = graph.invoke(input_state) + print(f"Output type: {type(result)}") + print(f"Output content: {result}") + + # Convert back to Pydantic model if needed + output_model = ComplexState(**result) + print(f"Converted back to Pydantic: {type(output_model)}") + ``` + + + + Pydantic performs runtime type coercion for certain data types. This can be helpful but also lead to unexpected behavior if you're not aware of it. + + ```python + from langgraph.graph import StateGraph, START, END + from pydantic import BaseModel + + class CoercionExample(BaseModel): + # Pydantic will coerce string numbers to integers + number: int + # Pydantic will parse string booleans to bool + flag: bool + + def inspect_node(state: CoercionExample): + print(f"number: {state.number} (type: {type(state.number)})") + print(f"flag: {state.flag} (type: {type(state.flag)})") + return {} + + builder = StateGraph(CoercionExample) + builder.add_node("inspect", inspect_node) + builder.add_edge(START, "inspect") + builder.add_edge("inspect", END) + graph = builder.compile() + + # Demonstrate coercion with string inputs that will be converted + result = graph.invoke({"number": "42", "flag": "true"}) + + # This would fail with a validation error + try: + graph.invoke({"number": "not-a-number", "flag": "true"}) + except Exception as e: + print(f"\nExpected validation error: {e}") + ``` + + + + When working with LangChain message types in your state schema, there are important considerations for serialization. You should use `AnyMessage` (rather than `BaseMessage`) for proper serialization/deserialization when using message objects over the wire. + + ```python + from langgraph.graph import StateGraph, START, END + from pydantic import BaseModel + from langchain_core.messages import HumanMessage, AIMessage, AnyMessage + from typing import List + + class ChatState(BaseModel): + messages: List[AnyMessage] + context: str + + def add_message(state: ChatState): + return {"messages": state.messages + [AIMessage(content="Hello there!")]} + + builder = StateGraph(ChatState) + builder.add_node("add_message", add_message) + builder.add_edge(START, "add_message") + builder.add_edge("add_message", END) + graph = builder.compile() + + # Create input with a message + initial_state = ChatState( + messages=[HumanMessage(content="Hi")], context="Customer support chat" + ) + + result = graph.invoke(initial_state) + print(f"Output: {result}") + + # Convert back to Pydantic model to see message types + output_model = ChatState(**result) + for i, msg in enumerate(output_model.messages): + print(f"Message {i}: {type(msg).__name__} - {msg.content}") + ``` + + +## Add runtime configuration + +Sometimes you want to be able to configure your graph when calling it. For example, you might want to be able to specify what LLM or system prompt to use at runtime, _without polluting the graph state with these parameters_. + +To add runtime configuration: + +1. Specify a schema for your configuration +2. Add the configuration to the function signature for nodes or conditional edges +3. Pass the configuration into the graph. + +See below for a simple example: + +```python +from langgraph.graph import END, StateGraph, START +from langgraph.runtime import Runtime +from typing_extensions import TypedDict + +# 1. Specify config schema +class ContextSchema(TypedDict): + my_runtime_value: str + +# 2. Define a graph that accesses the config in a node +class State(TypedDict): + my_state_value: str + +# highlight-next-line +def node(state: State, runtime: Runtime[ContextSchema]): + # highlight-next-line + if runtime.context["my_runtime_value"] == "a": + return {"my_state_value": 1} + # highlight-next-line + elif runtime.context["my_runtime_value"] == "b": + return {"my_state_value": 2} + else: + raise ValueError("Unknown values.") + +# highlight-next-line +builder = StateGraph(State, context_schema=ContextSchema) +builder.add_node(node) +builder.add_edge(START, "node") +builder.add_edge("node", END) + +graph = builder.compile() + +# 3. Pass in configuration at runtime: +# highlight-next-line +print(graph.invoke({}, context={"my_runtime_value": "a"})) +# highlight-next-line +print(graph.invoke({}, context={"my_runtime_value": "b"})) +``` + +``` +{'my_state_value': 1} +{'my_state_value': 2} +``` + + + Below we demonstrate a practical example in which we configure what LLM to use at runtime. We will use both OpenAI and Anthropic models. + + ```python + from dataclasses import dataclass + + from langchain.chat_models import init_chat_model + from langgraph.graph import MessagesState, END, StateGraph, START + from langgraph.runtime import Runtime + from typing_extensions import TypedDict + + @dataclass + class ContextSchema: + model_provider: str = "anthropic" + + MODELS = { + "anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"), + "openai": init_chat_model("openai:gpt-4.1-mini"), + } + + def call_model(state: MessagesState, runtime: Runtime[ContextSchema]): + model = MODELS[runtime.context.model_provider] + response = model.invoke(state["messages"]) + return {"messages": [response]} + + builder = StateGraph(MessagesState, context_schema=ContextSchema) + builder.add_node("model", call_model) + builder.add_edge(START, "model") + builder.add_edge("model", END) + + graph = builder.compile() + + # Usage + input_message = {"role": "user", "content": "hi"} + # With no configuration, uses default (Anthropic) + response_1 = graph.invoke({"messages": [input_message]})["messages"][-1] + # Or, can set OpenAI + response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1] + + print(response_1.response_metadata["model_name"]) + print(response_2.response_metadata["model_name"]) + ``` + + ``` + claude-3-5-haiku-20241022 + gpt-4.1-mini-2025-04-14 + ``` + + + + Below we demonstrate a practical example in which we configure two parameters: the LLM and system message to use at runtime. + + ```python + from dataclasses import dataclass + from typing import Optional + from langchain.chat_models import init_chat_model + from langchain_core.messages import SystemMessage + from langgraph.graph import END, MessagesState, StateGraph, START + from langgraph.runtime import Runtime + from typing_extensions import TypedDict + + @dataclass + class ContextSchema: + model_provider: str = "anthropic" + system_message: str | None = None + + MODELS = { + "anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"), + "openai": init_chat_model("openai:gpt-4.1-mini"), + } + + def call_model(state: MessagesState, runtime: Runtime[ContextSchema]): + model = MODELS[runtime.context.model_provider] + messages = state["messages"] + if (system_message := runtime.context.system_message): + messages = [SystemMessage(system_message)] + messages + response = model.invoke(messages) + return {"messages": [response]} + + builder = StateGraph(MessagesState, context_schema=ContextSchema) + builder.add_node("model", call_model) + builder.add_edge(START, "model") + builder.add_edge("model", END) + + graph = builder.compile() + + # Usage + input_message = {"role": "user", "content": "hi"} + response = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai", "system_message": "Respond in Italian."}) + for message in response["messages"]: + message.pretty_print() + ``` + + ``` + ================================ Human Message ================================ + + hi + ================================== Ai Message ================================== + + Ciao! Come posso aiutarti oggi? + ``` + + +## Add retry policies + +There are many use cases where you may wish for your node to have a custom retry policy, for example if you are calling an API, querying a database, or calling an LLM, etc. LangGraph lets you add retry policies to nodes. + +To configure a retry policy, pass the `retry_policy` parameter to the [add_node](https://python.langchain.com/api_reference/langgraph/graph/langgraph.graph.state.StateGraph.html#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node: + +```python +from langgraph.pregel import RetryPolicy + +builder.add_node( + "node_name", + node_function, + retry_policy=RetryPolicy(), +) +``` + +By default, the `retry_on` parameter uses the `default_retry_on` function, which retries on any exception except for the following: + +* `ValueError` +* `TypeError` +* `ArithmeticError` +* `ImportError` +* `LookupError` +* `NameError` +* `SyntaxError` +* `RuntimeError` +* `ReferenceError` +* `StopIteration` +* `StopAsyncIteration` +* `OSError` + +In addition, for exceptions from popular http request libraries such as `requests` and `httpx` it only retries on 5xx status codes. + + + Consider an example in which we are reading from a SQL database. Below we pass two different retry policies to nodes: + + ```python + import sqlite3 + from typing_extensions import TypedDict + from langchain.chat_models import init_chat_model + from langgraph.graph import END, MessagesState, StateGraph, START + from langgraph.pregel import RetryPolicy + from langchain_community.utilities import SQLDatabase + from langchain_core.messages import AIMessage + + db = SQLDatabase.from_uri("sqlite:///:memory:") + model = init_chat_model("anthropic:claude-3-5-haiku-latest") + + def query_database(state: MessagesState): + query_result = db.run("SELECT * FROM Artist LIMIT 10;") + return {"messages": [AIMessage(content=query_result)]} + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": [response]} + + # Define a new graph + builder = StateGraph(MessagesState) + builder.add_node( + "query_database", + query_database, + retry_policy=RetryPolicy(retry_on=sqlite3.OperationalError), + ) + builder.add_node("model", call_model, retry_policy=RetryPolicy(max_attempts=5)) + builder.add_edge(START, "model") + builder.add_edge("model", "query_database") + builder.add_edge("query_database", END) + graph = builder.compile() + ``` + + +## Add node caching + +Node caching is useful in cases where you want to avoid repeating operations, like when doing something expensive (either in terms of time or cost). LangGraph lets you add individualized caching policies to nodes in a graph. + +To configure a cache policy, pass the `cache_policy` parameter to the [add_node](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.state.StateGraph.add_node) function. In the following example, a [`CachePolicy`](https://langchain-ai.github.io/langgraph/reference/types/?h=cachepolicy#langgraph.types.CachePolicy) object is instantiated with a time to live of 120 seconds and the default `key_func` generator. Then it is associated with a node: + +```python +from langgraph.types import CachePolicy + +builder.add_node( + "node_name", + node_function, + cache_policy=CachePolicy(ttl=120), +) +``` + +Then, to enable node-level caching for a graph, set the `cache` argument when compiling the graph. The example below uses `InMemoryCache` to set up a graph with in-memory cache, but `SqliteCache` is also available. + +```python +from langgraph.cache.memory import InMemoryCache + +graph = builder.compile(cache=InMemoryCache()) +``` + +## Create a sequence of steps + + + **Prerequisites** + This guide assumes familiarity with the above section on [state](#define-and-update-state). + + +Here we demonstrate how to construct a simple sequence of steps. We will show: + +1. How to build a sequential graph +2. Built-in short-hand for constructing similar graphs. + +To add a sequence of nodes, we use the `.add_node` and `.add_edge` methods of our [graph](/oss/graph-api#stategraph): + +```python +from langgraph.graph import START, StateGraph + +builder = StateGraph(State) + +# Add nodes +builder.add_node(step_1) +builder.add_node(step_2) +builder.add_node(step_3) + +# Add edges +builder.add_edge(START, "step_1") +builder.add_edge("step_1", "step_2") +builder.add_edge("step_2", "step_3") +``` + +We can also use the built-in shorthand `.add_sequence`: + +```python +builder = StateGraph(State).add_sequence([step_1, step_2, step_3]) +builder.add_edge(START, "step_1") +``` + + + + +LangGraph makes it easy to add an underlying persistence layer to your application. +This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern: + +* How state updates are [checkpointed](/oss/persistence) +* How interruptions are resumed in [human-in-the-loop](/oss/human-in-the-loop) workflows +* How we can "rewind" and branch-off executions using LangGraph's [time travel](/oss/time-travel) features + +They also determine how execution steps are [streamed](/oss/streaming), and how your application is visualized +and debugged using [LangGraph Studio](/langgraph-platform/langgraph-studio). + +Let's demonstrate an end-to-end example. We will create a sequence of three steps: + +1. Populate a value in a key of the state +2. Update the same value +3. Populate a different value + +Let's first define our [state](/oss/graph-api#state). This governs the [schema of the graph](/oss/graph-api#schema), and can also specify how to apply updates. See [this section](#process-state-updates-with-reducers) for more detail. + +In our case, we will just keep track of two values: + +```python +from typing_extensions import TypedDict + +class State(TypedDict): + value_1: str + value_2: int +``` + +Our [nodes](/oss/graph-api#nodes) are just Python functions that read our graph's state and make updates to it. The first argument to this function will always be the state: + +```python +def step_1(state: State): + return {"value_1": "a"} + +def step_2(state: State): + current_value_1 = state["value_1"] + return {"value_1": f"{current_value_1} b"} + +def step_3(state: State): + return {"value_2": 10} +``` + + + Note that when issuing updates to the state, each node can just specify the value of the key it wishes to update. + + By default, this will **overwrite** the value of the corresponding key. You can also use [reducers](/oss/graph-api#reducers) to control how updates are processed— for example, you can append successive updates to a key instead. See [this section](#process-state-updates-with-reducers) for more detail. + + +Finally, we define the graph. We use [StateGraph](/oss/graph-api#stategraph) to define a graph that operates on this state. + +:::python +We will then use [add_node](/oss/graph-api#messagesstate) and [add_edge](/oss/graph-api#edges) to populate our graph and define its control flow. +::: + +```python +from langgraph.graph import START, StateGraph + +builder = StateGraph(State) + +# Add nodes +builder.add_node(step_1) +builder.add_node(step_2) +builder.add_node(step_3) + +# Add edges +builder.add_edge(START, "step_1") +builder.add_edge("step_1", "step_2") +builder.add_edge("step_2", "step_3") +``` + + + **Specifying custom names** + You can specify custom names for nodes using `.add_node`: + + ```python + builder.add_node("my_node", step_1) + ``` + + +Note that: + +* `.add_edge` takes the names of nodes, which for functions defaults to `node.__name__`. +* We must specify the entry point of the graph. For this we add an edge with the [START node](/oss/graph-api#start-node). +* The graph halts when there are no more nodes to execute. + +We next [compile](/oss/graph-api#compiling-your-graph) our graph. This provides a few basic checks on the structure of the graph (e.g., identifying orphaned nodes). If we were adding persistence to our application via a [checkpointer](/oss/persistence), it would also be passed in here. + +```python +graph = builder.compile() +``` + +LangGraph provides built-in utilities for visualizing your graph. Let's inspect our sequence. See [this guide](#visualize-your-graph) for detail on visualization. + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Sequence of steps graph](/oss/images/graph_api_image_2.png) + +Let's proceed with a simple invocation: + +```python +graph.invoke({"value_1": "c"}) +``` + +``` +{'value_1': 'a b', 'value_2': 10} +``` + +Note that: + +* We kicked off invocation by providing a value for a single state key. We must always provide a value for at least one key. +* The value we passed in was overwritten by the first node. +* The second node updated the value. +* The third node populated a different value. + + + **Built-in shorthand** + `langgraph>=0.2.46` includes a built-in short-hand `add_sequence` for adding node sequences. You can compile the same graph as follows: + + ```python + # highlight-next-line + builder = StateGraph(State).add_sequence([step_1, step_2, step_3]) + builder.add_edge(START, "step_1") + + graph = builder.compile() + + graph.invoke({"value_1": "c"}) + ``` + + +## Create branches + +Parallel execution of nodes is essential to speed up overall graph operation. LangGraph offers native support for parallel execution of nodes, which can significantly enhance the performance of graph-based workflows. This parallelization is achieved through fan-out and fan-in mechanisms, utilizing both standard edges and [conditional_edges](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.MessageGraph.add_conditional_edges). Below are some examples showing how to add create branching dataflows that work for you. + +### Run graph nodes in parallel + +In this example, we fan out from `Node A` to `B and C` and then fan in to `D`. With our state, [we specify the reducer add operation](/oss/graph-api#reducers). This will combine or accumulate values for the specific key in the State, rather than simply overwriting the existing value. For lists, this means concatenating the new list with the existing list. See the above section on [state reducers](#process-state-updates-with-reducers) for more detail on updating state with reducers. + +```python +import operator +from typing import Annotated, Any +from typing_extensions import TypedDict +from langgraph.graph import StateGraph, START, END + +class State(TypedDict): + # The operator.add reducer fn makes this append-only + aggregate: Annotated[list, operator.add] + +def a(state: State): + print(f'Adding "A" to {state["aggregate"]}') + return {"aggregate": ["A"]} + +def b(state: State): + print(f'Adding "B" to {state["aggregate"]}') + return {"aggregate": ["B"]} + +def c(state: State): + print(f'Adding "C" to {state["aggregate"]}') + return {"aggregate": ["C"]} + +def d(state: State): + print(f'Adding "D" to {state["aggregate"]}') + return {"aggregate": ["D"]} + +builder = StateGraph(State) +builder.add_node(a) +builder.add_node(b) +builder.add_node(c) +builder.add_node(d) +builder.add_edge(START, "a") +builder.add_edge("a", "b") +builder.add_edge("a", "c") +builder.add_edge("b", "d") +builder.add_edge("c", "d") +builder.add_edge("d", END) +graph = builder.compile() +``` + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Parallel execution graph](/oss/images/graph_api_image_3.png) + +With the reducer, you can see that the values added in each node are accumulated. + +```python +graph.invoke({"aggregate": []}, {"configurable": {"thread_id": "foo"}}) +``` + +``` +Adding "A" to [] +Adding "B" to ['A'] +Adding "C" to ['A'] +Adding "D" to ['A', 'B', 'C'] +``` + + + In the above example, nodes `"b"` and `"c"` are executed concurrently in the same [superstep](/oss/graph-api#graphs). Because they are in the same step, node `"d"` executes after both `"b"` and `"c"` are finished. + + Importantly, updates from a parallel superstep may not be ordered consistently. If you need a consistent, predetermined ordering of updates from a parallel superstep, you should write the outputs to a separate field in the state together with a value with which to order them. + + + + LangGraph executes nodes within [supersteps](/oss/graph-api#graphs), meaning that while parallel branches are executed in parallel, the entire superstep is **transactional**. If any of these branches raises an exception, **none** of the updates are applied to the state (the entire superstep errors). + + Importantly, when using a [checkpointer](/oss/persistence), results from successful nodes within a superstep are saved, and don't repeat when resumed. + + If you have error-prone (perhaps want to handle flakey API calls), LangGraph provides two ways to address this: + + 1. You can write regular python code within your node to catch and handle exceptions. + 2. You can set a **[retry_policy](https://python.langchain.com/api_reference/langgraph/types/langgraph.types.RetryPolicy.html)** to direct the graph to retry nodes that raise certain types of exceptions. Only failing branches are retried, so you needn't worry about performing redundant work. + + Together, these let you perform parallel execution and fully control exception handling. + + +### Defer node execution + +Deferring node execution is useful when you want to delay the execution of a node until all other pending tasks are completed. This is particularly relevant when branches have different lengths, which is common in workflows like map-reduce flows. + +The above example showed how to fan-out and fan-in when each path was only one step. But what if one branch had more than one step? Let's add a node `"b_2"` in the `"b"` branch: + +```python +import operator +from typing import Annotated, Any +from typing_extensions import TypedDict +from langgraph.graph import StateGraph, START, END + +class State(TypedDict): + # The operator.add reducer fn makes this append-only + aggregate: Annotated[list, operator.add] + +def a(state: State): + print(f'Adding "A" to {state["aggregate"]}') + return {"aggregate": ["A"]} + +def b(state: State): + print(f'Adding "B" to {state["aggregate"]}') + return {"aggregate": ["B"]} + +def b_2(state: State): + print(f'Adding "B_2" to {state["aggregate"]}') + return {"aggregate": ["B_2"]} + +def c(state: State): + print(f'Adding "C" to {state["aggregate"]}') + return {"aggregate": ["C"]} + +def d(state: State): + print(f'Adding "D" to {state["aggregate"]}') + return {"aggregate": ["D"]} + +builder = StateGraph(State) +builder.add_node(a) +builder.add_node(b) +builder.add_node(b_2) +builder.add_node(c) +# highlight-next-line +builder.add_node(d, defer=True) +builder.add_edge(START, "a") +builder.add_edge("a", "b") +builder.add_edge("a", "c") +builder.add_edge("b", "b_2") +builder.add_edge("b_2", "d") +builder.add_edge("c", "d") +builder.add_edge("d", END) +graph = builder.compile() +``` + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Deferred execution graph](/oss/images/graph_api_image_4.png) + +```python +graph.invoke({"aggregate": []}) +``` + +``` +Adding "A" to [] +Adding "B" to ['A'] +Adding "C" to ['A'] +Adding "B_2" to ['A', 'B', 'C'] +Adding "D" to ['A', 'B', 'C', 'B_2'] +``` + +In the above example, nodes `"b"` and `"c"` are executed concurrently in the same superstep. We set `defer=True` on node `d` so it will not execute until all pending tasks are finished. In this case, this means that `"d"` waits to execute until the entire `"b"` branch is finished. + +### Conditional branching + +If your fan-out should vary at runtime based on the state, you can use [add_conditional_edges](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.StateGraph.add_conditional_edges) to select one or more paths using the graph state. See example below, where node `a` generates a state update that determines the following node. + +```python +import operator +from typing import Annotated, Literal, Sequence +from typing_extensions import TypedDict +from langgraph.graph import StateGraph, START, END + +class State(TypedDict): + aggregate: Annotated[list, operator.add] + # Add a key to the state. We will set this key to determine + # how we branch. + which: str + +def a(state: State): + print(f'Adding "A" to {state["aggregate"]}') + # highlight-next-line + return {"aggregate": ["A"], "which": "c"} + +def b(state: State): + print(f'Adding "B" to {state["aggregate"]}') + return {"aggregate": ["B"]} + +def c(state: State): + print(f'Adding "C" to {state["aggregate"]}') + return {"aggregate": ["C"]} + +builder = StateGraph(State) +builder.add_node(a) +builder.add_node(b) +builder.add_node(c) +builder.add_edge(START, "a") +builder.add_edge("b", END) +builder.add_edge("c", END) + +def conditional_edge(state: State) -> Literal["b", "c"]: + # Fill in arbitrary logic here that uses the state + # to determine the next node + return state["which"] + +# highlight-next-line +builder.add_conditional_edges("a", conditional_edge) + +graph = builder.compile() +``` + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Conditional branching graph](/oss/images/graph_api_image_5.png) + +```python +result = graph.invoke({"aggregate": []}) +print(result) +``` + +``` +Adding "A" to [] +Adding "C" to ['A'] +{'aggregate': ['A', 'C'], 'which': 'c'} +``` + + + Your conditional edges can route to multiple destination nodes. For example: + + ```python + def route_bc_or_cd(state: State) -> Sequence[str]: + if state["which"] == "cd": + return ["c", "d"] + return ["b", "c"] + ``` + + +## Map-Reduce and the Send API + +LangGraph supports map-reduce and other advanced branching patterns using the Send API. Here is an example of how to use it: + +```python +from langgraph.graph import StateGraph, START, END +from langgraph.types import Send +from typing_extensions import TypedDict, Annotated +import operator + +class OverallState(TypedDict): + topic: str + subjects: list[str] + jokes: Annotated[list[str], operator.add] + best_selected_joke: str + +def generate_topics(state: OverallState): + return {"subjects": ["lions", "elephants", "penguins"]} + +def generate_joke(state: OverallState): + joke_map = { + "lions": "Why don't lions like fast food? Because they can't catch it!", + "elephants": "Why don't elephants use computers? They're afraid of the mouse!", + "penguins": "Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice." + } + return {"jokes": [joke_map[state["subject"]]]} + +def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] + +def best_joke(state: OverallState): + return {"best_selected_joke": "penguins"} + +builder = StateGraph(OverallState) +builder.add_node("generate_topics", generate_topics) +builder.add_node("generate_joke", generate_joke) +builder.add_node("best_joke", best_joke) +builder.add_edge(START, "generate_topics") +builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"]) +builder.add_edge("generate_joke", "best_joke") +builder.add_edge("best_joke", END) +builder.add_edge("generate_topics", END) +graph = builder.compile() +``` + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Map-reduce graph with fanout](/oss/images/graph_api_image_6.png) + +```python +# Call the graph: here we call it to generate a list of jokes +for step in graph.stream({"topic": "animals"}): + print(step) +``` + +``` +{'generate_topics': {'subjects': ['lions', 'elephants', 'penguins']}} +{'generate_joke': {'jokes': ["Why don't lions like fast food? Because they can't catch it!"]}} +{'generate_joke': {'jokes': ["Why don't elephants use computers? They're afraid of the mouse!"]}} +{'generate_joke': {'jokes': ['Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice.']}} +{'best_joke': {'best_selected_joke': 'penguins'}} +``` + +## Create and control loops + +When creating a graph with a loop, we require a mechanism for terminating execution. This is most commonly done by adding a [conditional edge](/oss/graph-api#conditional-edges) that routes to the [END](/oss/graph-api#end-node) node once we reach some termination condition. + +You can also set the graph recursion limit when invoking or streaming the graph. The recursion limit sets the number of [supersteps](/oss/graph-api#graphs) that the graph is allowed to execute before it raises an error. Read more about the concept of recursion limits [here](/oss/graph-api#recursion-limit). + +Let's consider a simple graph with a loop to better understand how these mechanisms work. + + + To return the last value of your state instead of receiving a recursion limit error, see the [next section](#impose-a-recursion-limit). + + +When creating a loop, you can include a conditional edge that specifies a termination condition: + +```python +builder = StateGraph(State) +builder.add_node(a) +builder.add_node(b) + +def route(state: State) -> Literal["b", END]: + if termination_condition(state): + return END + else: + return "b" + +builder.add_edge(START, "a") +builder.add_conditional_edges("a", route) +builder.add_edge("b", "a") +graph = builder.compile() +``` + +To control the recursion limit, specify `"recursion_limit"` in the config. This will raise a `GraphRecursionError`, which you can catch and handle: + +```python +from langgraph.errors import GraphRecursionError + +try: + graph.invoke(inputs, {"recursion_limit": 3}) +except GraphRecursionError: + print("Recursion Error") +``` + +Let's define a graph with a simple loop. Note that we use a conditional edge to implement a termination condition. + +```python +import operator +from typing import Annotated, Literal +from typing_extensions import TypedDict +from langgraph.graph import StateGraph, START, END + +class State(TypedDict): + # The operator.add reducer fn makes this append-only + aggregate: Annotated[list, operator.add] + +def a(state: State): + print(f'Node A sees {state["aggregate"]}') + return {"aggregate": ["A"]} + +def b(state: State): + print(f'Node B sees {state["aggregate"]}') + return {"aggregate": ["B"]} + +# Define nodes +builder = StateGraph(State) +builder.add_node(a) +builder.add_node(b) + +# Define edges +def route(state: State) -> Literal["b", END]: + if len(state["aggregate"]) < 7: + return "b" + else: + return END + +builder.add_edge(START, "a") +builder.add_conditional_edges("a", route) +builder.add_edge("b", "a") +graph = builder.compile() +``` + +```python +from IPython.display import Image, display + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Simple loop graph](/oss/images/graph_api_image_3.png) + +This architecture is similar to a [ReAct agent](/oss/agentic-architectures) in which node `"a"` is a tool-calling model, and node `"b"` represents the tools. + +In our `route` conditional edge, we specify that we should end after the `"aggregate"` list in the state passes a threshold length. + +Invoking the graph, we see that we alternate between nodes `"a"` and `"b"` before terminating once we reach the termination condition. + +```python +graph.invoke({"aggregate": []}) +``` + +``` +Node A sees [] +Node B sees ['A'] +Node A sees ['A', 'B'] +Node B sees ['A', 'B', 'A'] +Node A sees ['A', 'B', 'A', 'B'] +Node B sees ['A', 'B', 'A', 'B', 'A'] +Node A sees ['A', 'B', 'A', 'B', 'A', 'B'] +``` + +### Impose a recursion limit + +In some applications, we may not have a guarantee that we will reach a given termination condition. In these cases, we can set the graph's [recursion limit](/oss/graph-api#recursion-limit). This will raise a `GraphRecursionError` after a given number of [supersteps](/oss/graph-api#graphs). We can then catch and handle this exception: + +```python +from langgraph.errors import GraphRecursionError + +try: + graph.invoke({"aggregate": []}, {"recursion_limit": 4}) +except GraphRecursionError: + print("Recursion Error") +``` + +``` +Node A sees [] +Node B sees ['A'] +Node C sees ['A', 'B'] +Node D sees ['A', 'B'] +Node A sees ['A', 'B', 'C', 'D'] +Recursion Error +``` + + + Instead of raising `GraphRecursionError`, we can introduce a new key to the state that keeps track of the number of steps remaining until reaching the recursion limit. We can then use this key to determine if we should end the run. + + LangGraph implements a special `RemainingSteps` annotation. Under the hood, it creates a `ManagedValue` channel -- a state channel that will exist for the duration of our graph run and no longer. + + ```python + import operator + from typing import Annotated, Literal + from typing_extensions import TypedDict + from langgraph.graph import StateGraph, START, END + from langgraph.managed.is_last_step import RemainingSteps + + class State(TypedDict): + aggregate: Annotated[list, operator.add] + remaining_steps: RemainingSteps + + def a(state: State): + print(f'Node A sees {state["aggregate"]}') + return {"aggregate": ["A"]} + + def b(state: State): + print(f'Node B sees {state["aggregate"]}') + return {"aggregate": ["B"]} + + # Define nodes + builder = StateGraph(State) + builder.add_node(a) + builder.add_node(b) + + # Define edges + def route(state: State) -> Literal["b", END]: + if state["remaining_steps"] <= 2: + return END + else: + return "b" + + builder.add_edge(START, "a") + builder.add_conditional_edges("a", route) + builder.add_edge("b", "a") + graph = builder.compile() + + # Test it out + result = graph.invoke({"aggregate": []}, {"recursion_limit": 4}) + print(result) + ``` + + ``` + Node A sees [] + Node B sees ['A'] + Node A sees ['A', 'B'] + {'aggregate': ['A', 'B', 'A']} + ``` + + + + To better understand how the recursion limit works, let's consider a more complex example. Below we implement a loop, but one step fans out into two nodes: + + ```python + import operator + from typing import Annotated, Literal + from typing_extensions import TypedDict + from langgraph.graph import StateGraph, START, END + + class State(TypedDict): + aggregate: Annotated[list, operator.add] + + def a(state: State): + print(f'Node A sees {state["aggregate"]}') + return {"aggregate": ["A"]} + + def b(state: State): + print(f'Node B sees {state["aggregate"]}') + return {"aggregate": ["B"]} + + def c(state: State): + print(f'Node C sees {state["aggregate"]}') + return {"aggregate": ["C"]} + + def d(state: State): + print(f'Node D sees {state["aggregate"]}') + return {"aggregate": ["D"]} + + # Define nodes + builder = StateGraph(State) + builder.add_node(a) + builder.add_node(b) + builder.add_node(c) + builder.add_node(d) + + # Define edges + def route(state: State) -> Literal["b", END]: + if len(state["aggregate"]) < 7: + return "b" + else: + return END + + builder.add_edge(START, "a") + builder.add_conditional_edges("a", route) + builder.add_edge("b", "c") + builder.add_edge("b", "d") + builder.add_edge(["c", "d"], "a") + graph = builder.compile() + ``` + + ```python + from IPython.display import Image, display + + display(Image(graph.get_graph().draw_mermaid_png())) + ``` + + ![Complex loop graph with branches](/oss/images/graph_api_image_8.png) + + This graph looks complex, but can be conceptualized as loop of [supersteps](/oss/graph-api#graphs): + + 1. Node A + 2. Node B + 3. Nodes C and D + 4. Node A + 5. ... + + We have a loop of four supersteps, where nodes C and D are executed concurrently. + + Invoking the graph as before, we see that we complete two full "laps" before hitting the termination condition: + + ```python + result = graph.invoke({"aggregate": []}) + ``` + + ``` + Node A sees [] + Node B sees ['A'] + Node D sees ['A', 'B'] + Node C sees ['A', 'B'] + Node A sees ['A', 'B', 'C', 'D'] + Node B sees ['A', 'B', 'C', 'D', 'A'] + Node D sees ['A', 'B', 'C', 'D', 'A', 'B'] + Node C sees ['A', 'B', 'C', 'D', 'A', 'B'] + Node A sees ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'] + ``` + + However, if we set the recursion limit to four, we only complete one lap because each lap is four supersteps: + + ```python + from langgraph.errors import GraphRecursionError + + try: + result = graph.invoke({"aggregate": []}, {"recursion_limit": 4}) + except GraphRecursionError: + print("Recursion Error") + ``` + + ``` + Node A sees [] + Node B sees ['A'] + Node C sees ['A', 'B'] + Node D sees ['A', 'B'] + Node A sees ['A', 'B', 'C', 'D'] + Recursion Error + ``` + + +## Async + +Using the [async](https://docs.python.org/3/library/asyncio.html) programming paradigm can produce significant performance improvements when running [IO-bound](https://en.wikipedia.org/wiki/I/O_bound) code concurrently (e.g., making concurrent API requests to a chat model provider). + +To convert a `sync` implementation of the graph to an `async` implementation, you will need to: + +1. Update `nodes` use `async def` instead of `def`. +2. Update the code inside to use `await` appropriately. +3. Invoke the graph with `.ainvoke` or `.astream` as desired. + +Because many LangChain objects implement the [Runnable Protocol](https://python.langchain.com/docs/expression_language/interface/) which has `async` variants of all the `sync` methods it's typically fairly quick to upgrade a `sync` graph to an `async` graph. + +See example below. To demonstrate async invocations of underlying LLMs, we will include a chat model: + + + +```python +from langchain.chat_models import init_chat_model +from langgraph.graph import MessagesState, StateGraph + +# highlight-next-line +async def node(state: MessagesState): # (1)! + # highlight-next-line + new_message = await llm.ainvoke(state["messages"]) # (2)! + return {"messages": [new_message]} + +builder = StateGraph(MessagesState).add_node(node).set_entry_point("node") +graph = builder.compile() + +input_message = {"role": "user", "content": "Hello"} +# highlight-next-line +result = await graph.ainvoke({"messages": [input_message]}) # (3)! +``` + +1. Declare nodes to be async functions. +2. Use async invocations when available within the node. +3. Use async invocations on the graph object itself. + + + **Async streaming** + See the [streaming guide](/oss/streaming) for examples of streaming with async. + + +## Combine control flow and state updates with `Command` + +It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [Command](https://python.langchain.com/api_reference/langgraph/types/langgraph.types.Command.html) object from node functions: + +```python +def my_node(state: State) -> Command[Literal["my_other_node"]]: + return Command( + # state update + update={"foo": "bar"}, + # control flow + goto="my_other_node" + ) +``` + +We show an end-to-end example below. Let's create a simple graph with 3 nodes: A, B and C. We will first execute node A, and then decide whether to go to Node B or Node C next based on the output of node A. + +```python +import random +from typing_extensions import TypedDict, Literal +from langgraph.graph import StateGraph, START +from langgraph.types import Command + +# Define graph state +class State(TypedDict): + foo: str + +# Define the nodes + +def node_a(state: State) -> Command[Literal["node_b", "node_c"]]: + print("Called A") + value = random.choice(["b", "c"]) + # this is a replacement for a conditional edge function + if value == "b": + goto = "node_b" + else: + goto = "node_c" + + # note how Command allows you to BOTH update the graph state AND route to the next node + return Command( + # this is the state update + update={"foo": value}, + # this is a replacement for an edge + goto=goto, + ) + +def node_b(state: State): + print("Called B") + return {"foo": state["foo"] + "b"} + +def node_c(state: State): + print("Called C") + return {"foo": state["foo"] + "c"} +``` + +We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](/oss/graph-api#conditional-edges) for routing! This is because control flow is defined with `Command` inside `node_a`. + +```python +builder = StateGraph(State) +builder.add_edge(START, "node_a") +builder.add_node(node_a) +builder.add_node(node_b) +builder.add_node(node_c) +# NOTE: there are no edges between nodes A, B and C! + +graph = builder.compile() +``` + + + You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal["node_b", "node_c"]]`. This is necessary for the graph rendering and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`. + + +```python +from IPython.display import display, Image + +display(Image(graph.get_graph().draw_mermaid_png())) +``` + +![Command-based graph navigation](/oss/images/graph_api_image_11.png) + +If we run the graph multiple times, we'd see it take different paths (A -> B or A -> C) based on the random choice in node A. + +```python +graph.invoke({"foo": ""}) +``` + +``` +Called A +Called C +``` + +### Navigate to a node in a parent graph + +If you are using [subgraphs](/oss/subgraphs), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`: + +```python +def my_node(state: State) -> Command[Literal["my_other_node"]]: + return Command( + update={"foo": "bar"}, + goto="other_subgraph", # where `other_subgraph` is a node in the parent graph + graph=Command.PARENT + ) +``` + +Let's demonstrate this using the above example. We'll do so by changing `node_a` in the above example into a single-node graph that we'll add as a subgraph to our parent graph. + + + **State updates with `Command.PARENT`** + When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](/oss/graph-api#schema), you **must** define a [reducer](/oss/graph-api#reducers) for the key you're updating in the parent graph state. See the example below. + + +```python +import operator +from typing_extensions import Annotated + +class State(TypedDict): + # NOTE: we define a reducer here + # highlight-next-line + foo: Annotated[str, operator.add] + +def node_a(state: State): + print("Called A") + value = random.choice(["a", "b"]) + # this is a replacement for a conditional edge function + if value == "a": + goto = "node_b" + else: + goto = "node_c" + + # note how Command allows you to BOTH update the graph state AND route to the next node + return Command( + update={"foo": value}, + goto=goto, + # this tells LangGraph to navigate to node_b or node_c in the parent graph + # NOTE: this will navigate to the closest parent graph relative to the subgraph + # highlight-next-line + graph=Command.PARENT, + ) + +subgraph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile() + +def node_b(state: State): + print("Called B") + # NOTE: since we've defined a reducer, we don't need to manually append + # new characters to existing 'foo' value. instead, reducer will append these + # automatically (via operator.add) + # highlight-next-line + return {"foo": "b"} + +def node_c(state: State): + print("Called C") + # highlight-next-line + return {"foo": "c"} + +builder = StateGraph(State) +builder.add_edge(START, "subgraph") +builder.add_node("subgraph", subgraph) +builder.add_node(node_b) +builder.add_node(node_c) + +graph = builder.compile() +``` + +```python +graph.invoke({"foo": ""}) +``` + +``` +Called A +Called C +``` + +### Use inside tools + +A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool: + +```python +@tool +def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig): + """Use this to look up user information to better assist them with their questions.""" + user_info = get_user_info(config.get("configurable", {}).get("user_id")) + return Command( + update={ + # update the state keys + "user_info": user_info, + # update the message history + "messages": [ToolMessage("Successfully looked up user information", tool_call_id=tool_call_id)] + } + ) +``` + + + You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages). + + +If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`](https://python.langchain.com/api_reference/langgraph/prebuilt/langgraph.prebuilt.tool_node.ToolNode.html) which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from the node. + +## Visualize your graph + +Here we demonstrate how to visualize the graphs you create. + +You can visualize any arbitrary [Graph](https://langchain-ai.github.io/langgraph/reference/graphs/), including [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.state.StateGraph). Let's have some fun by drawing fractals :). + +```python +import random +from typing import Annotated, Literal +from typing_extensions import TypedDict +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages + +class State(TypedDict): + messages: Annotated[list, add_messages] + +class MyNode: + def __init__(self, name: str): + self.name = name + def __call__(self, state: State): + return {"messages": [("assistant", f"Called node {self.name}")]} + +def route(state) -> Literal["entry_node", "__end__"]: + if len(state["messages"]) > 10: + return "__end__" + return "entry_node" + +def add_fractal_nodes(builder, current_node, level, max_level): + if level > max_level: + return + # Number of nodes to create at this level + num_nodes = random.randint(1, 3) # Adjust randomness as needed + for i in range(num_nodes): + nm = ["A", "B", "C"][i] + node_name = f"node_{current_node}_{nm}" + builder.add_node(node_name, MyNode(node_name)) + builder.add_edge(current_node, node_name) + # Recursively add more nodes + r = random.random() + if r > 0.2 and level + 1 < max_level: + add_fractal_nodes(builder, node_name, level + 1, max_level) + elif r > 0.05: + builder.add_conditional_edges(node_name, route, node_name) + else: + # End + builder.add_edge(node_name, "__end__") + +def build_fractal_graph(max_level: int): + builder = StateGraph(State) + entry_point = "entry_node" + builder.add_node(entry_point, MyNode(entry_point)) + builder.add_edge(START, entry_point) + add_fractal_nodes(builder, entry_point, 1, max_level) + # Optional: set a finish point if required + builder.add_edge(entry_point, END) # or any specific node + return builder.compile() + +app = build_fractal_graph(3) +``` + +### Mermaid + +We can also convert a graph class into Mermaid syntax. + +```python +print(app.get_graph().draw_mermaid()) +``` + +``` +%%{init: {'flowchart': {'curve': 'linear'}}}%% +graph TD; + tart__([

__start__

]):::first + ry_node(entry_node) + e_entry_node_A(node_entry_node_A) + e_entry_node_B(node_entry_node_B) + e_node_entry_node_B_A(node_node_entry_node_B_A) + e_node_entry_node_B_B(node_node_entry_node_B_B) + e_node_entry_node_B_C(node_node_entry_node_B_C) + nd__([

__end__

]):::last + tart__ --> entry_node; + ry_node --> __end__; + ry_node --> node_entry_node_A; + ry_node --> node_entry_node_B; + e_entry_node_B --> node_node_entry_node_B_A; + e_entry_node_B --> node_node_entry_node_B_B; + e_entry_node_B --> node_node_entry_node_B_C; + e_entry_node_A -.-> entry_node; + e_entry_node_A -.-> __end__; + e_node_entry_node_B_A -.-> entry_node; + e_node_entry_node_B_A -.-> __end__; + e_node_entry_node_B_B -.-> entry_node; + e_node_entry_node_B_B -.-> __end__; + e_node_entry_node_B_C -.-> entry_node; + e_node_entry_node_B_C -.-> __end__; + ssDef default fill:#f2f0ff,line-height:1.2 + ssDef first fill-opacity:0 + ssDef last fill:#bfb6fc +``` + +### PNG + +If preferred, we could render the Graph into a `.png`. Here we could use three options: + +* Using Mermaid.ink API (does not require additional packages) +* Using Mermaid + Pyppeteer (requires `pip install pyppeteer`) +* Using graphviz (which requires `pip install graphviz`) + +**Using Mermaid.Ink** + +By default, `draw_mermaid_png()` uses Mermaid.Ink's API to generate the diagram. + +```python +from IPython.display import Image, display +from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles + +display(Image(app.get_graph().draw_mermaid_png())) +``` + +![Fractal graph visualization](/oss/images/graph_api_image_5.png) + +**Using Mermaid + Pyppeteer** + +```python +import nest_asyncio + +nest_asyncio.apply() # Required for Jupyter Notebook to run async functions + +display( + Image( + app.get_graph().draw_mermaid_png( + curve_style=CurveStyle.LINEAR, + node_colors=NodeStyles(first="#ffdfba", last="#baffc9", default="#fad7de"), + wrap_label_n_words=9, + output_file_path=None, + draw_method=MermaidDrawMethod.PYPPETEER, + background_color="white", + padding=10, + ) + ) +) +``` + +**Using Graphviz** + +```python +try: + display(Image(app.get_graph().draw_png())) +except ImportError: + print( + "You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt" + ) +``` diff --git a/src/oss/use-mcp.mdx b/src/oss/use-mcp.mdx new file mode 100644 index 00000000..a3f937ea --- /dev/null +++ b/src/oss/use-mcp.mdx @@ -0,0 +1,496 @@ +--- +title: Use MCP +--- + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide tools and context to language models. LangGraph agents can use tools defined on MCP servers through the `langchain-mcp-adapters` library. + +![MCP](/oss/images/mcp.png) + +:::python +Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph: + +```bash +pip install langchain-mcp-adapters +``` +::: + +:::js +Install the `@langchain/mcp-adapters` library to use MCP tools in LangGraph: + +```bash +npm install langchain-mcp-adapters +``` +::: + +## Use MCP tools + +:::python +The `langchain-mcp-adapters` package enables agents to use tools defined across one or more MCP servers. + + + + ```python title="Agent using tools defined on MCP servers" + # highlight-next-line + from langchain_mcp_adapters.client import MultiServerMCPClient + from langgraph.prebuilt import create_react_agent + + # highlight-next-line + client = MultiServerMCPClient( + { + "math": { + "command": "python", + # Replace with absolute path to your math_server.py file + "args": ["/path/to/math_server.py"], + "transport": "stdio", + }, + "weather": { + # Ensure you start your weather server on port 8000 + "url": "http://localhost:8000/mcp", + "transport": "streamable_http", + } + } + ) + # highlight-next-line + tools = await client.get_tools() + agent = create_react_agent( + "anthropic:claude-3-7-sonnet-latest", + # highlight-next-line + tools + ) + math_response = await agent.ainvoke( + {"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]} + ) + weather_response = await agent.ainvoke( + {"messages": [{"role": "user", "content": "what is the weather in nyc?"}]} + ) + ``` + + + ```python title="Workflow using MCP tools with ToolNode" + from langchain_mcp_adapters.client import MultiServerMCPClient + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START, END + from langgraph.prebuilt import ToolNode + + # Initialize the model + model = init_chat_model("anthropic:claude-3-5-sonnet-latest") + + # Set up MCP client + client = MultiServerMCPClient( + { + "math": { + "command": "python", + # Make sure to update to the full absolute path to your math_server.py file + "args": ["./examples/math_server.py"], + "transport": "stdio", + }, + "weather": { + # make sure you start your weather server on port 8000 + "url": "http://localhost:8000/mcp/", + "transport": "streamable_http", + } + } + ) + tools = await client.get_tools() + + # Bind tools to model + model_with_tools = model.bind_tools(tools) + + # Create ToolNode + tool_node = ToolNode(tools) + + def should_continue(state: MessagesState): + messages = state["messages"] + last_message = messages[-1] + if last_message.tool_calls: + return "tools" + return END + + # Define call_model function + async def call_model(state: MessagesState): + messages = state["messages"] + response = await model_with_tools.ainvoke(messages) + return {"messages": [response]} + + # Build the graph + builder = StateGraph(MessagesState) + builder.add_node("call_model", call_model) + builder.add_node("tools", tool_node) + + builder.add_edge(START, "call_model") + builder.add_conditional_edges( + "call_model", + should_continue, + ) + builder.add_edge("tools", "call_model") + + # Compile the graph + graph = builder.compile() + + # Test the graph + math_response = await graph.ainvoke( + {"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]} + ) + weather_response = await graph.ainvoke( + {"messages": [{"role": "user", "content": "what is the weather in nyc?"}]} + ) + ``` + + +::: + +:::js +The `@langchain/mcp-adapters` package enables agents to use tools defined across one or more MCP servers. + + + + ```typescript title="Agent using tools defined on MCP servers" + // highlight-next-line + import { MultiServerMCPClient } from "langchain-mcp-adapters/client"; + import { ChatAnthropic } from "@langchain/langgraph/prebuilt"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + // highlight-next-line + const client = new MultiServerMCPClient({ + math: { + command: "node", + // Replace with absolute path to your math_server.js file + args: ["/path/to/math_server.js"], + transport: "stdio", + }, + weather: { + // Ensure you start your weather server on port 8000 + url: "http://localhost:8000/mcp", + transport: "streamable_http", + }, + }); + + // highlight-next-line + const tools = await client.getTools(); + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-7-sonnet-latest" }), + // highlight-next-line + tools, + }); + + const mathResponse = await agent.invoke({ + messages: [{ role: "user", content: "what's (3 + 5) x 12?" }], + }); + + const weatherResponse = await agent.invoke({ + messages: [{ role: "user", content: "what is the weather in nyc?" }], + }); + ``` + + + ```typescript + import { MultiServerMCPClient } from "langchain-mcp-adapters/client"; + import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; + import { ToolNode } from "@langchain/langgraph/prebuilt"; + import { ChatOpenAI } from "@langchain/openai"; + import { AIMessage } from "@langchain/core/messages"; + import { z } from "zod"; + + const model = new ChatOpenAI({ model: "gpt-4" }); + + const client = new MultiServerMCPClient({ + math: { + command: "node", + // Make sure to update to the full absolute path to your math_server.js file + args: ["./examples/math_server.js"], + transport: "stdio", + }, + weather: { + // make sure you start your weather server on port 8000 + url: "http://localhost:8000/mcp/", + transport: "streamable_http", + }, + }); + + const tools = await client.getTools(); + + const builder = new StateGraph(MessagesZodState) + .addNode("callModel", async (state) => { + const response = await model.bindTools(tools).invoke(state.messages); + return { messages: [response] }; + }) + .addNode("tools", new ToolNode(tools)) + .addEdge(START, "callModel") + .addConditionalEdges("callModel", (state) => { + const lastMessage = state.messages.at(-1) as AIMessage | undefined; + if (!lastMessage?.tool_calls?.length) { + return "__end__"; + } + return "tools"; + }) + .addEdge("tools", "callModel"); + + const graph = builder.compile(); + + const mathResponse = await graph.invoke({ + messages: [{ role: "user", content: "what's (3 + 5) x 12?" }], + }); + + const weatherResponse = await graph.invoke({ + messages: [{ role: "user", content: "what is the weather in nyc?" }], + }); + ``` + + +::: + +## Custom MCP servers + +:::python +To create your own MCP servers, you can use the `mcp` library. This library provides a simple way to define tools and run them as servers. + +Install the MCP library: + +```bash +pip install mcp +``` +::: + +:::js +To create your own MCP servers, you can use the `@modelcontextprotocol/sdk` library. This library provides a simple way to define tools and run them as servers. + +Install the MCP SDK: + +```bash +npm install @modelcontextprotocol/sdk +``` +::: + +Use the following reference implementations to test your agent with MCP tool servers. + +:::python +```python title="Example Math Server (stdio transport)" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("Math") + +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + +@mcp.tool() +def multiply(a: int, b: int) -> int: + """Multiply two numbers""" + return a * b + +if __name__ == "__main__": + mcp.run(transport="stdio") +``` +::: + +:::js +```typescript title="Example Math Server (stdio transport)" +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +const server = new Server( + { + name: "math-server", + version: "0.1.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "add", + description: "Add two numbers", + inputSchema: { + type: "object", + properties: { + a: { + type: "number", + description: "First number", + }, + b: { + type: "number", + description: "Second number", + }, + }, + required: ["a", "b"], + }, + }, + { + name: "multiply", + description: "Multiply two numbers", + inputSchema: { + type: "object", + properties: { + a: { + type: "number", + description: "First number", + }, + b: { + type: "number", + description: "Second number", + }, + }, + required: ["a", "b"], + }, + }, + ], + }; +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + switch (request.params.name) { + case "add": { + const { a, b } = request.params.arguments as { a: number; b: number }; + return { + content: [ + { + type: "text", + text: String(a + b), + }, + ], + }; + } + case "multiply": { + const { a, b } = request.params.arguments as { a: number; b: number }; + return { + content: [ + { + type: "text", + text: String(a * b), + }, + ], + }; + } + default: + throw new Error(`Unknown tool: ${request.params.name}`); + } +}); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Math MCP server running on stdio"); +} + +main(); +``` +::: + +:::python +```python title="Example Weather Server (Streamable HTTP transport)" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("Weather") + +@mcp.tool() +async def get_weather(location: str) -> str: + """Get weather for location.""" + return "It's always sunny in New York" + +if __name__ == "__main__": + mcp.run(transport="streamable-http") +``` +::: + +:::js +```typescript title="Example Weather Server (HTTP transport)" +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import express from "express"; + +const app = express(); +app.use(express.json()); + +const server = new Server( + { + name: "weather-server", + version: "0.1.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "get_weather", + description: "Get weather for location", + inputSchema: { + type: "object", + properties: { + location: { + type: "string", + description: "Location to get weather for", + }, + }, + required: ["location"], + }, + }, + ], + }; +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + switch (request.params.name) { + case "get_weather": { + const { location } = request.params.arguments as { location: string }; + return { + content: [ + { + type: "text", + text: `It's always sunny in ${location}`, + }, + ], + }; + } + default: + throw new Error(`Unknown tool: ${request.params.name}`); + } +}); + +app.post("/mcp", async (req, res) => { + const transport = new SSEServerTransport("/mcp", res); + await server.connect(transport); +}); + +const PORT = process.env.PORT || 8000; +app.listen(PORT, () => { + console.log(`Weather MCP server running on port ${PORT}`); +}); +``` +::: + +:::python +## Additional resources + +* [MCP documentation](https://modelcontextprotocol.io/introduction) +* [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports) +* [langchain_mcp_adapters](https://github.com/langchain-ai/langchain-mcp-adapters) +::: + +:::js +## Additional resources + +* [MCP documentation](https://modelcontextprotocol.io/introduction) +* [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports) +* [`@langchain/mcp-adapters`](https://npmjs.com/package/@langchain/mcp-adapters) +::: diff --git a/src/oss/use-streaming.mdx b/src/oss/use-streaming.mdx new file mode 100644 index 00000000..23beaf0e --- /dev/null +++ b/src/oss/use-streaming.mdx @@ -0,0 +1,1822 @@ +--- +title: Stream outputs +--- +You can [stream outputs](/oss/streaming) from a LangGraph agent or workflow. + +## Supported stream modes + +:::python +Pass one or more of the following stream modes as a list to the @[`stream()`][CompiledStateGraph.stream] or @[`astream()`][CompiledStateGraph.astream] methods: +::: + +:::js +Pass one or more of the following stream modes as a list to the @[`stream()`][CompiledStateGraph.stream] method: +::: + +| Mode | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `values` | Streams the full value of the state after each step of the graph. | +| `updates` | Streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g., multiple nodes are run), those updates are streamed separately. | +| `custom` | Streams custom data from inside your graph nodes. | +| `messages` | Streams 2-tuples (LLM token, metadata) from any graph nodes where an LLM is invoked. | +| `debug` | Streams as much information as possible throughout the execution of the graph. | + +## Stream from an agent + +### Agent progress + +:::python +To stream agent progress, use the @[`stream()`][CompiledStateGraph.stream] or @[`astream()`][CompiledStateGraph.astream] methods with `stream_mode="updates"`. This emits an event after every agent step. +::: + +:::js +To stream agent progress, use the @[`stream()`][CompiledStateGraph.stream] method with `streamMode: "updates"`. This emits an event after every agent step. +::: + +For example, if you have an agent that calls a tool once, you should see the following updates: + +* **LLM node**: AI message with tool call requests +* **Tool node**: Tool message with execution result +* **LLM node**: Final AI response + +:::python + + + ```python + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + # highlight-next-line + for chunk in agent.stream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode="updates" + ): + print(chunk) + print("\n") + ``` + + + ```python + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + # highlight-next-line + async for chunk in agent.astream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode="updates" + ): + print(chunk) + print("\n") + ``` + + +::: + +:::js +```typescript +const agent = createReactAgent({ + llm: model, + tools: [getWeather], +}); + +for await (const chunk of await agent.stream( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + { streamMode: "updates" } +)) { + console.log(chunk); + console.log("\n"); +} +``` +::: + +### LLM tokens + +:::python +To stream tokens as they are produced by the LLM, use `stream_mode="messages"`: + + + + ```python + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + # highlight-next-line + for token, metadata in agent.stream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode="messages" + ): + print("Token", token) + print("Metadata", metadata) + print("\n") + ``` + + + ```python + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + # highlight-next-line + async for token, metadata in agent.astream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode="messages" + ): + print("Token", token) + print("Metadata", metadata) + print("\n") + ``` + + +::: + +:::js +To stream tokens as they are produced by the LLM, use `streamMode: "messages"`: + +```typescript +const agent = createReactAgent({ + llm: model, + tools: [getWeather], +}); + +for await (const [token, metadata] of await agent.stream( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + { streamMode: "messages" } +)) { + console.log("Token", token); + console.log("Metadata", metadata); + console.log("\n"); +} +``` +::: + +### Tool updates + +:::python +To stream updates from tools as they are executed, you can use @[get_stream_writer][get_stream_writer]. + + + + ```python + # highlight-next-line + from langgraph.config import get_stream_writer + + def get_weather(city: str) -> str: + """Get weather for a given city.""" + # highlight-next-line + writer = get_stream_writer() + # stream any arbitrary data + # highlight-next-line + writer(f"Looking up data for city: {city}") + return f"It's always sunny in {city}!" + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + + for chunk in agent.stream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode="custom" + ): + print(chunk) + print("\n") + ``` + + + ```python + # highlight-next-line + from langgraph.config import get_stream_writer + + def get_weather(city: str) -> str: + """Get weather for a given city.""" + # highlight-next-line + writer = get_stream_writer() + # stream any arbitrary data + # highlight-next-line + writer(f"Looking up data for city: {city}") + return f"It's always sunny in {city}!" + + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + + async for chunk in agent.astream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode="custom" + ): + print(chunk) + print("\n") + ``` + + + + + If you add `get_stream_writer` inside your tool, you won't be able to invoke the tool outside of a LangGraph execution context. + +::: + +:::js +To stream updates from tools as they are executed, you can use the `writer` parameter from the configuration. + +```typescript +import { LangGraphRunnableConfig } from "@langchain/langgraph"; + +const getWeather = tool( + async (input, config: LangGraphRunnableConfig) => { + // Stream any arbitrary data + config.writer?.("Looking up data for city: " + input.city); + return `It's always sunny in ${input.city}!`; + }, + { + name: "get_weather", + description: "Get weather for a given city.", + schema: z.object({ + city: z.string().describe("The city to get weather for."), + }), + } +); + +const agent = createReactAgent({ + llm: model, + tools: [getWeather], +}); + +for await (const chunk of await agent.stream( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + { streamMode: "custom" } +)) { + console.log(chunk); + console.log("\n"); +} +``` + + + If you add the `writer` parameter to your tool, you won't be able to invoke the tool outside of a LangGraph execution context without providing a writer function. + +::: + +### Stream multiple modes + +:::python +You can specify multiple streaming modes by passing stream mode as a list: `stream_mode=["updates", "messages", "custom"]`: + + + + ```python + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + + for stream_mode, chunk in agent.stream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode=["updates", "messages", "custom"] + ): + print(chunk) + print("\n") + ``` + + + ```python + agent = create_react_agent( + model="anthropic:claude-3-7-sonnet-latest", + tools=[get_weather], + ) + + async for stream_mode, chunk in agent.astream( + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, + # highlight-next-line + stream_mode=["updates", "messages", "custom"] + ): + print(chunk) + print("\n") + ``` + + +::: + +:::js +You can specify multiple streaming modes by passing streamMode as an array: `streamMode: ["updates", "messages", "custom"]`: + +```typescript +const agent = createReactAgent({ + llm: model, + tools: [getWeather], +}); + +for await (const chunk of await agent.stream( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + { streamMode: ["updates", "messages", "custom"] } +)) { + console.log(chunk); + console.log("\n"); +} +``` +::: + +### Disable streaming + +In some applications you might need to disable streaming of individual tokens for a given model. This is useful in [multi-agent](/oss/multi-agent) systems to control which agents stream their output. + +See the [Models](/oss/models#disable-streaming) guide to learn how to disable streaming. + +## Stream from a workflow + +### Basic usage example + +:::python +LangGraph graphs expose the @[`.stream()`][Pregel.stream] (sync) and @[`.astream()`][Pregel.astream] (async) methods to yield streamed outputs as iterators. + + + + ```python + for chunk in graph.stream(inputs, stream_mode="updates"): + print(chunk) + ``` + + + ```python + async for chunk in graph.astream(inputs, stream_mode="updates"): + print(chunk) + ``` + + +::: + +:::js +LangGraph graphs expose the @[`.stream()`][Pregel.stream] method to yield streamed outputs as iterators. + +```typescript +for await (const chunk of await graph.stream(inputs, { + streamMode: "updates", +})) { + console.log(chunk); +} +``` +::: + + + :::python + ```python + from typing import TypedDict + from langgraph.graph import StateGraph, START, END + + class State(TypedDict): + topic: str + joke: str + + def refine_topic(state: State): + return {"topic": state["topic"] + " and cats"} + + def generate_joke(state: State): + return {"joke": f"This is a joke about {state['topic']}"} + + graph = ( + StateGraph(State) + .add_node(refine_topic) + .add_node(generate_joke) + .add_edge(START, "refine_topic") + .add_edge("refine_topic", "generate_joke") + .add_edge("generate_joke", END) + .compile() + ) + + # highlight-next-line + for chunk in graph.stream( # (1)! + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="updates", # (2)! + ): + print(chunk) + ``` + + 1. The `stream()` method returns an iterator that yields streamed outputs. + 2. Set `stream_mode="updates"` to stream only the updates to the graph state after each node. Other stream modes are also available. See [supported stream modes](#supported-stream-modes) for details. + ::: + + :::js + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { z } from "zod"; + + const State = z.object({ + topic: z.string(), + joke: z.string(), + }); + + const graph = new StateGraph(State) + .addNode("refineTopic", (state) => { + return { topic: state.topic + " and cats" }; + }) + .addNode("generateJoke", (state) => { + return { joke: `This is a joke about ${state.topic}` }; + }) + .addEdge(START, "refineTopic") + .addEdge("refineTopic", "generateJoke") + .addEdge("generateJoke", END) + .compile(); + + for await (const chunk of await graph.stream( + { topic: "ice cream" }, + { streamMode: "updates" } // (1)! + )) { + console.log(chunk); + } + ``` + + 1. Set `streamMode: "updates"` to stream only the updates to the graph state after each node. Other stream modes are also available. See [supported stream modes](#supported-stream-modes) for details. + ::: + + ```output + {'refineTopic': {'topic': 'ice cream and cats'}} + {'generateJoke': {'joke': 'This is a joke about ice cream and cats'}} + ``` + + +### Stream multiple modes + +:::python +You can pass a list as the `stream_mode` parameter to stream multiple modes at once. + +The streamed outputs will be tuples of `(mode, chunk)` where `mode` is the name of the stream mode and `chunk` is the data streamed by that mode. + + + + ```python + for mode, chunk in graph.stream(inputs, stream_mode=["updates", "custom"]): + print(chunk) + ``` + + + ```python + async for mode, chunk in graph.astream(inputs, stream_mode=["updates", "custom"]): + print(chunk) + ``` + + +::: + +:::js +You can pass an array as the `streamMode` parameter to stream multiple modes at once. + +The streamed outputs will be tuples of `[mode, chunk]` where `mode` is the name of the stream mode and `chunk` is the data streamed by that mode. + +```typescript +for await (const [mode, chunk] of await graph.stream(inputs, { + streamMode: ["updates", "custom"], +})) { + console.log(chunk); +} +``` +::: + +### Stream graph state + +Use the stream modes `updates` and `values` to stream the state of the graph as it executes. + +* `updates` streams the **updates** to the state after each step of the graph. +* `values` streams the **full value** of the state after each step of the graph. + +:::python +```python +from typing import TypedDict +from langgraph.graph import StateGraph, START, END + + +class State(TypedDict): + topic: str + joke: str + + +def refine_topic(state: State): + return {"topic": state["topic"] + " and cats"} + + +def generate_joke(state: State): + return {"joke": f"This is a joke about {state['topic']}"} + +graph = ( + StateGraph(State) + .add_node(refine_topic) + .add_node(generate_joke) + .add_edge(START, "refine_topic") + .add_edge("refine_topic", "generate_joke") + .add_edge("generate_joke", END) + .compile() +) +``` +::: + +:::js +```typescript +import { StateGraph, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + topic: z.string(), + joke: z.string(), +}); + +const graph = new StateGraph(State) + .addNode("refineTopic", (state) => { + return { topic: state.topic + " and cats" }; + }) + .addNode("generateJoke", (state) => { + return { joke: `This is a joke about ${state.topic}` }; + }) + .addEdge(START, "refineTopic") + .addEdge("refineTopic", "generateJoke") + .addEdge("generateJoke", END) + .compile(); +``` +::: + + + + Use this to stream only the **state updates** returned by the nodes after each step. The streamed outputs include the name of the node as well as the update. + + :::python + ```python + for chunk in graph.stream( + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="updates", + ): + print(chunk) + ``` + ::: + + :::js + ```typescript + for await (const chunk of await graph.stream( + { topic: "ice cream" }, + { streamMode: "updates" } + )) { + console.log(chunk); + } + ``` + ::: + + + Use this to stream the **full state** of the graph after each step. + + :::python + ```python + for chunk in graph.stream( + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="values", + ): + print(chunk) + ``` + ::: + + :::js + ```typescript + for await (const chunk of await graph.stream( + { topic: "ice cream" }, + { streamMode: "values" } + )) { + console.log(chunk); + } + ``` + ::: + + + +### Stream subgraph outputs + +:::python +To include outputs from [subgraphs](/oss/subgraphs) in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. + +The outputs will be streamed as tuples `(namespace, data)`, where `namespace` is a tuple with the path to the node where a subgraph is invoked, e.g. `("parent_node:", "child_node:")`. + +```python +for chunk in graph.stream( + {"foo": "foo"}, + # highlight-next-line + subgraphs=True, # (1)! + stream_mode="updates", +): + print(chunk) +``` + +1. Set `subgraphs=True` to stream outputs from subgraphs. +::: + +:::js +To include outputs from [subgraphs](/oss/subgraphs) in the streamed outputs, you can set `subgraphs: true` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. + +The outputs will be streamed as tuples `[namespace, data]`, where `namespace` is a tuple with the path to the node where a subgraph is invoked, e.g. `["parent_node:", "child_node:"]`. + +```typescript +for await (const chunk of await graph.stream( + { foo: "foo" }, + { + subgraphs: true, // (1)! + streamMode: "updates", + } +)) { + console.log(chunk); +} +``` + +1. Set `subgraphs: true` to stream outputs from subgraphs. +::: + + + :::python + ```python + from langgraph.graph import START, StateGraph + from typing import TypedDict + + # Define subgraph + class SubgraphState(TypedDict): + foo: str # note that this key is shared with the parent graph state + bar: str + + def subgraph_node_1(state: SubgraphState): + return {"bar": "bar"} + + def subgraph_node_2(state: SubgraphState): + return {"foo": state["foo"] + state["bar"]} + + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node(subgraph_node_1) + subgraph_builder.add_node(subgraph_node_2) + subgraph_builder.add_edge(START, "subgraph_node_1") + subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2") + subgraph = subgraph_builder.compile() + + # Define parent graph + class ParentState(TypedDict): + foo: str + + def node_1(state: ParentState): + return {"foo": "hi! " + state["foo"]} + + builder = StateGraph(ParentState) + builder.add_node("node_1", node_1) + builder.add_node("node_2", subgraph) + builder.add_edge(START, "node_1") + builder.add_edge("node_1", "node_2") + graph = builder.compile() + + for chunk in graph.stream( + {"foo": "foo"}, + stream_mode="updates", + # highlight-next-line + subgraphs=True, # (1)! + ): + print(chunk) + ``` + + 1. Set `subgraphs=True` to stream outputs from subgraphs. + ::: + + :::js + ```typescript + import { StateGraph, START } from "@langchain/langgraph"; + import { z } from "zod"; + + // Define subgraph + const SubgraphState = z.object({ + foo: z.string(), // note that this key is shared with the parent graph state + bar: z.string(), + }); + + const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("subgraphNode1", (state) => { + return { bar: "bar" }; + }) + .addNode("subgraphNode2", (state) => { + return { foo: state.foo + state.bar }; + }) + .addEdge(START, "subgraphNode1") + .addEdge("subgraphNode1", "subgraphNode2"); + const subgraph = subgraphBuilder.compile(); + + // Define parent graph + const ParentState = z.object({ + foo: z.string(), + }); + + const builder = new StateGraph(ParentState) + .addNode("node1", (state) => { + return { foo: "hi! " + state.foo }; + }) + .addNode("node2", subgraph) + .addEdge(START, "node1") + .addEdge("node1", "node2"); + const graph = builder.compile(); + + for await (const chunk of await graph.stream( + { foo: "foo" }, + { + streamMode: "updates", + subgraphs: true, // (1)! + } + )) { + console.log(chunk); + } + ``` + + 1. Set `subgraphs: true` to stream outputs from subgraphs. + ::: + + :::python + ``` + ((), {'node_1': {'foo': 'hi! foo'}}) + (('node_2:dfddc4ba-c3c5-6887-5012-a243b5b377c2',), {'subgraph_node_1': {'bar': 'bar'}}) + (('node_2:dfddc4ba-c3c5-6887-5012-a243b5b377c2',), {'subgraph_node_2': {'foo': 'hi! foobar'}}) + ((), {'node_2': {'foo': 'hi! foobar'}}) + ``` + ::: + + :::js + ``` + [[], {'node1': {'foo': 'hi! foo'}}] + [['node2:dfddc4ba-c3c5-6887-5012-a243b5b377c2'], {'subgraphNode1': {'bar': 'bar'}}] + [['node2:dfddc4ba-c3c5-6887-5012-a243b5b377c2'], {'subgraphNode2': {'foo': 'hi! foobar'}}] + [[], {'node2': {'foo': 'hi! foobar'}}] + ``` + ::: + + **Note** that we are receiving not just the node updates, but we also the namespaces which tell us what graph (or subgraph) we are streaming from. + + + +### Debugging + +Use the `debug` streaming mode to stream as much information as possible throughout the execution of the graph. The streamed outputs include the name of the node as well as the full state. + +:::python +```python +for chunk in graph.stream( + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="debug", +): + print(chunk) +``` +::: + +:::js +```typescript +for await (const chunk of await graph.stream( + { topic: "ice cream" }, + { streamMode: "debug" } +)) { + console.log(chunk); +} +``` +::: + + +### LLM tokens + +Use the `messages` streaming mode to stream Large Language Model (LLM) outputs **token by token** from any part of your graph, including nodes, tools, subgraphs, or tasks. + +:::python +The streamed output from [`messages` mode](#supported-stream-modes) is a tuple `(message_chunk, metadata)` where: + +* `message_chunk`: the token or message segment from the LLM. +* `metadata`: a dictionary containing details about the graph node and LLM invocation. + +> If your LLM is not available as a LangChain integration, you can stream its outputs using `custom` mode instead. See [use with any LLM](#use-with-any-llm) for details. + + + **Manual config required for async in Python < 3.11** + When using Python < 3.11 with async code, you must explicitly pass `RunnableConfig` to `ainvoke()` to enable proper streaming. See [Async with Python < 3.11](#async) for details or upgrade to Python 3.11+. + + +```python +from dataclasses import dataclass + +from langchain.chat_models import init_chat_model +from langgraph.graph import StateGraph, START + + +@dataclass +class MyState: + topic: str + joke: str = "" + + +llm = init_chat_model(model="openai:gpt-4o-mini") + +def call_model(state: MyState): + """Call the LLM to generate a joke about a topic""" + # highlight-next-line + llm_response = llm.invoke( # (1)! + [ + {"role": "user", "content": f"Generate a joke about {state.topic}"} + ] + ) + return {"joke": llm_response.content} + +graph = ( + StateGraph(MyState) + .add_node(call_model) + .add_edge(START, "call_model") + .compile() +) + +for message_chunk, metadata in graph.stream( # (2)! + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="messages", +): + if message_chunk.content: + print(message_chunk.content, end="|", flush=True) +``` + +1. Note that the message events are emitted even when the LLM is run using `.invoke` rather than `.stream`. +2. The "messages" stream mode returns an iterator of tuples `(message_chunk, metadata)` where `message_chunk` is the token streamed by the LLM and `metadata` is a dictionary with information about the graph node where the LLM was called and other information. +::: + +:::js +The streamed output from [`messages` mode](#supported-stream-modes) is a tuple `[message_chunk, metadata]` where: + +* `message_chunk`: the token or message segment from the LLM. +* `metadata`: a dictionary containing details about the graph node and LLM invocation. + +> If your LLM is not available as a LangChain integration, you can stream its outputs using `custom` mode instead. See [use with any LLM](#use-with-any-llm) for details. + +```typescript +import { ChatOpenAI } from "@langchain/openai"; +import { StateGraph, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const MyState = z.object({ + topic: z.string(), + joke: z.string().default(""), +}); + +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + +const callModel = async (state: z.infer) => { + // Call the LLM to generate a joke about a topic + const llmResponse = await llm.invoke([ + { role: "user", content: `Generate a joke about ${state.topic}` }, + ]); // (1)! + return { joke: llmResponse.content }; +}; + +const graph = new StateGraph(MyState) + .addNode("callModel", callModel) + .addEdge(START, "callModel") + .compile(); + +for await (const [messageChunk, metadata] of await graph.stream( + // (2)! + { topic: "ice cream" }, + { streamMode: "messages" } +)) { + if (messageChunk.content) { + console.log(messageChunk.content + "|"); + } +} +``` + +1. Note that the message events are emitted even when the LLM is run using `.invoke` rather than `.stream`. +2. The "messages" stream mode returns an iterator of tuples `[messageChunk, metadata]` where `messageChunk` is the token streamed by the LLM and `metadata` is a dictionary with information about the graph node where the LLM was called and other information. +::: + +#### Filter by LLM invocation + +You can associate `tags` with LLM invocations to filter the streamed tokens by LLM invocation. + +:::python +```python +from langchain.chat_models import init_chat_model + +llm_1 = init_chat_model(model="openai:gpt-4o-mini", tags=['joke']) # (1)! +llm_2 = init_chat_model(model="openai:gpt-4o-mini", tags=['poem']) # (2)! + +graph = ... # define a graph that uses these LLMs + +async for msg, metadata in graph.astream( # (3)! + {"topic": "cats"}, + # highlight-next-line + stream_mode="messages", +): + if metadata["tags"] == ["joke"]: # (4)! + print(msg.content, end="|", flush=True) +``` + +1. llm_1 is tagged with "joke". +2. llm_2 is tagged with "poem". +3. The `stream_mode` is set to "messages" to stream LLM tokens. The `metadata` contains information about the LLM invocation, including the tags. +4. Filter the streamed tokens by the `tags` field in the metadata to only include the tokens from the LLM invocation with the "joke" tag. +::: + +:::js +```typescript +import { ChatOpenAI } from "@langchain/openai"; + +const llm1 = new ChatOpenAI({ + model: "gpt-4o-mini", + tags: ['joke'] // (1)! +}); +const llm2 = new ChatOpenAI({ + model: "gpt-4o-mini", + tags: ['poem'] // (2)! +}); + +const graph = // ... define a graph that uses these LLMs + +for await (const [msg, metadata] of await graph.stream( // (3)! + { topic: "cats" }, + { streamMode: "messages" } +)) { + if (metadata.tags?.includes("joke")) { // (4)! + console.log(msg.content + "|"); + } +} +``` + +1. llm1 is tagged with "joke". +2. llm2 is tagged with "poem". +3. The `streamMode` is set to "messages" to stream LLM tokens. The `metadata` contains information about the LLM invocation, including the tags. +4. Filter the streamed tokens by the `tags` field in the metadata to only include the tokens from the LLM invocation with the "joke" tag. +::: + + + :::python + ```python + from typing import TypedDict + + from langchain.chat_models import init_chat_model + from langgraph.graph import START, StateGraph + + joke_model = init_chat_model(model="openai:gpt-4o-mini", tags=["joke"]) # (1)! + poem_model = init_chat_model(model="openai:gpt-4o-mini", tags=["poem"]) # (2)! + + + class State(TypedDict): + topic: str + joke: str + poem: str + + + async def call_model(state, config): + topic = state["topic"] + print("Writing joke...") + # Note: Passing the config through explicitly is required for python < 3.11 + # Since context var support wasn't added before then: https://docs.python.org/3/library/asyncio-task.html#creating-tasks + joke_response = await joke_model.ainvoke( + [{"role": "user", "content": f"Write a joke about {topic}"}], + config, # (3)! + ) + print("\n\nWriting poem...") + poem_response = await poem_model.ainvoke( + [{"role": "user", "content": f"Write a short poem about {topic}"}], + config, # (3)! + ) + return {"joke": joke_response.content, "poem": poem_response.content} + + + graph = ( + StateGraph(State) + .add_node(call_model) + .add_edge(START, "call_model") + .compile() + ) + + async for msg, metadata in graph.astream( + {"topic": "cats"}, + # highlight-next-line + stream_mode="messages", # (4)! + ): + if metadata["tags"] == ["joke"]: # (4)! + print(msg.content, end="|", flush=True) + ``` + + 1. The `joke_model` is tagged with "joke". + 2. The `poem_model` is tagged with "poem". + 3. The `config` is passed through explicitly to ensure the context vars are propagated correctly. This is required for Python < 3.11 when using async code. Please see the [async section](#async) for more details. + 4. The `stream_mode` is set to "messages" to stream LLM tokens. The `metadata` contains information about the LLM invocation, including the tags. + ::: + + :::js + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { StateGraph, START } from "@langchain/langgraph"; + import { z } from "zod"; + + const jokeModel = new ChatOpenAI({ + model: "gpt-4o-mini", + tags: ["joke"] // (1)! + }); + const poemModel = new ChatOpenAI({ + model: "gpt-4o-mini", + tags: ["poem"] // (2)! + }); + + const State = z.object({ + topic: z.string(), + joke: z.string(), + poem: z.string(), + }); + + const graph = new StateGraph(State) + .addNode("callModel", (state) => { + const topic = state.topic; + console.log("Writing joke..."); + + const jokeResponse = await jokeModel.invoke([ + { role: "user", content: `Write a joke about ${topic}` } + ]); + + console.log("\n\nWriting poem..."); + const poemResponse = await poemModel.invoke([ + { role: "user", content: `Write a short poem about ${topic}` } + ]); + + return { + joke: jokeResponse.content, + poem: poemResponse.content + }; + }) + .addEdge(START, "callModel") + .compile(); + + for await (const [msg, metadata] of await graph.stream( + { topic: "cats" }, + { streamMode: "messages" } // (3)! + )) { + if (metadata.tags?.includes("joke")) { // (4)! + console.log(msg.content + "|"); + } + } + ``` + + 1. The `jokeModel` is tagged with "joke". + 2. The `poemModel` is tagged with "poem". + 3. The `streamMode` is set to "messages" to stream LLM tokens. The `metadata` contains information about the LLM invocation, including the tags. + 4. Filter the streamed tokens by the `tags` field in the metadata to only include the tokens from the LLM invocation with the "joke" tag. + ::: + + +#### Filter by node + +To stream tokens only from specific nodes, use `stream_mode="messages"` and filter the outputs by the `langgraph_node` field in the streamed metadata: + +:::python +```python +for msg, metadata in graph.stream( # (1)! + inputs, + # highlight-next-line + stream_mode="messages", +): + # highlight-next-line + if msg.content and metadata["langgraph_node"] == "some_node_name": # (2)! + ... +``` + +1. The "messages" stream mode returns a tuple of `(message_chunk, metadata)` where `message_chunk` is the token streamed by the LLM and `metadata` is a dictionary with information about the graph node where the LLM was called and other information. +2. Filter the streamed tokens by the `langgraph_node` field in the metadata to only include the tokens from the `write_poem` node. +::: + +:::js +```typescript +for await (const [msg, metadata] of await graph.stream( + // (1)! + inputs, + { streamMode: "messages" } +)) { + if (msg.content && metadata.langgraph_node === "some_node_name") { + // (2)! + // ... + } +} +``` + +1. The "messages" stream mode returns a tuple of `[messageChunk, metadata]` where `messageChunk` is the token streamed by the LLM and `metadata` is a dictionary with information about the graph node where the LLM was called and other information. +2. Filter the streamed tokens by the `langgraph_node` field in the metadata to only include the tokens from the `writePoem` node. +::: + + + :::python + ```python + from typing import TypedDict + from langgraph.graph import START, StateGraph + from langchain_openai import ChatOpenAI + + model = ChatOpenAI(model="gpt-4o-mini") + + + class State(TypedDict): + topic: str + joke: str + poem: str + + + def write_joke(state: State): + topic = state["topic"] + joke_response = model.invoke( + [{"role": "user", "content": f"Write a joke about {topic}"}] + ) + return {"joke": joke_response.content} + + + def write_poem(state: State): + topic = state["topic"] + poem_response = model.invoke( + [{"role": "user", "content": f"Write a short poem about {topic}"}] + ) + return {"poem": poem_response.content} + + + graph = ( + StateGraph(State) + .add_node(write_joke) + .add_node(write_poem) + # write both the joke and the poem concurrently + .add_edge(START, "write_joke") + .add_edge(START, "write_poem") + .compile() + ) + + # highlight-next-line + for msg, metadata in graph.stream( # (1)! + {"topic": "cats"}, + stream_mode="messages", + ): + # highlight-next-line + if msg.content and metadata["langgraph_node"] == "write_poem": # (2)! + print(msg.content, end="|", flush=True) + ``` + + 1. The "messages" stream mode returns a tuple of `(message_chunk, metadata)` where `message_chunk` is the token streamed by the LLM and `metadata` is a dictionary with information about the graph node where the LLM was called and other information. + 2. Filter the streamed tokens by the `langgraph_node` field in the metadata to only include the tokens from the `write_poem` node. + ::: + + :::js + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { StateGraph, START } from "@langchain/langgraph"; + import { z } from "zod"; + + const model = new ChatOpenAI({ model: "gpt-4o-mini" }); + + const State = z.object({ + topic: z.string(), + joke: z.string(), + poem: z.string(), + }); + + const graph = new StateGraph(State) + .addNode("writeJoke", async (state) => { + const topic = state.topic; + const jokeResponse = await model.invoke([ + { role: "user", content: `Write a joke about ${topic}` } + ]); + return { joke: jokeResponse.content }; + }) + .addNode("writePoem", async (state) => { + const topic = state.topic; + const poemResponse = await model.invoke([ + { role: "user", content: `Write a short poem about ${topic}` } + ]); + return { poem: poemResponse.content }; + }) + // write both the joke and the poem concurrently + .addEdge(START, "writeJoke") + .addEdge(START, "writePoem") + .compile(); + + for await (const [msg, metadata] of await graph.stream( // (1)! + { topic: "cats" }, + { streamMode: "messages" } + )) { + if (msg.content && metadata.langgraph_node === "writePoem") { // (2)! + console.log(msg.content + "|"); + } + } + ``` + + 1. The "messages" stream mode returns a tuple of `[messageChunk, metadata]` where `messageChunk` is the token streamed by the LLM and `metadata` is a dictionary with information about the graph node where the LLM was called and other information. + 2. Filter the streamed tokens by the `langgraph_node` field in the metadata to only include the tokens from the `writePoem` node. + ::: + + +### Stream custom data + +:::python +To send **custom user-defined data** from inside a LangGraph node or tool, follow these steps: + +1. Use `get_stream_writer()` to access the stream writer and emit custom data. +2. Set `stream_mode="custom"` when calling `.stream()` or `.astream()` to get the custom data in the stream. You can combine multiple modes (e.g., `["updates", "custom"]`), but at least one must be `"custom"`. + + + **No `get_stream_writer()` in async for Python < 3.11** + In async code running on Python < 3.11, `get_stream_writer()` will not work. + Instead, add a `writer` parameter to your node or tool and pass it manually. + See [Async with Python < 3.11](#async) for usage examples. + + + + + ```python + from typing import TypedDict + from langgraph.config import get_stream_writer + from langgraph.graph import StateGraph, START + + class State(TypedDict): + query: str + answer: str + + def node(state: State): + writer = get_stream_writer() # (1)! + writer({"custom_key": "Generating custom data inside node"}) # (2)! + return {"answer": "some data"} + + graph = ( + StateGraph(State) + .add_node(node) + .add_edge(START, "node") + .compile() + ) + + inputs = {"query": "example"} + + # Usage + for chunk in graph.stream(inputs, stream_mode="custom"): # (3)! + print(chunk) + ``` + + 1. Get the stream writer to send custom data. + 2. Emit a custom key-value pair (e.g., progress update). + 3. Set `stream_mode="custom"` to receive the custom data in the stream. + + + ```python + from langchain_core.tools import tool + from langgraph.config import get_stream_writer + + @tool + def query_database(query: str) -> str: + """Query the database.""" + writer = get_stream_writer() # (1)! + # highlight-next-line + writer({"data": "Retrieved 0/100 records", "type": "progress"}) # (2)! + # perform query + # highlight-next-line + writer({"data": "Retrieved 100/100 records", "type": "progress"}) # (3)! + return "some-answer" + + + graph = ... # define a graph that uses this tool + + for chunk in graph.stream(inputs, stream_mode="custom"): # (4)! + print(chunk) + ``` + + 1. Access the stream writer to send custom data. + 2. Emit a custom key-value pair (e.g., progress update). + 3. Emit another custom key-value pair. + 4. Set `stream_mode="custom"` to receive the custom data in the stream. + + +::: + +:::js +To send **custom user-defined data** from inside a LangGraph node or tool, follow these steps: + +1. Use the `writer` parameter from the `LangGraphRunnableConfig` to emit custom data. +2. Set `streamMode: "custom"` when calling `.stream()` to get the custom data in the stream. You can combine multiple modes (e.g., `["updates", "custom"]`), but at least one must be `"custom"`. + + + + ```typescript + import { StateGraph, START, LangGraphRunnableConfig } from "@langchain/langgraph"; + import { z } from "zod"; + + const State = z.object({ + query: z.string(), + answer: z.string(), + }); + + const graph = new StateGraph(State) + .addNode("node", async (state, config) => { + config.writer({ custom_key: "Generating custom data inside node" }); // (1)! + return { answer: "some data" }; + }) + .addEdge(START, "node") + .compile(); + + const inputs = { query: "example" }; + + // Usage + for await (const chunk of await graph.stream(inputs, { streamMode: "custom" })) { // (2)! + console.log(chunk); + } + ``` + + 1. Use the writer to emit a custom key-value pair (e.g., progress update). + 2. Set `streamMode: "custom"` to receive the custom data in the stream. + + + ```typescript + import { tool } from "@langchain/core/tools"; + import { LangGraphRunnableConfig } from "@langchain/langgraph"; + import { z } from "zod"; + + const queryDatabase = tool( + async (input, config: LangGraphRunnableConfig) => { + config.writer({ data: "Retrieved 0/100 records", type: "progress" }); // (1)! + // perform query + config.writer({ data: "Retrieved 100/100 records", type: "progress" }); // (2)! + return "some-answer"; + }, + { + name: "query_database", + description: "Query the database.", + schema: z.object({ + query: z.string().describe("The query to execute."), + }), + } + ); + + const graph = // ... define a graph that uses this tool + + for await (const chunk of await graph.stream(inputs, { streamMode: "custom" })) { // (3)! + console.log(chunk); + } + ``` + + 1. Use the writer to emit a custom key-value pair (e.g., progress update). + 2. Emit another custom key-value pair. + 3. Set `streamMode: "custom"` to receive the custom data in the stream. + + +::: + +### Use with any LLM + +:::python +You can use `stream_mode="custom"` to stream data from **any LLM API** — even if that API does **not** implement the LangChain chat model interface. + +This lets you integrate raw LLM clients or external services that provide their own streaming interfaces, making LangGraph highly flexible for custom setups. + +```python +from langgraph.config import get_stream_writer + +def call_arbitrary_model(state): + """Example node that calls an arbitrary model and streams the output""" + # highlight-next-line + writer = get_stream_writer() # (1)! + # Assume you have a streaming client that yields chunks + for chunk in your_custom_streaming_client(state["topic"]): # (2)! + # highlight-next-line + writer({"custom_llm_chunk": chunk}) # (3)! + return {"result": "completed"} + +graph = ( + StateGraph(State) + .add_node(call_arbitrary_model) + # Add other nodes and edges as needed + .compile() +) + +for chunk in graph.stream( + {"topic": "cats"}, + # highlight-next-line + stream_mode="custom", # (4)! +): + # The chunk will contain the custom data streamed from the llm + print(chunk) +``` + +1. Get the stream writer to send custom data. +2. Generate LLM tokens using your custom streaming client. +3. Use the writer to send custom data to the stream. +4. Set `stream_mode="custom"` to receive the custom data in the stream. +::: + +:::js +You can use `streamMode: "custom"` to stream data from **any LLM API** — even if that API does **not** implement the LangChain chat model interface. + +This lets you integrate raw LLM clients or external services that provide their own streaming interfaces, making LangGraph highly flexible for custom setups. + +```typescript +import { LangGraphRunnableConfig } from "@langchain/langgraph"; + +const callArbitraryModel = async ( + state: any, + config: LangGraphRunnableConfig +) => { + // Example node that calls an arbitrary model and streams the output + // Assume you have a streaming client that yields chunks + for await (const chunk of yourCustomStreamingClient(state.topic)) { + // (1)! + config.writer({ custom_llm_chunk: chunk }); // (2)! + } + return { result: "completed" }; +}; + +const graph = new StateGraph(State) + .addNode("callArbitraryModel", callArbitraryModel) + // Add other nodes and edges as needed + .compile(); + +for await (const chunk of await graph.stream( + { topic: "cats" }, + { streamMode: "custom" } // (3)! +)) { + // The chunk will contain the custom data streamed from the llm + console.log(chunk); +} +``` + +1. Generate LLM tokens using your custom streaming client. +2. Use the writer to send custom data to the stream. +3. Set `streamMode: "custom"` to receive the custom data in the stream. +::: + + + :::python + ```python + import operator + import json + + from typing import TypedDict + from typing_extensions import Annotated + from langgraph.graph import StateGraph, START + + from openai import AsyncOpenAI + + openai_client = AsyncOpenAI() + model_name = "gpt-4o-mini" + + + async def stream_tokens(model_name: str, messages: list[dict]): + response = await openai_client.chat.completions.create( + messages=messages, model=model_name, stream=True + ) + role = None + async for chunk in response: + delta = chunk.choices[0].delta + + if delta.role is not None: + role = delta.role + + if delta.content: + yield {"role": role, "content": delta.content} + + + # this is our tool + async def get_items(place: str) -> str: + """Use this tool to list items one might find in a place you're asked about.""" + writer = get_stream_writer() + response = "" + async for msg_chunk in stream_tokens( + model_name, + [ + { + "role": "user", + "content": ( + "Can you tell me what kind of items " + f"i might find in the following place: '{place}'. " + "List at least 3 such items separating them by a comma. " + "And include a brief description of each item." + ), + } + ], + ): + response += msg_chunk["content"] + writer(msg_chunk) + + return response + + + class State(TypedDict): + messages: Annotated[list[dict], operator.add] + + + # this is the tool-calling graph node + async def call_tool(state: State): + ai_message = state["messages"][-1] + tool_call = ai_message["tool_calls"][-1] + + function_name = tool_call["function"]["name"] + if function_name != "get_items": + raise ValueError(f"Tool {function_name} not supported") + + function_arguments = tool_call["function"]["arguments"] + arguments = json.loads(function_arguments) + + function_response = await get_items(**arguments) + tool_message = { + "tool_call_id": tool_call["id"], + "role": "tool", + "name": function_name, + "content": function_response, + } + return {"messages": [tool_message]} + + + graph = ( + StateGraph(State) + .add_node(call_tool) + .add_edge(START, "call_tool") + .compile() + ) + ``` + + Let's invoke the graph with an AI message that includes a tool call: + + ```python + inputs = { + "messages": [ + { + "content": None, + "role": "assistant", + "tool_calls": [ + { + "id": "1", + "function": { + "arguments": '{"place":"bedroom"}', + "name": "get_items", + }, + "type": "function", + } + ], + } + ] + } + + async for chunk in graph.astream( + inputs, + stream_mode="custom", + ): + print(chunk["content"], end="|", flush=True) + ``` + ::: + + :::js + ```typescript + import { StateGraph, START, LangGraphRunnableConfig } from "@langchain/langgraph"; + import { z } from "zod"; + import OpenAI from "openai"; + + const openaiClient = new OpenAI(); + const modelName = "gpt-4o-mini"; + + async function* streamTokens(modelName: string, messages: any[]) { + const response = await openaiClient.chat.completions.create({ + messages, + model: modelName, + stream: true, + }); + + let role: string | null = null; + for await (const chunk of response) { + const delta = chunk.choices[0]?.delta; + + if (delta?.role) { + role = delta.role; + } + + if (delta?.content) { + yield { role, content: delta.content }; + } + } + } + + // this is our tool + const getItems = tool( + async (input, config: LangGraphRunnableConfig) => { + let response = ""; + for await (const msgChunk of streamTokens( + modelName, + [ + { + role: "user", + content: `Can you tell me what kind of items i might find in the following place: '${input.place}'. List at least 3 such items separating them by a comma. And include a brief description of each item.`, + }, + ] + )) { + response += msgChunk.content; + config.writer?.(msgChunk); + } + return response; + }, + { + name: "get_items", + description: "Use this tool to list items one might find in a place you're asked about.", + schema: z.object({ + place: z.string().describe("The place to look up items for."), + }), + } + ); + + const State = z.object({ + messages: z.array(z.any()), + }); + + const graph = new StateGraph(State) + // this is the tool-calling graph node + .addNode("callTool", async (state) => { + const aiMessage = state.messages.at(-1); + const toolCall = aiMessage.tool_calls?.at(-1); + + const functionName = toolCall?.function?.name; + if (functionName !== "get_items") { + throw new Error(`Tool ${functionName} not supported`); + } + + const functionArguments = toolCall?.function?.arguments; + const args = JSON.parse(functionArguments); + + const functionResponse = await getItems.invoke(args); + const toolMessage = { + tool_call_id: toolCall.id, + role: "tool", + name: functionName, + content: functionResponse, + }; + return { messages: [toolMessage] }; + }) + .addEdge(START, "callTool") + .compile(); + ``` + + Let's invoke the graph with an AI message that includes a tool call: + + ```typescript + const inputs = { + messages: [ + { + content: null, + role: "assistant", + tool_calls: [ + { + id: "1", + function: { + arguments: '{"place":"bedroom"}', + name: "get_items", + }, + type: "function", + } + ], + } + ] + }; + + for await (const chunk of await graph.stream( + inputs, + { streamMode: "custom" } + )) { + console.log(chunk.content + "|"); + } + ``` + ::: + + +### Disable streaming for specific chat models + +If your application mixes models that support streaming with those that do not, you may need to explicitly disable streaming for +models that do not support it. + +:::python +Set `disable_streaming=True` when initializing the model. + + + + ```python + from langchain.chat_models import init_chat_model + + model = init_chat_model( + "anthropic:claude-3-7-sonnet-latest", + # highlight-next-line + disable_streaming=True # (1)! + ) + ``` + + 1. Set `disable_streaming=True` to disable streaming for the chat model. + + + ```python + from langchain_openai import ChatOpenAI + + llm = ChatOpenAI(model="o1-preview", disable_streaming=True) # (1)! + ``` + + 1. Set `disable_streaming=True` to disable streaming for the chat model. + + +::: + +:::js +Set `streaming: false` when initializing the model. + +```typescript +import { ChatOpenAI } from "@langchain/openai"; + +const model = new ChatOpenAI({ + model: "o1-preview", + streaming: false, // (1)! +}); +``` +::: + +:::python + +### Async with Python < 3.11 + +In Python versions < 3.11, [asyncio tasks](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) do not support the `context` parameter. +This limits LangGraph ability to automatically propagate context, and affects LangGraph's streaming mechanisms in two key ways: + +1. You **must** explicitly pass [`RunnableConfig`](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) into async LLM calls (e.g., `ainvoke()`), as callbacks are not automatically propagated. +2. You **cannot** use `get_stream_writer()` in async nodes or tools — you must pass a `writer` argument directly. + + + ```python + from typing import TypedDict + from langgraph.graph import START, StateGraph + from langchain.chat_models import init_chat_model + + llm = init_chat_model(model="openai:gpt-4o-mini") + + class State(TypedDict): + topic: str + joke: str + + async def call_model(state, config): # (1)! + topic = state["topic"] + print("Generating joke...") + joke_response = await llm.ainvoke( + [{"role": "user", "content": f"Write a joke about {topic}"}], + # highlight-next-line + config, # (2)! + ) + return {"joke": joke_response.content} + + graph = ( + StateGraph(State) + .add_node(call_model) + .add_edge(START, "call_model") + .compile() + ) + + async for chunk, metadata in graph.astream( + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="messages", # (3)! + ): + if chunk.content: + print(chunk.content, end="|", flush=True) + ``` + + 1. Accept `config` as an argument in the async node function. + 2. Pass `config` to `llm.ainvoke()` to ensure proper context propagation. + 3. Set `stream_mode="messages"` to stream LLM tokens. + + + + ```python + from typing import TypedDict + from langgraph.types import StreamWriter + + class State(TypedDict): + topic: str + joke: str + + # highlight-next-line + async def generate_joke(state: State, writer: StreamWriter): # (1)! + writer({"custom_key": "Streaming custom data while generating a joke"}) + return {"joke": f"This is a joke about {state['topic']}"} + + graph = ( + StateGraph(State) + .add_node(generate_joke) + .add_edge(START, "generate_joke") + .compile() + ) + + async for chunk in graph.astream( + {"topic": "ice cream"}, + # highlight-next-line + stream_mode="custom", # (2)! + ): + print(chunk) + ``` + + 1. Add `writer` as an argument in the function signature of the async node or tool. LangGraph will automatically pass the stream writer to the function. + 2. Set `stream_mode="custom"` to receive the custom data in the stream. + +::: diff --git a/src/oss/use-subgraphs.mdx b/src/oss/use-subgraphs.mdx new file mode 100644 index 00000000..1bba1f55 --- /dev/null +++ b/src/oss/use-subgraphs.mdx @@ -0,0 +1,457 @@ +--- +title: Use subgraphs +--- +This guide explains the mechanics of using [subgraphs](/oss/subgraphs). A common application of subgraphs is to build [multi-agent](/oss/multi-agent) systems. + +When adding subgraphs, you need to define how the parent graph and the subgraph communicate: + +* [Shared state schemas](#shared-state-schemas) — parent and subgraph have **shared state keys** in their state [schemas](/oss/graph-api#state) +* [Different state schemas](#different-state-schemas) — **no shared state keys** in parent and subgraph [schemas](/oss/graph-api#state) + +## Setup + +```bash +pip install -U langgraph +``` + + + **Set up LangSmith for LangGraph development** + Sign up for [LangSmith](https://smith.langchain.com) 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](https://docs.smith.langchain.com). + + +## Shared state schemas + +A common case is for the parent graph and subgraph to communicate over a shared state key (channel) in the [schema](/oss/graph-api#state). For example, in [multi-agent](/oss/multi-agent) systems, the agents often communicate over a shared [messages](/oss/graph-api#why-use-messages) key. + +If your subgraph shares state keys with the parent graph, you can follow these steps to add it to your graph: + +1. Define the subgraph workflow (`subgraph_builder` in the example below) and compile it +2. Pass compiled subgraph to the `.add_node` method when defining the parent graph workflow + +```python +from typing_extensions import TypedDict +from langgraph.graph.state import StateGraph, START + +class State(TypedDict): + foo: str + +# Subgraph + +def subgraph_node_1(state: State): + return {"foo": "hi! " + state["foo"]} + +subgraph_builder = StateGraph(State) +subgraph_builder.add_node(subgraph_node_1) +subgraph_builder.add_edge(START, "subgraph_node_1") +subgraph = subgraph_builder.compile() + +# Parent graph + +builder = StateGraph(State) +builder.add_node("node_1", subgraph) +builder.add_edge(START, "node_1") +graph = builder.compile() +``` + + + ```python + from typing_extensions import TypedDict + from langgraph.graph.state import StateGraph, START + + # Define subgraph + class SubgraphState(TypedDict): + foo: str # (1)! + bar: str # (2)! + + def subgraph_node_1(state: SubgraphState): + return {"bar": "bar"} + + def subgraph_node_2(state: SubgraphState): + # note that this node is using a state key ('bar') that is only available in the subgraph + # and is sending update on the shared state key ('foo') + return {"foo": state["foo"] + state["bar"]} + + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node(subgraph_node_1) + subgraph_builder.add_node(subgraph_node_2) + subgraph_builder.add_edge(START, "subgraph_node_1") + subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2") + subgraph = subgraph_builder.compile() + + # Define parent graph + class ParentState(TypedDict): + foo: str + + def node_1(state: ParentState): + return {"foo": "hi! " + state["foo"]} + + builder = StateGraph(ParentState) + builder.add_node("node_1", node_1) + builder.add_node("node_2", subgraph) + builder.add_edge(START, "node_1") + builder.add_edge("node_1", "node_2") + graph = builder.compile() + + for chunk in graph.stream({"foo": "foo"}): + print(chunk) + ``` + + 1. This key is shared with the parent graph state + 2. This key is private to the `SubgraphState` and is not visible to the parent graph + + ``` + {'node_1': {'foo': 'hi! foo'}} + {'node_2': {'foo': 'hi! foobar'}} + ``` + + +## Different state schemas + +For more complex systems you might want to define subgraphs that have a **completely different schema** from the parent graph (no shared keys). For example, you might want to keep a private message history for each of the agents in a [multi-agent](/oss/multi-agent) system. + +If that's the case for your application, you need to define a node **function that invokes the subgraph**. This function needs to transform the input (parent) state to the subgraph state before invoking the subgraph, and transform the results back to the parent state before returning the state update from the node. + +```python +from typing_extensions import TypedDict +from langgraph.graph.state import StateGraph, START + +class SubgraphState(TypedDict): + bar: str + +# Subgraph + +def subgraph_node_1(state: SubgraphState): + return {"bar": "hi! " + state["bar"]} + +subgraph_builder = StateGraph(SubgraphState) +subgraph_builder.add_node(subgraph_node_1) +subgraph_builder.add_edge(START, "subgraph_node_1") +subgraph = subgraph_builder.compile() + +# Parent graph + +class State(TypedDict): + foo: str + +def call_subgraph(state: State): + subgraph_output = subgraph.invoke({"bar": state["foo"]}) # (1)! + return {"foo": subgraph_output["bar"]} # (2)! + +builder = StateGraph(State) +builder.add_node("node_1", call_subgraph) +builder.add_edge(START, "node_1") +graph = builder.compile() +``` + +1. Transform the state to the subgraph state +2. Transform response back to the parent state + + + ```python + from typing_extensions import TypedDict + from langgraph.graph.state import StateGraph, START + + # Define subgraph + class SubgraphState(TypedDict): + # note that none of these keys are shared with the parent graph state + bar: str + baz: str + + def subgraph_node_1(state: SubgraphState): + return {"baz": "baz"} + + def subgraph_node_2(state: SubgraphState): + return {"bar": state["bar"] + state["baz"]} + + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node(subgraph_node_1) + subgraph_builder.add_node(subgraph_node_2) + subgraph_builder.add_edge(START, "subgraph_node_1") + subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2") + subgraph = subgraph_builder.compile() + + # Define parent graph + class ParentState(TypedDict): + foo: str + + def node_1(state: ParentState): + return {"foo": "hi! " + state["foo"]} + + def node_2(state: ParentState): + response = subgraph.invoke({"bar": state["foo"]}) # (1)! + return {"foo": response["bar"]} # (2)! + + + builder = StateGraph(ParentState) + builder.add_node("node_1", node_1) + builder.add_node("node_2", node_2) + builder.add_edge(START, "node_1") + builder.add_edge("node_1", "node_2") + graph = builder.compile() + + for chunk in graph.stream({"foo": "foo"}, subgraphs=True): + print(chunk) + ``` + + 1. Transform the state to the subgraph state + 2. Transform response back to the parent state + + ``` + ((), {'node_1': {'foo': 'hi! foo'}}) + (('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}}) + (('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'grandchild_2': {'bar': 'hi! foobaz'}}) + ((), {'node_2': {'foo': 'hi! foobaz'}}) + ``` + + + + This is an example with two levels of subgraphs: parent -> child -> grandchild. + + ```python + # Grandchild graph + from typing_extensions import TypedDict + from langgraph.graph.state import StateGraph, START, END + + class GrandChildState(TypedDict): + my_grandchild_key: str + + def grandchild_1(state: GrandChildState) -> GrandChildState: + # NOTE: child or parent keys will not be accessible here + return {"my_grandchild_key": state["my_grandchild_key"] + ", how are you"} + + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + + grandchild.add_edge(START, "grandchild_1") + grandchild.add_edge("grandchild_1", END) + + grandchild_graph = grandchild.compile() + + # Child graph + class ChildState(TypedDict): + my_child_key: str + + def call_grandchild_graph(state: ChildState) -> ChildState: + # NOTE: parent or grandchild keys won't be accessible here + grandchild_graph_input = {"my_grandchild_key": state["my_child_key"]} # (1)! + grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input) + return {"my_child_key": grandchild_graph_output["my_grandchild_key"] + " today?"} # (2)! + + child = StateGraph(ChildState) + child.add_node("child_1", call_grandchild_graph) # (3)! + child.add_edge(START, "child_1") + child.add_edge("child_1", END) + child_graph = child.compile() + + # Parent graph + class ParentState(TypedDict): + my_key: str + + def parent_1(state: ParentState) -> ParentState: + # NOTE: child or grandchild keys won't be accessible here + return {"my_key": "hi " + state["my_key"]} + + def parent_2(state: ParentState) -> ParentState: + return {"my_key": state["my_key"] + " bye!"} + + def call_child_graph(state: ParentState) -> ParentState: + child_graph_input = {"my_child_key": state["my_key"]} # (4)! + child_graph_output = child_graph.invoke(child_graph_input) + return {"my_key": child_graph_output["my_child_key"]} # (5)! + + parent = StateGraph(ParentState) + parent.add_node("parent_1", parent_1) + parent.add_node("child", call_child_graph) # (6)! + parent.add_node("parent_2", parent_2) + + parent.add_edge(START, "parent_1") + parent.add_edge("parent_1", "child") + parent.add_edge("child", "parent_2") + parent.add_edge("parent_2", END) + + parent_graph = parent.compile() + + for chunk in parent_graph.stream({"my_key": "Bob"}, subgraphs=True): + print(chunk) + ``` + + 1. We're transforming the state from the child state channels (`my_child_key`) to the child state channels (`my_grandchild_key`) + 2. We're transforming the state from the grandchild state channels (`my_grandchild_key`) back to the child state channels (`my_child_key`) + 3. We're passing a function here instead of just compiled graph (`grandchild_graph`) + 4. We're transforming the state from the parent state channels (`my_key`) to the child state channels (`my_child_key`) + 5. We're transforming the state from the child state channels (`my_child_key`) back to the parent state channels (`my_key`) + 6. We're passing a function here instead of just a compiled graph (`child_graph`) + + ``` + ((), {'parent_1': {'my_key': 'hi Bob'}}) + (('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child_1:781bb3b1-3971-84ce-810b-acf819a03f9c'), {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}}) + (('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b',), {'child_1': {'my_child_key': 'hi Bob, how are you today?'}}) + ((), {'child': {'my_key': 'hi Bob, how are you today?'}}) + ((), {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}}) + ``` + + +## Add persistence + +You only need to **provide the checkpointer when compiling the parent graph**. LangGraph will automatically propagate the checkpointer to the child subgraphs. + +```python +from langgraph.graph import START, StateGraph +from langgraph.checkpoint.memory import InMemorySaver +from typing_extensions import TypedDict + +class State(TypedDict): + foo: str + +# Subgraph + +def subgraph_node_1(state: State): + return {"foo": state["foo"] + "bar"} + +subgraph_builder = StateGraph(State) +subgraph_builder.add_node(subgraph_node_1) +subgraph_builder.add_edge(START, "subgraph_node_1") +subgraph = subgraph_builder.compile() + +# Parent graph + +builder = StateGraph(State) +builder.add_node("node_1", subgraph) +builder.add_edge(START, "node_1") + +checkpointer = InMemorySaver() +graph = builder.compile(checkpointer=checkpointer) +``` + +If you want the subgraph to **have its own memory**, you can compile it `with checkpointer=True`. This is useful in [multi-agent](/oss/multi-agent) systems, if you want agents to keep track of their internal message histories: + +```python +subgraph_builder = StateGraph(...) +subgraph = subgraph_builder.compile(checkpointer=True) +``` + +## View subgraph state + +When you enable [persistence](/oss/persistence), you can [inspect the graph state](/oss/persistence#checkpoints) (checkpoint) via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`. + + + **Available **only** when interrupted** + Subgraph state can only be viewed **when the subgraph is interrupted**. Once you resume the graph, you won't be able to access the subgraph state. + + + + ```python + from langgraph.graph import START, StateGraph + from langgraph.checkpoint.memory import InMemorySaver + from langgraph.types import interrupt, Command + from typing_extensions import TypedDict + + class State(TypedDict): + foo: str + + # Subgraph + + def subgraph_node_1(state: State): + value = interrupt("Provide value:") + return {"foo": state["foo"] + value} + + subgraph_builder = StateGraph(State) + subgraph_builder.add_node(subgraph_node_1) + subgraph_builder.add_edge(START, "subgraph_node_1") + + subgraph = subgraph_builder.compile() + + # Parent graph + + builder = StateGraph(State) + builder.add_node("node_1", subgraph) + builder.add_edge(START, "node_1") + + checkpointer = InMemorySaver() + graph = builder.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + + graph.invoke({"foo": ""}, config) + parent_state = graph.get_state(config) + subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state # (1)! + + # resume the subgraph + graph.invoke(Command(resume="bar"), config) + ``` + + 1. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state. + + +## Stream subgraph outputs + +To include outputs from subgraphs in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. + +```python +for chunk in graph.stream( + {"foo": "foo"}, + subgraphs=True, # (1)! + stream_mode="updates", +): + print(chunk) +``` + +1. Set `subgraphs=True` to stream outputs from subgraphs. + + + ```python + from typing_extensions import TypedDict + from langgraph.graph.state import StateGraph, START + + # Define subgraph + class SubgraphState(TypedDict): + foo: str + bar: str + + def subgraph_node_1(state: SubgraphState): + return {"bar": "bar"} + + def subgraph_node_2(state: SubgraphState): + # note that this node is using a state key ('bar') that is only available in the subgraph + # and is sending update on the shared state key ('foo') + return {"foo": state["foo"] + state["bar"]} + + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node(subgraph_node_1) + subgraph_builder.add_node(subgraph_node_2) + subgraph_builder.add_edge(START, "subgraph_node_1") + subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2") + subgraph = subgraph_builder.compile() + + # Define parent graph + class ParentState(TypedDict): + foo: str + + def node_1(state: ParentState): + return {"foo": "hi! " + state["foo"]} + + builder = StateGraph(ParentState) + builder.add_node("node_1", node_1) + builder.add_node("node_2", subgraph) + builder.add_edge(START, "node_1") + builder.add_edge("node_1", "node_2") + graph = builder.compile() + + for chunk in graph.stream( + {"foo": "foo"}, + stream_mode="updates", + subgraphs=True, # (1)! + ): + print(chunk) + ``` + + 1. Set `subgraphs=True` to stream outputs from subgraphs. + + ``` + ((), {'node_1': {'foo': 'hi! foo'}}) + (('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_1': {'bar': 'bar'}}) + (('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_2': {'foo': 'hi! foobar'}}) + ((), {'node_2': {'foo': 'hi! foobar'}}) + ``` + diff --git a/src/oss/use-time-travel.mdx b/src/oss/use-time-travel.mdx new file mode 100644 index 00000000..8b76e4a0 --- /dev/null +++ b/src/oss/use-time-travel.mdx @@ -0,0 +1,375 @@ +--- +title: Use time-travel +--- +To use [time-travel](/oss/time-travel) in LangGraph: + +:::python +1. [Run the graph](#1-run-the-graph) with initial inputs using @[`invoke`][CompiledStateGraph.invoke] or @[`stream`][CompiledStateGraph.stream] methods. +2. [Identify a checkpoint in an existing thread](#2-identify-a-checkpoint): Use the @[`get_state_history()`][get_state_history] method to retrieve the execution history for a specific `thread_id` and locate the desired `checkpoint_id`. + Alternatively, set an [interrupt](/oss/add-human-in-the-loop) before the node(s) where you want execution to pause. You can then find the most recent checkpoint recorded up to that interrupt. +3. [Update the graph state (optional)](#3-update-the-state-optional): Use the @[`update_state`][update_state] method to modify the graph's state at the checkpoint and resume execution from alternative state. +4. [Resume execution from the checkpoint](#4-resume-execution-from-the-checkpoint): Use the `invoke` or `stream` methods with an input of `None` and a configuration containing the appropriate `thread_id` and `checkpoint_id`. +::: + +:::js +1. [Run the graph](#1-run-the-graph) with initial inputs using @[`invoke`][CompiledStateGraph.invoke] or @[`stream`][CompiledStateGraph.stream] methods. +2. [Identify a checkpoint in an existing thread](#2-identify-a-checkpoint): Use the @[`getStateHistory()`][get_state_history] method to retrieve the execution history for a specific `thread_id` and locate the desired `checkpoint_id`. + Alternatively, set a [breakpoint](/oss/add-human-in-the-loop) before the node(s) where you want execution to pause. You can then find the most recent checkpoint recorded up to that breakpoint. +3. [Update the graph state (optional)](#3-update-the-state-optional): Use the @[`updateState`][update_state] method to modify the graph's state at the checkpoint and resume execution from alternative state. +4. [Resume execution from the checkpoint](#4-resume-execution-from-the-checkpoint): Use the `invoke` or `stream` methods with an input of `null` and a configuration containing the appropriate `thread_id` and `checkpoint_id`. +::: + + + For a conceptual overview of time-travel, see [Time travel](/oss/time-travel). + + +## In a workflow + +This example builds a simple LangGraph workflow that generates a joke topic and writes a joke using an LLM. It demonstrates how to run the graph, retrieve past execution checkpoints, optionally modify the state, and resume execution from a chosen checkpoint to explore alternate outcomes. + +### Setup + +First we need to install the packages required + +:::python +```python +%%capture --no-stderr +%pip install --quiet -U langgraph langchain_anthropic +``` +::: + +:::js +```bash +npm install @langchain/langgraph @langchain/anthropic +``` +::: + +Next, we need to set API keys for Anthropic (the LLM we will use) + +:::python +```python +import getpass +import os + + +def _set_env(var: str): + if not os.environ.get(var): + os.environ[var] = getpass.getpass(f"{var}: ") + + +_set_env("ANTHROPIC_API_KEY") +``` +::: + +:::js +```typescript +process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY"; +``` +::: + +
+

Set up LangSmith for LangGraph development

+

+ +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. + +

+
+ +:::python +```python +import uuid + +from typing_extensions import TypedDict, NotRequired +from langgraph.graph import StateGraph, START, END +from langchain.chat_models import init_chat_model +from langgraph.checkpoint.memory import InMemorySaver + + +class State(TypedDict): + topic: NotRequired[str] + joke: NotRequired[str] + + +llm = init_chat_model( + "anthropic:claude-3-7-sonnet-latest", + temperature=0, +) + + +def generate_topic(state: State): + """LLM call to generate a topic for the joke""" + msg = llm.invoke("Give me a funny topic for a joke") + return {"topic": msg.content} + + +def write_joke(state: State): + """LLM call to write a joke based on the topic""" + msg = llm.invoke(f"Write a short joke about {state['topic']}") + return {"joke": msg.content} + + +# Build workflow +workflow = StateGraph(State) + +# Add nodes +workflow.add_node("generate_topic", generate_topic) +workflow.add_node("write_joke", write_joke) + +# Add edges to connect nodes +workflow.add_edge(START, "generate_topic") +workflow.add_edge("generate_topic", "write_joke") +workflow.add_edge("write_joke", END) + +# Compile +checkpointer = InMemorySaver() +graph = workflow.compile(checkpointer=checkpointer) +graph +``` +::: + +:::js +```typescript +import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; +import { StateGraph, START, END } from "@langchain/langgraph"; +import { ChatAnthropic } from "@langchain/anthropic"; +import { MemorySaver } from "@langchain/langgraph"; + +const State = z.object({ + topic: z.string().optional(), + joke: z.string().optional(), +}); + +const llm = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", + temperature: 0, +}); + +// Build workflow +const workflow = new StateGraph(State) + // Add nodes + .addNode("generateTopic", async (state) => { + // LLM call to generate a topic for the joke + const msg = await llm.invoke("Give me a funny topic for a joke"); + return { topic: msg.content }; + }) + .addNode("writeJoke", async (state) => { + // LLM call to write a joke based on the topic + const msg = await llm.invoke(`Write a short joke about ${state.topic}`); + return { joke: msg.content }; + }) + // Add edges to connect nodes + .addEdge(START, "generateTopic") + .addEdge("generateTopic", "writeJoke") + .addEdge("writeJoke", END); + +// Compile +const checkpointer = new MemorySaver(); +const graph = workflow.compile({ checkpointer }); +``` +::: + +### 1. Run the graph + +:::python +```python +config = { + "configurable": { + "thread_id": uuid.uuid4(), + } +} +state = graph.invoke({}, config) + +print(state["topic"]) +print() +print(state["joke"]) +``` +::: + +:::js +```typescript +const config = { + configurable: { + thread_id: uuidv4(), + }, +}; + +const state = await graph.invoke({}, config); + +console.log(state.topic); +console.log(); +console.log(state.joke); +``` +::: + +**Output:** + +``` +How about "The Secret Life of Socks in the Dryer"? You know, exploring the mysterious phenomenon of how socks go into the laundry as pairs but come out as singles. Where do they go? Are they starting new lives elsewhere? Is there a sock paradise we don't know about? There's a lot of comedic potential in the everyday mystery that unites us all! + +# The Secret Life of Socks in the Dryer + +I finally discovered where all my missing socks go after the dryer. Turns out they're not missing at all—they've just eloped with someone else's socks from the laundromat to start new lives together. + +My blue argyle is now living in Bermuda with a red polka dot, posting vacation photos on Sockstagram and sending me lint as alimony. +``` + +### 2. Identify a checkpoint + +:::python +```python +# The states are returned in reverse chronological order. +states = list(graph.get_state_history(config)) + +for state in states: + print(state.next) + print(state.config["configurable"]["checkpoint_id"]) + print() +``` + +**Output:** + +``` +() +1f02ac4a-ec9f-6524-8002-8f7b0bbeed0e + +('write_joke',) +1f02ac4a-ce2a-6494-8001-cb2e2d651227 + +('generate_topic',) +1f02ac4a-a4e0-630d-8000-b73c254ba748 + +('__start__',) +1f02ac4a-a4dd-665e-bfff-e6c8c44315d9 +``` +::: + +:::js +```typescript +// The states are returned in reverse chronological order. +const states = []; +for await (const state of graph.getStateHistory(config)) { + states.push(state); +} + +for (const state of states) { + console.log(state.next); + console.log(state.config.configurable?.checkpoint_id); + console.log(); +} +``` + +**Output:** + +``` +[] +1f02ac4a-ec9f-6524-8002-8f7b0bbeed0e + +['writeJoke'] +1f02ac4a-ce2a-6494-8001-cb2e2d651227 + +['generateTopic'] +1f02ac4a-a4e0-630d-8000-b73c254ba748 + +['__start__'] +1f02ac4a-a4dd-665e-bfff-e6c8c44315d9 +``` +::: + +:::python +```python +# This is the state before last (states are listed in chronological order) +selected_state = states[1] +print(selected_state.next) +print(selected_state.values) +``` + +**Output:** + +``` +('write_joke',) +{'topic': 'How about "The Secret Life of Socks in the Dryer"? You know, exploring the mysterious phenomenon of how socks go into the laundry as pairs but come out as singles. Where do they go? Are they starting new lives elsewhere? Is there a sock paradise we don\\'t know about? There\\'s a lot of comedic potential in the everyday mystery that unites us all!'} +``` +::: + +:::js +```typescript +// This is the state before last (states are listed in chronological order) +const selectedState = states[1]; +console.log(selectedState.next); +console.log(selectedState.values); +``` + +**Output:** + +``` +['writeJoke'] +{'topic': 'How about "The Secret Life of Socks in the Dryer"? You know, exploring the mysterious phenomenon of how socks go into the laundry as pairs but come out as singles. Where do they go? Are they starting new lives elsewhere? Is there a sock paradise we don\\'t know about? There\\'s a lot of comedic potential in the everyday mystery that unites us all!'} +``` +::: + + +### 3. Update the state + +:::python +`update_state` will create a new checkpoint. The new checkpoint will be associated with the same thread, but a new checkpoint ID. + +```python +new_config = graph.update_state(selected_state.config, values={"topic": "chickens"}) +print(new_config) +``` + +**Output:** + +``` +{'configurable': {'thread_id': 'c62e2e03-c27b-4cb6-8cea-ea9bfedae006', 'checkpoint_ns': '', 'checkpoint_id': '1f02ac4a-ecee-600b-8002-a1d21df32e4c'}} +``` +::: + +:::js +`updateState` will create a new checkpoint. The new checkpoint will be associated with the same thread, but a new checkpoint ID. + +```typescript +const newConfig = await graph.updateState(selectedState.config, { + topic: "chickens", +}); +console.log(newConfig); +``` + +**Output:** + +``` +{'configurable': {'thread_id': 'c62e2e03-c27b-4cb6-8cea-ea9bfedae006', 'checkpoint_ns': '', 'checkpoint_id': '1f02ac4a-ecee-600b-8002-a1d21df32e4c'}} +``` +::: + +### 4. Resume execution from the checkpoint + +:::python +```python +graph.invoke(None, new_config) +``` + +**Output:** + +```python +{'topic': 'chickens', + 'joke': 'Why did the chicken join a band?\n\nBecause it had excellent drumsticks!'} +``` +::: + +:::js +```typescript +await graph.invoke(null, newConfig); +``` + +**Output:** + +```typescript +{ + 'topic': 'chickens', + 'joke': 'Why did the chicken join a band?\n\nBecause it had excellent drumsticks!' +} +``` +::: diff --git a/src/oss/why-langgraph.mdx b/src/oss/why-langgraph.mdx new file mode 100644 index 00000000..820697d7 --- /dev/null +++ b/src/oss/why-langgraph.mdx @@ -0,0 +1,29 @@ +--- +title: Low-level orchestration with LangGraph +sidebarTitle: Overview +--- + +LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for: + +* **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course. +* **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case. +* **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time. + +## Learn LangGraph basics + +To get acquainted with LangGraph's key concepts and features, complete the following LangGraph basics tutorials series: + +1. [Build a basic chatbot](/oss/1-build-basic-chatbot) +2. [Add tools](/oss/2-add-tools) +3. [Add memory](/oss/3-add-memory) +4. [Add human-in-the-loop controls](/oss/4-human-in-the-loop) +5. [Customize state](/oss/5-customize-state) +6. [Time travel](/oss/6-time-travel) + +In completing this series of tutorials, you will build a support chatbot in LangGraph that can: + +* ✅ **Answer common questions** by searching the web +* ✅ **Maintain conversation state** across calls +* ✅ **Route complex queries** to a human for review +* ✅ **Use custom state** to control its behavior +* ✅ **Rewind and explore** alternative conversation paths diff --git a/src/oss/workflows-and-agents.mdx b/src/oss/workflows-and-agents.mdx new file mode 100644 index 00000000..70a930d2 --- /dev/null +++ b/src/oss/workflows-and-agents.mdx @@ -0,0 +1,2258 @@ +--- +title: Workflows and agents +--- + +This guide reviews common patterns for agentic systems. In describing these systems, it can be useful to make a distinction between "workflows" and "agents". One way to think about this difference is nicely explained in Anthropic's `Building Effective Agents` blog post: + +> Workflows are systems where LLMs and tools are orchestrated through predefined code paths. +> Agents, on the other hand, are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks. + +Here is a simple way to visualize these differences: + +![Agent Workflow](/oss/images/agent_workflow.png) + +When building agents and workflows, LangGraph offers a number of benefits including persistence, streaming, and support for debugging as well as deployment. + +## Set up + +:::python +You can use [any chat model](https://python.langchain.com/docs/integrations/chat/) that supports structured outputs and tool calling. Below, we show the process of installing the packages, setting API keys, and testing structured outputs / tool calling for Anthropic. + +Install dependencies + +```bash +pip install langchain_core langchain-anthropic langgraph +``` + +Initialize an LLM + +```python +import os +import getpass + +from langchain_anthropic import ChatAnthropic + +def _set_env(var: str): + if not os.environ.get(var): + os.environ[var] = getpass.getpass(f"{var}: ") + + +_set_env("ANTHROPIC_API_KEY") + +llm = ChatAnthropic(model="claude-3-5-sonnet-latest") +``` +::: + +:::js +You can use [any chat model](https://js.langchain.com/docs/integrations/chat/) that supports structured outputs and tool calling. Below, we show the process of installing the packages, setting API keys, and testing structured outputs / tool calling for Anthropic. + +Install dependencies + +```bash +npm install @langchain/core @langchain/anthropic @langchain/langgraph +``` + +Initialize an LLM + +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; + +process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY"; + +const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); +``` +::: + +## Building Blocks: The Augmented LLM + +LLM have augmentations that support building workflows and agents. These include structured outputs and tool calling, as shown in this image from the Anthropic blog on `Building Effective Agents`: + +![augmented_llm.png](/oss/images/augmented_llm.png) + +:::python +```python +# Schema for structured output +from pydantic import BaseModel, Field + +class SearchQuery(BaseModel): + search_query: str = Field(None, description="Query that is optimized web search.") + justification: str = Field( + None, description="Why this query is relevant to the user's request." + ) + + +# Augment the LLM with schema for structured output +structured_llm = llm.with_structured_output(SearchQuery) + +# Invoke the augmented LLM +output = structured_llm.invoke("How does Calcium CT score relate to high cholesterol?") + +# Define a tool +def multiply(a: int, b: int) -> int: + return a * b + +# Augment the LLM with tools +llm_with_tools = llm.bind_tools([multiply]) + +# Invoke the LLM with input that triggers the tool call +msg = llm_with_tools.invoke("What is 2 times 3?") + +# Get the tool call +msg.tool_calls +``` +::: + +:::js +```typescript +import { z } from "zod"; +import { tool } from "@langchain/core/tools"; + +// Schema for structured output +const SearchQuery = z.object({ + search_query: z.string().describe("Query that is optimized web search."), + justification: z + .string() + .describe("Why this query is relevant to the user's request."), +}); + +// Augment the LLM with schema for structured output +const structuredLlm = llm.withStructuredOutput(SearchQuery); + +// Invoke the augmented LLM +const output = await structuredLlm.invoke( + "How does Calcium CT score relate to high cholesterol?" +); + +// Define a tool +const multiply = tool( + async ({ a, b }: { a: number; b: number }) => { + return a * b; + }, + { + name: "multiply", + description: "Multiply two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), + } +); + +// Augment the LLM with tools +const llmWithTools = llm.bindTools([multiply]); + +// Invoke the LLM with input that triggers the tool call +const msg = await llmWithTools.invoke("What is 2 times 3?"); + +// Get the tool call +console.log(msg.tool_calls); +``` +::: + +## Prompt chaining + +In prompt chaining, each LLM call processes the output of the previous one. + +As noted in the Anthropic blog on `Building Effective Agents`: + +> Prompt chaining decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one. You can add programmatic checks (see "gate" in the diagram below) on any intermediate steps to ensure that the process is still on track. + +> When to use this workflow: This workflow is ideal for situations where the task can be easily and cleanly decomposed into fixed subtasks. The main goal is to trade off latency for higher accuracy, by making each LLM call an easier task. + +![prompt_chain.png](/oss/images/prompt_chain.png) + + + + :::python + ```python + from typing_extensions import TypedDict + from langgraph.graph import StateGraph, START, END + from IPython.display import Image, display + + + # Graph state + class State(TypedDict): + topic: str + joke: str + improved_joke: str + final_joke: str + + + # Nodes + def generate_joke(state: State): + """First LLM call to generate initial joke""" + + msg = llm.invoke(f"Write a short joke about {state['topic']}") + return {"joke": msg.content} + + + def check_punchline(state: State): + """Gate function to check if the joke has a punchline""" + + # Simple check - does the joke contain "?" or "!" + if "?" in state["joke"] or "!" in state["joke"]: + return "Pass" + return "Fail" + + + def improve_joke(state: State): + """Second LLM call to improve the joke""" + + msg = llm.invoke(f"Make this joke funnier by adding wordplay: {state['joke']}") + return {"improved_joke": msg.content} + + + def polish_joke(state: State): + """Third LLM call for final polish""" + + msg = llm.invoke(f"Add a surprising twist to this joke: {state['improved_joke']}") + return {"final_joke": msg.content} + + + # Build workflow + workflow = StateGraph(State) + + # Add nodes + workflow.add_node("generate_joke", generate_joke) + workflow.add_node("improve_joke", improve_joke) + workflow.add_node("polish_joke", polish_joke) + + # Add edges to connect nodes + workflow.add_edge(START, "generate_joke") + workflow.add_conditional_edges( + "generate_joke", check_punchline, {"Fail": "improve_joke", "Pass": END} + ) + workflow.add_edge("improve_joke", "polish_joke") + workflow.add_edge("polish_joke", END) + + # Compile + chain = workflow.compile() + + # Show workflow + display(Image(chain.get_graph().draw_mermaid_png())) + + # Invoke + state = chain.invoke({"topic": "cats"}) + print("Initial joke:") + print(state["joke"]) + print("\n--- --- ---\n") + if "improved_joke" in state: + print("Improved joke:") + print(state["improved_joke"]) + print("\n--- --- ---\n") + + print("Final joke:") + print(state["final_joke"]) + else: + print("Joke failed quality gate - no punchline detected!") + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/a0281fca-3a71-46de-beee-791468607b75/r + + **Resources:** + + **LangChain Academy** + + See our lesson on Prompt Chaining [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-1/chain.ipynb). + ::: + + :::js + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { z } from "zod"; + + // Graph state + const State = z.object({ + topic: z.string(), + joke: z.string().optional(), + improved_joke: z.string().optional(), + final_joke: z.string().optional(), + }); + + // Nodes + const generateJoke = async (state: z.infer) => { + // First LLM call to generate initial joke + const msg = await llm.invoke(`Write a short joke about ${state.topic}`); + return { joke: msg.content }; + }; + + const checkPunchline = (state: z.infer) => { + // Gate function to check if the joke has a punchline + // Simple check - does the joke contain "?" or "!" + if (state.joke && (state.joke.includes("?") || state.joke.includes("!"))) { + return "Pass"; + } + return "Fail"; + }; + + const improveJoke = async (state: z.infer) => { + // Second LLM call to improve the joke + const msg = await llm.invoke(`Make this joke funnier by adding wordplay: ${state.joke}`); + return { improved_joke: msg.content }; + }; + + const polishJoke = async (state: z.infer) => { + // Third LLM call for final polish + const msg = await llm.invoke(`Add a surprising twist to this joke: ${state.improved_joke}`); + return { final_joke: msg.content }; + }; + + // Build workflow + const workflow = new StateGraph(State) + .addNode("generate_joke", generateJoke) + .addNode("improve_joke", improveJoke) + .addNode("polish_joke", polishJoke) + .addEdge(START, "generate_joke") + .addConditionalEdges( + "generate_joke", + checkPunchline, + { "Fail": "improve_joke", "Pass": END } + ) + .addEdge("improve_joke", "polish_joke") + .addEdge("polish_joke", END); + + // Compile + const chain = workflow.compile(); + + // Show workflow + import * as fs from "node:fs/promises"; + const drawableGraph = await chain.getGraphAsync(); + const image = await drawableGraph.drawMermaidPng(); + const imageBuffer = new Uint8Array(await image.arrayBuffer()); + await fs.writeFile("workflow.png", imageBuffer); + + // Invoke + const state = await chain.invoke({ topic: "cats" }); + console.log("Initial joke:"); + console.log(state.joke); + console.log("\n--- --- ---\n"); + if (state.improved_joke) { + console.log("Improved joke:"); + console.log(state.improved_joke); + console.log("\n--- --- ---\n"); + + console.log("Final joke:"); + console.log(state.final_joke); + } else { + console.log("Joke failed quality gate - no punchline detected!"); + } + ``` + ::: + + + :::python + ```python + from langgraph.func import entrypoint, task + + + # Tasks + @task + def generate_joke(topic: str): + """First LLM call to generate initial joke""" + msg = llm.invoke(f"Write a short joke about {topic}") + return msg.content + + + def check_punchline(joke: str): + """Gate function to check if the joke has a punchline""" + # Simple check - does the joke contain "?" or "!" + if "?" in joke or "!" in joke: + return "Fail" + + return "Pass" + + + @task + def improve_joke(joke: str): + """Second LLM call to improve the joke""" + msg = llm.invoke(f"Make this joke funnier by adding wordplay: {joke}") + return msg.content + + + @task + def polish_joke(joke: str): + """Third LLM call for final polish""" + msg = llm.invoke(f"Add a surprising twist to this joke: {joke}") + return msg.content + + + @entrypoint() + def prompt_chaining_workflow(topic: str): + original_joke = generate_joke(topic).result() + if check_punchline(original_joke) == "Pass": + return original_joke + + improved_joke = improve_joke(original_joke).result() + return polish_joke(improved_joke).result() + + # Invoke + for step in prompt_chaining_workflow.stream("cats", stream_mode="updates"): + print(step) + print("\n") + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/332fa4fc-b6ca-416e-baa3-161625e69163/r + ::: + + :::js + ```typescript + import { entrypoint, task } from "@langchain/langgraph"; + + // Tasks + const generateJoke = task("generate_joke", async (topic: string) => { + // First LLM call to generate initial joke + const msg = await llm.invoke(`Write a short joke about ${topic}`); + return msg.content; + }); + + const checkPunchline = (joke: string) => { + // Gate function to check if the joke has a punchline + // Simple check - does the joke contain "?" or "!" + if (joke.includes("?") || joke.includes("!")) { + return "Pass"; + } + return "Fail"; + }; + + const improveJoke = task("improve_joke", async (joke: string) => { + // Second LLM call to improve the joke + const msg = await llm.invoke(`Make this joke funnier by adding wordplay: ${joke}`); + return msg.content; + }); + + const polishJoke = task("polish_joke", async (joke: string) => { + // Third LLM call for final polish + const msg = await llm.invoke(`Add a surprising twist to this joke: ${joke}`); + return msg.content; + }); + + const promptChainingWorkflow = entrypoint("promptChainingWorkflow", async (topic: string) => { + const originalJoke = await generateJoke(topic); + if (checkPunchline(originalJoke) === "Pass") { + return originalJoke; + } + + const improvedJoke = await improveJoke(originalJoke); + return await polishJoke(improvedJoke); + }); + + // Invoke + const stream = await promptChainingWorkflow.stream("cats", { streamMode: "updates" }); + for await (const step of stream) { + console.log(step); + console.log("\n"); + } + ``` + ::: + + + +## Parallelization + +With parallelization, LLMs work simultaneously on a task: + +> LLMs can sometimes work simultaneously on a task and have their outputs aggregated programmatically. This workflow, parallelization, manifests in two key variations: Sectioning: Breaking a task into independent subtasks run in parallel. Voting: Running the same task multiple times to get diverse outputs. + +> When to use this workflow: Parallelization is effective when the divided subtasks can be parallelized for speed, or when multiple perspectives or attempts are needed for higher confidence results. For complex tasks with multiple considerations, LLMs generally perform better when each consideration is handled by a separate LLM call, allowing focused attention on each specific aspect. + +![parallelization.png](/oss/images/parallelization.png) + + + + :::python + ```python + # Graph state + class State(TypedDict): + topic: str + joke: str + story: str + poem: str + combined_output: str + + + # Nodes + def call_llm_1(state: State): + """First LLM call to generate initial joke""" + + msg = llm.invoke(f"Write a joke about {state['topic']}") + return {"joke": msg.content} + + + def call_llm_2(state: State): + """Second LLM call to generate story""" + + msg = llm.invoke(f"Write a story about {state['topic']}") + return {"story": msg.content} + + + def call_llm_3(state: State): + """Third LLM call to generate poem""" + + msg = llm.invoke(f"Write a poem about {state['topic']}") + return {"poem": msg.content} + + + def aggregator(state: State): + """Combine the joke and story into a single output""" + + combined = f"Here's a story, joke, and poem about {state['topic']}!\n\n" + combined += f"STORY:\n{state['story']}\n\n" + combined += f"JOKE:\n{state['joke']}\n\n" + combined += f"POEM:\n{state['poem']}" + return {"combined_output": combined} + + + # Build workflow + parallel_builder = StateGraph(State) + + # Add nodes + parallel_builder.add_node("call_llm_1", call_llm_1) + parallel_builder.add_node("call_llm_2", call_llm_2) + parallel_builder.add_node("call_llm_3", call_llm_3) + parallel_builder.add_node("aggregator", aggregator) + + # Add edges to connect nodes + parallel_builder.add_edge(START, "call_llm_1") + parallel_builder.add_edge(START, "call_llm_2") + parallel_builder.add_edge(START, "call_llm_3") + parallel_builder.add_edge("call_llm_1", "aggregator") + parallel_builder.add_edge("call_llm_2", "aggregator") + parallel_builder.add_edge("call_llm_3", "aggregator") + parallel_builder.add_edge("aggregator", END) + parallel_workflow = parallel_builder.compile() + + # Show workflow + display(Image(parallel_workflow.get_graph().draw_mermaid_png())) + + # Invoke + state = parallel_workflow.invoke({"topic": "cats"}) + print(state["combined_output"]) + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/3be2e53c-ca94-40dd-934f-82ff87fac277/r + + **Resources:** + + **Documentation** + + See our documentation on parallelization [here](/oss/graph-api#create-branches). + + **LangChain Academy** + + See our lesson on parallelization [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-1/simple-graph.ipynb). + ::: + + :::js + ```typescript + // Graph state + const State = z.object({ + topic: z.string(), + joke: z.string().optional(), + story: z.string().optional(), + poem: z.string().optional(), + combined_output: z.string().optional(), + }); + + // Nodes + const callLlm1 = async (state: z.infer) => { + // First LLM call to generate initial joke + const msg = await llm.invoke(`Write a joke about ${state.topic}`); + return { joke: msg.content }; + }; + + const callLlm2 = async (state: z.infer) => { + // Second LLM call to generate story + const msg = await llm.invoke(`Write a story about ${state.topic}`); + return { story: msg.content }; + }; + + const callLlm3 = async (state: z.infer) => { + // Third LLM call to generate poem + const msg = await llm.invoke(`Write a poem about ${state.topic}`); + return { poem: msg.content }; + }; + + const aggregator = (state: z.infer) => { + // Combine the joke and story into a single output + let combined = `Here's a story, joke, and poem about ${state.topic}!\n\n`; + combined += `STORY:\n${state.story}\n\n`; + combined += `JOKE:\n${state.joke}\n\n`; + combined += `POEM:\n${state.poem}`; + return { combined_output: combined }; + }; + + // Build workflow + const parallelBuilder = new StateGraph(State) + .addNode("call_llm_1", callLlm1) + .addNode("call_llm_2", callLlm2) + .addNode("call_llm_3", callLlm3) + .addNode("aggregator", aggregator) + .addEdge(START, "call_llm_1") + .addEdge(START, "call_llm_2") + .addEdge(START, "call_llm_3") + .addEdge("call_llm_1", "aggregator") + .addEdge("call_llm_2", "aggregator") + .addEdge("call_llm_3", "aggregator") + .addEdge("aggregator", END); + + const parallelWorkflow = parallelBuilder.compile(); + + // Invoke + const state = await parallelWorkflow.invoke({ topic: "cats" }); + console.log(state.combined_output); + ``` + ::: + + + :::python + ```python + @task + def call_llm_1(topic: str): + """First LLM call to generate initial joke""" + msg = llm.invoke(f"Write a joke about {topic}") + return msg.content + + + @task + def call_llm_2(topic: str): + """Second LLM call to generate story""" + msg = llm.invoke(f"Write a story about {topic}") + return msg.content + + + @task + def call_llm_3(topic): + """Third LLM call to generate poem""" + msg = llm.invoke(f"Write a poem about {topic}") + return msg.content + + + @task + def aggregator(topic, joke, story, poem): + """Combine the joke and story into a single output""" + + combined = f"Here's a story, joke, and poem about {topic}!\n\n" + combined += f"STORY:\n{story}\n\n" + combined += f"JOKE:\n{joke}\n\n" + combined += f"POEM:\n{poem}" + return combined + + + # Build workflow + @entrypoint() + def parallel_workflow(topic: str): + joke_fut = call_llm_1(topic) + story_fut = call_llm_2(topic) + poem_fut = call_llm_3(topic) + return aggregator( + topic, joke_fut.result(), story_fut.result(), poem_fut.result() + ).result() + + # Invoke + for step in parallel_workflow.stream("cats", stream_mode="updates"): + print(step) + print("\n") + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/623d033f-e814-41e9-80b1-75e6abb67801/r + ::: + + :::js + ```typescript + const callLlm1 = task("call_llm_1", async (topic: string) => { + // First LLM call to generate initial joke + const msg = await llm.invoke(`Write a joke about ${topic}`); + return msg.content; + }); + + const callLlm2 = task("call_llm_2", async (topic: string) => { + // Second LLM call to generate story + const msg = await llm.invoke(`Write a story about ${topic}`); + return msg.content; + }); + + const callLlm3 = task("call_llm_3", async (topic: string) => { + // Third LLM call to generate poem + const msg = await llm.invoke(`Write a poem about ${topic}`); + return msg.content; + }); + + const aggregator = task("aggregator", (topic: string, joke: string, story: string, poem: string) => { + // Combine the joke and story into a single output + let combined = `Here's a story, joke, and poem about ${topic}!\n\n`; + combined += `STORY:\n${story}\n\n`; + combined += `JOKE:\n${joke}\n\n`; + combined += `POEM:\n${poem}`; + return combined; + }); + + // Build workflow + const parallelWorkflow = entrypoint("parallelWorkflow", async (topic: string) => { + const jokeFut = callLlm1(topic); + const storyFut = callLlm2(topic); + const poemFut = callLlm3(topic); + + return await aggregator( + topic, + await jokeFut, + await storyFut, + await poemFut + ); + }); + + // Invoke + const stream = await parallelWorkflow.stream("cats", { streamMode: "updates" }); + for await (const step of stream) { + console.log(step); + console.log("\n"); + } + ``` + ::: + + + +## Routing + +Routing classifies an input and directs it to a followup task. As noted in the Anthropic blog on `Building Effective Agents`: + +> Routing classifies an input and directs it to a specialized followup task. This workflow allows for separation of concerns, and building more specialized prompts. Without this workflow, optimizing for one kind of input can hurt performance on other inputs. + +> When to use this workflow: Routing works well for complex tasks where there are distinct categories that are better handled separately, and where classification can be handled accurately, either by an LLM or a more traditional classification model/algorithm. + +![routing.png](/oss/images/routing.png) + + + + :::python + ```python + from typing_extensions import Literal + from langchain_core.messages import HumanMessage, SystemMessage + + + # Schema for structured output to use as routing logic + class Route(BaseModel): + step: Literal["poem", "story", "joke"] = Field( + None, description="The next step in the routing process" + ) + + + # Augment the LLM with schema for structured output + router = llm.with_structured_output(Route) + + + # State + class State(TypedDict): + input: str + decision: str + output: str + + + # Nodes + def llm_call_1(state: State): + """Write a story""" + + result = llm.invoke(state["input"]) + return {"output": result.content} + + + def llm_call_2(state: State): + """Write a joke""" + + result = llm.invoke(state["input"]) + return {"output": result.content} + + + def llm_call_3(state: State): + """Write a poem""" + + result = llm.invoke(state["input"]) + return {"output": result.content} + + + def llm_call_router(state: State): + """Route the input to the appropriate node""" + + # Run the augmented LLM with structured output to serve as routing logic + decision = router.invoke( + [ + SystemMessage( + content="Route the input to story, joke, or poem based on the user's request." + ), + HumanMessage(content=state["input"]), + ] + ) + + return {"decision": decision.step} + + + # Conditional edge function to route to the appropriate node + def route_decision(state: State): + # Return the node name you want to visit next + if state["decision"] == "story": + return "llm_call_1" + elif state["decision"] == "joke": + return "llm_call_2" + elif state["decision"] == "poem": + return "llm_call_3" + + + # Build workflow + router_builder = StateGraph(State) + + # Add nodes + router_builder.add_node("llm_call_1", llm_call_1) + router_builder.add_node("llm_call_2", llm_call_2) + router_builder.add_node("llm_call_3", llm_call_3) + router_builder.add_node("llm_call_router", llm_call_router) + + # Add edges to connect nodes + router_builder.add_edge(START, "llm_call_router") + router_builder.add_conditional_edges( + "llm_call_router", + route_decision, + { # Name returned by route_decision : Name of next node to visit + "llm_call_1": "llm_call_1", + "llm_call_2": "llm_call_2", + "llm_call_3": "llm_call_3", + }, + ) + router_builder.add_edge("llm_call_1", END) + router_builder.add_edge("llm_call_2", END) + router_builder.add_edge("llm_call_3", END) + + # Compile workflow + router_workflow = router_builder.compile() + + # Show the workflow + display(Image(router_workflow.get_graph().draw_mermaid_png())) + + # Invoke + state = router_workflow.invoke({"input": "Write me a joke about cats"}) + print(state["output"]) + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/c4580b74-fe91-47e4-96fe-7fac598d509c/r + + **Resources:** + + **LangChain Academy** + + See our lesson on routing [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-1/router.ipynb). + + **Examples** + + See our video [here](https://www.youtube.com/watch?v=bq1Plo2RhYI). + ::: + + :::js + ```typescript + import { SystemMessage, HumanMessage } from "@langchain/core/messages"; + + // Schema for structured output to use as routing logic + const Route = z.object({ + step: z.enum(["poem", "story", "joke"]).describe("The next step in the routing process"), + }); + + // Augment the LLM with schema for structured output + const router = llm.withStructuredOutput(Route); + + // State + const State = z.object({ + input: z.string(), + decision: z.string().optional(), + output: z.string().optional(), + }); + + // Nodes + const llmCall1 = async (state: z.infer) => { + // Write a story + const result = await llm.invoke(state.input); + return { output: result.content }; + }; + + const llmCall2 = async (state: z.infer) => { + // Write a joke + const result = await llm.invoke(state.input); + return { output: result.content }; + }; + + const llmCall3 = async (state: z.infer) => { + // Write a poem + const result = await llm.invoke(state.input); + return { output: result.content }; + }; + + const llmCallRouter = async (state: z.infer) => { + // Route the input to the appropriate node + const decision = await router.invoke([ + new SystemMessage("Route the input to story, joke, or poem based on the user's request."), + new HumanMessage(state.input), + ]); + + return { decision: decision.step }; + }; + + // Conditional edge function to route to the appropriate node + const routeDecision = (state: z.infer) => { + // Return the node name you want to visit next + if (state.decision === "story") { + return "llm_call_1"; + } else if (state.decision === "joke") { + return "llm_call_2"; + } else if (state.decision === "poem") { + return "llm_call_3"; + } + }; + + // Build workflow + const routerBuilder = new StateGraph(State) + .addNode("llm_call_1", llmCall1) + .addNode("llm_call_2", llmCall2) + .addNode("llm_call_3", llmCall3) + .addNode("llm_call_router", llmCallRouter) + .addEdge(START, "llm_call_router") + .addConditionalEdges( + "llm_call_router", + routeDecision, + { + "llm_call_1": "llm_call_1", + "llm_call_2": "llm_call_2", + "llm_call_3": "llm_call_3", + } + ) + .addEdge("llm_call_1", END) + .addEdge("llm_call_2", END) + .addEdge("llm_call_3", END); + + const routerWorkflow = routerBuilder.compile(); + + // Invoke + const state = await routerWorkflow.invoke({ input: "Write me a joke about cats" }); + console.log(state.output); + ``` + ::: + + + :::python + ```python + from typing_extensions import Literal + from pydantic import BaseModel + from langchain_core.messages import HumanMessage, SystemMessage + + + # Schema for structured output to use as routing logic + class Route(BaseModel): + step: Literal["poem", "story", "joke"] = Field( + None, description="The next step in the routing process" + ) + + + # Augment the LLM with schema for structured output + router = llm.with_structured_output(Route) + + + @task + def llm_call_1(input_: str): + """Write a story""" + result = llm.invoke(input_) + return result.content + + + @task + def llm_call_2(input_: str): + """Write a joke""" + result = llm.invoke(input_) + return result.content + + + @task + def llm_call_3(input_: str): + """Write a poem""" + result = llm.invoke(input_) + return result.content + + + def llm_call_router(input_: str): + """Route the input to the appropriate node""" + # Run the augmented LLM with structured output to serve as routing logic + decision = router.invoke( + [ + SystemMessage( + content="Route the input to story, joke, or poem based on the user's request." + ), + HumanMessage(content=input_), + ] + ) + return decision.step + + + # Create workflow + @entrypoint() + def router_workflow(input_: str): + next_step = llm_call_router(input_) + if next_step == "story": + llm_call = llm_call_1 + elif next_step == "joke": + llm_call = llm_call_2 + elif next_step == "poem": + llm_call = llm_call_3 + + return llm_call(input_).result() + + # Invoke + for step in router_workflow.stream("Write me a joke about cats", stream_mode="updates"): + print(step) + print("\n") + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/5e2eb979-82dd-402c-b1a0-a8cceaf2a28a/r + ::: + + :::js + ```typescript + import { SystemMessage, HumanMessage } from "@langchain/core/messages"; + + // Schema for structured output to use as routing logic + const Route = z.object({ + step: z.enum(["poem", "story", "joke"]).describe( + "The next step in the routing process" + ), + }); + + // Augment the LLM with schema for structured output + const router = llm.withStructuredOutput(Route); + + const llmCall1 = task("llm_call_1", async (input: string) => { + // Write a story + const result = await llm.invoke(input); + return result.content; + }); + + const llmCall2 = task("llm_call_2", async (input: string) => { + // Write a joke + const result = await llm.invoke(input); + return result.content; + }); + + const llmCall3 = task("llm_call_3", async (input: string) => { + // Write a poem + const result = await llm.invoke(input); + return result.content; + }); + + const llmCallRouter = async (input: string) => { + // Route the input to the appropriate node + const decision = await router.invoke([ + new SystemMessage("Route the input to story, joke, or poem based on the user's request."), + new HumanMessage(input), + ]); + return decision.step; + }; + + // Create workflow + const routerWorkflow = entrypoint("routerWorkflow", async (input: string) => { + const nextStep = await llmCallRouter(input); + + let llmCall: typeof llmCall1; + if (nextStep === "story") { + llmCall = llmCall1; + } else if (nextStep === "joke") { + llmCall = llmCall2; + } else if (nextStep === "poem") { + llmCall = llmCall3; + } + + return await llmCall(input); + }); + + // Invoke + const stream = await routerWorkflow.stream("Write me a joke about cats", { streamMode: "updates" }); + for await (const step of stream) { + console.log(step); + console.log("\n"); + } + ``` + ::: + + + +## Orchestrator-Worker + +With orchestrator-worker, an orchestrator breaks down a task and delegates each sub-task to workers. As noted in the Anthropic blog on `Building Effective Agents`: + +> In the orchestrator-workers workflow, a central LLM dynamically breaks down tasks, delegates them to worker LLMs, and synthesizes their results. + +> When to use this workflow: This workflow is well-suited for complex tasks where you can't predict the subtasks needed (in coding, for example, the number of files that need to be changed and the nature of the change in each file likely depend on the task). Whereas it's topographically similar, the key difference from parallelization is its flexibility—subtasks aren't pre-defined, but determined by the orchestrator based on the specific input. + +![worker.png](/oss/images/worker.png) + + + + :::python + ```python + from typing import Annotated, List + import operator + + + # Schema for structured output to use in planning + class Section(BaseModel): + name: str = Field( + description="Name for this section of the report.", + ) + description: str = Field( + description="Brief overview of the main topics and concepts to be covered in this section.", + ) + + + class Sections(BaseModel): + sections: List[Section] = Field( + description="Sections of the report.", + ) + + + # Augment the LLM with schema for structured output + planner = llm.with_structured_output(Sections) + ``` + + **Creating Workers in LangGraph** + + Because orchestrator-worker workflows are common, LangGraph **has the `Send` API to support this**. It lets you dynamically create worker nodes and send each one a specific input. Each worker has its own state, and all worker outputs are written to a *shared state key* that is accessible to the orchestrator graph. This gives the orchestrator access to all worker output and allows it to synthesize them into a final output. As you can see below, we iterate over a list of sections and `Send` each to a worker node. See further documentation [here](/oss/graph-api#map-reduce-and-the-send-api) and [here](/oss/graph-api#send). + + ```python + from langgraph.types import Send + + + # Graph state + class State(TypedDict): + topic: str # Report topic + sections: list[Section] # List of report sections + completed_sections: Annotated[ + list, operator.add + ] # All workers write to this key in parallel + final_report: str # Final report + + + # Worker state + class WorkerState(TypedDict): + section: Section + completed_sections: Annotated[list, operator.add] + + + # Nodes + def orchestrator(state: State): + """Orchestrator that generates a plan for the report""" + + # Generate queries + report_sections = planner.invoke( + [ + SystemMessage(content="Generate a plan for the report."), + HumanMessage(content=f"Here is the report topic: {state['topic']}"), + ] + ) + + return {"sections": report_sections.sections} + + + def llm_call(state: WorkerState): + """Worker writes a section of the report""" + + # Generate section + section = llm.invoke( + [ + SystemMessage( + content="Write a report section following the provided name and description. Include no preamble for each section. Use markdown formatting." + ), + HumanMessage( + content=f"Here is the section name: {state['section'].name} and description: {state['section'].description}" + ), + ] + ) + + # Write the updated section to completed sections + return {"completed_sections": [section.content]} + + + def synthesizer(state: State): + """Synthesize full report from sections""" + + # List of completed sections + completed_sections = state["completed_sections"] + + # Format completed section to str to use as context for final sections + completed_report_sections = "\n\n---\n\n".join(completed_sections) + + return {"final_report": completed_report_sections} + + + # Conditional edge function to create llm_call workers that each write a section of the report + def assign_workers(state: State): + """Assign a worker to each section in the plan""" + + # Kick off section writing in parallel via Send() API + return [Send("llm_call", {"section": s}) for s in state["sections"]] + + + # Build workflow + orchestrator_worker_builder = StateGraph(State) + + # Add the nodes + orchestrator_worker_builder.add_node("orchestrator", orchestrator) + orchestrator_worker_builder.add_node("llm_call", llm_call) + orchestrator_worker_builder.add_node("synthesizer", synthesizer) + + # Add edges to connect nodes + orchestrator_worker_builder.add_edge(START, "orchestrator") + orchestrator_worker_builder.add_conditional_edges( + "orchestrator", assign_workers, ["llm_call"] + ) + orchestrator_worker_builder.add_edge("llm_call", "synthesizer") + orchestrator_worker_builder.add_edge("synthesizer", END) + + # Compile the workflow + orchestrator_worker = orchestrator_worker_builder.compile() + + # Show the workflow + display(Image(orchestrator_worker.get_graph().draw_mermaid_png())) + + # Invoke + state = orchestrator_worker.invoke({"topic": "Create a report on LLM scaling laws"}) + + from IPython.display import Markdown + Markdown(state["final_report"]) + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/78cbcfc3-38bf-471d-b62a-b299b144237d/r + + **Resources:** + + **LangChain Academy** + + See our lesson on orchestrator-worker [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-4/map-reduce.ipynb). + + **Examples** + + [Here](https://github.com/langchain-ai/report-mAIstro) is a project that uses orchestrator-worker for report planning and writing. See our video [here](https://www.youtube.com/watch?v=wSxZ7yFbbas). + ::: + + :::js + ```typescript + import "@langchain/langgraph/zod"; + + // Schema for structured output to use in planning + const Section = z.object({ + name: z.string().describe("Name for this section of the report."), + description: z.string().describe("Brief overview of the main topics and concepts to be covered in this section."), + }); + + const Sections = z.object({ + sections: z.array(Section).describe("Sections of the report."), + }); + + // Augment the LLM with schema for structured output + const planner = llm.withStructuredOutput(Sections); + ``` + + **Creating Workers in LangGraph** + + Because orchestrator-worker workflows are common, LangGraph **has the `Send` API to support this**. It lets you dynamically create worker nodes and send each one a specific input. Each worker has its own state, and all worker outputs are written to a *shared state key* that is accessible to the orchestrator graph. This gives the orchestrator access to all worker output and allows it to synthesize them into a final output. As you can see below, we iterate over a list of sections and `Send` each to a worker node. See further documentation [here](/oss/graph-api#map-reduce-and-the-send-api) and [here](/oss/graph-api#send). + + ```typescript + import { withLangGraph } from "@langchain/langgraph/zod"; + import { Send } from "@langchain/langgraph"; + + // Graph state + const State = z.object({ + topic: z.string(), // Report topic + sections: z.array(Section).optional(), // List of report sections + // All workers write to this key + completed_sections: withLangGraph(z.array(z.string()), { + reducer: { + fn: (x, y) => x.concat(y), + }, + default: () => [], + }), + final_report: z.string().optional(), // Final report + }); + + // Worker state + const WorkerState = z.object({ + section: Section, + completed_sections: withLangGraph(z.array(z.string()), { + reducer: { + fn: (x, y) => x.concat(y), + }, + default: () => [], + }), + }); + + // Nodes + const orchestrator = async (state: z.infer) => { + // Orchestrator that generates a plan for the report + const reportSections = await planner.invoke([ + new SystemMessage("Generate a plan for the report."), + new HumanMessage(`Here is the report topic: ${state.topic}`), + ]); + + return { sections: reportSections.sections }; + }; + + const llmCall = async (state: z.infer) => { + // Worker writes a section of the report + const section = await llm.invoke([ + new SystemMessage( + "Write a report section following the provided name and description. Include no preamble for each section. Use markdown formatting." + ), + new HumanMessage( + `Here is the section name: ${state.section.name} and description: ${state.section.description}` + ), + ]); + + // Write the updated section to completed sections + return { completed_sections: [section.content] }; + }; + + const synthesizer = (state: z.infer) => { + // Synthesize full report from sections + const completedSections = state.completed_sections; + const completedReportSections = completedSections.join("\n\n---\n\n"); + return { final_report: completedReportSections }; + }; + + // Conditional edge function to create llm_call workers + const assignWorkers = (state: z.infer) => { + // Assign a worker to each section in the plan + return state.sections!.map((s) => new Send("llm_call", { section: s })); + }; + + // Build workflow + const orchestratorWorkerBuilder = new StateGraph(State) + .addNode("orchestrator", orchestrator) + .addNode("llm_call", llmCall) + .addNode("synthesizer", synthesizer) + .addEdge(START, "orchestrator") + .addConditionalEdges("orchestrator", assignWorkers, ["llm_call"]) + .addEdge("llm_call", "synthesizer") + .addEdge("synthesizer", END); + + // Compile the workflow + const orchestratorWorker = orchestratorWorkerBuilder.compile(); + + // Invoke + const state = await orchestratorWorker.invoke({ topic: "Create a report on LLM scaling laws" }); + console.log(state.final_report); + ``` + ::: + + + :::python + ```python + from typing import List + + + # Schema for structured output to use in planning + class Section(BaseModel): + name: str = Field( + description="Name for this section of the report.", + ) + description: str = Field( + description="Brief overview of the main topics and concepts to be covered in this section.", + ) + + + class Sections(BaseModel): + sections: List[Section] = Field( + description="Sections of the report.", + ) + + + # Augment the LLM with schema for structured output + planner = llm.with_structured_output(Sections) + + + @task + def orchestrator(topic: str): + """Orchestrator that generates a plan for the report""" + # Generate queries + report_sections = planner.invoke( + [ + SystemMessage(content="Generate a plan for the report."), + HumanMessage(content=f"Here is the report topic: {topic}"), + ] + ) + + return report_sections.sections + + + @task + def llm_call(section: Section): + """Worker writes a section of the report""" + + # Generate section + result = llm.invoke( + [ + SystemMessage(content="Write a report section."), + HumanMessage( + content=f"Here is the section name: {section.name} and description: {section.description}" + ), + ] + ) + + # Write the updated section to completed sections + return result.content + + + @task + def synthesizer(completed_sections: list[str]): + """Synthesize full report from sections""" + final_report = "\n\n---\n\n".join(completed_sections) + return final_report + + + @entrypoint() + def orchestrator_worker(topic: str): + sections = orchestrator(topic).result() + section_futures = [llm_call(section) for section in sections] + final_report = synthesizer( + [section_fut.result() for section_fut in section_futures] + ).result() + return final_report + + # Invoke + report = orchestrator_worker.invoke("Create a report on LLM scaling laws") + from IPython.display import Markdown + Markdown(report) + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/75a636d0-6179-4a12-9836-e0aa571e87c5/r + ::: + + :::js + ```typescript + // Schema for structured output to use in planning + const Section = z.object({ + name: z.string().describe("Name for this section of the report."), + description: z.string().describe("Brief overview of the main topics and concepts to be covered in this section."), + }); + + const Sections = z.object({ + sections: z.array(Section).describe("Sections of the report."), + }); + + // Augment the LLM with schema for structured output + const planner = llm.withStructuredOutput(Sections); + + const orchestrator = task("orchestrator", async (topic: string) => { + // Orchestrator that generates a plan for the report + const reportSections = await planner.invoke([ + new SystemMessage("Generate a plan for the report."), + new HumanMessage(`Here is the report topic: ${topic}`), + ]); + return reportSections.sections; + }); + + const llmCall = task("llm_call", async (section: z.infer) => { + // Worker writes a section of the report + const result = await llm.invoke([ + new SystemMessage("Write a report section."), + new HumanMessage( + `Here is the section name: ${section.name} and description: ${section.description}` + ), + ]); + return result.content; + }); + + const synthesizer = task("synthesizer", (completedSections: string[]) => { + // Synthesize full report from sections + const finalReport = completedSections.join("\n\n---\n\n"); + return finalReport; + }); + + const orchestratorWorker = entrypoint("orchestratorWorker", async (topic: string) => { + const sections = await orchestrator(topic); + const sectionFutures = sections.map((section) => llmCall(section)); + const finalReport = await synthesizer( + await Promise.all(sectionFutures) + ); + return finalReport; + }); + + // Invoke + const report = await orchestratorWorker.invoke("Create a report on LLM scaling laws"); + console.log(report); + ``` + ::: + + + +## Evaluator-optimizer + +In the evaluator-optimizer workflow, one LLM call generates a response while another provides evaluation and feedback in a loop: + +> When to use this workflow: This workflow is particularly effective when we have clear evaluation criteria, and when iterative refinement provides measurable value. The two signs of good fit are, first, that LLM responses can be demonstrably improved when a human articulates their feedback; and second, that the LLM can provide such feedback. This is analogous to the iterative writing process a human writer might go through when producing a polished document. + +![evaluator_optimizer.png](/oss/images/evaluator_optimizer.png) + + + + :::python + ```python + # Graph state + class State(TypedDict): + joke: str + topic: str + feedback: str + funny_or_not: str + + + # Schema for structured output to use in evaluation + class Feedback(BaseModel): + grade: Literal["funny", "not funny"] = Field( + description="Decide if the joke is funny or not.", + ) + feedback: str = Field( + description="If the joke is not funny, provide feedback on how to improve it.", + ) + + + # Augment the LLM with schema for structured output + evaluator = llm.with_structured_output(Feedback) + + + # Nodes + def llm_call_generator(state: State): + """LLM generates a joke""" + + if state.get("feedback"): + msg = llm.invoke( + f"Write a joke about {state['topic']} but take into account the feedback: {state['feedback']}" + ) + else: + msg = llm.invoke(f"Write a joke about {state['topic']}") + return {"joke": msg.content} + + + def llm_call_evaluator(state: State): + """LLM evaluates the joke""" + + grade = evaluator.invoke(f"Grade the joke {state['joke']}") + return {"funny_or_not": grade.grade, "feedback": grade.feedback} + + + # Conditional edge function to route back to joke generator or end based upon feedback from the evaluator + def route_joke(state: State): + """Route back to joke generator or end based upon feedback from the evaluator""" + + if state["funny_or_not"] == "funny": + return "Accepted" + elif state["funny_or_not"] == "not funny": + return "Rejected + Feedback" + + + # Build workflow + optimizer_builder = StateGraph(State) + + # Add the nodes + optimizer_builder.add_node("llm_call_generator", llm_call_generator) + optimizer_builder.add_node("llm_call_evaluator", llm_call_evaluator) + + # Add edges to connect nodes + optimizer_builder.add_edge(START, "llm_call_generator") + optimizer_builder.add_edge("llm_call_generator", "llm_call_evaluator") + optimizer_builder.add_conditional_edges( + "llm_call_evaluator", + route_joke, + { # Name returned by route_joke : Name of next node to visit + "Accepted": END, + "Rejected + Feedback": "llm_call_generator", + }, + ) + + # Compile the workflow + optimizer_workflow = optimizer_builder.compile() + + # Show the workflow + display(Image(optimizer_workflow.get_graph().draw_mermaid_png())) + + # Invoke + state = optimizer_workflow.invoke({"topic": "Cats"}) + print(state["joke"]) + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/86ab3e60-2000-4bff-b988-9b89a3269789/r + + **Resources:** + + **Examples** + + [Here](https://github.com/langchain-ai/local-deep-researcher) is an assistant that uses evaluator-optimizer to improve a report. See our video [here](https://www.youtube.com/watch?v=XGuTzHoqlj8). + + See our video [here](https://www.youtube.com/watch?v=bq1Plo2RhYI). + ::: + + :::js + ```typescript + // Graph state + const State = z.object({ + joke: z.string().optional(), + topic: z.string(), + feedback: z.string().optional(), + funny_or_not: z.string().optional(), + }); + + // Schema for structured output to use in evaluation + const Feedback = z.object({ + grade: z.enum(["funny", "not funny"]).describe("Decide if the joke is funny or not."), + feedback: z.string().describe("If the joke is not funny, provide feedback on how to improve it."), + }); + + // Augment the LLM with schema for structured output + const evaluator = llm.withStructuredOutput(Feedback); + + // Nodes + const llmCallGenerator = async (state: z.infer) => { + // LLM generates a joke + let msg; + if (state.feedback) { + msg = await llm.invoke( + `Write a joke about ${state.topic} but take into account the feedback: ${state.feedback}` + ); + } else { + msg = await llm.invoke(`Write a joke about ${state.topic}`); + } + return { joke: msg.content }; + }; + + const llmCallEvaluator = async (state: z.infer) => { + // LLM evaluates the joke + const grade = await evaluator.invoke(`Grade the joke ${state.joke}`); + return { funny_or_not: grade.grade, feedback: grade.feedback }; + }; + + // Conditional edge function to route back to joke generator or end + const routeJoke = (state: z.infer) => { + // Route back to joke generator or end based upon feedback from the evaluator + if (state.funny_or_not === "funny") { + return "Accepted"; + } else if (state.funny_or_not === "not funny") { + return "Rejected + Feedback"; + } + }; + + // Build workflow + const optimizerBuilder = new StateGraph(State) + .addNode("llm_call_generator", llmCallGenerator) + .addNode("llm_call_evaluator", llmCallEvaluator) + .addEdge(START, "llm_call_generator") + .addEdge("llm_call_generator", "llm_call_evaluator") + .addConditionalEdges( + "llm_call_evaluator", + routeJoke, + { + "Accepted": END, + "Rejected + Feedback": "llm_call_generator", + } + ); + + // Compile the workflow + const optimizerWorkflow = optimizerBuilder.compile(); + + // Invoke + const state = await optimizerWorkflow.invoke({ topic: "Cats" }); + console.log(state.joke); + ``` + ::: + + + :::python + ```python + # Schema for structured output to use in evaluation + class Feedback(BaseModel): + grade: Literal["funny", "not funny"] = Field( + description="Decide if the joke is funny or not.", + ) + feedback: str = Field( + description="If the joke is not funny, provide feedback on how to improve it.", + ) + + + # Augment the LLM with schema for structured output + evaluator = llm.with_structured_output(Feedback) + + + # Nodes + @task + def llm_call_generator(topic: str, feedback: Feedback): + """LLM generates a joke""" + if feedback: + msg = llm.invoke( + f"Write a joke about {topic} but take into account the feedback: {feedback}" + ) + else: + msg = llm.invoke(f"Write a joke about {topic}") + return msg.content + + + @task + def llm_call_evaluator(joke: str): + """LLM evaluates the joke""" + feedback = evaluator.invoke(f"Grade the joke {joke}") + return feedback + + + @entrypoint() + def optimizer_workflow(topic: str): + feedback = None + while True: + joke = llm_call_generator(topic, feedback).result() + feedback = llm_call_evaluator(joke).result() + if feedback.grade == "funny": + break + + return joke + + # Invoke + for step in optimizer_workflow.stream("Cats", stream_mode="updates"): + print(step) + print("\n") + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/f66830be-4339-4a6b-8a93-389ce5ae27b4/r + ::: + + :::js + ```typescript + // Schema for structured output to use in evaluation + const Feedback = z.object({ + grade: z.enum(["funny", "not funny"]).describe("Decide if the joke is funny or not."), + feedback: z.string().describe("If the joke is not funny, provide feedback on how to improve it."), + }); + + // Augment the LLM with schema for structured output + const evaluator = llm.withStructuredOutput(Feedback); + + // Nodes + const llmCallGenerator = task("llm_call_generator", async (topic: string, feedback?: string) => { + // LLM generates a joke + if (feedback) { + const msg = await llm.invoke( + `Write a joke about ${topic} but take into account the feedback: ${feedback}` + ); + return msg.content; + } else { + const msg = await llm.invoke(`Write a joke about ${topic}`); + return msg.content; + } + }); + + const llmCallEvaluator = task("llm_call_evaluator", async (joke: string) => { + // LLM evaluates the joke + const feedback = await evaluator.invoke(`Grade the joke ${joke}`); + return feedback; + }); + + const optimizerWorkflow = entrypoint("optimizerWorkflow", async (topic: string) => { + let feedback; + while (true) { + const joke = await llmCallGenerator(topic, feedback?.feedback); + feedback = await llmCallEvaluator(joke); + if (feedback.grade === "funny") { + return joke; + } + } + }); + + // Invoke + const stream = await optimizerWorkflow.stream("Cats", { streamMode: "updates" }); + for await (const step of stream) { + console.log(step); + console.log("\n"); + } + ``` + ::: + + + +## Agent + +Agents are typically implemented as an LLM performing actions (via tool-calling) based on environmental feedback in a loop. As noted in the Anthropic blog on `Building Effective Agents`: + +> Agents can handle sophisticated tasks, but their implementation is often straightforward. They are typically just LLMs using tools based on environmental feedback in a loop. It is therefore crucial to design toolsets and their documentation clearly and thoughtfully. + +> When to use agents: Agents can be used for open-ended problems where it's difficult or impossible to predict the required number of steps, and where you can't hardcode a fixed path. The LLM will potentially operate for many turns, and you must have some level of trust in its decision-making. Agents' autonomy makes them ideal for scaling tasks in trusted environments. + +![agent.png](/oss/images/agent.png) + +:::python +```python +from langchain_core.tools import tool + + +# Define tools +@tool +def multiply(a: int, b: int) -> int: + """Multiply a and b. + + Args: + a: first int + b: second int + """ + return a * b + + +@tool +def add(a: int, b: int) -> int: + """Adds a and b. + + Args: + a: first int + b: second int + """ + return a + b + + +@tool +def divide(a: int, b: int) -> float: + """Divide a and b. + + Args: + a: first int + b: second int + """ + return a / b + + +# Augment the LLM with tools +tools = [add, multiply, divide] +tools_by_name = {tool.name: tool for tool in tools} +llm_with_tools = llm.bind_tools(tools) +``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; + +// Define tools +const multiply = tool( + async ({ a, b }: { a: number; b: number }) => { + return a * b; + }, + { + name: "multiply", + description: "Multiply a and b.", + schema: z.object({ + a: z.number().describe("first int"), + b: z.number().describe("second int"), + }), + } +); + +const add = tool( + async ({ a, b }: { a: number; b: number }) => { + return a + b; + }, + { + name: "add", + description: "Adds a and b.", + schema: z.object({ + a: z.number().describe("first int"), + b: z.number().describe("second int"), + }), + } +); + +const divide = tool( + async ({ a, b }: { a: number; b: number }) => { + return a / b; + }, + { + name: "divide", + description: "Divide a and b.", + schema: z.object({ + a: z.number().describe("first int"), + b: z.number().describe("second int"), + }), + } +); + +// Augment the LLM with tools +const tools = [add, multiply, divide]; +const toolsByName = Object.fromEntries(tools.map((tool) => [tool.name, tool])); +const llmWithTools = llm.bindTools(tools); +``` +::: + + + + :::python + ```python + from langgraph.graph import MessagesState + from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage + + + # Nodes + def llm_call(state: MessagesState): + """LLM decides whether to call a tool or not""" + + return { + "messages": [ + llm_with_tools.invoke( + [ + SystemMessage( + content="You are a helpful assistant tasked with performing arithmetic on a set of inputs." + ) + ] + + state["messages"] + ) + ] + } + + + def tool_node(state: dict): + """Performs the tool call""" + + result = [] + for tool_call in state["messages"][-1].tool_calls: + tool = tools_by_name[tool_call["name"]] + observation = tool.invoke(tool_call["args"]) + result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"])) + return {"messages": result} + + + # Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call + def should_continue(state: MessagesState) -> Literal["environment", END]: + """Decide if we should continue the loop or stop based upon whether the LLM made a tool call""" + + messages = state["messages"] + last_message = messages[-1] + # If the LLM makes a tool call, then perform an action + if last_message.tool_calls: + return "Action" + # Otherwise, we stop (reply to the user) + return END + + + # Build workflow + agent_builder = StateGraph(MessagesState) + + # Add nodes + agent_builder.add_node("llm_call", llm_call) + agent_builder.add_node("environment", tool_node) + + # Add edges to connect nodes + agent_builder.add_edge(START, "llm_call") + agent_builder.add_conditional_edges( + "llm_call", + should_continue, + { + # Name returned by should_continue : Name of next node to visit + "Action": "environment", + END: END, + }, + ) + agent_builder.add_edge("environment", "llm_call") + + # Compile the agent + agent = agent_builder.compile() + + # Show the agent + display(Image(agent.get_graph(xray=True).draw_mermaid_png())) + + # Invoke + messages = [HumanMessage(content="Add 3 and 4.")] + messages = agent.invoke({"messages": messages}) + for m in messages["messages"]: + m.pretty_print() + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/051f0391-6761-4f8c-a53b-22231b016690/r + + **Resources:** + + **LangChain Academy** + + See our lesson on agents [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-1/agent.ipynb). + + **Examples** + + [Here](https://github.com/langchain-ai/memory-agent) is a project that uses a tool calling agent to create / store long-term memories. + ::: + + :::js + ```typescript + import { MessagesZodState, ToolNode } from "@langchain/langgraph/prebuilt"; + import { SystemMessage, HumanMessage, ToolMessage, isAIMessage } from "@langchain/core/messages"; + + // Nodes + const llmCall = async (state: z.infer) => { + // LLM decides whether to call a tool or not + const response = await llmWithTools.invoke([ + new SystemMessage( + "You are a helpful assistant tasked with performing arithmetic on a set of inputs." + ), + ...state.messages, + ]); + return { messages: [response] }; + }; + + const toolNode = new ToolNode(tools); + + // Conditional edge function to route to the tool node or end + const shouldContinue = (state: z.infer) => { + // Decide if we should continue the loop or stop + const messages = state.messages; + const lastMessage = messages[messages.length - 1]; + // If the LLM makes a tool call, then perform an action + if (isAIMessage(lastMessage) && lastMessage.tool_calls?.length) { + return "Action"; + } + // Otherwise, we stop (reply to the user) + return END; + }; + + // Build workflow + const agentBuilder = new StateGraph(MessagesZodState) + .addNode("llm_call", llmCall) + .addNode("environment", toolNode) + .addEdge(START, "llm_call") + .addConditionalEdges( + "llm_call", + shouldContinue, + { + "Action": "environment", + [END]: END, + } + ) + .addEdge("environment", "llm_call"); + + // Compile the agent + const agent = agentBuilder.compile(); + + // Invoke + const messages = [new HumanMessage("Add 3 and 4.")]; + const result = await agent.invoke({ messages }); + for (const m of result.messages) { + console.log(`${m.getType()}: ${m.content}`); + } + ``` + ::: + + + :::python + ```python + from langgraph.graph import add_messages + from langchain_core.messages import ( + SystemMessage, + HumanMessage, + BaseMessage, + ToolCall, + ) + + + @task + def call_llm(messages: list[BaseMessage]): + """LLM decides whether to call a tool or not""" + return llm_with_tools.invoke( + [ + SystemMessage( + content="You are a helpful assistant tasked with performing arithmetic on a set of inputs." + ) + ] + + messages + ) + + + @task + def call_tool(tool_call: ToolCall): + """Performs the tool call""" + tool = tools_by_name[tool_call["name"]] + return tool.invoke(tool_call) + + + @entrypoint() + def agent(messages: list[BaseMessage]): + llm_response = call_llm(messages).result() + + while True: + if not llm_response.tool_calls: + break + + # Execute tools + tool_result_futures = [ + call_tool(tool_call) for tool_call in llm_response.tool_calls + ] + tool_results = [fut.result() for fut in tool_result_futures] + messages = add_messages(messages, [llm_response, *tool_results]) + llm_response = call_llm(messages).result() + + messages = add_messages(messages, llm_response) + return messages + + # Invoke + messages = [HumanMessage(content="Add 3 and 4.")] + for chunk in agent.stream(messages, stream_mode="updates"): + print(chunk) + print("\n") + ``` + + **LangSmith Trace** + + https://smith.langchain.com/public/42ae8bf9-3935-4504-a081-8ddbcbfc8b2e/r + ::: + + :::js + ```typescript + import { addMessages } from "@langchain/langgraph"; + import { + SystemMessage, + HumanMessage, + BaseMessage, + ToolCall, + } from "@langchain/core/messages"; + + const callLlm = task("call_llm", async (messages: BaseMessage[]) => { + // LLM decides whether to call a tool or not + return await llmWithTools.invoke([ + new SystemMessage( + "You are a helpful assistant tasked with performing arithmetic on a set of inputs." + ), + ...messages, + ]); + }); + + const callTool = task("call_tool", async (toolCall: ToolCall) => { + // Performs the tool call + const tool = toolsByName[toolCall.name]; + return await tool.invoke(toolCall); + }); + + const agent = entrypoint("agent", async (messages: BaseMessage[]) => { + let currentMessages = messages; + let llmResponse = await callLlm(currentMessages); + + while (true) { + if (!llmResponse.tool_calls?.length) { + break; + } + + // Execute tools + const toolResults = await Promise.all( + llmResponse.tool_calls.map((toolCall) => callTool(toolCall)) + ); + + // Append to message list + currentMessages = addMessages(currentMessages, [ + llmResponse, + ...toolResults, + ]); + + // Call model again + llmResponse = await callLlm(currentMessages); + } + + return llmResponse; + }); + + // Invoke + const messages = [new HumanMessage("Add 3 and 4.")]; + const stream = await agent.stream(messages, { streamMode: "updates" }); + for await (const chunk of stream) { + console.log(chunk); + console.log("\n"); + } + ``` + ::: + + + +#### Pre-built + +:::python +LangGraph also provides a **pre-built method** for creating an agent as defined above (using the @[`create_react_agent`][create_react_agent] function): + +/oss/agentic-architectures + +```python +from langgraph.prebuilt import create_react_agent + +# Pass in: +# (1) the augmented LLM with tools +# (2) the tools list (which is used to create the tool node) +pre_built_agent = create_react_agent(llm, tools=tools) + +# Show the agent +display(Image(pre_built_agent.get_graph().draw_mermaid_png())) + +# Invoke +messages = [HumanMessage(content="Add 3 and 4.")] +messages = pre_built_agent.invoke({"messages": messages}) +for m in messages["messages"]: + m.pretty_print() +``` + +**LangSmith Trace** + +https://smith.langchain.com/public/abab6a44-29f6-4b97-8164-af77413e494d/r +::: + +:::js +LangGraph also provides a **pre-built method** for creating an agent as defined above (using the @[`createReactAgent`][create_react_agent] function): + +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +// Pass in: +// (1) the augmented LLM with tools +// (2) the tools list (which is used to create the tool node) +const preBuiltAgent = createReactAgent({ llm, tools }); + +// Invoke +const messages = [new HumanMessage("Add 3 and 4.")]; +const result = await preBuiltAgent.invoke({ messages }); +for (const m of result.messages) { + console.log(`${m.getType()}: ${m.content}`); +} +``` +::: + +## What LangGraph provides + +By constructing each of the above in LangGraph, we get a few things: + +### Persistence: Human-in-the-Loop + +LangGraph persistence layer supports interruption and approval of actions (e.g., Human In The Loop). See [Module 3 of LangChain Academy](https://github.com/langchain-ai/langchain-academy/tree/main/module-3). + +### Persistence: Memory + +LangGraph persistence layer supports conversational (short-term) memory and long-term memory. See [Modules 2](https://github.com/langchain-ai/langchain-academy/tree/main/module-2) [and 5](https://github.com/langchain-ai/langchain-academy/tree/main/module-5) of LangChain Academy: + +### Streaming + +LangGraph provides several ways to stream workflow / agent outputs or intermediate state. See [Module 3 of LangChain Academy](https://github.com/langchain-ai/langchain-academy/blob/main/module-3/streaming-interruption.ipynb). + +### Deployment + +LangGraph provides an easy on-ramp for deployment, observability, and evaluation. See [module 6](https://github.com/langchain-ai/langchain-academy/tree/main/module-6) of LangChain Academy. diff --git a/src/snippets/chat_model_tabs.mdx b/src/snippets/chat-model-tabs.mdx similarity index 100% rename from src/snippets/chat_model_tabs.mdx rename to src/snippets/chat-model-tabs.mdx diff --git a/src/snippets/snippet-intro.mdx b/src/snippets/snippet-intro.mdx deleted file mode 100644 index c57e7c75..00000000 --- a/src/snippets/snippet-intro.mdx +++ /dev/null @@ -1,4 +0,0 @@ -One of the core principles of software development is DRY (Don't Repeat -Yourself). This is a principle that apply to documentation as -well. If you find yourself repeating the same content in multiple places, you -should consider creating a custom snippet to keep your content in sync.