diff --git a/docs/docs/agents/agents.md b/docs/docs/agents/agents.md index b00184266..29204c15e 100644 --- a/docs/docs/agents/agents.md +++ b/docs/docs/agents/agents.md @@ -15,22 +15,39 @@ This guide shows you how to set up and use LangGraph's **prebuilt**, **reusable* Before you start this tutorial, ensure you have the following: -- An [Anthropic](https://console.anthropic.com/settings/keys) API key +- 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]" ``` -!!! info +!!! info 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 +``` + +!!! info + + 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`][langgraph.prebuilt.chat_agent_executor.create_react_agent]: ```python @@ -56,9 +73,52 @@ agent.invoke( 2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](./models.md) 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](./tools.md) page. +2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](./models.md) 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 @@ -79,19 +139,45 @@ agent = create_react_agent( ) ``` +::: + +:::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](./models.md). ## 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. +- **Static**: A string is interpreted as a **system message**. +- **Dynamic**: A list of messages generated at **runtime**, based on input or configuration. === "Static prompt" Define a fixed prompt string or list of messages: + :::python ```python from langgraph.prebuilt import create_react_agent @@ -107,9 +193,30 @@ Prompts instruct the LLM how to behave. Add one of the following types of prompt {"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" }] + }); + ``` + ::: === "Dynamic prompt" + :::python Define a function that returns a message list based on the agent's state and configuration: ```python @@ -144,12 +251,52 @@ Prompts instruct the LLM how to behave. Add one of the following types of prompt - 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](./context.md) 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](./context.md). ## 5. Add memory -To allow multi-turn conversations with an agent, you need to enable [persistence](../concepts/persistence.md) 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): +To allow multi-turn conversations with an agent, you need to enable [persistence](../concepts/persistence.md) 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 @@ -182,8 +329,50 @@ ny_response = agent.invoke( 1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](../how-tos/memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](../concepts/human_in_the_loop.md) 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](../how-tos/memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](../concepts/human_in_the_loop.md) 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. @@ -191,6 +380,7 @@ For more information, see [Memory](../how-tos/memory/add-memory.md). ## 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 @@ -215,9 +405,43 @@ response = agent.invoke( 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. +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)`. + 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 }`. + + ::: !!! Note "LLM post-processing" diff --git a/docs/docs/agents/context.md b/docs/docs/agents/context.md index 542379a44..1bb7c594f 100644 --- a/docs/docs/agents/context.md +++ b/docs/docs/agents/context.md @@ -2,7 +2,7 @@ **Context engineering** is the practice of building dynamic systems that provide the right information and tools, in the right format, so that a language model can plausibly accomplish a task. -Context includes *any* data outside the message list that can shape behavior. This can be: +Context includes _any_ data outside the message list that can shape behavior. This can be: - Information passed at runtime, like a `user_id` or API credentials. - Internal state updated during a multi-step reasoning process. @@ -11,10 +11,10 @@ Context includes *any* data outside the message list that can shape behavior. Th LangGraph provides **three** primary ways to supply context: | Type | Description | Mutable? | Lifetime | -|------------------------------------------------------------------------------|-----------------------------------------------|----------|-------------------------| -| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run | -| [**Short-term memory (State)**](#short-term-memory-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation | -| [**Long-term memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations | +| ---------------------------------------------------------------------------- | --------------------------------------------- | -------- | ----------------------- | +| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run | +| [**Short-term memory (State)**](#short-term-memory-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation | +| [**Long-term memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations | ## Provide runtime context @@ -26,6 +26,8 @@ when you have values that don't change mid-run. Specify configuration using a key called **"configurable"** which is reserved for this purpose: +:::python + ```python graph.invoke( # (1)! {"messages": [{"role": "user", "content": "hi!"}]}, # (2)! @@ -34,12 +36,28 @@ graph.invoke( # (1)! ) ``` +::: + +:::js + +```typescript +await graph.invoke( + // (1)! + { messages: [{ role: "user", content: "hi!" }] }, // (2)! + // highlight-next-line + { configurable: { user_id: "user_123" } } // (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 configuration data. The `config` parameter allows you to provide additional context that the agent can use during its execution. === "Agent prompt" + :::python ```python from langchain_core.messages import AnyMessage from langchain_core.runnables import RunnableConfig @@ -64,11 +82,41 @@ graph.invoke( # (1)! config={"configurable": {"user_name": "John Smith"}} ) ``` + ::: + + :::js + ```typescript + import type { BaseMessage } from "@langchain/core/messages"; + import type { RunnableConfig } from "@langchain/core/runnables"; + import type { AgentState } from "@langchain/langgraph/prebuilt"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + + // highlight-next-line + const prompt = (state: AgentState, config: RunnableConfig): BaseMessage[] => { + 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: model, + tools: [getWeather], + prompt, + }); + + await agent.invoke( + { messages: [{ role: "user", content: "what is the weather in sf" }] }, + // highlight-next-line + { configurable: { user_name: "John Smith" } } + ); + ``` + ::: * See [Agents](../agents/agents.md) for details. === "Workflow node" + :::python ```python from langchain_core.runnables import RunnableConfig @@ -77,11 +125,25 @@ graph.invoke( # (1)! user_name = config["configurable"].get("user_name") ... ``` + ::: + + :::js + ```typescript + import type { RunnableConfig } from "@langchain/core/runnables"; + + // highlight-next-line + const node = (state: State, config?: RunnableConfig) => { + const userName = config?.configurable?.user_name; + // ... + }; + ``` + ::: * See [the Graph API](https://langchain-ai.github.io/langgraph/how-tos/graph-api/#add-runtime-configuration) for details. === "In a tool" + :::python ```python from langchain_core.runnables import RunnableConfig @@ -92,6 +154,27 @@ graph.invoke( # (1)! user_id = config["configurable"].get("user_id") return "User is John Smith" if user_id == "user_123" else "Unknown user" ``` + ::: + + :::js + ```typescript + import type { RunnableConfig } from "@langchain/core/runnables"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + + // highlight-next-line + const getUserInfo = tool( + async (_, config: RunnableConfig): Promise => { + 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." + } + ); + ``` + ::: See the [tool calling guide](../how-tos/tool-calling.md#configuration) for details. @@ -105,6 +188,7 @@ State acts as [short-term memory](../concepts/memory.md) during a run. It holds State can also be accessed by the agent's **tools**, which can read or update the state as needed. See [tool calling guide](../how-tos/tool-calling.md#short-term-memory) for details. + :::python ```python from langchain_core.messages import AnyMessage from langchain_core.runnables import RunnableConfig @@ -139,10 +223,51 @@ State acts as [short-term memory](../concepts/memory.md) during a run. It holds 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. + ::: === "In a workflow" + :::python ```python from typing_extensions import TypedDict from langchain_core.messages import AnyMessage @@ -167,11 +292,42 @@ State acts as [short-term memory](../concepts/memory.md) during a run. It holds 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. + ::: !!! tip "Turning on memory" @@ -179,6 +335,6 @@ State acts as [short-term memory](../concepts/memory.md) during a run. It holds ### Long-term memory (cross-conversation context) -For context that spans *across* conversations or sessions, LangGraph allows access to **long-term memory** via a `store`. This can be used to read or update persistent facts (e.g., user profiles, preferences, prior interactions). +For context that spans _across_ conversations or sessions, LangGraph allows access to **long-term memory** via a `store`. This can be used to read or update persistent facts (e.g., user profiles, preferences, prior interactions). -For more information, see the [Memory guide](../how-tos/memory/add-memory.md). \ No newline at end of file +For more information, see the [Memory guide](../how-tos/memory/add-memory.md). diff --git a/docs/docs/agents/deployment.md b/docs/docs/agents/deployment.md index 5e9a6bd07..65d36a1c7 100644 --- a/docs/docs/agents/deployment.md +++ b/docs/docs/agents/deployment.md @@ -11,19 +11,21 @@ hide: To deploy your LangGraph agent, create and configure a LangGraph app. This setup supports both local development and production deployments. -Features: +Features: -* 🖥️ Local server for development -* 🧩 Studio Web UI for visual debugging -* ☁️ Cloud and 🔧 self-hosted deployment options -* 📊 LangSmith integration for tracing and observability +- 🖥️ Local server for development +- 🧩 Studio Web UI for visual debugging +- ☁️ Cloud and 🔧 self-hosted deployment options +- 📊 LangSmith integration for tracing and observability -!!! info "Requirements" +!!! info "Requirements" - ✅ You **must** have a [LangSmith account](https://www.langchain.com/langsmith). You can sign up for **free** and get started with the free tier. ## Create a LangGraph app +:::python + ```bash pip install -U "langgraph-cli[inmem]" langgraph new path/to/your/app --template new-langgraph-project-python @@ -45,6 +47,46 @@ graph = create_react_agent( ) ``` +::: + +:::js + +```bash +npm install -g @langchain/langgraph-cli +langgraph new path/to/your/app --template new-langgraph-project-js +``` + +This will create an empty LangGraph project. You can modify it by replacing the code in `src/agent/graph.ts` with your agent code. For example: + +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +const getWeather = tool( + (input) => { + 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"), + }), + } +); + +export const graph = createReactAgent({ + llm: "anthropic:claude-3-5-sonnet-latest", + tools: [getWeather], + stateModifier: "You are a helpful assistant", +}); +``` + +::: + +:::python + ### 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: @@ -53,6 +95,8 @@ In the root of your new LangGraph app, install the dependencies in `edit` mode s pip install -e . ``` +::: + ### Create an `.env` file You will find a `.env.example` in the root of your new LangGraph app. Create @@ -71,13 +115,13 @@ langgraph dev This will start up the LangGraph API server locally. If this runs successfully, you should see something like: -> 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 +> 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 See this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/) to learn more about running LangGraph app locally. @@ -85,7 +129,7 @@ See this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/langgraph LangGraph Studio Web is a specialized UI that you can connect to LangGraph API server to enable visualization, interaction, and debugging of your application locally. Test your graph in the LangGraph Studio Web UI 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 +> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024 ## Deployment diff --git a/docs/docs/agents/evals.md b/docs/docs/agents/evals.md index 74fdb5a63..fb6f14afa 100644 --- a/docs/docs/agents/evals.md +++ b/docs/docs/agents/evals.md @@ -11,6 +11,8 @@ hide: 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 @@ -20,16 +22,51 @@ def evaluator(*, outputs: dict, reference_outputs: dict): 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 @@ -80,8 +117,63 @@ result = evaluator( ) ``` -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) +::: +:::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). @@ -89,6 +181,8 @@ As a next step, learn more about how to [customize trajectory match evaluator](h 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 ( @@ -103,6 +197,24 @@ evaluator = create_trajectory_llm_as_judge( ) ``` +::: + +:::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: @@ -110,6 +222,8 @@ To run an evaluator, you will first need to create a [LangSmith dataset](https:/ - **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 @@ -125,4 +239,27 @@ experiment_results = client.evaluate( data="", evaluators=[evaluator] ) -``` \ No newline at end of file +``` + +::: + +:::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/docs/docs/agents/mcp.md b/docs/docs/agents/mcp.md index 9b654ffa5..6cbdbbfe1 100644 --- a/docs/docs/agents/mcp.md +++ b/docs/docs/agents/mcp.md @@ -13,17 +13,29 @@ hide: ![MCP](./assets/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. - === "In an agent" ```python title="Agent using tools defined on MCP servers" @@ -107,10 +119,111 @@ The `langchain-mcp-adapters` package enables agents to use tools defined across weather_response = await graph.ainvoke({"messages": "what is the weather in nyc?"}) ``` +::: +:::js +The `@langchain/mcp-adapters` package enables agents to use tools defined across one or more MCP servers. + +=== "In an agent" + + ```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?" }], + }); + ``` + +=== "In a workflow" + + ```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: @@ -118,8 +231,24 @@ 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 @@ -139,6 +268,115 @@ 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 @@ -153,8 +391,100 @@ 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) \ No newline at end of file +- [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/docs/docs/agents/models.md b/docs/docs/agents/models.md index 6b8af56a7..099fda4e9 100644 --- a/docs/docs/agents/models.md +++ b/docs/docs/agents/models.md @@ -2,18 +2,70 @@ 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: {!snippets/chat_model_tabs.md!} +::: + +:::js +Use model provider classes to initialize models: + +=== "OpenAI" + + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + + const model = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, + }); + ``` + +=== "Anthropic" + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + + const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-20240620", + temperature: 0, + maxTokens: 2048, + }); + ``` + +=== "Google" + + ```typescript + import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; + + const model = new ChatGoogleGenerativeAI({ + model: "gemini-1.5-pro", + temperature: 0, + }); + ``` + +=== "Groq" + + ```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. @@ -26,19 +78,20 @@ model = ChatAnthropic( ) ``` +::: + !!! important "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](../concepts/tools.md). 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. === "model name" - ```python from langgraph.prebuilt import create_react_agent @@ -70,10 +123,33 @@ When using `create_react_agent` you can specify the model by its name string, wh ) ``` +::: + +:::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, +}); +``` + +::: + ## Advanced model configuration ### Disable streaming +:::python To disable streaming of the individual LLM tokens, set `disable_streaming=True` when initializing the model: === "`init_chat_model`" @@ -101,9 +177,25 @@ To disable streaming of the individual LLM tokens, set `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([...])`: === "`init_chat_model`" @@ -136,6 +228,28 @@ You can add a fallback to a different model or a different LLM provider using `m ``` 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 @@ -152,28 +266,49 @@ rate_limiter = InMemoryRateLimiter( ) model = ChatAnthropic( - model_name="claude-3-opus-20240229", + 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. + ::: 2. **Direct invocation with custom streaming**: Use your model directly by [adding custom streaming logic](../how-tos/streaming.md#use-with-any-llm) with `StreamWriter`. Refer to the [custom streaming documentation](../how-tos/streaming.md#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/docs/docs/agents/multi-agent.md b/docs/docs/agents/multi-agent.md index 54ba13c9c..03e2e456a 100644 --- a/docs/docs/agents/multi-agent.md +++ b/docs/docs/agents/multi-agent.md @@ -22,6 +22,7 @@ Two of the most popular multi-agent architectures are: ![Supervisor](./assets/supervisor.png) +:::python Use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent system: ```bash @@ -82,10 +83,76 @@ for chunk in supervisor.stream( 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](./assets/swarm.png) +:::python Use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent system: ```bash @@ -143,18 +210,82 @@ for chunk in swarm.stream( 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: +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 +1. Create a special tool that can transfer control to a different agent ```python def transfer_to_bob(): @@ -173,7 +304,7 @@ To implement handoffs with `create_react_agent`, you need to: ) ``` -1. Create individual agents that have access to handoff tools: +2. Create individual agents that have access to handoff tools: ```python flight_assistant = create_react_agent( @@ -184,20 +315,72 @@ To implement handoffs with `create_react_agent`, you need to: ) ``` -1. Define a parent graph that contains individual agents as nodes: +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) - ... - ) + ```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 @@ -298,11 +481,156 @@ for chunk in multi_agent_graph.stream( 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. + ::: !!! Note - This handoff implementation assumes that: +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 - 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. \ No newline at end of file +:::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/docs/docs/agents/overview.md b/docs/docs/agents/overview.md index 84d496a82..f12cf40ba 100644 --- a/docs/docs/agents/overview.md +++ b/docs/docs/agents/overview.md @@ -14,7 +14,7 @@ hide: ## 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. +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. @@ -27,12 +27,12 @@ The LLM operates in a loop. In each iteration, it selects a tool to invoke, prov LangGraph includes several capabilities essential for building robust, production-ready agentic systems: -- [**Memory integration**](../how-tos/memory/add-memory.md): 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**](../concepts/human_in_the_loop.md): 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. +- [**Memory integration**](../how-tos/memory/add-memory.md): 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**](../concepts/human_in_the_loop.md): 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**](../how-tos/streaming.md): Real-time streaming of agent state, model tokens, tool outputs, or combined streams. - [**Deployment tooling**](./deployment.md): Includes infrastructure-free deployment tools. [**LangGraph Platform**](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) supports testing, debugging, and deployment. - - **[Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/)**: A visual IDE for inspecting and debugging workflows. - - Supports multiple [**deployment options**](https://langchain-ai.github.io/langgraph/concepts/deployment_options.md) for production. + - **[Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/)**: A visual IDE for inspecting and debugging workflows. + - Supports multiple [**deployment options**](https://langchain-ai.github.io/langgraph/concepts/deployment_options.md) for production. ## High-level building blocks @@ -40,18 +40,20 @@ LangGraph comes with a set of prebuilt components that implement common agent be 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**](./agents.md) | `pip install -U langgraph langchain` | -| `langgraph-supervisor` | Tools for building [**supervisor**](./multi-agent.md#supervisor) agents | `pip install -U langgraph-supervisor` | -| `langgraph-swarm` | Tools for building a [**swarm**](./multi-agent.md#swarm) multi-agent system | `pip install -U langgraph-swarm` | -| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](./mcp.md) for tool and resource integration | `pip install -U langchain-mcp-adapters` | -| `langmem` | Agent memory management: [**short-term and long-term**](../how-tos/memory/add-memory.md) | `pip install -U langmem` | -| `agentevals` | Utilities to [**evaluate agent performance**](./evals.md) | `pip install -U agentevals` | +| Package | Description | Installation | +| ------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------- | +| `langgraph-prebuilt` (part of `langgraph`) | Prebuilt components to [**create agents**](./agents.md) | `pip install -U langgraph langchain` | +| `langgraph-supervisor` | Tools for building [**supervisor**](./multi-agent.md#supervisor) agents | `pip install -U langgraph-supervisor` | +| `langgraph-swarm` | Tools for building a [**swarm**](./multi-agent.md#swarm) multi-agent system | `pip install -U langgraph-swarm` | +| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](./mcp.md) for tool and resource integration | `pip install -U langchain-mcp-adapters` | +| `langmem` | Agent memory management: [**short-term and long-term**](../how-tos/memory/add-memory.md) | `pip install -U langmem` | +| `agentevals` | Utilities to [**evaluate agent performance**](./evals.md) | `pip install -U agentevals` | ## Visualize an agent graph @@ -60,10 +62,10 @@ Use the following tool to visualize the graph generated by 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`](../agents/tools.md): A list of tools (functions, APIs, or other callable objects) that the agent can use to perform tasks. -* [`pre_model_hook`](../how-tos/create-react-agent-manage-message-history.ipynb): 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`](../agents/agents.md#6-configure-structured-output): A data structure used to constrain the type of the final output, e.g., a `pydantic` `BaseModel`. +- [`tools`](../agents/tools.md): A list of tools (functions, APIs, or other callable objects) that the agent can use to perform tasks. +- [`pre_model_hook`](../how-tos/create-react-agent-manage-message-history.ipynb): 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`](../agents/agents.md#6-configure-structured-output): A data structure used to constrain the type of the final output, e.g., a `pydantic` `BaseModel`.
@@ -82,7 +84,6 @@ It allows you to explore the infrastructure of the agent as defined by the prese
- The following code snippet shows how to create the above agent (and underlying graph) with [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent]: @@ -90,7 +91,6 @@ The following code snippet shows how to create the above agent (and underlying g
- + +::: + +:::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**](./agents.md) | `npm install @langchain/langgraph @langchain/core` | +| `langgraph-supervisor` | Tools for building [**supervisor**](./multi-agent.md#supervisor) agents | `npm install @langchain/langgraph-supervisor` | +| `langgraph-swarm` | Tools for building a [**swarm**](./multi-agent.md#swarm) multi-agent system | `npm install @langchain/langgraph-swarm` | +| `langchain-mcp-adapters` | Interfaces to [**MCP servers**](./mcp.md) for tool and resource integration | `npm install @langchain/mcp-adapters` | +| `agentevals` | Utilities to [**evaluate agent performance**](./evals.md) | `npm install agentevals` | + +## Visualize an agent graph + +Use the following tool to visualize the graph generated by [`createReactAgent`](/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html) 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`](./tools.md): 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`](./agents.md#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`](/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html): + +
+
+
+ + + +::: diff --git a/docs/docs/agents/prebuilt.md b/docs/docs/agents/prebuilt.md index 8ebd5c17b..77f59aaf5 100644 --- a/docs/docs/agents/prebuilt.md +++ b/docs/docs/agents/prebuilt.md @@ -5,8 +5,9 @@ If you’re looking for other prebuilt libraries, explore the community-built op 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** | [hinthornw/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) @@ -32,13 +33,41 @@ To share your project, simply open a Pull Request adding an entry for your packa **Guidelines** -- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm - for JavaScript/TypeScript, etc.) 📦 +- 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** | [langchain-ai/langchainjs](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** | [langchain-ai/langgraphjs](https://github.com/langchain-ai/langgraphjs) | Build supervisor multi-agent systems with LangGraph | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraphjs?style=social) +| **@langchain/langgraph-swarm** | [langchain-ai/langgraphjs](https://github.com/langchain-ai/langgraphjs) | Build multi-agent swarms with LangGraph | -12345 | ![GitHub stars](https://img.shields.io/github/stars/langchain-ai/langgraphjs?style=social) +| **@langchain/langgraph-cua** | [langchain-ai/langgraphjs](https://github.com/langchain-ai/langgraphjs) | 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/docs/docs/agents/run_agents.md b/docs/docs/agents/run_agents.md index 4ea2d07e5..0b86cc217 100644 --- a/docs/docs/agents/run_agents.md +++ b/docs/docs/agents/run_agents.md @@ -9,19 +9,28 @@ hide: # Running agents - Agents support both synchronous and asynchronous execution using either `.invoke()` / `await .ainvoke()` for full responses, or `.stream()` / `.astream()` for **incremental** [streaming](../how-tos/streaming.md) 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 === "Sync invocation" - ```python + + ````python from langgraph.prebuilt import create_react_agent agent = create_react_agent(...) @@ -31,14 +40,33 @@ Agents can be executed in two primary modes: ``` === "Async invocation" - ```python - from langgraph.prebuilt import create_react_agent + +````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](../concepts/low_level.md#working-with-messages-in-graph-state). @@ -47,33 +75,73 @@ Agents use a language model that expects a list of `messages` as an input. There Agent input must be a dictionary with a `messages` key. Supported formats are: -| Format | Example | +:::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` | +| 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. +::: !!! tip "Using custom agent state" - 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](./context.md) for full details. +:::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](./context.md) 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](./context.md) for full details. +::: !!! note - 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. +:::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](./agents.md#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](./agents.md#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](./context.md) for more details on working with custom state schemas and accessing context. @@ -87,6 +155,7 @@ Agents support streaming responses for more responsive applications. This includ Streaming is available in both sync and async modes: +:::python === "Sync streaming" ```python @@ -107,14 +176,36 @@ Streaming is available in both sync and async modes: 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); +} +``` + +::: + !!! tip For full details, see the [streaming guide](../how-tos/streaming.md). ## 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 === "Runtime" ```python @@ -163,6 +254,70 @@ To control agent execution and avoid infinite loops, set a recursion limit. This print("Agent stopped due to max iterations.") ``` +::: + +:::js +=== "Runtime" + + ```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."); + } + } + ``` + +=== "`.withConfig()`" + + ```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) +- [Async programming in LangChain](https://python.langchain.com/docs/concepts/async) + ::: diff --git a/docs/docs/agents/tools.md b/docs/docs/agents/tools.md index e9f45effb..83e08a64f 100644 --- a/docs/docs/agents/tools.md +++ b/docs/docs/agents/tools.md @@ -9,6 +9,7 @@ hide: # Tools +:::python [Tools](https://python.langchain.com/docs/concepts/tools/) are a way to encapsulate a function and its input schema in a way that can be passed to a chat model that supports tool calling. This allows the model to request the execution of this function with specific inputs. You can either [define your own tools](#define-simple-tools) or use [prebuilt integrations](#prebuilt-tools) that LangChain provides. @@ -31,9 +32,37 @@ create_react_agent( ``` `create_react_agent` automatically converts vanilla functions to [LangChain tools](https://python.langchain.com/docs/concepts/tools/#tool-interface). +::: + +:::js +[Tools](https://js.langchain.com/docs/concepts/tools/) are a way to encapsulate a function and its input schema in a way that can be passed to a chat model that supports tool calling. This allows the model to request the execution of this function with specific inputs. + +You can either [define your own tools](#define-simple-tools) or use [prebuilt integrations](#prebuilt-tools) that LangChain provides. + +## Define simple tools + +You can pass a vanilla function to `createReactAgent` to use as a tool: + +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +function multiply(a: number, b: number): number { + return a * b; +} + +createReactAgent({ + llm: new ChatAnthropic({ model: "anthropic:claude-3-7-sonnet" }), + tools: [multiply], +}); +``` + +`createReactAgent` automatically converts vanilla functions to [LangChain tools](https://js.langchain.com/docs/concepts/tools/#tool-interface). +::: ## Customize tools +:::python For more control over tool behavior, use the `@tool` decorator: ```python @@ -69,6 +98,34 @@ def multiply(a: int, b: int) -> int: ``` For additional customization, refer to the [custom tools guide](https://python.langchain.com/docs/how_to/custom_tools/). +::: + +:::js +For more control over tool behavior, use the `tool` function: + +```typescript +// highlight-next-line +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", + description: "Multiply two numbers", + schema: z.object({ + a: z.number().describe("First operand"), + b: z.number().describe("Second operand"), + }), + } +); +``` + +For additional customization, refer to the [custom tools guide](https://js.langchain.com/docs/how_to/custom_tools/). +::: ## Hide arguments from the model @@ -77,6 +134,8 @@ Some tools require runtime-only arguments (e.g., user ID or session context) tha You can put these arguments in the `state` or `config` of the agent, and access this information inside the tool: +:::python + ```python from langgraph.prebuilt import InjectedState from langgraph.prebuilt.chat_agent_executor import AgentState @@ -98,11 +157,49 @@ def my_tool( ... ``` +::: + +:::js + +```typescript +import { tool } from "@langchain/core/tools"; +import { LangGraphRunnableConfig } from "@langchain/langgraph"; +import { z } from "zod"; + +const myTool = tool( + async (input, config: LangGraphRunnableConfig) => { + // This will be populated by an LLM + const toolArg = input.toolArg; + + // access information that's dynamically updated inside the agent + // highlight-next-line + const state = config.store; + + // access static data that is passed at agent invocation + // highlight-next-line + const userId = config.configurable?.userId; + + // Use state and config in your tool logic + return "Tool result"; + }, + { + name: "my_tool", + description: "My tool", + schema: z.object({ + toolArg: z.string().describe("Tool argument"), + }), + } +); +``` + +::: + ## Disable parallel tool calling Some model providers support executing multiple tools in parallel, but allow users to disable this feature. +:::python For supported providers, you can disable parallel tool calling by setting `parallel_tool_calls=False` via the `model.bind_tools()` method: ```python @@ -130,8 +227,58 @@ agent.invoke( ) ``` +::: + +:::js +For supported providers, you can disable parallel tool calling by setting `parallel_tool_calls: false` via the `bindTools()` method: + +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; +import { tool } from "@langchain/core/tools"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { z } from "zod"; + +const add = tool((input) => input.a + input.b, { + name: "add", + description: "Add two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), +}); + +const multiply = tool((input) => input.a * input.b, { + name: "multiply", + description: "Multiply two numbers", + schema: z.object({ + a: z.number(), + b: z.number(), + }), +}); + +const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", + 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, +}); + +await agent.invoke({ + messages: [{ role: "user", content: "what's 3 + 5 and 4 * 7?" }], +}); +``` + +::: + ## Return tool results directly +:::python Use `return_direct=True` to return tool results immediately and stop the agent loop: ```python @@ -153,8 +300,42 @@ agent.invoke( ) ``` +::: + +:::js +Use `returnDirect: true` to return tool results immediately and stop the agent loop: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const add = tool((input) => 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: model, + tools: [add], +}); + +await agent.invoke({ + messages: [{ role: "user", content: "what's 3 + 5?" }], +}); +``` + +::: + ## Force tool use +:::python To force the agent to use specific tools, you can set the `tool_choice` option in `model.bind_tools()`: ```python @@ -179,6 +360,41 @@ agent.invoke( ) ``` +::: + +:::js +To force the agent to use specific tools, you can set the `tool_choice` option in `bindTools()`: + +```typescript +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +// highlight-next-line +const greet = tool((input) => `Hello ${input.userName}!`, { + name: "greet", + description: "Greet user", + schema: z.object({ + userName: z.string(), + }), + // highlight-next-line + returnDirect: true, +}); + +const tools = [greet]; + +const agent = createReactAgent({ + // highlight-next-line + llm: model.bindTools(tools, { tool_choice: { type: "tool", name: "greet" } }), + tools, +}); + +await agent.invoke({ + messages: [{ role: "user", content: "Hi, I am Bob" }], +}); +``` + +::: + !!! Warning "Avoid infinite loops" Forcing tool usage without stopping conditions can create infinite loops. Use one of the following safeguards: @@ -188,10 +404,17 @@ agent.invoke( ## Handle tool errors +:::python By default, the agent will catch all exceptions raised during tool calls and will pass those as tool messages to the LLM. To control how the errors are handled, you can use the prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] — the node that executes tools inside `create_react_agent` — via its `handle_tool_errors` parameter: +::: + +:::js +By default, the agent will catch all exceptions raised during tool calls and will pass those as tool messages to the LLM. To control how the errors are handled, you can use the prebuilt [`ToolNode`][] — the node that executes tools inside `createReactAgent` — via its `handleToolErrors` parameter: +::: === "Enable error handling (default)" + :::python ```python from langgraph.prebuilt import create_react_agent @@ -210,9 +433,47 @@ By default, the agent will catch all exceptions raised during tool calls and wil {"messages": [{"role": "user", "content": "what's 42 x 7?"}]} ) ``` + ::: + + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { createReactAgent } 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(), + }), + } + ); + + // Run with error handling (default) + const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-7-sonnet-latest" }), + tools: [multiply] + }); + + await agent.invoke({ + messages: [{ role: "user", content: "what's 42 x 7?" }] + }); + ``` + ::: === "Disable error handling" + :::python ```python from langgraph.prebuilt import create_react_agent, ToolNode @@ -238,9 +499,58 @@ By default, the agent will catch all exceptions raised during tool calls and wil ``` 1. This disables error handling (enabled by default). See all available strategies in the [API reference][langgraph.prebuilt.tool_node.ToolNode]. + ::: + + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + 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(), + }), + } + ); + + // highlight-next-line + const toolNode = new ToolNode( + [multiply], + { + // highlight-next-line + handleToolErrors: false // (1)! + } + ); + + const agentNoErrorHandling = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-7-sonnet-latest" }), + tools: toolNode + }); + + await agentNoErrorHandling.invoke({ + messages: [{ role: "user", content: "what's 42 x 7?" }] + }); + ``` + + 1. This disables error handling (enabled by default). See all available strategies in the [API reference][toolnode]. + ::: === "Custom error handling" + :::python ```python from langgraph.prebuilt import create_react_agent, ToolNode @@ -268,25 +578,80 @@ By default, the agent will catch all exceptions raised during tool calls and wil ``` 1. This provides a custom message to send to the LLM in case of an exception. See all available strategies in the [API reference][langgraph.prebuilt.tool_node.ToolNode]. + ::: + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + 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(), + }), + } + ); + + // highlight-next-line + const toolNode = new ToolNode( + [multiply], + { + // highlight-next-line + handleToolErrors: "Can't use 42 as a first operand, you must switch operands!" // (1)! + } + ); + + const agentCustomErrorHandling = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-7-sonnet-latest" }), + tools: toolNode + }); + + await agentCustomErrorHandling.invoke({ + messages: [{ role: "user", content: "what's 42 x 7?" }] + }); + ``` + + 1. This provides a custom message to send to the LLM in case of an exception. See all available strategies in the [API reference][toolnode]. + ::: + +:::python See [API reference][langgraph.prebuilt.tool_node.ToolNode] for more information on different tool error handling options. +::: + +:::js +See [API reference][toolnode] for more information on different tool error handling options. +::: ## Working with memory LangGraph allows access to short-term and long-term memory from tools. See [Memory](../how-tos/memory/add-memory.md) guide for more information on: -* how to [read](../how-tos/memory/add-memory.md#read-short-term) from and [write](../how-tos/memory/add-memory.md#write-short-term) to **short-term** memory -* how to [read](../how-tos/memory/add-memory.md#read-long-term) from and [write](../how-tos/memory/add-memory.md#write-long-term) to **long-term** memory +- how to [read](../how-tos/memory/add-memory.md#read-short-term) from and [write](../how-tos/memory/add-memory.md#write-short-term) to **short-term** memory +- how to [read](../how-tos/memory/add-memory.md#read-long-term) from and [write](../how-tos/memory/add-memory.md#write-long-term) to **long-term** memory ## Prebuilt 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", + model="openai:gpt-4o-mini", tools=[{"type": "web_search_preview"}] ) response = agent.invoke( @@ -297,6 +662,31 @@ response = agent.invoke( 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. You can browse the full list of available integrations in the [LangChain integrations directory](https://python.langchain.com/docs/integrations/tools/). +::: + +:::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 { ChatAnthropic } from "@langchain/anthropic"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; + +const agent = createReactAgent({ + llm: new ChatAnthropic({ model: "claude-3-7-sonnet-latest" }), + tools: [{ type: "web_search_preview" }], +}); + +const response = await agent.invoke({ + messages: [ + { role: "user", content: "What was a positive news story from today?" }, + ], +}); +``` + +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. + +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: @@ -307,4 +697,3 @@ Some commonly used tool categories include: - **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. - diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index 45ab2c210..23b6285de 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -95,7 +95,7 @@ Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_s ::: :::js -Your [`@auth.authenticate`]() handler in LangGraph handles steps 4-6, while your [`@auth.on`]() handlers implement step 7. +Your [`auth.authenticate`]() handler in LangGraph handles steps 4-6, while your [`auth.on`]() handlers implement step 7. ::: ## Authentication @@ -149,15 +149,15 @@ Authentication in LangGraph runs as middleware on every request. Your [`authenti 3. Raise an [HTTPException]() if invalid ```typescript -import { Auth } from "@langchain/langgraph-sdk"; +import { Auth, HTTPException } from "@langchain/langgraph-sdk"; export const auth = new Auth(); -auth.authenticate(async (headers: Record) => { +auth.authenticate(async (request) => { // Validate credentials (e.g., API key, JWT token) - const apiKey = headers["x-api-key"]; + const apiKey = request.headers.get("x-api-key"); if (!apiKey || !isValidKey(apiKey)) { - throw new Auth.exceptions.HTTPException(401, "Invalid API key"); + throw new HTTPException(401, "Invalid API key"); } // Return user info - only identity and isAuthenticated are required diff --git a/docs/docs/concepts/langgraph_cli.md b/docs/docs/concepts/langgraph_cli.md index 888568d42..40591b5a8 100644 --- a/docs/docs/concepts/langgraph_cli.md +++ b/docs/docs/concepts/langgraph_cli.md @@ -7,29 +7,64 @@ search: **LangGraph CLI** is a multi-platform command-line tool for building and running the [LangGraph API server](./langgraph_server.md) locally. The resulting server includes all API endpoints for your graph's runs, threads, assistants, etc. as well as the other services required to run your agent, including a managed database for checkpointing and storage. +::: python + ## Installation The LangGraph CLI can be installed via pip or [Homebrew](https://brew.sh/): -=== "pip" - ```bash +=== "pip" +`bash pip install langgraph-cli - ``` + ` === "Homebrew" - ```bash +`bash brew install langgraph-cli - ``` + ` +::: + +::: js + +## Installation + +The LangGraph.js CLI can be installed from the NPM registry: + +=== "npx" +`bash + npx @langchain/langgraph-cli + ` + +=== "npm" +`bash + npm install @langchain/langgraph-cli + ` + +=== "yarn" +`bash + yarn add @langchain/langgraph-cli + ` + +=== "pnpm" +`bash + pnpm add @langchain/langgraph-cli + ` + +=== "bun" +`bash + bun add @langchain/langgraph-cli + ` +::: ## Commands LangGraph CLI provides the following core functionality: -| Command | Description | -| -------- | -------| -| [`langgraph build`](../cloud/reference/cli.md#build) | Builds a Docker image for the [LangGraph API server](./langgraph_server.md) that can be directly deployed. | -| [`langgraph dev`](../cloud/reference/cli.md#dev) | Starts a lightweight development server that requires no Docker installation. This server is ideal for rapid development and testing. This is available in version 0.1.55 and up. +| Command | Description | +| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`langgraph build`](../cloud/reference/cli.md#build) | Builds a Docker image for the [LangGraph API server](./langgraph_server.md) that can be directly deployed. | +| [`langgraph dev`](../cloud/reference/cli.md#dev) | Starts a lightweight development server that requires no Docker installation. This server is ideal for rapid development and testing. | | [`langgraph dockerfile`](../cloud/reference/cli.md#dockerfile) | Generates a [Dockerfile](https://docs.docker.com/reference/dockerfile/) that can be used to build images for and deploy instances of the [LangGraph API server](./langgraph_server.md). This is useful if you want to further customize the dockerfile or deploy in a more custom way. | -| [`langgraph up`](../cloud/reference/cli.md#up) | Starts an instance of the [LangGraph API server](./langgraph_server.md) locally in a docker container. This requires the docker server to be running locally. It also requires a LangSmith API key for local development or a license key for production use. | +| [`langgraph up`](../cloud/reference/cli.md#up) | Starts an instance of the [LangGraph API server](./langgraph_server.md) locally in a docker container. This requires the docker server to be running locally. It also requires a LangSmith API key for local development or a license key for production use. | For more information, see the [LangGraph CLI Reference](../cloud/reference/cli.md). diff --git a/docs/docs/concepts/langgraph_cloud.md b/docs/docs/concepts/langgraph_cloud.md index b9c835913..b8b2a3de5 100644 --- a/docs/docs/concepts/langgraph_cloud.md +++ b/docs/docs/concepts/langgraph_cloud.md @@ -11,11 +11,11 @@ To deploy a [LangGraph Server](../concepts/langgraph_server.md), follow the how- The Cloud SaaS deployment option is a fully managed model for deployment where we manage the [control plane](./langgraph_control_plane.md) and [data plane](./langgraph_data_plane.md) in our cloud. -| | [Control plane](../concepts/langgraph_control_plane.md) | [Data plane](../concepts/langgraph_data_plane.md) | -|-------------------|-------------------|------------| -| **What is it?** |
  • Control plane UI for creating deployments and revisions
  • Control plane APIs for creating deployments and revisions
|
  • Data plane "listener" for reconciling deployments with control plane state
  • LangGraph Servers
  • Postgres, Redis, etc
| -| **Where is it hosted?** | LangChain's cloud | LangChain's cloud | -| **Who provisions and manages it?** | LangChain | LangChain | +| | [Control plane](../concepts/langgraph_control_plane.md) | [Data plane](../concepts/langgraph_data_plane.md) | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **What is it?** |
  • Control plane UI for creating deployments and revisions
  • Control plane APIs for creating deployments and revisions
|
  • Data plane "listener" for reconciling deployments with control plane state
  • LangGraph Servers
  • Postgres, Redis, etc
| +| **Where is it hosted?** | LangChain's cloud | LangChain's cloud | +| **Who provisions and manages it?** | LangChain | LangChain | ## Architecture diff --git a/docs/docs/concepts/langgraph_components.md b/docs/docs/concepts/langgraph_components.md index 78b9e7c8d..9ae6ff57c 100644 --- a/docs/docs/concepts/langgraph_components.md +++ b/docs/docs/concepts/langgraph_components.md @@ -10,4 +10,4 @@ The LangGraph Platform consists of components that work together to support the - [LangGraph control plane](./langgraph_control_plane.md): The LangGraph Control Plane refers to the Control Plane UI where users create and update LangGraph Servers and the Control Plane APIs that support the UI experience. - [LangGraph data plane](./langgraph_data_plane.md): The LangGraph Data Plane refers to LangGraph Servers, the corresponding infrastructure for each server, and the "listener" application that continuously polls for updates from the LangGraph Control Plane. -![LangGraph components](img/lg_platform.png) \ No newline at end of file +![LangGraph components](img/lg_platform.png) diff --git a/docs/docs/concepts/langgraph_control_plane.md b/docs/docs/concepts/langgraph_control_plane.md index 2e3ecc84d..bec1fc13d 100644 --- a/docs/docs/concepts/langgraph_control_plane.md +++ b/docs/docs/concepts/langgraph_control_plane.md @@ -49,7 +49,7 @@ This section describes various features of the control plane. For simplicity, the control plane offers two deployment types with different resource allocations: `Development` and `Production`. | **Deployment Type** | **CPU/Memory** | **Scaling** | **Database** | -|---------------------|-----------------|---------------------|----------------------------------------------------------------------------------| +| ------------------- | --------------- | ------------------- | -------------------------------------------------------------------------------- | | Development | 1 CPU, 1 GB RAM | Up to 1 container | 10 GB disk, no backups | | Production | 2 CPU, 2 GB RAM | Up to 10 containers | Autoscaling disk, automatic backups, highly available (multi-zone configuration) | @@ -60,7 +60,7 @@ CPU and memory resources are per container. Once a deployment is created, the deployment type cannot be changed. !!! info "Resource Customization" - For `Production` type deployments, resources can be manually increased on a case-by-case basis depending on use case and capacity constraints. Contact support@langchain.dev to request an increase in resources. +For `Production` type deployments, resources can be manually increased on a case-by-case basis depending on use case and capacity constraints. Contact support@langchain.dev to request an increase in resources. For `Development` types deployments, database disk size can be manually increased on a case-by-case basis depending on use case and capacity constraints. For most use cases, [TTLs](../how-tos/ttl/configure_ttl.md) should be configured to manage disk usage. Contact support@langchain.dev to request an increase in resources. @@ -77,7 +77,7 @@ There is no direct access to the database. All access to the database occurs thr The database is never deleted until the deployment itself is deleted. !!! info - A custom Postgres instance can be configured for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments. +A custom Postgres instance can be configured for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments. ### Asynchronous Deployment diff --git a/docs/docs/concepts/langgraph_data_plane.md b/docs/docs/concepts/langgraph_data_plane.md index 0b52b8b6f..8441a4009 100644 --- a/docs/docs/concepts/langgraph_data_plane.md +++ b/docs/docs/concepts/langgraph_data_plane.md @@ -69,25 +69,25 @@ Scale down actions are delayed for 30 minutes before any action is taken. In oth ### Static IP Addresses !!! info "Only for Cloud SaaS" - Static IP addresses are only available for [Cloud SaaS](../concepts/langgraph_cloud.md) deployments. +Static IP addresses are only available for [Cloud SaaS](../concepts/langgraph_cloud.md) deployments. All traffic from deployments created after January 6th 2025 will come through a NAT gateway. This NAT gateway will have several static IP addresses depending on the data region. Refer to the table below for the list of static IP addresses: | US | EU | -|----------------|----------------| +| -------------- | -------------- | | 35.197.29.146 | 34.13.192.67 | | 34.145.102.123 | 34.147.105.64 | | 34.169.45.153 | 34.90.22.166 | | 34.82.222.17 | 34.147.36.213 | -| 35.227.171.135 | 34.32.137.113 | +| 35.227.171.135 | 34.32.137.113 | | 34.169.88.30 | 34.91.238.184 | | 34.19.93.202 | 35.204.101.241 | | 34.19.34.50 | 35.204.48.32 | ### Custom Postgres -!!! info - Custom Postgres instances are only available for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments. +!!! info +Custom Postgres instances are only available for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments. A custom Postgres instance can be used instead of the [one automatically created by the control plane](./langgraph_control_plane.md#database-provisioning). Specify the [`POSTGRES_URI_CUSTOM`](../cloud/reference/env_var.md#postgres_uri_custom) environment variable to use a custom Postgres instance. @@ -96,33 +96,32 @@ Multiple deployments can share the same Postgres instance. For example, for `Dep ### Custom Redis !!! info - Custom Redis instances are only available for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_control_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments. +Custom Redis instances are only available for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_control_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments. A custom Redis instance can be used instead of the one automatically created by the control plane. Specify the [REDIS_URI_CUSTOM](../cloud/reference/env_var.md#redis_uri_custom) environment variable to use a custom Redis instance. - Multiple deployments can share the same Redis instance. For example, for `Deployment A`, `REDIS_URI_CUSTOM` can be set to `redis://:/1` and for `Deployment B`, `REDIS_URI_CUSTOM` can be set to `redis://:/2`. `1` and `2` are different database numbers within the same instance, but `` is shared. **The same database number cannot be used for separate deployments**. ### LangSmith Tracing LangGraph Server is automatically configured to send traces to LangSmith. See the table below for details with respect to each deployment option. -| Cloud SaaS | Self-Hosted Data Plane | Self-Hosted Control Plane | Standalone Container | -|------------|------------------------|---------------------------|----------------------| +| Cloud SaaS | Self-Hosted Data Plane | Self-Hosted Control Plane | Standalone Container | +| ---------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | Required

Trace to LangSmith SaaS. | Optional

Disable tracing or trace to LangSmith SaaS. | Optional

Disable tracing or trace to Self-Hosted LangSmith. | Optional

Disable tracing, trace to LangSmith SaaS, or trace to Self-Hosted LangSmith. | ### Telemetry LangGraph Server is automatically configured to report telemetry metadata for billing purposes. See the table below for details with respect to each deployment option. -| Cloud SaaS | Self-Hosted Data Plane | Self-Hosted Control Plane | Standalone Container | -|------------|------------------------|---------------------------|----------------------| +| Cloud SaaS | Self-Hosted Data Plane | Self-Hosted Control Plane | Standalone Container | +| --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Telemetry sent to LangSmith SaaS. | Telemetry sent to LangSmith SaaS. | Self-reported usage (audit) for air-gapped license key.

Telemetry sent to LangSmith SaaS for LangGraph Platform License Key. | Self-reported usage (audit) for air-gapped license key.

Telemetry sent to LangSmith SaaS for LangGraph Platform License Key. | ### Licensing LangGraph Server is automatically configured to perform license key validation. See the table below for details with respect to each deployment option. -| Cloud SaaS | Self-Hosted Data Plane | Self-Hosted Control Plane | Standalone Container | -|------------|------------------------|---------------------------|----------------------| +| Cloud SaaS | Self-Hosted Data Plane | Self-Hosted Control Plane | Standalone Container | +| --------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | LangSmith API Key validated against LangSmith SaaS. | LangSmith API Key validated against LangSmith SaaS. | Air-gapped license key or LangGraph Platform License Key validated against LangSmith SaaS. | Air-gapped license key or LangGraph Platform License Key validated against LangSmith SaaS. | diff --git a/docs/docs/concepts/langgraph_self_hosted_control_plane.md b/docs/docs/concepts/langgraph_self_hosted_control_plane.md index 7434072ce..d0dab3b1c 100644 --- a/docs/docs/concepts/langgraph_self_hosted_control_plane.md +++ b/docs/docs/concepts/langgraph_self_hosted_control_plane.md @@ -3,11 +3,11 @@ There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane). !!! info "Important" - The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan. +The Self-Hosted Control Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan. ## Requirements -- You use `langgraph-cli` and/or [LangGraph Studio](./langgraph_studio.md) app to test graph locally. +- You use the [LangGraph CLI](./langgraph_cli.md) and/or [LangGraph Studio](./langgraph_studio.md) app to test graph locally. - You use `langgraph build` command to build image. - You have a Self-Hosted LangSmith instance deployed. - You are using Ingress for your LangSmith instance. All agents will be deployed as Kubernetes services behind this ingress. @@ -16,11 +16,11 @@ There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](. The [Self-Hosted Control Plane](./langgraph_self_hosted_control_plane.md) deployment option is a fully self-hosted model for deployment where you manage the [control plane](./langgraph_control_plane.md) and [data plane](./langgraph_data_plane.md) in your cloud. This option gives you full control and responsibility of the control plane and data plane infrastructure. -| | [Control plane](../concepts/langgraph_control_plane.md) | [Data plane](../concepts/langgraph_data_plane.md) | -|-------------------|-------------------|------------| -| **What is it?** |
  • Control plane UI for creating deployments and revisions
  • Control plane APIs for creating deployments and revisions
|
  • Data plane "listener" for reconciling deployments with control plane state
  • LangGraph Servers
  • Postgres, Redis, etc
| -| **Where is it hosted?** | Your cloud | Your cloud | -| **Who provisions and manages it?** | You | You | +| | [Control plane](../concepts/langgraph_control_plane.md) | [Data plane](../concepts/langgraph_data_plane.md) | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **What is it?** |
  • Control plane UI for creating deployments and revisions
  • Control plane APIs for creating deployments and revisions
|
  • Data plane "listener" for reconciling deployments with control plane state
  • LangGraph Servers
  • Postgres, Redis, etc
| +| **Where is it hosted?** | Your cloud | Your cloud | +| **Who provisions and manages it?** | You | You | ### Architecture @@ -28,7 +28,7 @@ The [Self-Hosted Control Plane](./langgraph_self_hosted_control_plane.md) deploy ### Compute Platforms - - **Kubernetes**: The Self-Hosted Control Plane deployment option supports deploying control plane and data plane infrastructure to any Kubernetes cluster. +- **Kubernetes**: The Self-Hosted Control Plane deployment option supports deploying control plane and data plane infrastructure to any Kubernetes cluster. !!! tip - If you would like to enable this on your LangSmith instance, please follow the [Self-Hosted Control Plane deployment guide](../cloud/deployment/self_hosted_control_plane.md). \ No newline at end of file +If you would like to enable this on your LangSmith instance, please follow the [Self-Hosted Control Plane deployment guide](../cloud/deployment/self_hosted_control_plane.md). diff --git a/docs/docs/concepts/langgraph_self_hosted_data_plane.md b/docs/docs/concepts/langgraph_self_hosted_data_plane.md index 710e58018..48f06e9f9 100644 --- a/docs/docs/concepts/langgraph_self_hosted_data_plane.md +++ b/docs/docs/concepts/langgraph_self_hosted_data_plane.md @@ -8,7 +8,7 @@ search: There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](./deployment_options.md#self-hosted-data-plane) and [Self-Hosted Control Plane](./deployment_options.md#self-hosted-control-plane). !!! info "Important" - The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan. +The Self-Hosted Data Plane deployment option is currently in beta stage and requires an [Enterprise](plans.md) plan. ## Requirements @@ -19,11 +19,11 @@ There are two versions of the self-hosted deployment: [Self-Hosted Data Plane](. The [Self-Hosted Data Plane](../cloud/deployment/self_hosted_data_plane.md) deployment option is a "hybrid" model for deployment where we manage the [control plane](./langgraph_control_plane.md) in our cloud and you manage the [data plane](./langgraph_data_plane.md) in your cloud. This option provides a way to securely manage your data plane infrastructure, while offloading control plane management to us. When using the Self-Hosted Data Plane version, you authenticate with a [LangSmith](https://smith.langchain.com/) API key. -| | [Control plane](../concepts/langgraph_control_plane.md) | [Data plane](../concepts/langgraph_data_plane.md) | -|-------------------|-------------------|------------| -| **What is it?** |
  • Control plane UI for creating deployments and revisions
  • Control plane APIs for creating deployments and revisions
|
  • Data plane "listener" for reconciling deployments with control plane state
  • LangGraph Servers
  • Postgres, Redis, etc
| -| **Where is it hosted?** | LangChain's cloud | Your cloud | -| **Who provisions and manages it?** | LangChain | You | +| | [Control plane](../concepts/langgraph_control_plane.md) | [Data plane](../concepts/langgraph_data_plane.md) | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **What is it?** |
  • Control plane UI for creating deployments and revisions
  • Control plane APIs for creating deployments and revisions
|
  • Data plane "listener" for reconciling deployments with control plane state
  • LangGraph Servers
  • Postgres, Redis, etc
| +| **Where is it hosted?** | LangChain's cloud | Your cloud | +| **Who provisions and manages it?** | LangChain | You | For information on how to deploy a [LangGraph Server](../concepts/langgraph_server.md) to Self-Hosted Data Plane, see [Deploy to Self-Hosted Data Plane](../cloud/deployment/self_hosted_data_plane.md) @@ -37,4 +37,4 @@ For information on how to deploy a [LangGraph Server](../concepts/langgraph_serv - **Amazon ECS**: Coming soon! !!! tip - If you would like to deploy to Kubernetes, you can follow the [Self-Hosted Data Plane deployment guide](../cloud/deployment/self_hosted_data_plane.md). \ No newline at end of file +If you would like to deploy to Kubernetes, you can follow the [Self-Hosted Data Plane deployment guide](../cloud/deployment/self_hosted_data_plane.md). diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index f277b742e..b06655234 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -9,13 +9,13 @@ search: 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 Python type, but is typically a `TypedDict` or Pydantic `BaseModel`. +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): Python 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`. +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): Python functions that determine which `Node` to execute next based on the current `State`. They can be conditional branches or fixed transitions. +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 Python functions - they can contain an LLM or just good ol' Python code. +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_. @@ -33,21 +33,51 @@ To build your graph, you first define the [state](#state), you then add [nodes]( 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](./persistence.md) 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 `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/graph-api.ipynb#use-pydantic-models-for-graph-state) as your graph state to add **default values** and additional data validation. 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](../how-tos/graph-api.ipynb#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 @@ -56,12 +86,16 @@ Typically, all graph nodes communicate with a single schema. This means that the - 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`. See [this guide](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) for more detail. +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`. + +See [this guide](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) for more detail. 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](../how-tos/graph-api.ipynb#define-input-and-output-schemas) for more detail. Let's look at an example: +:::python + ```python class InputState(TypedDict): user_input: str @@ -100,14 +134,80 @@ builder.add_edge("node_3", END) graph = builder.compile() graph.invoke({"user_input":"My"}) -{'graph_output': 'My name is Lance'} +# {'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 @@ -119,6 +219,8 @@ These two examples show how to use the default reducer: **Example A:** +:::python + ```python from typing_extensions import TypedDict @@ -127,10 +229,33 @@ class State(TypedDict): bar: list[str] ``` -In this example, no reducer functions are specified for any key. 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": ["bye"]}` +::: + +:::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 @@ -142,21 +267,56 @@ class State(TypedDict): ``` 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 @@ -179,6 +339,45 @@ 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: @@ -190,10 +389,19 @@ class State(MessagesState): documents: list[str] ``` +::: + ## Nodes +:::python In LangGraph, nodes are typically python functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`). +::: +:::js +In LangGraph, nodes are typically functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`). +::: + +:::python Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method: ```python @@ -224,47 +432,117 @@ builder.add_node("other_node", my_other_node) ... ``` +::: + +:::js +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://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)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. +- 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: -```py +```python import time from typing_extensions import TypedDict from langgraph.graph import StateGraph @@ -300,6 +578,40 @@ print(graph.invoke({"x": 5}, stream_mode='updates')) # (2)! 1. First run takes the full second 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 @@ -314,14 +626,27 @@ A node can have MULTIPLE outgoing edges. If a node has multiple out-going edges, ### Normal Edges +:::python If you **always** want to go from node A to node B, you can use the [add_edge][langgraph.graph.StateGraph.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`](insert-ref) 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][langgraph.graph.StateGraph.add_conditional_edges] method. This method accepts the name of a node and a "routing function" to call after that node is executed: ```python @@ -338,11 +663,36 @@ You can optionally provide a dictionary that maps the `routing_function`'s outpu 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`](insert-ref) 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", +}); +``` + +::: + !!! tip - Use [`Command`](#command) instead of conditional edges if you want to combine state updates and routing in a single function. +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`][langgraph.graph.StateGraph.add_edge] method from the virtual [`START`][langgraph.constants.START] node to the first node to execute to specify where to enter the graph. ```python @@ -351,8 +701,22 @@ 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`](insert-ref) method from the virtual [`START`](insert-ref) 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`][langgraph.graph.StateGraph.add_conditional_edges] from the virtual [`START`][langgraph.constants.START] node to accomplish this. ```python @@ -367,8 +731,31 @@ You can optionally provide a dictionary that maps the `routing_function`'s outpu 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`](insert-ref) from the virtual [`START`](insert-ref) 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](https://langchain-ai.github.io/langgraph/how-tos/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`][langgraph.types.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. @@ -380,8 +767,26 @@ def continue_to_jokes(state: OverallState): 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`](insert-ref) 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`][langgraph.types.Command] object from node functions: ```python @@ -402,20 +807,69 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]: return Command(update={"foo": "baz"}, goto="my_other_node") ``` +Check out this [how-to guide](../how-tos/graph-api.ipynb#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`. +::: + +:::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], +}); +``` + +Check out this [how-to guide](../how-tos/graph-api.ipynb#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`. +::: +::: + !!! important 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](../how-tos/graph-api.ipynb#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? +:::python Use `Command` when you need to **both** update the graph state **and** route to a different node. For example, when implementing [multi-agent handoffs](./multi_agent.md#handoffs) where it's important to route to a different agent and pass some information to that agent. +::: + +:::js +Use `Command` when you need to **both** update the graph state **and** route to a different node. For example, when implementing [multi-agent handoffs](./multi_agent.md#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](./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`: ```python @@ -435,6 +889,33 @@ def my_node(state: State) -> Command[Literal["other_subgraph"]]: 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](../how-tos/graph-api.ipynb#navigate-to-a-node-in-a-parent-graph). +::: + +:::js +If you are using [subgraphs](./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, + }); +}); +``` + +!!! note + + Setting `graph` to `Command.PARENT` will navigate to the closest parent graph. + +!!! important "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](#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](./multi_agent.md#handoffs). Check out [this guide](../how-tos/graph-api.ipynb#navigate-to-a-node-in-a-parent-graph) for detail. @@ -447,7 +928,13 @@ Refer to [this guide](../how-tos/graph-api.ipynb#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](./human_in_the_loop.md) 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](./human_in_the_loop.md) for more information. +::: ## Graph Migrations @@ -463,7 +950,9 @@ LangGraph can easily handle migrations of graph definitions (nodes, edges, and s 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. +You can optionally specify a config schema when creating a graph. + +:::python ```python class ConfigSchema(TypedDict): @@ -472,16 +961,48 @@ class ConfigSchema(TypedDict): graph = StateGraph(State, config_schema=ConfigSchema) ``` +::: + +:::js + +```typescript +import { z } from "zod"; + +const ConfigSchema = z.object({ + llm: z.string(), +}); + +const graph = new StateGraph(State, ConfigSchema); +``` + +::: + You can then pass this configuration into the graph using the `configurable` config field. +:::python + ```python config = {"configurable": {"llm": "anthropic"}} graph.invoke(inputs, config=config) ``` +::: + +:::js + +```typescript +const config = { configurable: { llm: "anthropic" } }; + +await graph.invoke(inputs, config); +``` + +::: + You can then access and use this configuration inside a node or conditional edge: +:::python + ```python def node_a(state, config): llm_type = config.get("configurable", {}).get("llm", "openai") @@ -490,9 +1011,23 @@ def node_a(state, config): ``` See [this guide](../how-tos/graph-api.ipynb#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 @@ -500,6 +1035,19 @@ graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthr ``` Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/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 diff --git a/docs/docs/concepts/memory.md b/docs/docs/concepts/memory.md index 6e59a4cff..83d3f29a0 100644 --- a/docs/docs/concepts/memory.md +++ b/docs/docs/concepts/memory.md @@ -87,11 +87,25 @@ Regardless of memory management approach, the central point is that the agent wi [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](persistence.md#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](persistence.md#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 @@ -105,6 +119,7 @@ For example, we built a [Tweet generator](https://www.youtube.com/watch?v=Vn8A3B The below pseudo-code shows how you might implement this with the LangGraph memory [store](persistence.md#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): @@ -125,6 +140,39 @@ def update_instructions(state: State, store: BaseStore): 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 + }); + // ... +}; +``` +::: ![](img/memory/update-instructions.png) @@ -154,6 +202,7 @@ See our [memory-service](https://github.com/langchain-ai/memory-template) templa LangGraph stores long-term memories as JSON documents in a [store](persistence.md#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 @@ -186,5 +235,47 @@ 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](persistence.md#memory-store) guide. \ No newline at end of file diff --git a/docs/docs/concepts/multi_agent.md b/docs/docs/concepts/multi_agent.md index 0cd4e7f61..6758b4567 100644 --- a/docs/docs/concepts/multi_agent.md +++ b/docs/docs/concepts/multi_agent.md @@ -1,8 +1,3 @@ ---- -search: - boost: 2 ---- - # Multi-agent systems An [agent](./agentic_concepts.md#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: @@ -25,21 +20,23 @@ The primary benefits of using multi-agent systems are: There are several ways to connect agents in a multi-agent system: -- **Network**: each agent can communicate with [every other agent](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/). Any agent can decide which other agent to call next. -- **Supervisor**: each agent communicates with a single [supervisor](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/) agent. Supervisor agent makes decisions on which agent should be called next. +- **Network**: each agent can communicate with [every other agent](../tutorials/multi_agent/multi-agent-collaboration/). Any agent can decide which other agent to call next. +- **Supervisor**: each agent communicates with a single [supervisor](../tutorials/multi_agent/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](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/). This is a generalization of the supervisor architecture and allows for more complex control flows. +- **Hierarchical**: you can define a multi-agent system with [a supervisor of supervisors](../tutorials/multi_agent/hierarchical_agent_teams/). 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: +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) +- **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`](./low_level.md#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. @@ -52,6 +49,26 @@ def agent(state) -> Command[Literal["agent", "another_agent"]]: ) ``` +::: + +:::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](./subgraphs.md)), 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 @@ -64,8 +81,30 @@ def some_node_inside_alice(state): ) ``` +::: + +:::js +In a more complex scenario where each agent node is itself a graph (i.e., a [subgraph](./subgraphs.md)), 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, + }); +}); +``` + +::: + !!! note - 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, e.g. instead of this: + + :::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) @@ -80,9 +119,30 @@ def some_node_inside_alice(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](../agents/overview.md). For those types of agents, a common pattern is wrapping a handoff in a tool call, e.g.: +One of the most common agent types is a [tool-calling agent](../agents/overview.md). For those types of agents, a common pattern is wrapping a handoff in a tool call: + +:::python ```python from langchain_core.tools import tool @@ -101,18 +161,69 @@ def transfer_to_bob(): ) ``` +::: + +:::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. !!! important - If you want to use tools that return `Command`, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.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 - ``` + If you want to use tools that return `Command`, you can either use prebuilt components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them: + +:::python +You can use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own: + +```python +def call_tools(state): + ... + commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls] + return commands +``` + +::: + +:::js +You can use prebuilt [`createReactAgent`][] / [`ToolNode`][] components, or implement your own: + +```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. @@ -120,6 +231,7 @@ Let's now take a closer look at the different multi-agent architectures. 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 @@ -164,10 +276,70 @@ 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`](./low_level.md#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](../how-tos/graph-api.ipynb#map-reduce-and-the-send-api) pattern. +:::python + ```python from typing import Literal from langchain_openai import ChatOpenAI @@ -211,12 +383,70 @@ 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(); +``` + +::: + Check out this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/) for an example of supervisor multi-agent architecture. ### Supervisor (tool-calling) In this variant of the [supervisor](#supervisor) architecture, we define a supervisor [agent](./agentic_concepts.md#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](./agentic_concepts.md#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 @@ -245,12 +475,67 @@ tools = [agent_1, agent_2] 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 @@ -319,6 +604,97 @@ 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: @@ -327,6 +703,8 @@ In this architecture we add individual agents as graph nodes and define the orde - **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`](./low_level.md#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 @@ -349,6 +727,37 @@ 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. @@ -390,12 +799,27 @@ It can be helpful to indicate which agent a particular AI message is from, espec ### 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). @@ -403,16 +827,25 @@ In practice, we see that most developers opt for option (1). 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? +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](./subgraphs.md) 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](../how-tos/subgraph.ipynb#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs. +- Define [subgraph](./subgraphs.md) 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](../how-tos/subgraph.ipynb#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](../how-tos/graph-api.ipynb/#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/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md index 7c612cf60..dadf9b943 100644 --- a/docs/docs/concepts/persistence.md +++ b/docs/docs/concepts/persistence.md @@ -5,7 +5,7 @@ search: # Persistence -LangGraph has a built-in persistence layer, implemented through checkpointers. When you compile 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. +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](img/persistence/checkpoints.jpg) @@ -17,19 +17,35 @@ LangGraph has a built-in persistence layer, implemented through checkpointers. W 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](./assistants.md#execution). When a run is executed, the [state](../concepts/low_level.md#state) of the underlying graph of the assistant will be persisted to the thread. -When invoking graph with a checkpointer, you **must** specify a `thread_id` as part of the `configurable` portion of the config: +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](../cloud/reference/api/api_ref.html#tag/threads) 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. +- `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. @@ -39,6 +55,8 @@ Checkpoints are persisted and can be used to restore the state of a thread at a 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 @@ -71,18 +89,73 @@ 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); +``` + +::: + +:::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 +- 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. -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 -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 +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 @@ -94,6 +167,29 @@ config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-652 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: ``` @@ -107,8 +203,44 @@ StateSnapshot( ) ``` +::: + +:::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 @@ -116,6 +248,21 @@ 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: ``` @@ -158,29 +305,184 @@ In our example, the output of `get_state_history` will look like this: ] ``` +::: + +:::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](img/persistence/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. +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. +- `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) ``` -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](../how-tos/human_in_the_loop/time-travel.md). +::: + +:::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](../how-tos/human_in_the_loop/time-travel.md). ![Replay](img/persistence/re_play.png) ### Update state -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: +:::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` @@ -192,6 +494,8 @@ These are the values that will be used to update the state. Note that this updat 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 @@ -202,29 +506,92 @@ class State(TypedDict): 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](../how-tos/human_in_the_loop/time-travel.md). +::: + +:::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](../how-tos/human_in_the_loop/time-travel.md). +::: ![Update](img/persistence/checkpoints_full_story.jpg) @@ -234,7 +601,7 @@ The final thing you can optionally specify when calling `update_state` is `as_no A [state schema](low_level.md#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! +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`](../reference/store.md#langgraph.store.base.BaseStore) 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. @@ -246,28 +613,73 @@ With checkpointers alone, we cannot share information across threads. This motiv 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 + +```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() @@ -279,6 +691,7 @@ memories[-1].dict() ``` 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 @@ -287,10 +700,39 @@ The attributes it has are: - `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 @@ -303,8 +745,28 @@ store = InMemoryStore( ) ``` +::: + +:::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) @@ -315,8 +777,25 @@ memories = store.search( ) ``` +::: + +:::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( @@ -338,9 +817,37 @@ store.put( ) ``` +::: + +:::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 -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 +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 @@ -354,8 +861,29 @@ checkpointer = InMemorySaver() 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" @@ -368,19 +896,40 @@ for update in graph.stream( print(update) ``` -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: +::: + +:::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()) @@ -389,8 +938,46 @@ def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseSt ``` +::: + +:::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'}, @@ -400,8 +987,27 @@ memories[-1].dict() '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 @@ -409,7 +1015,7 @@ def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore # Namespace the memory namespace = (user_id, "memories") - + # Search based on the most recent message memories = store.search( namespace, @@ -417,11 +1023,42 @@ def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore limit=3 ) info = "\n".join([d.value["memory"] for d in memories]) - + # ... 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. +::: + +:::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 @@ -434,6 +1071,25 @@ for update in graph.stream( 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 @@ -455,28 +1111,50 @@ See the [deployment guide](../cloud/deployment/semantic_search.md) for more deta Under the hood, checkpointing is powered by checkpointer objects that conform to [BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver] interface. LangGraph provides several checkpointer implementations, all implemented via standalone, installable libraries: -* `langgraph-checkpoint`: The base interface for checkpointer savers ([BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver]) and serialization/deserialization interface ([SerializerProtocol][langgraph.checkpoint.serde.base.SerializerProtocol]). Includes in-memory checkpointer implementation ([InMemorySaver][langgraph.checkpoint.memory.InMemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included. -* `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database ([SqliteSaver][langgraph.checkpoint.sqlite.SqliteSaver] / [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately. -* `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database ([PostgresSaver][langgraph.checkpoint.postgres.PostgresSaver] / [AsyncPostgresSaver][langgraph.checkpoint.postgres.aio.AsyncPostgresSaver]), used in LangGraph Platform. Ideal for using in production. Needs to be installed separately. +:::python +- `langgraph-checkpoint`: The base interface for checkpointer savers ([BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver]) and serialization/deserialization interface ([SerializerProtocol][langgraph.checkpoint.serde.base.SerializerProtocol]). Includes in-memory checkpointer implementation ([InMemorySaver][langgraph.checkpoint.memory.InMemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included. +- `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database ([SqliteSaver][langgraph.checkpoint.sqlite.SqliteSaver] / [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately. +- `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database ([PostgresSaver][langgraph.checkpoint.postgres.PostgresSaver] / [AsyncPostgresSaver][langgraph.checkpoint.postgres.aio.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][]) and serialization/deserialization interface ([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][langgraph.checkpoint.base.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()` +- `.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`). !!! note Note - For running your graph asynchronously, you can use `InMemorySaver`, or async versions of Sqlite/Postgres checkpointers -- `AsyncSqliteSaver` / `AsyncPostgresSaver` checkpointers. +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][] 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][langgraph.checkpoint.serde.base.SerializerProtocol] for implementing serializers provides a default implementation ([JsonPlusSerializer][langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer]) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more. #### Serialization with `pickle` @@ -520,6 +1198,11 @@ 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`][langgraph.checkpoint.serde.base.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 @@ -529,7 +1212,7 @@ First, checkpointers facilitate [human-in-the-loop workflows](agentic_concepts.m ### Memory -Second, checkpointers allow for ["memory"](../concepts/memory.md) 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](../how-tos/memory/add-memory.md) for information on how to add and manage conversation memory using checkpointers. +Second, checkpointers allow for ["memory"](../concepts/memory.md) 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](../how-tos/memory/add-memory.md) for information on how to add and manage conversation memory using checkpointers. ### Time Travel diff --git a/docs/docs/concepts/pregel.md b/docs/docs/concepts/pregel.md index d5a87d87a..1184d3441 100644 --- a/docs/docs/concepts/pregel.md +++ b/docs/docs/concepts/pregel.md @@ -5,14 +5,32 @@ search: # LangGraph runtime +:::python [Pregel][langgraph.pregel.Pregel] implements LangGraph's runtime, managing the execution of LangGraph applications. Compiling a [StateGraph][langgraph.graph.StateGraph] or creating an [entrypoint][langgraph.func.entrypoint] produces a [Pregel][langgraph.pregel.Pregel] instance that can be invoked with input. +::: + +:::js +[Pregel][] implements LangGraph's runtime, managing the execution of LangGraph applications. + +Compiling a [StateGraph][] or creating an [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][langgraph.pregel.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. @@ -33,21 +51,36 @@ An **actor** is a `PregelNode`. It subscribes to channels, reads data from them, 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][langgraph.channels.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][langgraph.channels.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][langgraph.channels.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 -While most users will interact with Pregel through the [StateGraph][langgraph.graph.StateGraph] API or -the [entrypoint][langgraph.func.entrypoint] decorator, it is possible to interact with Pregel directly. +:::python +While most users will interact with Pregel through the [StateGraph][langgraph.graph.StateGraph] API or the [entrypoint][langgraph.func.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. === "Single node" + :::python ```python - from langgraph.channels import EphemeralValue from langgraph.pregel import Pregel, NodeBuilder @@ -73,9 +106,39 @@ Below are a few different examples to give you a sense of the Pregel API. ```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' } + ``` + ::: === "Multiple nodes" + :::python ```python from langgraph.channels import LastValue, EphemeralValue from langgraph.pregel import Pregel, NodeBuilder @@ -110,9 +173,45 @@ Below are a few different examples to give you a sense of the Pregel API. ```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' } + ``` + ::: === "Topic" + :::python ```python from langgraph.channels import EphemeralValue, Topic from langgraph.pregel import Pregel, NodeBuilder @@ -146,11 +245,47 @@ Below are a few different examples to give you a sense of the Pregel API. ```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'] } + ``` + ::: === "BinaryOperatorAggregate" 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 @@ -187,12 +322,53 @@ Below are a few different examples to give you a sense of the Pregel API. 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" }); + ``` + ::: === "Cycle" + :::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. + until a `None` value is written to the channel. ```python from langgraph.channels import EphemeralValue @@ -219,6 +395,39 @@ Below are a few different examples to give you a sense of the Pregel API. ```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 @@ -226,6 +435,8 @@ LangGraph provides two high-level APIs for creating a Pregel application: the [S === "StateGraph (Graph API)" + :::python + The [StateGraph (Graph API)][langgraph.graph.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 @@ -258,9 +469,53 @@ LangGraph provides two high-level APIs for creating a Pregel application: the [S # This will return a Pregel instance. graph = builder.compile() ``` + ::: + + :::js + + The [StateGraph (Graph API)][] 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) ``` @@ -294,11 +549,53 @@ LangGraph provides two high-level APIs for creating a Pregel application: the [S '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 { ... } + } + ``` + ::: === "Functional API" - In the [Functional API](functional_api.md), you can use an [`entrypoint`][langgraph.func.entrypoint] to create - a Pregel application. The `entrypoint` decorator allows you to define a function that takes input and returns output. + :::python + + In the [Functional API](functional_api.md), you can use an [`entrypoint`][langgraph.func.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 @@ -332,3 +629,47 @@ LangGraph provides two high-level APIs for creating a Pregel application: the [S Channels: {'__start__': , '__end__': , '__previous__': } ``` + ::: + + :::js + + In the [Functional API](functional_api.md), you can use an [`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/docs/docs/concepts/sdk.md b/docs/docs/concepts/sdk.md index 221fcccde..3120dabf3 100644 --- a/docs/docs/concepts/sdk.md +++ b/docs/docs/concepts/sdk.md @@ -5,25 +5,20 @@ search: # LangGraph SDK -LangGraph Platform provides both a Python SDK for interacting with [LangGraph Server](./langgraph_server.md). +:::python +LangGraph Platform provides a python SDK for interacting with [LangGraph Server](./langgraph_server.md). !!! tip "Python SDK reference" - + For detailed information about the Python SDK, see [Python SDK reference docs](../cloud/reference/sdk/python_sdk_ref.md). ## Installation -You can install the packages using the appropriate package manager for your language: +You can install the LangGraph SDK using the following command: -=== "Python" - ```bash - pip install langgraph-sdk - ``` - -=== "JS" - ```bash - yarn add @langchain/langgraph-sdk - ``` +```bash +pip install langgraph-sdk +``` ## Python sync vs. async @@ -39,16 +34,32 @@ The Python SDK provides both synchronous (`get_sync_client`) and asynchronous (` ``` === "Async" - ```python + + ````python from langgraph_sdk import get_client client = get_client(url=..., api_key=...) await client.assistants.search() ``` - ## Learn more - [Python SDK Reference](../cloud/reference/sdk/python_sdk_ref.md) - [LangGraph CLI API Reference](../cloud/reference/cli.md) -- [JS/TS SDK Reference](../cloud/reference/sdk/js_ts_sdk_ref.md) \ No newline at end of file + ::: + +:::js +LangGraph Platform provides a JS/TS SDK for interacting with [LangGraph Server](./langgraph_server.md). + +## Installation + +You can add the LangGraph SDK to your project using the following command: + +```bash +npm install @langchain/langgraph-sdk +``` + +## Learn more + +- [LangGraph CLI API Reference](../cloud/reference/cli.md) + ::: diff --git a/docs/docs/concepts/server-mcp.md b/docs/docs/concepts/server-mcp.md index 7f144e87f..a2e9123cd 100644 --- a/docs/docs/concepts/server-mcp.md +++ b/docs/docs/concepts/server-mcp.md @@ -9,7 +9,7 @@ hide: # MCP endpoint in LangGraph Server The **Model Context Protocol (MCP)** is an open protocol for describing tools and data sources in a model-agnostic format, enabling LLMs to discover -and use them via a structured API. +and use them via a structured API. [LangGraph Server](./langgraph_server.md) implements MCP using the [Streamable HTTP transport](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/#streamable-http). This allows LangGraph **agents** to be exposed as **MCP tools**, making them usable with any MCP-compliant client supporting Streamable HTTP. @@ -17,6 +17,7 @@ The MCP endpoint is available at `/mcp` on [LangGraph Server](./langgraph_server ## Requirements +:::python To use MCP, ensure you have the following dependencies installed: - `langgraph-api >= 0.2.3` @@ -28,8 +29,18 @@ Install them with: pip install "langgraph-api>=0.2.3" "langgraph-sdk>=0.1.61" ``` -## Exposing an agent as MCP tool +::: +:::js +To use MCP, ensure you have both the api and sdk packages installed. + +```bash +npm install @langchain/langgraph-api @langchain/langgraph-sdk +``` + +::: + +## Exposing an agent as MCP tool When deployed, your agent will appear as a tool in the MCP endpoint with this configuration: @@ -38,22 +49,41 @@ with this configuration: - **Tool description**: The agent's description. - **Tool input schema**: The agent's input schema. -### Setting name and description +### Setting name and description You can set the name and description of your agent in `langgraph.json`: +:::python + ```json { - "graphs": { - "my_agent": { - "path": "./my_agent/agent.py:graph", - "description": "A description of what the agent does" - } - }, - "env": ".env" + "graphs": { + "my_agent": { + "path": "./my_agent/agent.py:graph", + "description": "A description of what the agent does" + } + }, + "env": ".env" } ``` +::: +:::js + +```json +{ + "graphs": { + "my_agent": { + "path": "./my_agent/agent.ts:graph", + "description": "A description of what the agent does" + } + }, + "env": ".env" +} +``` + +::: + After deployment, you can update the name and description using the LangGraph SDK. ### Schema @@ -100,7 +130,6 @@ print(graph.invoke({"question": "hi"})) For more details, see the [low-level concepts guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#state). - ## Usage overview To enable MCP: @@ -109,100 +138,108 @@ To enable MCP: - MCP tools (agents) will be automatically exposed. - Connect with any MCP-compliant client that supports Streamable HTTP. - ### Client -Use an MCP-compliant client to connect to the LangGraph server. The following examples show how to connect using different programming languages. +:::python +Use an MCP-compliant client to connect to the LangGraph server. The following example shows how to connect using [langchain-mcp-adapters](https://github.com/langchain-ai/langchain-mcp-adapters). -=== "JavaScript/TypeScript" +Install the adapter with: - ```bash - npm install @modelcontextprotocol/sdk - ``` +```bash +pip install langchain-mcp-adapters +``` - > **Note** - > Replace `serverUrl` with your LangGraph server URL and configure authentication headers as needed. +Here is an example of how to connect to a remote MCP endpoint and use an agent as a tool: - ```js - import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +```python +# Create server parameters for stdio connection +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client +import asyncio - // Connects to the LangGraph MCP endpoint - async function connectClient(url) { - const baseUrl = new URL(url); - const client = new Client({ - name: 'streamable-http-client', - version: '1.0.0' - }); +from langchain_mcp_adapters.tools import load_mcp_tools +from langgraph.prebuilt import create_react_agent - const transport = new StreamableHTTPClientTransport(baseUrl); - await client.connect(transport); - - console.log("Connected using Streamable HTTP transport"); - console.log(JSON.stringify(await client.listTools(), null, 2)); - return client; +server_params = { + "url": "https://mcp-finance-agent.xxx.us.langgraph.app/mcp", + "headers": { + "X-Api-Key":"lsv2_pt_your_api_key" } +} - const serverUrl = "http://localhost:2024/mcp"; +async def main(): + async with streamablehttp_client(**server_params) as (read, write, _): + async with ClientSession(read, write) as session: + # Initialize the connection + await session.initialize() - connectClient(serverUrl) - .then(() => { - console.log("Client connected successfully"); - }) - .catch(error => { - console.error("Failed to connect client:", error); - }); - ``` + # Load the remote graph as if it was a tool + tools = await load_mcp_tools(session) -=== "Python" + # Create and run a react agent with the tools + agent = create_react_agent("openai:gpt-4.1", tools) + # Invoke the agent with a message + agent_response = await agent.ainvoke({"messages": "What can the finance agent do for me?"}) + print(agent_response) - Install the adapter with: +if __name__ == "__main__": + asyncio.run(main()) +``` - ```bash - pip install langchain-mcp-adapters - ``` +::: - Here is an example of how to connect to a remote MCP endpoint and use an agent as a tool: +:::js +Use an MCP-compliant client to connect to the LangGraph server. The following example shows how to connect using [`@langchain/mcp-adapters`](https://npmjs.com/package/@langchain/mcp-adapters). - ```python - # Create server parameters for stdio connection - from mcp import ClientSession - from mcp.client.streamable_http import streamablehttp_client - import asyncio +```bash +npm install @langchain/mcp-adapters +``` - from langchain_mcp_adapters.tools import load_mcp_tools - from langgraph.prebuilt import create_react_agent +Here is an example of how to connect to a remote MCP endpont and use an agent as a tool: - server_params = { - "url": "https://mcp-finance-agent.xxx.us.langgraph.app/mcp", - "headers": { - "X-Api-Key":"lsv2_pt_your_api_key" - } - } +```typescript +import { MultiServerMCPClient } from "@langchain/mcp-adapters"; +import { createReactAgent } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; - async def main(): - async with streamablehttp_client(**server_params) as (read, write, _): - async with ClientSession(read, write) as session: - # Initialize the connection - await session.initialize() +async function main() { + const client = new MultiServerMCPClient({ + mcpServers: { + "finance-agent": { + url: "https://mcp-finance-agent.xxx.us.langgraph.app/mcp", + headers: { + "X-Api-Key": "lsv2_pt_your_api_key", + }, + }, + }, + }); - # Load the remote graph as if it was a tool - tools = await load_mcp_tools(session) + const tools = await client.getTools(); - # Create and run a react agent with the tools - agent = create_react_agent("openai:gpt-4.1", tools) + const model = new ChatOpenAI({ + model: "gpt-4o-mini", + temperature: 0, + }); - # Invoke the agent with a message - agent_response = await agent.ainvoke({"messages": "What can the finance agent do for me?"}) - print(agent_response) + const agent = createReactAgent({ + model, + tools, + }); - if __name__ == "__main__": - asyncio.run(main()) - ``` + const response = await agent.invoke({ + input: "What can the finance agent do for me?", + }); + console.log(response); +} -## Session behavior +main(); +``` + +::: + +## Session behavior The current LangGraph MCP implementation does not support sessions. Each `/mcp` request is stateless and independent. @@ -222,4 +259,4 @@ To disable the MCP endpoint, set `disable_mcp` to `true` in your `langgraph.json } ``` -This will prevent the server from exposing the `/mcp` endpoint. \ No newline at end of file +This will prevent the server from exposing the `/mcp` endpoint. diff --git a/docs/docs/concepts/subgraphs.md b/docs/docs/concepts/subgraphs.md index 6a4aefb23..6c5503431 100644 --- a/docs/docs/concepts/subgraphs.md +++ b/docs/docs/concepts/subgraphs.md @@ -12,71 +12,152 @@ Some reasons for using subgraphs are: The main question when adding subgraphs is how the parent graph and subgraph communicate, i.e. how they pass the [state](./low_level.md#state) between each other during the graph execution. There are two scenarios: -* parent and subgraph have **shared state keys** in their state [schemas](./low_level.md#state). In this case, you can [include the subgraph as a node in the parent graph](../how-tos/subgraph.ipynb#shared-state-schemas) +- parent and subgraph have **shared state keys** in their state [schemas](./low_level.md#state). In this case, you can [include the subgraph as a node in the parent graph](../how-tos/subgraph.ipynb#shared-state-schemas) - ```python - from langgraph.graph import StateGraph, MessagesState, START + :::python - # Subgraph + ```python + from langgraph.graph import StateGraph, MessagesState, START - def call_model(state: MessagesState): - response = model.invoke(state["messages"]) - return {"messages": response} + # Subgraph - subgraph_builder = StateGraph(State) - subgraph_builder.add_node(call_model) - ... - # highlight-next-line - subgraph = subgraph_builder.compile() + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} - # Parent graph + subgraph_builder = StateGraph(State) + subgraph_builder.add_node(call_model) + ... + # highlight-next-line + subgraph = subgraph_builder.compile() - 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!"}]}) - ``` + # Parent graph -* parent graph and subgraph have **different schemas** (no shared state keys in their state [schemas](./low_level.md#state)). In this case, you have to [call the subgraph from inside a node in the parent graph](../how-tos/subgraph.ipynb#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 + 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!"}]}) + ``` - ```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] + :::js - # Subgraph + ```typescript + import { StateGraph, MessagesZodState, START } from "@langchain/langgraph"; - # highlight-next-line - def call_model(state: SubgraphMessagesState): - response = model.invoke(state["subgraph_messages"]) - return {"subgraph_messages": response} + // Subgraph - 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() + 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 + // Parent graph - def call_subgraph(state: MessagesState): - response = subgraph.invoke({"subgraph_messages": state["messages"]}) - return {"messages": response["subgraph_messages"]} + 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!" }] }); + ``` - 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!"}]}) - ``` + ::: + +- parent graph and subgraph have **different schemas** (no shared state keys in their state [schemas](./low_level.md#state)). In this case, you have to [call the subgraph from inside a node in the parent graph](../how-tos/subgraph.ipynb#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/docs/docs/concepts/template_applications.md b/docs/docs/concepts/template_applications.md index 76ebff698..15fbc16f6 100644 --- a/docs/docs/concepts/template_applications.md +++ b/docs/docs/concepts/template_applications.md @@ -9,6 +9,7 @@ Templates are open source reference applications designed to help you get starte You can create an application from a template using the LangGraph CLI. +:::python !!! info "Requirements" - Python >= 3.11 @@ -16,56 +17,74 @@ You can create an application from a template using the LangGraph CLI. ## Install the LangGraph CLI -=== "Python" +```bash +pip install "langgraph-cli[inmem]" --upgrade +``` - ```bash - pip install "langgraph-cli[inmem]" --upgrade - ``` +Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): - Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): +```bash +uvx --from "langgraph-cli[inmem]" langgraph dev --help +``` - ```bash - uvx --from "langgraph-cli[inmem]" langgraph dev --help - ``` +::: -=== "JS" +:::js - ```bash - npx @langchain/langgraph-cli --help - ``` +```bash +npx @langchain/langgraph-cli --help +``` + +::: ## Available Templates -| Template | Description | Python | JS/TS | -|---------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------| -| **New LangGraph Project** | A simple, minimal chatbot with memory. | [Repo](https://github.com/langchain-ai/new-langgraph-project) | [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) | [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) | [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) | [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) | [Repo](https://github.com/langchain-ai/data-enrichment-js) | +:::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" +:::python - ```bash - langgraph new - ``` +```bash +langgraph new +``` - Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): +Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): - ```bash - uvx --from "langgraph-cli[inmem]" langgraph new - ``` +```bash +uvx --from "langgraph-cli[inmem]" langgraph new +``` -=== "JS" +::: - ```bash - npx @langchain/langgraph-cli new - ``` +:::js + +```bash +npx @langchain/langgraph-cli new +``` + +::: ## Next Steps @@ -73,26 +92,31 @@ Review the `README.md` file in the root of your new LangGraph app for more infor After configuring the app properly and adding your API keys, you can start the app using the LangGraph CLI: -=== "Python" +:::python - ```bash - langgraph dev - ``` +```bash +langgraph dev +``` - Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): +Or via [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended): - ```bash - uvx --from "langgraph-cli[inmem]" --with-editable . langgraph dev - ``` +```bash +uvx --from "langgraph-cli[inmem]" --with-editable . langgraph dev +``` - ??? info "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`. +!!! info "Missing Local Package?" -=== "JS" + 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`. - ```bash - npx @langchain/langgraph-cli dev - ``` +::: + +:::js + +```bash +npx @langchain/langgraph-cli dev +``` + +::: See the following guides for more information on how to deploy your app: diff --git a/docs/docs/concepts/tools.md b/docs/docs/concepts/tools.md index 8a2e693af..5eaf25023 100644 --- a/docs/docs/concepts/tools.md +++ b/docs/docs/concepts/tools.md @@ -2,7 +2,13 @@ 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](../how-tos/tool-calling.md) 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 @@ -10,17 +16,63 @@ Many AI applications interact with users via natural language. However, some use 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](../how-tos/tool-calling.md) for more details. @@ -29,18 +81,25 @@ See the [tool calling guide](../how-tos/tool-calling.md) for more details. 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. +- **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 @@ -52,6 +111,32 @@ def multiply(a: int, b: int) -> int: 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](../how-tos/tool-calling.md) for more details. ## Tool execution @@ -60,5 +145,14 @@ While the model determines when to call a tool, execution of the tool call must LangGraph provides prebuilt components for this: -* [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode]: A prebuilt node that executes tools. -* [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent]: Constructs a full agent that manages tool calling automatically. +:::python + +- [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode]: A prebuilt node that executes tools. +- [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent]: Constructs a full agent that manages tool calling automatically. + ::: + +:::js + +- [`ToolNode`][]: A prebuilt node that executes tools. +- [`createReactAgent`][]: Constructs a full agent that manages tool calling automatically. + ::: diff --git a/docs/docs/how-tos/auth/custom_auth.md b/docs/docs/how-tos/auth/custom_auth.md index 64f95e1ef..0b15dc090 100644 --- a/docs/docs/how-tos/auth/custom_auth.md +++ b/docs/docs/how-tos/auth/custom_auth.md @@ -6,7 +6,7 @@ * [**Authentication & Access Control**](../../concepts/auth.md) * [**LangGraph Platform**](../../concepts/langgraph_platform.md) - + For a more guided walkthrough, see [**setting up custom authentication**](../../tutorials/auth/getting_started.md) tutorial. ???+ note "Support by deployment type" @@ -17,6 +17,8 @@ This guide shows how to add custom authentication to your LangGraph Platform app ## 1. Implement authentication +:::python + ```python from langgraph_sdk import Auth @@ -55,10 +57,51 @@ async def authorize_store(ctx: Auth.types.AuthContext, value: dict): ``` +::: + +:::js + +```typescript +import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; + +const auth = new Auth() + .authenticate(async (request) => { + const authorization = request.headers.get("Authorization"); + const token = authorization?.split(" ")[1]; // "Bearer " + if (!token) { + throw new HTTPException(401, "No token provided"); + } + try { + const user = await verifyToken(token); + return user; + } catch (error) { + throw new HTTPException(401, "Invalid token"); + } + }) + // Add authorization rules to actually control access to resources + .on("*", async ({ user, value }) => { + const filters = { owner: user.identity }; + const metadata = value.metadata ?? {}; + metadata.update(filters); + return filters; + }) + // Assumes you organize information in store like (user_id, resource_type, resource_id) + .on("store", async ({ user, value }) => { + const namespace = value.namespace; + if (namespace[0] !== user.identity) { + throw new HTTPException(403, "Not authorized"); + } + }); +``` + +::: + ## 2. Update configuration In your `langgraph.json`, add the path to your auth file: +:::python + ```json hl_lines="7-9" { "dependencies": ["."], @@ -72,11 +115,31 @@ In your `langgraph.json`, add the path to your auth file: } ``` +::: + +:::js + +```json hl_lines="7-9" +{ + "dependencies": ["."], + "graphs": { + "agent": "./agent.ts:graph" + }, + "env": ".env", + "auth": { + "path": "./auth.ts:my_auth" + } +} +``` + +::: + ## 3. Connect from the client Once you've set up authentication in your server, requests must include the required authorization information based on your chosen scheme. Assuming you are using JWT token authentication, you could access your deployments using any of the following methods: +:::python === "Python Client" ```python @@ -94,7 +157,7 @@ Assuming you are using JWT token authentication, you could access your deploymen ```python from langgraph.pregel.remote import RemoteGraph - + my_token = "your-token" # In practice, you would generate a signed token with your auth provider remote_graph = RemoteGraph( "agent", @@ -104,9 +167,18 @@ Assuming you are using JWT token authentication, you could access your deploymen threads = await remote_graph.ainvoke(...) ``` -=== "JavaScript Client" +=== "CURL" - ```javascript + ```bash + curl -H "Authorization: Bearer ${your-token}" http://localhost:2024/threads + ``` + +::: + +:::js +=== "Client" + + ```typescript import { Client } from "@langchain/langgraph-sdk"; const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider @@ -117,9 +189,9 @@ Assuming you are using JWT token authentication, you could access your deploymen const threads = await client.threads.search(); ``` -=== "JavaScript RemoteGraph" +=== "RemoteGraph" - ```javascript + ```typescript import { RemoteGraph } from "@langchain/langgraph/remote"; const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider @@ -136,3 +208,5 @@ Assuming you are using JWT token authentication, you could access your deploymen ```bash curl -H "Authorization: Bearer ${your-token}" http://localhost:2024/threads ``` + +::: diff --git a/docs/docs/how-tos/auth/openapi_security.md b/docs/docs/how-tos/auth/openapi_security.md index 05215b497..76cc94011 100644 --- a/docs/docs/how-tos/auth/openapi_security.md +++ b/docs/docs/how-tos/auth/openapi_security.md @@ -3,7 +3,7 @@ This guide shows how to customize the OpenAPI security schema for your LangGraph Platform API documentation. A well-documented security schema helps API consumers understand how to authenticate with your API and even enables automatic client generation. See the [Authentication & Access Control conceptual guide](../../concepts/auth.md) for more details about LangGraph's authentication system. !!! note "Implementation vs Documentation" - This guide only covers how to document your security requirements in OpenAPI. To implement the actual authentication logic, see [How to add custom authentication](./custom_auth.md). +This guide only covers how to document your security requirements in OpenAPI. To implement the actual authentication logic, see [How to add custom authentication](./custom_auth.md). This guide applies to all LangGraph Platform deployments (Cloud and self-hosted). It does not apply to usage of the LangGraph open source library if you are not using LangGraph Platform. @@ -38,6 +38,7 @@ To customize the security schema in your OpenAPI documentation, add an `openapi` Note that LangGraph Platform does not provide authentication endpoints - you'll need to handle user authentication in your client application and pass the resulting credentials to the LangGraph API. +:::python === "OAuth2 with Bearer Token" ```json @@ -89,6 +90,62 @@ Note that LangGraph Platform does not provide authentication endpoints - you'll } ``` +::: + +:::js +=== "OAuth2 with Bearer Token" + + ```json + { + "auth": { + "path": "./auth.ts:my_auth", // Implement auth logic here + "openapi": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://your-auth-server.com/oauth/authorize", + "scopes": { + "me": "Read information about the current user", + "threads": "Access to create and manage threads" + } + } + } + } + }, + "security": [ + {"OAuth2": ["me", "threads"]} + ] + } + } + } + ``` + +=== "API Key" + + ```json + { + "auth": { + "path": "./auth.ts:my_auth", // Implement auth logic here + "openapi": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "security": [ + {"apiKeyAuth": []} + ] + } + } + } + ``` + +::: + ## Testing After updating your configuration: diff --git a/docs/docs/how-tos/http/custom_middleware.md b/docs/docs/how-tos/http/custom_middleware.md index a351b345d..f81d6802e 100644 --- a/docs/docs/how-tos/http/custom_middleware.md +++ b/docs/docs/how-tos/http/custom_middleware.md @@ -12,7 +12,7 @@ Below is an example using FastAPI. ## Create app -Starting from an **existing** LangGraph Platform application, add the following middleware code to your `webapp.py` file. If you are starting from scratch, you can create a new app from a template using the CLI. +Starting from an **existing** LangGraph Platform application, add the following middleware code to your webapp file. If you are starting from scratch, you can create a new app from a template using the CLI. ```bash langgraph new --template=new-langgraph-project-python my_new_project @@ -72,4 +72,4 @@ You can deploy this app as-is to LangGraph Platform or to your self-hosted platf ## Next steps -Now that you've added custom middleware to your deployment, you can use similar techniques to add [custom routes](./custom_routes.md) or define [custom lifespan events](./custom_lifespan.md) to further customize your server's behavior. \ No newline at end of file +Now that you've added custom middleware to your deployment, you can use similar techniques to add [custom routes](./custom_routes.md) or define [custom lifespan events](./custom_lifespan.md) to further customize your server's behavior. diff --git a/docs/docs/how-tos/http/custom_routes.md b/docs/docs/how-tos/http/custom_routes.md index b385a8aa0..76f715abc 100644 --- a/docs/docs/how-tos/http/custom_routes.md +++ b/docs/docs/how-tos/http/custom_routes.md @@ -10,7 +10,7 @@ Below is an example using FastAPI. ## Create app -Starting from an **existing** LangGraph Platform application, add the following custom route code to your `webapp.py` file. If you are starting from scratch, you can create a new app from a template using the CLI. +Starting from an **existing** LangGraph Platform application, add the following custom route code to your webapp file. If you are starting from scratch, you can create a new app from a template using the CLI. ```bash langgraph new --template=new-langgraph-project-python my_new_project @@ -60,7 +60,6 @@ langgraph dev --no-browser If you navigate to `localhost:2024/hello` in your browser (`2024` is the default development port), you should see the `/hello` endpoint returning `{"Hello": "World"}`. - !!! note "Shadowing default endpoints" The routes you create in the app are given priority over the system defaults, meaning you can shadow and redefine the behavior of any default endpoint. @@ -71,4 +70,4 @@ You can deploy this app as-is to LangGraph Platform or to your self-hosted platf ## Next steps -Now that you've added a custom route to your deployment, you can use this same technique to further customize how your server behaves, such as defining custom [custom middleware](./custom_middleware.md) and [custom lifespan events](./custom_lifespan.md). +Now that you've added a custom route to your deployment, you can use this same technique to further customize how your server behaves, such as defining custom [custom middleware](./custom_middleware.md) and [custom lifespan events](./custom_lifespan.md). diff --git a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md index 01166b008..075e0bc72 100644 --- a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md +++ b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md @@ -15,14 +15,21 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's ## Pause using `interrupt` +:::python The [`interrupt` function][langgraph.types.interrupt] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context. +::: +:::js +The [`interrupt` function][] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming 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**](../../concepts/persistence.md#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**](../../concepts/persistence.md#threads) until the `interrupt` is hit. -4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](#resume-using-the-command-primitive)). +4. **Resume execution** using `invoke`/`stream` (see [**The `Command` primitive**](#resume-using-the-command-primitive)). + +:::python ```python # highlight-next-line @@ -48,11 +55,11 @@ result = graph.invoke({"some_text": "original text"}, config=config) # (5)! print(result['__interrupt__']) # (6)! # > [ # > Interrupt( -# > value={'text_to_revise': 'original text'}, +# > 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)! @@ -66,9 +73,60 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! 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. + ::: ??? example "Extended example: using `interrupt`" + :::python ```python from typing import TypedDict import uuid @@ -112,11 +170,11 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! print(result['__interrupt__']) # (6)! # > [ # > Interrupt( - # > value={'text_to_revise': 'original text'}, + # > 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)! @@ -130,38 +188,125 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! 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. + ::: !!! tip "New in 0.4.0" - `__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. + :::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. + ::: + + :::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. + ::: !!! warning - Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that 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. + :::python + Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that 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. However, it's important to note that 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. + ::: ## Resume using the `Command` primitive !!! warning + :::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. -To resume execution, use the [`Command`][langgraph.types.Command] primitive, which can be supplied via the `invoke`, `ainvoke`, `stream`, or `astream` 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 +To resume execution, use the [`Command`][langgraph.types.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`][] 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 -If you have multiple interrupts in the task queue, you can use `Command.resume` with a dictionary mapping of interrupt ids to resume with a single `invoke` / `stream` call. +If you have multiple interrupts in the task queue, you can use `Command.resume` with a dictionary/object mapping interrupt ids to values to resume with a single `invoke` / `stream` call. For example, once your graph has been interrupted (multiple times, theoretically) and is stalled: +:::python + ```python resume_map = { i.interrupt_id: f"human input for prompt {i.value}" @@ -171,6 +316,24 @@ resume_map = { parent_graph.invoke(Command(resume=resume_map), config=thread_config) ``` +::: + +:::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`. @@ -184,6 +347,8 @@ Below we show different design patterns that can be implemented using `interrupt 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 @@ -214,8 +379,42 @@ 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); +``` + +::: + ??? example "Extended example: approve or reject with interrupt" + :::python ```python from typing import Literal, TypedDict import uuid @@ -283,6 +482,102 @@ graph.invoke(Command(resume=True), config=thread_config) 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 @@ -292,6 +587,8 @@ graph.invoke(Command(resume=True), config=thread_config) +:::python + ```python from langgraph.types import interrupt @@ -308,7 +605,7 @@ def human_editing(state: State): # Update the state with the edited text return { - "llm_generated_summary": result["edited_text"] + "llm_generated_summary": result["edited_text"] } # Add the node to the graph in an appropriate location @@ -322,13 +619,51 @@ graph = graph_builder.compile(checkpointer=checkpointer) # Resume it with the edited text. thread_config = {"configurable": {"thread_id": "some_id"}} graph.invoke( - Command(resume={"edited_text": "The edited text"}), + 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 +); +``` + +::: + ??? example "Extended example: edit state with interrupt" + :::python ```python from typing import TypedDict import uuid @@ -402,6 +737,89 @@ graph.invoke( ) 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 @@ -415,7 +833,9 @@ critical in applications where the tool calls requested by the LLM may be sensit To add a human approval step to a tool: 1. Use `interrupt()` in the tool to pause execution. -2. Resume with a `Command(resume=...)` to continue based on human input. +2. Resume with a `Command` to continue based on human input. + +:::python ```python from langgraph.checkpoint.memory import InMemorySaver @@ -452,9 +872,64 @@ agent = create_react_agent( 1. The [`interrupt` function][langgraph.types.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](../memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](../../concepts/human_in_the_loop.md) 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][] 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](../memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](../../concepts/human_in_the_loop.md) 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": { @@ -472,9 +947,37 @@ for chunk in agent.stream( 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(resume=...)` to continue based on human input. +Resume the agent with a `Command` to continue based on human input. + +:::python ```python from langgraph.types import Command @@ -490,16 +993,40 @@ for chunk in agent.stream( ``` 1. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`][langgraph.types.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][] is used in conjunction with the [`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). +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.types import interrupt from langgraph.prebuilt.interrupt import HumanInterruptConfig, HumanInterrupt def add_human_in_the_loop( @@ -507,7 +1034,7 @@ def add_human_in_the_loop( *, interrupt_config: HumanInterruptConfig = None, ) -> BaseTool: - """Wrap a tool to support human-in-the-loop review.""" + """Wrap a tool to support human-in-the-loop review.""" if not isinstance(tool, BaseTool): tool = create_tool(tool) @@ -554,11 +1081,89 @@ def add_human_in_the_loop( ``` 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`][langgraph.prebuilt.interrupt.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=[...])`) +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`][langgraph.prebuilt.interrupt.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 `add_human_in_the_loop` wrapper to add `interrupt()` to any tool without having to add it *inside* the tool: +:::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 @@ -595,14 +1200,69 @@ for chunk in agent.stream( ``` 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. + ::: -> You should see that the agent runs until it reaches the `interrupt()` call, -> at which point it pauses and waits for human input. +:::js -Resume the agent with a `Command(resume=...)` to continue based on human input. +```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 +from langgraph.types import Command for chunk in agent.stream( # highlight-next-line @@ -614,10 +1274,34 @@ for chunk in agent.stream( 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 @@ -636,15 +1320,49 @@ def human_node(state: State): 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, + }; +}); +``` + +::: + ??? example "Extended example: validating user input" + :::python ```python from typing import TypedDict import uuid @@ -711,6 +1429,82 @@ def human_node(state: State): 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 + ``` + ::: ## Considerations @@ -722,27 +1516,44 @@ Place code with side effects, such as API calls, after the `interrupt` or in a s === "Side effects after interrupt" + :::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 + } + ``` + ::: === "Side effects in a separate node" + :::python ```python from langgraph.types import interrupt def human_node(state: State): """Human node with validation.""" - + answer = interrupt(question) - + return { "answer": answer } @@ -750,11 +1561,34 @@ Place code with side effects, such as API calls, after the `interrupt` or in a s 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. @@ -764,6 +1598,22 @@ def node_in_parent_graph(state: State): ... ``` +::: + +:::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); + // ... +} +``` + +::: + ??? example "Extended example: parent and subgraph execution flow" Say we have a parent graph with 3 nodes: @@ -785,6 +1635,7 @@ def node_in_parent_graph(state: State): 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 @@ -880,6 +1731,107 @@ def node_in_parent_graph(state: State): 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 @@ -887,16 +1839,17 @@ Using multiple interrupts within a **single** node can be helpful for patterns l 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. +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. ??? example "Extended example: incorrect code that introduces non-determinism" + :::python ```python import uuid from typing import TypedDict, Optional from langgraph.graph import StateGraph - from langgraph.constants import START + from langgraph.constants import START from langgraph.types import interrupt, Command from langgraph.checkpoint.memory import MemorySaver @@ -918,9 +1871,9 @@ To avoid issues, refrain from dynamically changing the node's structure between age = interrupt("what is your age?") else: age = "N/A" - + print(f"Name: {name}. Age: {age}") - + return { "age": age, "name": name, @@ -953,4 +1906,78 @@ To avoid issues, refrain from dynamically changing the node's structure between Name: N/A. Age: John {'human_node': {'age': 'John', 'name': 'N/A'}} ``` + ::: + :::js + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { z } from "zod"; + import { + StateGraph, + START, + interrupt, + Command, + MemorySaver + } from "@langchain/langgraph"; + + // The graph state. + const StateAnnotation = z.object({ + age: z.string().optional(), + name: z.string().optional(), + }); + + const builder = new StateGraph(StateAnnotation) + .addNode("humanNode", (state) => { + let name: string; + if (!state.name) { + name = interrupt("what is your name?") as string; + } else { + name = "N/A"; + } + + let age: string; + if (!state.age) { + age = interrupt("what is your age?") as string; + } else { + age = "N/A"; + } + + console.log(`Name: ${name}. Age: ${age}`); + + return { + age, + name, + }; + }) + .addEdge(START, "humanNode"); + + // A checkpointer must be enabled for interrupts to work! + const checkpointer = new MemorySaver(); + const graph = builder.compile({ checkpointer }); + + const config = { + configurable: { + thread_id: uuidv4(), + } + }; + + const stream = await graph.stream({ age: undefined, name: undefined }, config); + for await (const chunk of stream) { + console.log(chunk); + } + + const resumeStream = await graph.stream( + new Command({ resume: "John", update: { name: "foo" } }), + config + ); + for await (const chunk of resumeStream) { + console.log(chunk); + } + ``` + + ``` + { __interrupt__: [{ value: 'what is your name?', resumable: true, ns: ['humanNode:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when: 'during' }] } + Name: N/A. Age: John + { humanNode: { age: 'John', name: 'N/A' } } + ``` + ::: diff --git a/docs/docs/how-tos/human_in_the_loop/breakpoints.md b/docs/docs/how-tos/human_in_the_loop/breakpoints.md index 16c8fb552..ba982741e 100644 --- a/docs/docs/how-tos/human_in_the_loop/breakpoints.md +++ b/docs/docs/how-tos/human_in_the_loop/breakpoints.md @@ -10,7 +10,12 @@ To use breakpoints, you will need to: 1. [**Specify a checkpointer**](../../concepts/persistence.md#checkpoints) to save the graph state after each step. 2. **Set breakpoints** to specify where execution should pause. 3. **Run the graph** with a [**thread ID**](../../concepts/persistence.md#threads) to pause execution at the breakpoint. -4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` passing a `None` as the argument for the inputs. + +:::python 4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` passing a `None` as the argument for the inputs. +::: + +:::js 4. **Resume execution** using `invoke`/`stream` passing `null` as the argument for the inputs. +::: !!! tip @@ -18,13 +23,20 @@ To use breakpoints, you will need to: ## Static breakpoints +:::python Static breakpoints are triggered either before or after a node executes. You can set static breakpoints by specifying `interrupt_before` and `interrupt_after` at compile time or run time. +::: + +:::js +Static breakpoints are triggered either before or after a node executes. You can set static breakpoints by specifying `interruptBefore` and `interruptAfter` at compile time or run time. +::: Static breakpoints can be especially useful for debugging if you want to step through the graph execution one node at a time or if you want to pause the graph execution at specific nodes. === "Compile time" + :::python ```python # highlight-next-line graph = graph_builder.compile( # (1)! @@ -54,20 +66,54 @@ node at a time or if you want to pause the graph execution at specific nodes. 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. + ::: + + :::js + ```typescript + // highlight-next-line + const graph = graphBuilder.compile({ // (1)! + // highlight-next-line + interruptBefore: ["nodeA"], // (2)! + // highlight-next-line + interruptAfter: ["nodeB", "nodeC"], // (3)! + checkpointer: checkpointer, // (4)! + }); + + const config = { + configurable: { + thread_id: "some_thread" + } + }; + + // Run the graph until the breakpoint + await graph.invoke(inputs, config); // (5)! + + // Resume the graph + await graph.invoke(null, config); // (6)! + ``` + + 1. The breakpoints are set during `compile` time. + 2. `interruptBefore` specifies the nodes where execution should pause before the node is executed. + 3. `interruptAfter` 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 `null` for the input. This will run the graph until the next breakpoint is hit. + ::: === "Run time" + :::python ```python # highlight-next-line graph.invoke( # (1)! - inputs, + 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 = { @@ -88,6 +134,30 @@ node at a time or if you want to pause the graph execution at specific nodes. 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. + ::: + + :::js + ```typescript + const config = { + configurable: { thread_id: "some_thread" }, + // highlight-next-line + interruptBefore: ["nodeA"], // (1)! + // highlight-next-line + interruptAfter: ["nodeB", "nodeC"] // (2)! + }; + + // Run the graph until the breakpoint + await graph.invoke(inputs, config); // (3)! + + // Resume the graph + await graph.invoke(null, config); // (4)! + ``` + + 1. `interruptBefore` specifies the nodes where execution should pause before the node is executed. + 2. `interruptAfter` specifies the nodes where execution should pause after the node is executed. + 3. The graph is run until the first breakpoint is hit. + 4. The graph is resumed by passing in `null` for the input. This will run the graph until the next breakpoint is hit. + ::: !!! note @@ -96,33 +166,34 @@ node at a time or if you want to pause the graph execution at specific nodes. ??? example "Setting static breakpoints" + :::python ```python from IPython.display import Image, display from typing_extensions import TypedDict - - from langgraph.checkpoint.memory import InMemorySaver + + 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) @@ -131,42 +202,108 @@ node at a time or if you want to pause the graph execution at specific nodes. builder.add_edge("step_1", "step_2") builder.add_edge("step_2", "step_3") builder.add_edge("step_3", END) - - # Set up a checkpointer + + # 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) ``` + ::: + + :::js + ```typescript + import { z } from "zod"; + import { MemorySaver, StateGraph, START, END } from "@langchain/langgraph"; + + const State = z.object({ + input: z.string(), + }); + + const builder = new StateGraph(State) + .addNode("step1", (state) => { + console.log("---Step 1---"); + return state; + }) + .addNode("step2", (state) => { + console.log("---Step 2---"); + return state; + }) + .addNode("step3", (state) => { + console.log("---Step 3---"); + return state; + }) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + + // Set up a checkpointer + const checkpointer = new MemorySaver(); // (1)! + + const graph = builder.compile({ + checkpointer: checkpointer, // (2)! + interruptBefore: ["step3"] // (3)! + }); + + // Input + const initialInput = { input: "hello world" }; + + // Thread + const threadConfig = { configurable: { thread_id: "1" } }; + + // Run the graph until the first interruption + for await (const event of await graph.stream(initialInput, { + ...threadConfig, + streamMode: "values" + })) { + console.log(event); + } + + // This will run until the breakpoint + // You can get the state of the graph at this point + console.log(await graph.getState(threadConfig)); + + // You can continue the graph execution by passing in `null` for the input + for await (const event of await graph.stream(null, { + ...threadConfig, + streamMode: "values" + })) { + console.log(event); + } + ``` + ::: ## Dynamic breakpoints Use dynamic breakpoints if you need to interrupt the graph from inside a given node based on a condition. +:::python + ```python from langgraph.errors import NodeInterrupt @@ -181,9 +318,32 @@ def step_2(state: State) -> State: ``` 1. raise NodeInterrupt exception based on a some condition. In this example, we create a dynamic breakpoint if the length of the attribute `input` is longer than 5 characters. + ::: + +:::js + +```typescript +import { NodeInterrupt } from "@langchain/langgraph"; + +graph.addNode("step2", (state) => { + // highlight-next-line + if (state.input.length > 5) { + // highlight-next-line + throw new NodeInterrupt( // (1)! + `Received input that is longer than 5 characters: ${state.input}` + ); + } + return state; +}); +``` + +1. Throw NodeInterrupt exception based on some condition. In this example, we create a dynamic breakpoint if the length of the attribute `input` is longer than 5 characters. + :::
Using dynamic breakpoints +:::python + ```python from typing_extensions import TypedDict from IPython.display import Image, display @@ -288,17 +448,127 @@ print(state.next) print(state.tasks) ``` +::: + +:::js + +```typescript +import { z } from "zod"; +import { + StateGraph, + START, + END, + MemorySaver, + NodeInterrupt, +} from "@langchain/langgraph"; + +const State = z.object({ + input: z.string(), +}); + +const builder = new StateGraph(State) + .addNode("step1", (state) => { + console.log("---Step 1---"); + return state; + }) + .addNode("step2", (state) => { + console.log("---Step 2---"); + return state; + }) + .addNode("step3", (state) => { + console.log("---Step 3---"); + return state; + }) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + +// Set up memory +const memory = new MemorySaver(); + +// Compile the graph with memory +const graph = builder.compile({ checkpointer: memory }); +``` + +First, let's run the graph with an input that's <= 5 characters long. This should safely ignore the interrupt condition we defined and return the original input at the end of the graph execution. + +```typescript +const initialInput = { input: "hello" }; +const threadConfig = { configurable: { thread_id: "1" } }; + +for await (const event of await graph.stream(initialInput, { + ...threadConfig, + streamMode: "values", +})) { + console.log(event); +} +``` + +If we inspect the graph at this point, we can see that there are no more tasks left to run and that the graph indeed finished execution. + +```typescript +const state = await graph.getState(threadConfig); +console.log(state.next); +console.log(state.tasks); +``` + +Now, let's run the graph with an input that's longer than 5 characters. This should trigger the dynamic interrupt we defined via throwing a `NodeInterrupt` error inside the `step2` node. + +```typescript +const initialInput2 = { input: "hello world" }; +const threadConfig2 = { configurable: { thread_id: "2" } }; + +// Run the graph until the first interruption +for await (const event of await graph.stream(initialInput2, { + ...threadConfig2, + streamMode: "values", +})) { + console.log(event); +} +``` + +We can see that the graph now stopped while executing `step2`. If we inspect the graph state at this point, we can see the information on what node is set to execute next (`step2`), as well as what node raised the interrupt (also `step2`), and additional information about the interrupt. + +```typescript +const state2 = await graph.getState(threadConfig2); +console.log(state2.next); +console.log(state2.tasks); +``` + +If we try to resume the graph from the breakpoint, we will simply interrupt again as our inputs & graph state haven't changed. + +```typescript +// NOTE: to resume the graph from a dynamic interrupt we use the same syntax as with regular interrupts -- we pass null as the input +for await (const event of await graph.stream(null, { + ...threadConfig2, + streamMode: "values", +})) { + console.log(event); +} +``` + +```typescript +const state3 = await graph.getState(threadConfig2); +console.log(state3.next); +console.log(state3.tasks); +``` + +::: +
## Use with subgraphs To add breakpoints to subgraph either: -* Define [static breakpoints](#static-breakpoints) by specifying them when **compiling** the subgraph. -* Define [dynamic breakpoints](#dynamic-breakpoints). +- Define [static breakpoints](#static-breakpoints) by specifying them when **compiling** the subgraph. +- Define [dynamic breakpoints](#dynamic-breakpoints).
Add breakpoints to subgraphs +:::python + ```python from typing_extensions import TypedDict @@ -339,4 +609,47 @@ print(graph.get_state(config, subgraphs=True).tasks[0].state) graph.invoke(None, config) ``` -
\ No newline at end of file +::: + +:::js + +```typescript +import { z } from "zod"; +import { StateGraph, START, MemorySaver } from "@langchain/langgraph"; + +const State = z.object({ + foo: z.string(), +}); + +const subgraphBuilder = new StateGraph(State) + .addNode("subgraphNode1", (state) => { + return { foo: state.foo }; + }) + .addEdge(START, "subgraphNode1"); + +const subgraph = subgraphBuilder.compile({ + interruptBefore: ["subgraphNode1"], +}); + +const builder = new StateGraph(State) + .addNode("node1", subgraph) // directly include subgraph as a node + .addEdge(START, "node1"); + +const checkpointer = new MemorySaver(); +const graph = builder.compile({ checkpointer }); + +const config = { configurable: { thread_id: "1" } }; + +await graph.invoke({ foo: "" }, config); + +// Fetch state including subgraph state. +const state = await graph.getState(config, { subgraphs: true }); +console.log(state.tasks[0].state); + +// resume the subgraph +await graph.invoke(null, config); +``` + +::: + + diff --git a/docs/docs/how-tos/human_in_the_loop/time-travel.md b/docs/docs/how-tos/human_in_the_loop/time-travel.md index 4b1a78182..2808895de 100644 --- a/docs/docs/how-tos/human_in_the_loop/time-travel.md +++ b/docs/docs/how-tos/human_in_the_loop/time-travel.md @@ -2,11 +2,23 @@ To use [time-travel](../../concepts/time-travel.md) in LangGraph: +:::python + 1. [Run the graph](#1-run-the-graph) with initial inputs using [`invoke`][langgraph.graph.state.CompiledStateGraph.invoke] or [`stream`][langgraph.graph.state.CompiledStateGraph.stream] methods. 2. [Identify a checkpoint in an existing thread](#2-identify-a-checkpoint): Use the [`get_state_history()`][langgraph.graph.state.CompiledStateGraph.get_state_history] method to retrieve the execution history for a specific `thread_id` and locate the desired `checkpoint_id`. Alternatively, set a [breakpoint](../../concepts/breakpoints.md) 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 [`update_state`][langgraph.graph.state.CompiledStateGraph.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`][] or [`stream`][] methods. +2. [Identify a checkpoint in an existing thread](#2-identify-a-checkpoint): Use the [`getStateHistory()`][] method to retrieve the execution history for a specific `thread_id` and locate the desired `checkpoint_id`. + Alternatively, set a [breakpoint](../../concepts/breakpoints.md) 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`][] 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`. + ::: !!! tip @@ -20,13 +32,27 @@ This example builds a simple LangGraph workflow that generates a joke topic and 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 @@ -40,6 +66,16 @@ def _set_env(var: str): _set_env("ANTHROPIC_API_KEY") ``` +::: + +:::js + +```typescript +process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY"; +``` + +::: +

Set up LangSmith for LangGraph development

@@ -47,6 +83,8 @@ _set_env("ANTHROPIC_API_KEY")

+:::python + ```python import uuid @@ -97,8 +135,56 @@ 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": { @@ -112,7 +198,28 @@ 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! @@ -125,6 +232,8 @@ My blue argyle is now living in Bermuda with a red polka dot, posting vacation p ### 2. Identify a checkpoint +:::python + ```python # The states are returned in reverse chronological order. states = list(graph.get_state_history(config)) @@ -136,6 +245,7 @@ for state in states: ``` **Output:** + ``` () 1f02ac4a-ec9f-6524-8002-8f7b0bbeed0e @@ -150,6 +260,44 @@ for state in states: 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] @@ -158,13 +306,34 @@ 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 (optional) +:::python `update_state` will create a new checkpoint. The new checkpoint will be associated with the same thread, but a new checkpoint ID. ```python @@ -173,18 +342,61 @@ 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!'} -``` \ No newline at end of file +``` + +::: + +:::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/docs/docs/how-tos/streaming.md b/docs/docs/how-tos/streaming.md index bf25419c4..f5cb45aba 100644 --- a/docs/docs/how-tos/streaming.md +++ b/docs/docs/how-tos/streaming.md @@ -4,28 +4,41 @@ You can [stream outputs](../concepts/streaming.md) from a LangGraph agent or wor ## Supported stream modes +:::python Pass one or more of the following stream modes as a list to the [`stream()`][langgraph.graph.state.CompiledStateGraph.stream] or [`astream()`][langgraph.graph.state.CompiledStateGraph.astream] methods: +::: -| 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. +:::js +Pass one or more of the following stream modes as a list to the [`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()`][langgraph.graph.state.CompiledStateGraph.stream] or [`astream()`][langgraph.graph.state.CompiledStateGraph.astream] methods with `stream_mode="updates"`. This emits an event after every agent step. +::: + +:::js +To stream agent progress, use the [`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 +- **LLM node**: AI message with tool call requests +- **Tool node**: Tool message with execution result +- **LLM node**: Final AI response +:::python === "Sync" ```python @@ -60,8 +73,30 @@ For example, if you have an agent that calls a tool once, you should see the fol 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"`: === "Sync" @@ -100,8 +135,32 @@ To stream tokens as they are produced by the LLM, use `stream_mode="messages"`: 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][langgraph.config.get_stream_writer]. === "Sync" @@ -163,10 +222,51 @@ To stream updates from tools as they are executed, you can use [get_stream_write ``` !!! Note - If you add `get_stream_writer` inside your tool, you won't be able to invoke the tool outside of a LangGraph execution context. +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"); +} +``` + +!!! Note +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"]`: === "Sync" @@ -203,16 +303,39 @@ You can specify multiple streaming modes by passing stream mode as a list: `stre 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](../agents/multi-agent.md) systems to control which agents stream their output. See the [Models](../agents/models.md#disable-streaming) guide to learn how to disable streaming. -## Stream from a workflow +## Stream from a workflow ### Basic usage example +:::python LangGraph graphs expose the [`.stream()`][langgraph.pregel.Pregel.stream] (sync) and [`.astream()`][langgraph.pregel.Pregel.astream] (async) methods to yield streamed outputs as iterators. === "Sync" @@ -229,8 +352,24 @@ LangGraph graphs expose the [`.stream()`][langgraph.pregel.Pregel.stream] (sync) print(chunk) ``` +::: + +:::js +LangGraph graphs expose the [`.stream()`][] method to yield streamed outputs as iterators. + +```typescript +for await (const chunk of await graph.stream(inputs, { + streamMode: "updates", +})) { + console.log(chunk); +} +``` + +::: + ??? example "Extended example: streaming updates" + :::python ```python from typing import TypedDict from langgraph.graph import StateGraph, START, END @@ -266,14 +405,49 @@ LangGraph graphs expose the [`.stream()`][langgraph.pregel.Pregel.stream] (sync) 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 - {'refine_topic': {'topic': 'ice cream and cats'}} - {'generate_joke': {'joke': 'This is a joke about ice cream and cats'}} + {'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. @@ -292,12 +466,31 @@ The streamed outputs will be tuples of `(mode, chunk)` where `mode` is the name 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. +- `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 @@ -327,11 +520,39 @@ graph = ( ) ``` +::: + +:::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(); +``` + +::: + === "updates" 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"}, @@ -340,11 +561,24 @@ graph = ( ): print(chunk) ``` + ::: -=== "values" + :::js + ```typescript + for await (const chunk of await graph.stream( + { topic: "ice cream" }, + { streamMode: "updates" } + )) { + console.log(chunk); + } + ``` + ::: + +=== "values" Use this to stream the **full state** of the graph after each step. + :::python ```python for chunk in graph.stream( {"topic": "ice cream"}, @@ -353,10 +587,22 @@ graph = ( ): 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](../concepts/subgraphs.md) 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:")`. @@ -372,9 +618,31 @@ for chunk in graph.stream( ``` 1. Set `subgraphs=True` to stream outputs from subgraphs. + ::: + +:::js +To include outputs from [subgraphs](../concepts/subgraphs.md) 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. + ::: ??? example "Extended example: streaming from subgraphs" + :::python ```python from langgraph.graph import START, StateGraph from typing import TypedDict @@ -419,15 +687,77 @@ for chunk in graph.stream( ): print(chunk) ``` - - 1. Set `subgraphs=True` to stream outputs from subgraphs. + 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. @@ -435,6 +765,8 @@ for chunk in graph.stream( 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"}, @@ -444,18 +776,33 @@ for chunk in graph.stream( print(chunk) ``` +::: + +:::js + +```typescript +for await (const chunk of await graph.stream( + { topic: "ice cream" }, + { streamMode: "debug" } +)) { + console.log(chunk); +} +``` + +::: ### LLM tokens {#messages} 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. - + !!! warning "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+. @@ -503,12 +850,62 @@ for message_chunk, metadata in graph.stream( # (2)! 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 @@ -530,10 +927,43 @@ async for msg, metadata in graph.astream( # (3)! 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. + ::: ??? example "Extended example: filtering by tags" + :::python ```python from typing import TypedDict @@ -587,12 +1017,73 @@ async for msg, metadata in graph.astream( # (3)! 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, @@ -606,12 +1097,33 @@ for msg, metadata in graph.stream( # (1)! 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. + ::: ??? example "Extended example: streaming LLM tokens from specific nodes" + :::python ```python from typing import TypedDict - from langgraph.graph import START, StateGraph + from langgraph.graph import START, StateGraph from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini") @@ -661,9 +1173,59 @@ for msg, metadata in graph.stream( # (1)! 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. @@ -671,11 +1233,10 @@ To send **custom user-defined data** from inside a LangGraph node or tool, follo !!! warning "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. + 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. - === "node" ```python @@ -725,7 +1286,7 @@ To send **custom user-defined data** from inside a LangGraph node or tool, follo # perform query # highlight-next-line writer({"data": "Retrieved 100/100 records", "type": "progress"}) # (3)! - return "some-answer" + return "some-answer" graph = ... # define a graph that uses this tool @@ -739,9 +1300,84 @@ To send **custom user-defined data** from inside a LangGraph node or tool, follo 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"`. + +=== "node" + + ```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. + +=== "tool" + + ```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 -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. +:::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. @@ -778,10 +1414,52 @@ for chunk in graph.stream( 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. + ::: ??? example "Extended example: streaming arbitrary chat model" - ```python + + :::python + ````python import operator import json @@ -862,7 +1540,7 @@ for chunk in graph.stream( graph = ( - StateGraph(State) + StateGraph(State) .add_node(call_tool) .add_edge(START, "call_tool") .compile() @@ -897,17 +1575,137 @@ for chunk in graph.stream( ): 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 +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. === "init_chat_model" - + ```python from langchain.chat_models import init_chat_model @@ -930,11 +1728,28 @@ Set `disable_streaming=True` when initializing the model. 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 { #async } 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: +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. @@ -979,7 +1794,7 @@ This limits LangGraph ability to automatically propagate context, and affects La ``` 1. Accept `config` as an argument in the async node function. - 2. Pass `config` to `llm.ainvoke()` to ensure proper context propagation. + 2. Pass `config` to `llm.ainvoke()` to ensure proper context propagation. 3. Set `stream_mode="messages"` to stream LLM tokens. ??? example "Extended example: async custom streaming with stream writer" @@ -1014,3 +1829,5 @@ This limits LangGraph ability to automatically propagate context, and affects La 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/docs/docs/how-tos/tool-calling.md b/docs/docs/how-tos/tool-calling.md index a2e711ba3..8c8a682b6 100644 --- a/docs/docs/how-tos/tool-calling.md +++ b/docs/docs/how-tos/tool-calling.md @@ -1,11 +1,12 @@ # Call tools -[Tools](../concepts/tools.md) 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 determine the appropriate arguments. +[Tools](../concepts/tools.md) 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 @@ -18,16 +19,57 @@ def multiply(a: int, b: int) -> int: 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", @@ -43,9 +85,35 @@ Output: 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][langgraph.prebuilt.chat_agent_executor.create_react_agent]: ```python @@ -66,6 +134,44 @@ agent = create_react_agent( 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?" }], +}); +``` + +::: + ## Use in a workflow If you are writing a custom workflow, you will need to: @@ -73,7 +179,8 @@ 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 -Use `model.bind_tools()` to register the tools with the model. +:::python +Use `model.bind_tools()` to register the tools with the model. ```python from langchain.chat_models import init_chat_model @@ -84,10 +191,27 @@ model = init_chat_model(model="claude-3-5-haiku-latest") 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. ??? example "Extended example: attach tools to a chat model" + :::python ```python from langchain_core.tools import tool from langchain.chat_models import init_chat_model @@ -114,21 +238,62 @@ LLMs automatically determine if a tool invocation is necessary and handle callin 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`][langgraph.prebuilt.tool_node.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. +- 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`](../concepts/low_level.md#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. - +- **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 @@ -150,12 +315,68 @@ 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: [...] }); +``` + +::: + ??? example "Single tool call" + :::python ```python from langchain_core.messages import AIMessage from langgraph.prebuilt import ToolNode - + # Define tools @tool def get_weather(location: str): @@ -164,10 +385,10 @@ tool_node.invoke({"messages": [...]}) 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=[ @@ -179,33 +400,83 @@ tool_node.invoke({"messages": [...]}) } ], ) - + 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" }] } + ``` + ::: ??? example "Multiple tool calls" + :::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]) @@ -241,30 +512,105 @@ tool_node.invoke({"messages": [...]}) ] } ``` + ::: + :::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" } + ] + } + ``` + ::: ??? example "Use with a chat model" + :::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]}) @@ -275,58 +621,103 @@ tool_node.invoke({"messages": [...]}) ``` {'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" }] } + ``` + ::: ??? example "Use in a tool-calling agent" This is an example of creating a tool-calling agent from scratch using `ToolNode`. You can also use LangGraph's prebuilt [agent](../agents/agents.md). + :::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': [ @@ -340,12 +731,92 @@ tool_node.invoke({"messages": [...]}) ] } ``` + ::: + :::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 ### Parameter descriptions +:::python Auto-generate descriptions from docstrings: ```python @@ -364,8 +835,36 @@ def multiply(a: int, b: int) -> int: 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 @@ -383,9 +882,13 @@ def multiply(a: int, b: int) -> int: return a * b ``` +::: + ### Tool name -Override the default tool name (function name) using the first argument: +Override the default tool name using the first argument or name property: + +:::python ```python from langchain_core.tools import tool @@ -397,18 +900,45 @@ def multiply(a: int, b: int) -> int: 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](#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 @@ -430,13 +960,47 @@ agent.invoke( ) ``` +::: + +:::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" } } +); +``` + +::: + ??? example "Extended example: Access config in tools" + :::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, @@ -445,24 +1009,61 @@ agent.invoke( # 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. +Short-term memory maintains **dynamic** state that changes during a single execution. -To **access** (read) the graph state inside the tools, you can use a special parameter **annotation** — [`InjectedState`][langgraph.prebuilt.InjectedState]: +:::python +To **access** (read) the graph state inside the tools, you can use a special parameter **annotation** — [`InjectedState`][langgraph.prebuilt.InjectedState]: ```python from typing import Annotated, NotRequired @@ -494,6 +1095,38 @@ agent = create_react_agent( agent.invoke({"messages": "what's my name?"}) ``` +::: + +:::js +To **access** (read) the graph state inside the tools, you can use the [`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 @@ -522,17 +1155,79 @@ def update_user_name( }) ``` +::: + +:::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"), + }), + } +); +``` + +::: + !!! important + :::python If you want to use tools that return `Command` and update graph state, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.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`][] / [`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 @@ -541,8 +1236,9 @@ Use [long-term memory](../concepts/memory.md#long-term-memory) to store user-spe To use long-term memory, you need to: 1. [Configure a store](memory/add-memory.md#add-long-term-memory) to persist data across invocations. -2. Use the [`get_store`][langgraph.config.get_store] function to access the store from within tools or prompts. +2. Access the store from within tools. +:::python To **access** information in the store: ```python @@ -555,7 +1251,7 @@ 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)` + # Same as that provided to `builder.compile(store=store)` # or `create_react_agent` # highlight-next-line store = get_store() @@ -569,18 +1265,52 @@ 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({}), + } +); +``` + +::: + ??? example "Access long-term memory" + :::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)! @@ -601,14 +1331,14 @@ graph = builder.compile(store=store) # 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"}]}, @@ -616,7 +1346,7 @@ graph = builder.compile(store=store) 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][../reference/store.md) 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][langgraph.store.base.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. @@ -625,7 +1355,75 @@ graph = builder.compile(store=store) 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](../reference/store.md) 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 @@ -638,7 +1436,7 @@ 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)` + # Same as that provided to `builder.compile(store=store)` # or `create_react_agent` # highlight-next-line store = get_store() @@ -652,8 +1450,44 @@ 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"), + }), + } +); +``` + +::: + ??? example "Update long-term memory" + :::python ```python from typing_extensions import TypedDict @@ -661,9 +1495,9 @@ graph = builder.compile(store=store) from langgraph.config import get_store from langgraph.prebuilt import create_react_agent from langgraph.store.memory import InMemoryStore - + store = InMemoryStore() # (1)! - + class UserInfo(TypedDict): # (2)! name: str @@ -677,36 +1511,99 @@ graph = builder.compile(store=store) # 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](../reference/store.md) 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](../reference/store.md) 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. @@ -719,8 +1616,40 @@ def add(a: int, b: int) -> int: 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, + } +); +``` + +::: + ??? example "Extended example: Using return_direct in a prebuilt agent" + :::python ```python from langchain_core.tools import tool from langgraph.prebuilt import create_react_agent @@ -740,20 +1669,63 @@ def add(a: int, b: int) -> int: {"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?" }] + }); + ``` + ::: !!! important "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. +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: @@ -768,11 +1740,42 @@ configured_model = model.bind_tools( # 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" } } +); +``` + +::: + ??? example "Extended example: Force tool usage in an agent" + :::python To force the agent to use specific tools, you can set the `tool_choice` option in `model.bind_tools()`: ```python @@ -796,14 +1799,63 @@ configured_model = model.bind_tools( {"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" }] + }); + ``` + ::: !!! Warning "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. + - Mark the tool with [`return_direct=True`](#immediate-return) to end the loop after execution. - Set [`recursion_limit`](../concepts/low_level.md#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`](../concepts/low_level.md#recursion-limit) to restrict the number of execution steps. + ::: !!! tip "Tool choice configuration" @@ -813,18 +1865,35 @@ configured_model = model.bind_tools( ### 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, + 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 } +); +``` + +::: + ??? example "Extended example: disable parallel tool calls in a prebuilt agent" + :::python ```python from langchain.chat_models import init_chat_model @@ -849,9 +1918,62 @@ model.bind_tools( {"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][langgraph.prebuilt.tool_node.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. @@ -894,19 +2016,96 @@ Output: ]} ``` +::: + +:::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 `handle_tool_errors` to a string: +Provide a custom error message by setting the error handling parameter to a string: + +:::python ```python tool_node = ToolNode( @@ -928,8 +2127,35 @@ Example output: ]} ``` +::: + +:::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 @@ -960,6 +2186,45 @@ agent_custom = create_react_agent( 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. @@ -972,13 +2237,14 @@ See [`langgraph-bigtool`](https://github.com/langchain-ai/langgraph-bigtool) pre ### 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", + model="openai:gpt-4o-mini", tools=[{"type": "web_search_preview"}] ) response = agent.invoke( @@ -987,11 +2253,35 @@ response = agent.invoke( ``` 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: @@ -1003,4 +2293,18 @@ Some commonly used tool categories include: - **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/docs/docs/how-tos/ttl/configure_ttl.md b/docs/docs/how-tos/ttl/configure_ttl.md index a5f1e0b05..de721d624 100644 --- a/docs/docs/how-tos/ttl/configure_ttl.md +++ b/docs/docs/how-tos/ttl/configure_ttl.md @@ -16,6 +16,7 @@ Checkpoints capture the state of conversation threads. Setting a TTL ensures old Add a `checkpointer.ttl` configuration to your `langgraph.json` file: +:::python ```json { "dependencies": ["."], @@ -31,6 +32,25 @@ Add a `checkpointer.ttl` configuration to your `langgraph.json` file: } } ``` +::: + +:::js +```json +{ + "dependencies": ["."], + "graphs": { + "agent": "./agent.ts:graph" + }, + "checkpointer": { + "ttl": { + "strategy": "delete", + "sweep_interval_minutes": 60, + "default_ttl": 43200 + } + } +} +``` +::: * `strategy`: Specifies the action taken on expiration. Currently, only `"delete"` is supported, which deletes all checkpoints in the thread upon expiration. * `sweep_interval_minutes`: Defines how often, in minutes, the system checks for expired checkpoints. @@ -42,6 +62,7 @@ Store items allow cross-thread data persistence. Configuring TTL for store items Add a `store.ttl` configuration to your `langgraph.json` file: +:::python ```json { "dependencies": ["."], @@ -57,6 +78,25 @@ Add a `store.ttl` configuration to your `langgraph.json` file: } } ``` +::: + +:::js +```json +{ + "dependencies": ["."], + "graphs": { + "agent": "./agent.ts:graph" + }, + "store": { + "ttl": { + "refresh_on_read": true, + "sweep_interval_minutes": 120, + "default_ttl": 10080 + } + } +} +``` +::: * `refresh_on_read`: (Optional, default `true`) If `true`, accessing an item via `get` or `search` resets its expiration timer. If `false`, TTL only refreshes on `put`. * `sweep_interval_minutes`: (Optional) Defines how often, in minutes, the system checks for expired items. If omitted, no sweeping occurs. @@ -66,6 +106,7 @@ Add a `store.ttl` configuration to your `langgraph.json` file: You can configure TTLs for both checkpoints and store items in the same `langgraph.json` file to set different policies for each data type. Here is an example: +:::python ```json { "dependencies": ["."], @@ -88,6 +129,32 @@ You can configure TTLs for both checkpoints and store items in the same `langgra } } ``` +::: + +:::js +```json +{ + "dependencies": ["."], + "graphs": { + "agent": "./agent.ts:graph" + }, + "checkpointer": { + "ttl": { + "strategy": "delete", + "sweep_interval_minutes": 60, + "default_ttl": 43200 + } + }, + "store": { + "ttl": { + "refresh_on_read": true, + "sweep_interval_minutes": 120, + "default_ttl": 10080 + } + } +} +``` +::: ## Runtime Overrides @@ -97,6 +164,4 @@ The default `store.ttl` settings from `langgraph.json` can be overridden at runt After configuring TTLs in `langgraph.json`, deploy or restart your LangGraph application for the changes to take effect. Use `langgraph dev` for local development or `langgraph up` for Docker deployment. - -See the [langgraph.json CLI reference][configuration-file] for more details on the other configurable options. - +See the [langgraph.json CLI reference][configuration-file] for more details on the other configurable options. \ No newline at end of file diff --git a/docs/docs/how-tos/use-functional-api.md b/docs/docs/how-tos/use-functional-api.md index 340bbb347..914667cf5 100644 --- a/docs/docs/how-tos/use-functional-api.md +++ b/docs/docs/how-tos/use-functional-api.md @@ -4,6 +4,8 @@ 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: @@ -11,11 +13,33 @@ def my_workflow(inputs: dict) -> int: another_value = inputs["another_value"] ... -my_workflow.invoke({"value": 1, "another_value": 2}) +my_workflow.invoke({"value": 1, "another_value": 2}) ``` -??? example "Extended example: simple workflow" +::: +:::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 }); +``` + +::: + +??? example "Extended example: simple workflow" + + :::python ```python import uuid from langgraph.func import entrypoint, task @@ -45,6 +69,41 @@ my_workflow.invoke({"value": 1, "another_value": 2}) 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); + ``` + ::: ??? example "Extended example: Compose an essay with an LLM" @@ -52,6 +111,7 @@ my_workflow.invoke({"value": 1, "another_value": 2}) 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 @@ -82,11 +142,50 @@ my_workflow.invoke({"value": 1, "another_value": 2}) 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: @@ -98,11 +197,30 @@ def graph(numbers: list[int]) -> list[str]: 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)); + } +); +``` + +::: ??? example "Extended example: parallel LLM calls" 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 @@ -136,13 +254,53 @@ def graph(numbers: list[int]) -> list[str]: 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 +## Calling graphs The **Functional API** and the [**Graph API**](../concepts/low_level.md) 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 @@ -163,8 +321,38 @@ def some_workflow(some_input: dict) -> int: } ``` +::: + +:::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, + }; + } +); +``` + +::: + ??? example "Extended example: calling a simple graph from the functional API" + :::python ```python import uuid from typing import TypedDict @@ -198,12 +386,51 @@ def some_workflow(some_input: dict) -> int: 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: @@ -215,8 +442,33 @@ def my_workflow(inputs: dict) -> int: 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; + } +); +``` + +::: + ??? example "Extended example: calling another entrypoint" + :::python ```python import uuid from langgraph.func import entrypoint @@ -240,7 +492,38 @@ def my_workflow(inputs: dict) -> int: 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 @@ -249,6 +532,8 @@ read the [**streaming guide**](../concepts/streaming.md) section for more detail 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 MemorySaver @@ -290,11 +575,9 @@ for mode, chunk in main.stream( # (5)! ('updates', {'main': 5}) ``` - - !!! important "Async with Python < 3.11" - If using Python < 3.11 and writing async code, using `get_stream_writer()` will not work. Instead please + 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](../how-tos/streaming.md#async) for more details. ```python @@ -306,8 +589,62 @@ for mode, chunk in main.stream( # (5)! ... ``` +::: + +:::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 MemorySaver from langgraph.func import entrypoint, task @@ -321,7 +658,7 @@ attempts = 0 # The default RetryPolicy is optimized for retrying specific network errors. retry_policy = RetryPolicy(retry_on=ValueError) -@task(retry_policy=retry_policy) +@task(retry_policy=retry_policy) def get_info(): global attempts attempts += 1 @@ -349,8 +686,69 @@ main.invoke({'any_input': 'foobar'}, config=config) '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 @@ -380,9 +778,57 @@ for chunk in main.stream({"x": 5}, stream_mode="updates"): ``` 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 MemorySaver @@ -458,6 +904,87 @@ main.invoke(None, config=config) '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](../concepts/human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive. @@ -470,6 +997,8 @@ We will create three [tasks](../concepts/functional_api.md#task): 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 @@ -492,10 +1021,38 @@ def human_feedback(input_query): 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](../concepts/functional_api.md#entrypoint): +:::python + ```python from langgraph.checkpoint.memory import MemorySaver @@ -511,10 +1068,35 @@ def graph(input_query): 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()](../how-tos/human_in_the_loop/add-human-in-the-loop.md#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"}} @@ -523,14 +1105,49 @@ for event in graph.stream("foo", config): 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](../how-tos/human_in_the_loop/add-human-in-the-loop.md#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 @@ -543,6 +1160,8 @@ Given a tool call, our function will `interrupt` for human review. At that point - 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 @@ -567,8 +1186,47 @@ def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]: ) ``` +::: + +:::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](../concepts/functional_api.md#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 MemorySaver from langgraph.graph.message import add_messages @@ -618,6 +1276,80 @@ def agent(messages, previous): 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](../concepts/functional_api.md#short-term-memory) for more details. @@ -628,6 +1360,8 @@ You can view and delete the information stored by the checkpointer. #### View thread state (checkpoint) +:::python + ```python config = { "configurable": { @@ -637,7 +1371,7 @@ config = { # otherwise the latest checkpoint is shown # highlight-next-line # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" - + } } # highlight-next-line @@ -646,7 +1380,7 @@ 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=(), + 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', @@ -656,14 +1390,63 @@ StateSnapshot( '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'}}, + 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 (checkpoints) +:::python + ```python config = { "configurable": { @@ -678,9 +1461,9 @@ 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'}}, + 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'}}, @@ -688,8 +1471,8 @@ list(graph.get_state_history(config)) 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',), + 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', @@ -698,9 +1481,9 @@ list(graph.get_state_history(config)) 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={...}, + 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={...} @@ -708,9 +1491,9 @@ list(graph.get_state_history(config)) 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={...}, + 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={...} @@ -718,34 +1501,79 @@ list(graph.get_state_history(config)) 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', + 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?')}),), + 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__',), + 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', + 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"}]}),), + 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. +- 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 @@ -768,11 +1596,41 @@ 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 `MemorySaver` 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 @@ -807,6 +1665,72 @@ 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); +} +``` + +::: + ??? example "Extended example: build a simple chatbot" [How to add thread-level persistence (functional API)](./persistence-functional.ipynb): Shows how to add thread-level persistence to a functional API workflow and implements a simple chatbot. @@ -815,21 +1739,20 @@ for chunk in workflow.stream([input_message], config, stream_mode="values"): [long-term memory](../concepts/memory.md#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. - ??? example "Extended example: add long-term memory" [How to add cross-thread persistence (functional API)](./cross-thread-persistence-functional.ipynb): Shows how to add cross-thread persistence to a functional API workflow and implements a simple chatbot. ## Workflows -* [Workflows and agent](../tutorials/workflows.md) guide for more examples of how to build workflows using the Functional API. +- [Workflows and agent](../tutorials/workflows.md) guide for more examples of how to build workflows using the Functional API. ## Agents -* [How to create an agent from scratch (Functional API)](./react-agent-from-scratch-functional.ipynb): Shows how to create a simple agent from scratch using the functional API. -* [How to build a multi-agent network](./multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API. -* [How to add multi-turn conversation in a multi-agent application (functional API)](./multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents. +- [How to create an agent from scratch (Functional API)](./react-agent-from-scratch-functional.ipynb): Shows how to create a simple agent from scratch using the functional API. +- [How to build a multi-agent network](./multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API. +- [How to add multi-turn conversation in a multi-agent application (functional API)](./multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents. ## Integrate with other libraries -* [Add LangGraph's features to other frameworks using the functional API](./autogen-integration-functional.ipynb): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box. \ No newline at end of file +- [Add LangGraph's features to other frameworks using the functional API](./autogen-integration-functional.ipynb): Add LangGraph features like persistence, memory and streaming to other agent frameworks that do not provide them out of the box. diff --git a/docs/docs/how-tos/use-remote-graph.md b/docs/docs/how-tos/use-remote-graph.md index 819c46f4b..052cbaa18 100644 --- a/docs/docs/how-tos/use-remote-graph.md +++ b/docs/docs/how-tos/use-remote-graph.md @@ -1,16 +1,16 @@ # How to interact with the deployment using RemoteGraph -!!! info "Prerequisites" - - [LangGraph Platform](../concepts/langgraph_platform.md) - - [LangGraph Server](../concepts/langgraph_server.md) +!!! info "Prerequisites" - [LangGraph Platform](../concepts/langgraph_platform.md) - [LangGraph Server](../concepts/langgraph_server.md) `RemoteGraph` is an interface that allows you to interact with your LangGraph Platform deployment as if it were a regular, locally-defined LangGraph graph (e.g. a `CompiledGraph`). This guide shows you how you can initialize a `RemoteGraph` and interact with it. ## Initializing the graph +:::python + When initializing a `RemoteGraph`, you must always specify: -- `name`: the name of the graph you want to interact with. This is the same graph name you use in `langgraph.json` configuration file for your deployment. +- `name`: the name of the graph you want to interact with. This is the same graph name you use in `langgraph.json` configuration file for your deployment. - `api_key`: a valid LangSmith API key. Can be set as an environment variable (`LANGSMITH_API_KEY`) or passed directly via the `api_key` argument. The API key could also be provided via the `client` / `sync_client` arguments, if `LangGraphClient` / `SyncLangGraphClient` were initialized with `api_key` argument. Additionally, you have to provide one of the following: @@ -23,57 +23,81 @@ Additionally, you have to provide one of the following: If you pass both `client` or `sync_client` as well as `url` argument, they will take precedence over the `url` argument. If none of the `client` / `sync_client` / `url` arguments are provided, `RemoteGraph` will raise a `ValueError` at runtime. +::: + +:::js + +When initializing a `RemoteGraph`, you must always specify: + +- `name`: the name of the graph you want to interact with. This is the same graph name you use in `langgraph.json` configuration file for your deployment. +- `apiKey`: a valid LangSmith API key. Can be set as an environment variable (`LANGSMITH_API_KEY`) or passed directly via the `apiKey` argument. The API key could also be provided via the `client`if `LangGraphClient` were initialized with `apiKey` argument. + +Additionally, you have to provide one of the following: + +- `url`: URL of the deployment you want to interact with. If you pass `url` argument, both sync and async clients will be created using the provided URL, headers (if provided) and default configuration values (e.g. timeout, etc). +- `client`: a `LangGraphClient` instance for interacting with the deployment asynchronously + +::: ### Using URL -=== "Python" +:::python - ```python - from langgraph.pregel.remote import RemoteGraph +```python +from langgraph.pregel.remote import RemoteGraph - url = - graph_name = "agent" - remote_graph = RemoteGraph(graph_name, url=url) - ``` +url = +graph_name = "agent" +remote_graph = RemoteGraph(graph_name, url=url) +``` -=== "JavaScript" +::: - ```ts - import { RemoteGraph } from "@langchain/langgraph/remote"; +:::js - const url = ``; - const graphName = "agent"; - const remoteGraph = new RemoteGraph({ graphId: graphName, url }); - ``` +```ts +import { RemoteGraph } from "@langchain/langgraph/remote"; + +const url = ``; +const graphName = "agent"; +const remoteGraph = new RemoteGraph({ graphId: graphName, url }); +``` + +::: ### Using clients -=== "Python" +:::python - ```python - from langgraph_sdk import get_client, get_sync_client - from langgraph.pregel.remote import RemoteGraph +```python +from langgraph_sdk import get_client, get_sync_client +from langgraph.pregel.remote import RemoteGraph - url = - graph_name = "agent" - client = get_client(url=url) - sync_client = get_sync_client(url=url) - remote_graph = RemoteGraph(graph_name, client=client, sync_client=sync_client) - ``` +url = +graph_name = "agent" +client = get_client(url=url) +sync_client = get_sync_client(url=url) +remote_graph = RemoteGraph(graph_name, client=client, sync_client=sync_client) +``` -=== "JavaScript" +::: - ```ts - import { Client } from "@langchain/langgraph-sdk"; - import { RemoteGraph } from "@langchain/langgraph/remote"; +:::js - const client = new Client({ apiUrl: `` }); - const graphName = "agent"; - const remoteGraph = new RemoteGraph({ graphId: graphName, client }); - ``` +```ts +import { Client } from "@langchain/langgraph-sdk"; +import { RemoteGraph } from "@langchain/langgraph/remote"; + +const client = new Client({ apiUrl: `` }); +const graphName = "agent"; +const remoteGraph = new RemoteGraph({ graphId: graphName, client }); +``` + +::: ## Invoking the graph +:::python Since `RemoteGraph` is a `Runnable` that implements the same methods as `CompiledGraph`, you can interact with it the same way you normally would with a compiled graph, i.e. by calling `.invoke()`, `.stream()`, `.get_state()`, `.update_state()`, etc (as well as their async counterparts). ### Asynchronously @@ -82,35 +106,18 @@ Since `RemoteGraph` is a `Runnable` that implements the same methods as `Compile To use the graph asynchronously, you must provide either the `url` or `client` when initializing the `RemoteGraph`. -=== "Python" +```python +# invoke the graph +result = await remote_graph.ainvoke({ + "messages": [{"role": "user", "content": "what's the weather in sf"}] +}) - ```python - # invoke the graph - result = await remote_graph.ainvoke({ - "messages": [{"role": "user", "content": "what's the weather in sf"}] - }) - - # stream outputs from the graph - async for chunk in remote_graph.astream({ - "messages": [{"role": "user", "content": "what's the weather in la"}] - }): - print(chunk) - ``` - -=== "JavaScript" - - ```ts - // invoke the graph - const result = await remoteGraph.invoke({ - messages: [{role: "user", content: "what's the weather in sf"}] - }) - - // stream outputs from the graph - for await (const chunk of await remoteGraph.stream({ - messages: [{role: "user", content: "what's the weather in la"}] - })): - console.log(chunk) - ``` +# stream outputs from the graph +async for chunk in remote_graph.astream({ + "messages": [{"role": "user", "content": "what's the weather in la"}] +}): + print(chunk) +``` ### Synchronously @@ -118,72 +125,97 @@ Since `RemoteGraph` is a `Runnable` that implements the same methods as `Compile To use the graph synchronously, you must provide either the `url` or `sync_client` when initializing the `RemoteGraph`. -=== "Python" +```python +# invoke the graph +result = remote_graph.invoke({ + "messages": [{"role": "user", "content": "what's the weather in sf"}] +}) - ```python - # invoke the graph - result = remote_graph.invoke({ - "messages": [{"role": "user", "content": "what's the weather in sf"}] - }) +# stream outputs from the graph +for chunk in remote_graph.stream({ + "messages": [{"role": "user", "content": "what's the weather in la"}] +}): + print(chunk) +``` - # stream outputs from the graph - for chunk in remote_graph.stream({ - "messages": [{"role": "user", "content": "what's the weather in la"}] - }): - print(chunk) - ``` +::: + +:::js +Since `RemoteGraph` is a `Runnable` that implements the same methods as `CompiledGraph`, you can interact with it the same way you normally would with a compiled graph, i.e. by calling `.invoke()`, `.stream()`, `.getState()`, `.updateState()`, etc. + +```ts +// invoke the graph +const result = await remoteGraph.invoke({ + messages: [{role: "user", content: "what's the weather in sf"}] +}) + +// stream outputs from the graph +for await (const chunk of await remoteGraph.stream({ + messages: [{role: "user", content: "what's the weather in la"}] +})): + console.log(chunk) +``` + +::: ## Thread-level persistence By default, the graph runs (i.e. `.invoke()` or `.stream()` invocations) are stateless - the checkpoints and the final state of the graph are not persisted. If you would like to persist the outputs of the graph run (for example, to enable human-in-the-loop features), you can create a thread and provide the thread ID via the `config` argument, same as you would with a regular compiled graph: -=== "Python" +:::python - ```python - from langgraph_sdk import get_sync_client - url = - graph_name = "agent" - sync_client = get_sync_client(url=url) - remote_graph = RemoteGraph(graph_name, url=url) +```python +from langgraph_sdk import get_sync_client +url = +graph_name = "agent" +sync_client = get_sync_client(url=url) +remote_graph = RemoteGraph(graph_name, url=url) - # create a thread (or use an existing thread instead) - thread = sync_client.threads.create() +# create a thread (or use an existing thread instead) +thread = sync_client.threads.create() - # invoke the graph with the thread config - config = {"configurable": {"thread_id": thread["thread_id"]}} - result = remote_graph.invoke({ - "messages": [{"role": "user", "content": "what's the weather in sf"}] - }, config=config) +# invoke the graph with the thread config +config = {"configurable": {"thread_id": thread["thread_id"]}} +result = remote_graph.invoke({ + "messages": [{"role": "user", "content": "what's the weather in sf"}] +}, config=config) - # verify that the state was persisted to the thread - thread_state = remote_graph.get_state(config) - print(thread_state) - ``` +# verify that the state was persisted to the thread +thread_state = remote_graph.get_state(config) +print(thread_state) +``` -=== "JavaScript" +::: - ```ts - import { Client } from "@langchain/langgraph-sdk"; - import { RemoteGraph } from "@langchain/langgraph/remote"; +:::js - const url = ``; - const graphName = "agent"; - const client = new Client({ apiUrl: url }); - const remoteGraph = new RemoteGraph({ graphId: graphName, url }); +```ts +import { Client } from "@langchain/langgraph-sdk"; +import { RemoteGraph } from "@langchain/langgraph/remote"; - // create a thread (or use an existing thread instead) - const thread = await client.threads.create(); +const url = ``; +const graphName = "agent"; +const client = new Client({ apiUrl: url }); +const remoteGraph = new RemoteGraph({ graphId: graphName, url }); - // invoke the graph with the thread config - const config = { configurable: { thread_id: thread.thread_id }}; - const result = await remoteGraph.invoke({ - messages: [{ role: "user", content: "what's the weather in sf" }], - }, config); +// create a thread (or use an existing thread instead) +const thread = await client.threads.create(); - // verify that the state was persisted to the thread - const threadState = await remoteGraph.getState(config); - console.log(threadState); - ``` +// invoke the graph with the thread config +const config = { configurable: { thread_id: thread.thread_id } }; +const result = await remoteGraph.invoke( + { + messages: [{ role: "user", content: "what's the weather in sf" }], + }, + config +); + +// verify that the state was persisted to the thread +const threadState = await remoteGraph.getState(config); +console.log(threadState); +``` + +::: ## Using as a subgraph @@ -191,66 +223,72 @@ By default, the graph runs (i.e. `.invoke()` or `.stream()` invocations) are sta If you need to use a `checkpointer` with a graph that has a `RemoteGraph` subgraph node, make sure to use UUIDs as thread IDs. - Since the `RemoteGraph` behaves the same way as a regular `CompiledGraph`, it can be also used as a subgraph in another graph. For example: -=== "Python" +:::python - ```python - from langgraph_sdk import get_sync_client - from langgraph.graph import StateGraph, MessagesState, START - from typing import TypedDict +```python +from langgraph_sdk import get_sync_client +from langgraph.graph import StateGraph, MessagesState, START +from typing import TypedDict - url = - graph_name = "agent" - remote_graph = RemoteGraph(graph_name, url=url) +url = +graph_name = "agent" +remote_graph = RemoteGraph(graph_name, url=url) - # define parent graph - builder = StateGraph(MessagesState) - # add remote graph directly as a node - builder.add_node("child", remote_graph) - builder.add_edge(START, "child") - graph = builder.compile() +# define parent graph +builder = StateGraph(MessagesState) +# add remote graph directly as a node +builder.add_node("child", remote_graph) +builder.add_edge(START, "child") +graph = builder.compile() - # invoke the parent graph - result = graph.invoke({ - "messages": [{"role": "user", "content": "what's the weather in sf"}] - }) - print(result) +# invoke the parent graph +result = graph.invoke({ + "messages": [{"role": "user", "content": "what's the weather in sf"}] +}) +print(result) - # stream outputs from both the parent graph and subgraph - for chunk in graph.stream({ - "messages": [{"role": "user", "content": "what's the weather in sf"}] - }, subgraphs=True): - print(chunk) - ``` +# stream outputs from both the parent graph and subgraph +for chunk in graph.stream({ + "messages": [{"role": "user", "content": "what's the weather in sf"}] +}, subgraphs=True): + print(chunk) +``` -=== "JavaScript" +::: - ```ts - import { MessagesAnnotation, StateGraph, START } from "@langchain/langgraph"; - import { RemoteGraph } from "@langchain/langgraph/remote"; +:::js - const url = ``; - const graphName = "agent"; - const remoteGraph = new RemoteGraph({ graphId: graphName, url }); +```ts +import { MessagesAnnotation, StateGraph, START } from "@langchain/langgraph"; +import { RemoteGraph } from "@langchain/langgraph/remote"; - // define parent graph and add remote graph directly as a node - const graph = new StateGraph(MessagesAnnotation) - .addNode("child", remoteGraph) - .addEdge(START, "child") - .compile() +const url = ``; +const graphName = "agent"; +const remoteGraph = new RemoteGraph({ graphId: graphName, url }); - // invoke the parent graph - const result = await graph.invoke({ - messages: [{ role: "user", content: "what's the weather in sf" }] - }); - console.log(result); +// define parent graph and add remote graph directly as a node +const graph = new StateGraph(MessagesAnnotation) + .addNode("child", remoteGraph) + .addEdge(START, "child") + .compile(); - // stream outputs from both the parent graph and subgraph - for await (const chunk of await graph.stream({ - messages: [{ role: "user", content: "what's the weather in la" }] - }, { subgraphs: true })) { - console.log(chunk); - } - ``` \ No newline at end of file +// invoke the parent graph +const result = await graph.invoke({ + messages: [{ role: "user", content: "what's the weather in sf" }], +}); +console.log(result); + +// stream outputs from both the parent graph and subgraph +for await (const chunk of await graph.stream( + { + messages: [{ role: "user", content: "what's the weather in la" }], + }, + { subgraphs: true } +)) { + console.log(chunk); +} +``` + +::: diff --git a/docs/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT.md b/docs/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT.md index 332294d16..5a2c9bed2 100644 --- a/docs/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT.md +++ b/docs/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT.md @@ -3,6 +3,8 @@ 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 @@ -17,13 +19,52 @@ 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}) -``` \ No newline at end of file +``` + +::: + +:::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/docs/docs/troubleshooting/errors/INVALID_CHAT_HISTORY.md b/docs/docs/troubleshooting/errors/INVALID_CHAT_HISTORY.md index 7582dd515..068de4068 100644 --- a/docs/docs/troubleshooting/errors/INVALID_CHAT_HISTORY.md +++ b/docs/docs/troubleshooting/errors/INVALID_CHAT_HISTORY.md @@ -1,16 +1,32 @@ # INVALID_CHAT_HISTORY +:::python This error is raised in the prebuilt [create_react_agent][langgraph.prebuilt.chat_agent_executor.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](insert-ref) 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][langgraph.prebuilt.tool_node.ToolNode] (`"tools"`) + 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][langgraph.prebuilt.tool_node.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](insert-ref) (`"tools"`) + ::: ## Troubleshooting @@ -19,12 +35,20 @@ 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: - - 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: +:::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)` + +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/docs/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md b/docs/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md index c87a1ce49..a5d200448 100644 --- a/docs/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md +++ b/docs/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md @@ -6,6 +6,8 @@ support it. One way this can occur is if you are using a [fanout](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/) 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 @@ -25,12 +27,49 @@ 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 @@ -40,10 +79,30 @@ class State(TypedDict): 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. \ No newline at end of file +- If your graph executes nodes in parallel, make sure you have defined relevant state keys with a reducer. diff --git a/docs/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md b/docs/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md index 41d4fb4a7..5fbcca2b7 100644 --- a/docs/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md +++ b/docs/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md @@ -1,5 +1,6 @@ # 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: @@ -30,9 +31,55 @@ For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/er ``` 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: -- If you have complex logic in your node, make sure all code paths return an appropriate dict for your defined state. \ No newline at end of file +:::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/docs/docs/troubleshooting/errors/MULTIPLE_SUBGRAPHS.md b/docs/docs/troubleshooting/errors/MULTIPLE_SUBGRAPHS.md index f14902a9b..e7be3badd 100644 --- a/docs/docs/troubleshooting/errors/MULTIPLE_SUBGRAPHS.md +++ b/docs/docs/troubleshooting/errors/MULTIPLE_SUBGRAPHS.md @@ -8,5 +8,14 @@ This is currently not allowed due to internal restrictions on how checkpoint nam 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`](https://langchain-ai.github.io/langgraph/concepts/low_level/#send) API. diff --git a/docs/docs/troubleshooting/studio.md b/docs/docs/troubleshooting/studio.md index b06f325de..ea1925a3e 100644 --- a/docs/docs/troubleshooting/studio.md +++ b/docs/docs/troubleshooting/studio.md @@ -6,19 +6,22 @@ Safari blocks plain-HTTP traffic on localhost. When running Studio with `langgra ### Solution 1: Use Cloudflare Tunnel -=== "Python" +:::python - ```shell - pip install -U langgraph-cli>=0.2.6 - langgraph dev --tunnel - ``` +```shell +pip install -U langgraph-cli>=0.2.6 +langgraph dev --tunnel +``` -=== "JS" +::: - ```shell - # Requires @langchain/langgraph-cli>=0.0.26 - npx @langchain/langgraph-cli dev - ``` +:::js + +```shell +npx @langchain/langgraph-cli dev +``` + +::: The command outputs a URL in this format: @@ -44,19 +47,22 @@ Disable Brave Shields for LangSmith using the Brave icon in the URL bar. ### Solution 2: Use Cloudflare Tunnel -=== "Python" +:::python - ```shell - pip install -U langgraph-cli>=0.2.6 - langgraph dev --tunnel - ``` +```shell +pip install -U langgraph-cli>=0.2.6 +langgraph dev --tunnel +``` -=== "JS" +::: - ```shell - # Requires @langchain/langgraph-cli>=0.0.26 - npx @langchain/langgraph-cli dev - ``` +:::js + +```shell +npx @langchain/langgraph-cli dev +``` + +::: The command outputs a URL in this format: @@ -68,6 +74,7 @@ Use this URL in Brave to load Studio. Here, the `baseUrl` parameter specifies yo ## Graph Edge Issues +:::python Undefined conditional edges may show unexpected connections in your graph. This is because without proper definition, LangGraph Studio assumes the conditional edge could access all other nodes. To address this, explicitly define the routing paths using one of these methods: @@ -75,17 +82,9 @@ because without proper definition, LangGraph Studio assumes the conditional edge Define a mapping between router outputs and target nodes: -=== "Python" - - ```python - graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) - ``` - -=== "Javascript" - - ```ts - graph.addConditionalEdges("node_a", routingFunction, { true: "node_b", false: "node_c" }); - ``` +```python +graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) +``` ### Solution 2: Router Type Definition (Python) @@ -98,3 +97,18 @@ def routing_function(state: GraphState) -> Literal["node_b","node_c"]: else: return "node_c" ``` + +::: + +:::js +Undefined conditional edges may show unexpected connections in your graph. This is because without proper definition, LangGraph Studio assumes the conditional edge could access all other nodes. +To address this, explicitly define a mapping between router outputs and target nodes: + +```typescript +graph.addConditionalEdges("node_a", routingFunction, { + true: "node_b", + false: "node_c", +}); +``` + +::: diff --git a/docs/docs/tutorials/auth/add_auth_server.md b/docs/docs/tutorials/auth/add_auth_server.md index 3ab37bd6b..2cb101225 100644 --- a/docs/docs/tutorials/auth/add_auth_server.md +++ b/docs/docs/tutorials/auth/add_auth_server.md @@ -2,7 +2,13 @@ In [the last tutorial](resource_auth.md), you added [resource authorization](../../tutorials/auth/resource_auth.md) to give users private conversations. However, you are still using hard-coded tokens for authentication, which is not secure. Now you'll replace those tokens with real user accounts using [OAuth2](../auth/getting_started.md). +:::python You'll keep the same [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object and [resource-level access control](../../concepts/auth.md#single-owner-resources), but upgrade authentication to use Supabase as your identity provider. While Supabase is used in this tutorial, the concepts apply to any OAuth2 provider. You'll learn how to: +::: + +:::js +You'll keep the same [`Auth`](../../cloud/reference/sdk/typescript_sdk_ref.md#auth) object and [resource-level access control](../../concepts/auth.md#single-owner-resources), but upgrade authentication to use Supabase as your identity provider. While Supabase is used in this tutorial, the concepts apply to any OAuth2 provider. You'll learn how to: +::: 1. Replace test tokens with real JWT tokens 2. Integrate with OAuth2 providers for secure user authentication @@ -18,7 +24,6 @@ OAuth2 involves three main roles: A standard OAuth2 flow works something like this: - ```mermaid sequenceDiagram participant User @@ -40,35 +45,49 @@ sequenceDiagram Before you start this tutorial, ensure you have: - The [bot from the second tutorial](resource_auth.md) running without errors. -- A [Supabase project](https://supabase.com/dashboard) to use its authentication server. - +- A [Supabase project](https://supabase.com/dashboard) to use as your authentication server. ## 1. Install dependencies Install the required dependencies. Start in your `custom-auth` directory and ensure you have the `langgraph-cli` installed: +:::python + ```bash cd custom-auth pip install -U "langgraph-cli[inmem]" ``` +::: + +:::js + +```bash +cd custom-auth +npm install -g @langchain/langgraph-cli +``` + +::: + ## 2. Set up the authentication provider {#setup-auth-provider} Next, fetch the URL of your auth server and the private key for authentication. Since you're using Supabase for this, you can do this in the Supabase dashboard: -1. In the left sidebar, click on t️⚙ Project Settings" and then click "API" -1. Copy your project URL and add it to your `.env` file +1. In the left sidebar, click on "⚙️ Project Settings" and then click "API" +2. Copy your project URL and add it to your `.env` file ```shell echo "SUPABASE_URL=your-project-url" >> .env ``` -1. Copy your service role secret key and add it to your `.env` file: + +3. Copy your service role secret key and add it to your `.env` file: ```shell echo "SUPABASE_SERVICE_KEY=your-service-role-key" >> .env ``` -1. Copy your "anon public" key and note it down. This will be used later when you set up our client code. + +4. Copy your "anon public" key and note it down. This will be used later when you set up our client code. ```bash SUPABASE_URL=your-project-url @@ -77,14 +96,23 @@ Since you're using Supabase for this, you can do this in the Supabase dashboard: ## 3. Implement token validation +:::python In the previous tutorials, you used the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object to [validate hard-coded tokens](getting_started.md) and [add resource ownership](resource_auth.md). Now you'll upgrade your authentication to validate real JWT tokens from Supabase. The main changes will all be in the [`@auth.authenticate`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) decorated function: +::: + +:::js +In the previous tutorials, you used the [`Auth`](../../cloud/reference/sdk/typescript_sdk_ref.md#auth) object to [validate hard-coded tokens](getting_started.md) and [add resource ownership](resource_auth.md). + +Now you'll upgrade your authentication to validate real JWT tokens from Supabase. The main changes will all be in the [`auth.authenticate`](../../cloud/reference/sdk/typescript_sdk_ref.md#auth) decorated function: +::: - Instead of checking against a hard-coded list of tokens, you'll make an HTTP request to Supabase to validate the token. - You'll extract real user information (ID, email) from the validated token. - The existing resource authorization logic remains unchanged. +:::python Update `src/security/auth.py` to implement this: ```python hl_lines="8-9 20-30" title="src/security/auth.py" @@ -138,6 +166,69 @@ async def add_owner(ctx, value): return filters ``` +::: + +:::js +Update `src/security/auth.ts` to implement this: + +```typescript hl_lines="1-2 9-10 21-31" title="src/security/auth.ts" +import { Auth } from "@langchain/langgraph-sdk"; + +// This is loaded from the `.env` file you created above +const SUPABASE_URL = process.env.SUPABASE_URL; +const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY; + +const auth = new Auth() + .authenticate(async (request) => { + // Validate JWT tokens and extract user information. + const apiKey = request.headers.get("x-api-key"); + if (!apiKey || !isValidKey(apiKey)) { + throw new HTTPException(401, "Invalid API key"); + } + + const [scheme, token] = apiKey.split(" "); + if (scheme.toLowerCase() !== "bearer") { + throw new Error("Invalid authorization scheme"); + } + + try { + // Verify token with auth provider + const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, { + headers: { + Authorization: authorization, + apiKey: SUPABASE_SERVICE_KEY!, + }, + }); + + if (response.status !== 200) { + throw new Error("Invalid token"); + } + + const user = await response.json(); + return { + identity: user.id, // Unique user identifier + email: user.email, + is_authenticated: true, + }; + } catch (e) { + throw new Auth.HTTPException(401, String(e)); + } + }) + .on(async ({ user, value }) => { + // Keep our resource authorization from the previous tutorial + // Make resources private to their creator using resource metadata. + const filters = { owner: user.identity }; + const metadata = value.metadata || {}; + Object.assign(metadata, filters); + value.metadata = metadata; + return filters; + }); + +export { auth }; +``` + +::: + The most important change is that we're now validating tokens with a real authentication server. Our authentication handler has the private key for our Supabase project, which we can use to validate the user's token and extract their information. ## 4. Test authentication flow @@ -148,6 +239,8 @@ Let's test out the new authentication flow. You can run the following code in a - A Supabase project URL (from [above](#setup-auth-provider)) - A Supabase anon **public key** (also from [above](#setup-auth-provider)) +:::python + ```python import os import httpx @@ -190,9 +283,63 @@ await sign_up(email1, password) await sign_up(email2, password) ``` +::: + +:::js + +```typescript +import { Client } from "@langchain/langgraph-sdk"; + +// Get email from command line +const email = process.env.TEST_EMAIL || "your-email@example.com"; +const baseEmail = email.split("@"); +const password = "secure-password"; // CHANGEME +const email1 = `${baseEmail[0]}+1@${baseEmail[1]}`; +const email2 = `${baseEmail[0]}+2@${baseEmail[1]}`; + +const SUPABASE_URL = process.env.SUPABASE_URL; +if (!SUPABASE_URL) { + throw new Error("SUPABASE_URL environment variable is required"); +} + +// This is your PUBLIC anon key (which is safe to use client-side) +// Do NOT mistake this for the secret service role key +const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY; +if (!SUPABASE_ANON_KEY) { + throw new Error("SUPABASE_ANON_KEY environment variable is required"); +} + +async function signUp(email: string, password: string) { + /**Create a new user account.*/ + const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, { + method: "POST", + headers: { + apiKey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + if (response.status !== 200) { + throw new Error(`Failed to sign up: ${response.statusText}`); + } + + return response.json(); +} + +// Create two test users +console.log(`Creating test users: ${email1} and ${email2}`); +await signUp(email1, password); +await signUp(email2, password); +``` + +::: + ⚠️ Before continuing: Check your email and click both confirmation links. Supabase will reject `/login` requests until after you have confirmed your users' email. -Now test that users can only see their own data. Make sure the server is running (run `langgraph dev`) before proceeding. The following snippet requires the "anon public" key that you copied from the Supabase dashboard while [setting up the auth provider](#setup-auth-provider) previously. +Now test that users can only see their own data. Make sure the server is running (run `langgraph dev`) before proceeding. The following snippet requires the "anon public" key that you copied from the Supabase dashboard while [setting up the auth provider](#setup-auth-provider) previously. + +:::python ```python async def login(email: str, password: str): @@ -243,6 +390,71 @@ try: except Exception as e: print("✅ User 2 blocked from User 1's thread:", e) ``` + +::: + +:::js + +```typescript +async function login(email: string, password: string): Promise { + /**Get an access token for an existing user.*/ + const response = await fetch( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + method: "POST", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + } + ); + + if (response.status !== 200) { + throw new Error(`Failed to login: ${response.statusText}`); + } + + const data = await response.json(); + return data.access_token; +} + +// Log in as user 1 +const user1Token = await login(email1, password); +const user1Client = new Client({ + apiUrl: "http://localhost:2024", + headers: { Authorization: `Bearer ${user1Token}` }, +}); + +// Create a thread as user 1 +const thread = await user1Client.threads.create(); +console.log(`✅ User 1 created thread: ${thread.thread_id}`); + +// Try to access without a token +const unauthenticatedClient = new Client({ apiUrl: "http://localhost:2024" }); +try { + await unauthenticatedClient.threads.create(); + console.log("❌ Unauthenticated access should fail!"); +} catch (e) { + console.log("✅ Unauthenticated access blocked:", e.message); +} + +// Try to access user 1's thread as user 2 +const user2Token = await login(email2, password); +const user2Client = new Client({ + apiUrl: "http://localhost:2024", + headers: { Authorization: `Bearer ${user2Token}` }, +}); + +try { + await user2Client.threads.get(thread.thread_id); + console.log("❌ User 2 shouldn't see User 1's thread!"); +} catch (e) { + console.log("✅ User 2 blocked from User 1's thread:", e.message); +} +``` + +::: + The output should look like this: ```shell @@ -272,4 +484,9 @@ Now that you have production authentication, consider: 1. Building a web UI with your preferred framework (see the [Custom Auth](https://github.com/langchain-ai/custom-auth) template for an example) 2. Learn more about the other aspects of authentication and authorization in the [conceptual guide on authentication](../../concepts/auth.md). -3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth). \ No newline at end of file + +:::python 3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth). +::: + +:::js 3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/typescript_sdk_ref.md#auth). +::: diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index b9b2340e0..7c3466ee3 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -10,8 +10,8 @@ This is part 1 of our authentication series: This guide assumes basic familiarity with the following concepts: -* [**Authentication & Access Control**](../../concepts/auth.md) -* [**LangGraph Platform**](../../concepts/langgraph_platform.md) +- [**Authentication & Access Control**](../../concepts/auth.md) +- [**LangGraph Platform**](../../concepts/langgraph_platform.md) !!! note @@ -21,26 +21,52 @@ This guide assumes basic familiarity with the following concepts: Create a new chatbot using the LangGraph starter template: +:::python + ```bash pip install -U "langgraph-cli[inmem]" langgraph new --template=new-langgraph-project-python custom-auth cd custom-auth ``` +::: + +:::js + +```bash +npx @langchain/langgraph-cli new --template=new-langgraph-project-typescript custom-auth +cd custom-auth +``` + +::: + The template gives us a placeholder LangGraph app. Try it out by installing the local dependencies and running the development server: +:::python + ```shell pip install -e . langgraph dev ``` +::: + +:::js + +```shell +npm install +npm run langgraph dev +``` + +::: + The server will start and open the studio in your browser: ``` > - 🚀 API: http://127.0.0.1:2024 > - 🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024 > - 📚 API Docs: http://127.0.0.1:2024/docs -> +> > This in-memory server is designed for development and testing. > For production use, please use LangGraph Platform. ``` @@ -49,7 +75,6 @@ If you were to self-host this on the public internet, anyone could access it! ![No auth](./img/no_auth.png) - ## 2. Add authentication Now that you have a base LangGraph app, add authentication to it. @@ -58,6 +83,7 @@ Now that you have a base LangGraph app, add authentication to it. In this tutorial, you will start with a hard-coded token for example purposes. You will get to a "production-ready" authentication scheme in the third tutorial. +:::python The [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject. Create a new file `src/security/auth.py`. This is where your code will live to check if users are allowed to access your bot: @@ -98,9 +124,61 @@ Notice that your [authentication](../../cloud/reference/sdk/python_sdk_ref.md#la 1. Checks if a valid token is provided in the request's [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) 2. Returns the user's [identity](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.MinimalUserDict) + ::: + +:::js +The [`Auth`](../../cloud/reference/sdk/js_sdk_ref.md#Auth) object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject. + +Create a new file `src/security/auth.ts`. This is where your code will live to check if users are allowed to access your bot: + +```typescript title="src/security/auth.ts" +import { Auth } from "@langchain/langgraph-sdk"; + +// This is our toy user database. Do not do this in production +const VALID_TOKENS: Record = { + "user1-token": { id: "user1", name: "Alice" }, + "user2-token": { id: "user2", name: "Bob" }, +}; + +// The "Auth" object is a container that LangGraph will use to mark our authentication function +const auth = new Auth(); + // The `authenticate` method tells LangGraph to call this function as middleware + // for every request. This will determine whether the request is allowed or not + .authenticate((request) => { + // Our authentication handler from the previous tutorial. + const apiKey = request.headers.get("x-api-key"); + if (!apiKey || !isValidKey(apiKey)) { + throw new HTTPException(401, "Invalid API key"); + } + + const [scheme, token] = apiKey.split(" "); + if (scheme.toLowerCase() !== "bearer") { + throw new Error("Bearer token required"); + } + + if (!VALID_TOKENS[token]) { + throw new HTTPException(401, "Invalid token"); + } + + const userData = VALID_TOKENS[token]; + return { + identity: userData.id, + }; + }); + +export { auth }; +``` + +Notice that your [authentication](../../cloud/reference/sdk/js_sdk_ref.md#Auth) handler does two important things: + +1. Checks if a valid token is provided in the request's [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) +2. Returns the user's [identity](../../cloud/reference/sdk/js_sdk_ref.md#Auth.types.MinimalUserDict) + ::: Now tell LangGraph to use authentication by adding the following to the [`langgraph.json`](../../cloud/reference/cli.md#configuration-file) configuration: +:::python + ```json hl_lines="7-9" title="langgraph.json" { "dependencies": ["."], @@ -114,6 +192,25 @@ Now tell LangGraph to use authentication by adding the following to the [`langgr } ``` +::: + +:::js + +```json hl_lines="7-9" title="langgraph.json" +{ + "dependencies": ["."], + "graphs": { + "agent": "./src/agent/graph.ts:graph" + }, + "env": ".env", + "auth": { + "path": "src/security/auth.ts:auth" + } +} +``` + +::: + ## 3. Test your bot Start the server again to test everything out: @@ -124,21 +221,39 @@ langgraph dev --no-browser If you didn't add the `--no-browser`, the studio UI will open in the browser. You may wonder, how is the studio able to still connect to our server? By default, we also permit access from the LangGraph studio, even when using custom auth. This makes it easier to develop and test your bot in the studio. You can remove this alternative authentication option by setting `disable_studio_auth: "true"` in your auth configuration: +:::python + ```json { - "auth": { - "path": "src/security/auth.py:auth", - "disable_studio_auth": "true" - } + "auth": { + "path": "src/security/auth.py:auth", + "disable_studio_auth": "true" + } } ``` +::: + +:::js + +```json +{ + "auth": { + "path": "src/security/auth.ts:auth", + "disable_studio_auth": "true" + } +} +``` + +::: + ## 4. Chat with your bot You should now only be able to access the bot if you provide a valid token in the request header. Users will still, however, be able to access each other's resources until you add [resource authorization handlers](../../concepts/auth.md#resource-specific-handlers) in the next section of the tutorial. ![Authentication, no authorization handlers](./img/authentication.png) +:::python Run the following code in a file or notebook: ```python @@ -170,6 +285,46 @@ print("✅ Bot responded:") print(response) ``` +::: + +:::js +Run the following code in a TypeScript file: + +```typescript +import { Client } from "@langchain/langgraph-sdk"; + +async function testAuth() { + // Try without a token (should fail) + const clientWithoutToken = new Client({ apiUrl: "http://localhost:2024" }); + try { + const thread = await clientWithoutToken.threads.create(); + console.log("❌ Should have failed without token!"); + } catch (e) { + console.log("✅ Correctly blocked access:", e); + } + + // Try with a valid token + const client = new Client({ + apiUrl: "http://localhost:2024", + headers: { Authorization: "Bearer user1-token" }, + }); + + // Create a thread and chat + const thread = await client.threads.create(); + console.log(`✅ Created thread as Alice: ${thread.thread_id}`); + + const response = await client.runs.create(thread.thread_id, "agent", { + input: { messages: [{ role: "user", content: "Hello!" }] }, + }); + console.log("✅ Bot responded:"); + console.log(response); +} + +testAuth().catch(console.error); +``` + +::: + You should see that: 1. Without a valid token, we can't access the bot @@ -183,4 +338,9 @@ Now that you can control who accesses your bot, you might want to: 1. Continue the tutorial by going to [Make conversations private](resource_auth.md) to learn about resource authorization. 2. Read more about [authentication concepts](../../concepts/auth.md). -3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication details. \ No newline at end of file + +:::python 3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication details. +::: + +:::js 3. Check out the [API reference](../../cloud/reference/sdk/js_sdk_ref.md) for more authentication details. +::: diff --git a/docs/docs/tutorials/auth/resource_auth.md b/docs/docs/tutorials/auth/resource_auth.md index 267e00f25..fc0f7d86b 100644 --- a/docs/docs/tutorials/auth/resource_auth.md +++ b/docs/docs/tutorials/auth/resource_auth.md @@ -10,10 +10,17 @@ Before you start this tutorial, ensure you have the [bot from the first tutorial ## 1. Add resource authorization +:::python Recall that in the last tutorial, the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object lets you register an [authentication function](../../concepts/auth.md#authentication), which LangGraph Platform uses to validate the bearer tokens in incoming requests. Now you'll use it to register an **authorization** handler. +::: + +:::js +Recall that in the last tutorial, the [`Auth`](insert-ref) object lets you register an [authentication function](../../concepts/auth.md#authentication), which LangGraph Platform uses to validate the bearer tokens in incoming requests. Now you'll use it to register an **authorization** handler. +::: Authorization handlers are functions that run **after** authentication succeeds. These handlers can add [metadata](../../concepts/auth.md#filter-operations) to resources (like who owns them) and filter what each user can see. +:::python Update your `src/security/auth.py` and add one authorization handler to run on every request: ```python hl_lines="29-39" title="src/security/auth.py" @@ -61,7 +68,7 @@ async def add_owner( # resource='threads', # action='create_run' # ) - # value: + # value: # { # 'thread_id': UUID('1e1b2733-303f-4dcd-9620-02d370287d72'), # 'assistant_id': UUID('fe096781-5601-53d2-b2f6-0d3403f7e9ca'), @@ -103,10 +110,112 @@ async def add_owner( return filters ``` +::: + +:::js +Update your `src/security/auth.ts` and add one authorization handler to run on every request: + +```typescript hl_lines="29-39" title="src/security/auth.ts" +import { Auth, HTTPException } from "@langchain/langgraph-sdk"; + +// Keep our test users from the previous tutorial +const VALID_TOKENS: Record = { + "user1-token": { id: "user1", name: "Alice" }, + "user2-token": { id: "user2", name: "Bob" }, +}; + +const auth = new Auth() + .authenticate(async (request) => { + // Our authentication handler from the previous tutorial. + const apiKey = request.headers.get("x-api-key"); + if (!apiKey || !isValidKey(apiKey)) { + throw new HTTPException(401, "Invalid API key"); + } + + const [scheme, token] = apiKey.split(" "); + if (scheme.toLowerCase() !== "bearer") { + throw new Error("Bearer token required"); + } + + if (!VALID_TOKENS[token]) { + throw new HTTPException(401, "Invalid token"); + } + + const userData = VALID_TOKENS[token]; + return { + identity: userData.id, + }; + }) + .on("*", ({ value, user }) => { + // This handler makes resources private to their creator by doing 2 things: + // 1. Add the user's ID to the resource's metadata. Each LangGraph resource has a `metadata` object that persists with the resource. + // this metadata is useful for filtering in read and update operations + // 2. Return a filter that lets users only see their own resources + // Examples: + // { + // user: ProxyUser { + // identity: 'user1', + // is_authenticated: true, + // display_name: 'user1' + // }, + // value: { + // 'thread_id': UUID('1e1b2733-303f-4dcd-9620-02d370287d72'), + // 'assistant_id': UUID('fe096781-5601-53d2-b2f6-0d3403f7e9ca'), + // 'run_id': UUID('1efbe268-1627-66d4-aa8d-b956b0f02a41'), + // 'status': 'pending', + // 'metadata': {}, + // 'prevent_insert_if_inflight': true, + // 'multitask_strategy': 'reject', + // 'if_not_exists': 'reject', + // 'after_seconds': 0, + // 'kwargs': { + // 'input': {'messages': [{'role': 'user', 'content': 'Hello!'}]}, + // 'command': null, + // 'config': { + // 'configurable': { + // 'langgraph_auth_user': ... Your user object... + // 'langgraph_auth_user_id': 'user1' + // } + // }, + // 'stream_mode': ['values'], + // 'interrupt_before': null, + // 'interrupt_after': null, + // 'webhook': null, + // 'feedback_keys': null, + // 'temporary': false, + // 'subgraphs': false + // } + // } + // } + + const filters = { owner: user.identity }; + const metadata = value.metadata || {}; + Object.assign(metadata, filters); + value.metadata = metadata; + + // Only let users see their own resources + return filters; + }); + +export { auth }; +``` + +::: + +:::python The handler receives two parameters: 1. `ctx` ([AuthContext](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.AuthContext)): contains info about the current `user`, the user's `permissions`, the `resource` ("threads", "crons", "assistants"), and the `action` being taken ("create", "read", "update", "delete", "search", "create_run") 2. `value` (`dict`): data that is being created or accessed. The contents of this dict depend on the resource and action being accessed. See [adding scoped authorization handlers](#scoped-authorization) below for information on how to get more tightly scoped access control. + ::: + +:::js +The handler receives an object with the following properties: + +1. `user` ([ProxyUser](../../cloud/reference/sdk/js_ts_sdk_ref.md#langgraph_sdk.auth.types.ProxyUser)): contains info about the current `user`, the user's `permissions`, the `resource` ("threads", "crons", "assistants") +2. `action` contains information about the action being taken ("create", "read", "update", "delete", "search", "create_run") +3. `value` (`Record`): data that is being created or accessed. The contents of this object depend on the resource and action being accessed. See [adding scoped authorization handlers](#scoped-authorization) below for information on how to get more tightly scoped access control. + ::: Notice that the simple handler does two things: @@ -117,6 +226,8 @@ Notice that the simple handler does two things: Test your authorization. If you have set things up correctly, you will see all ✅ messages. Be sure to have your development server running (run `langgraph dev`): +:::python + ```python from langgraph_sdk import get_client @@ -168,6 +279,64 @@ print(f"✅ Alice sees {len(alice_threads)} thread") print(f"✅ Bob sees {len(bob_threads)} thread") ``` +::: + +:::js + +```typescript +import { getClient } from "@langgraph/sdk"; + +// Create clients for both users +const alice = getClient({ + url: "http://localhost:2024", + headers: { Authorization: "Bearer user1-token" }, +}); + +const bob = getClient({ + url: "http://localhost:2024", + headers: { Authorization: "Bearer user2-token" }, +}); + +// Alice creates an assistant +const aliceAssistant = await alice.assistants.create(); +console.log(`✅ Alice created assistant: ${aliceAssistant.assistant_id}`); + +// Alice creates a thread and chats +const aliceThread = await alice.threads.create(); +console.log(`✅ Alice created thread: ${aliceThread.thread_id}`); + +await alice.runs.create(aliceThread.thread_id, "agent", { + input: { + messages: [{ role: "user", content: "Hi, this is Alice's private chat" }], + }, +}); + +// Bob tries to access Alice's thread +try { + await bob.threads.get(aliceThread.thread_id); + console.log("❌ Bob shouldn't see Alice's thread!"); +} catch (error) { + console.log("✅ Bob correctly denied access:", error); +} + +// Bob creates his own thread +const bobThread = await bob.threads.create(); +await bob.runs.create(bobThread.thread_id, "agent", { + input: { + messages: [{ role: "user", content: "Hi, this is Bob's private chat" }], + }, +}); +console.log(`✅ Bob created his own thread: ${bobThread.thread_id}`); + +// List threads - each user only sees their own +const aliceThreads = await alice.threads.search(); +const bobThreads = await bob.threads.search(); +console.log(`✅ Alice sees ${aliceThreads.length} thread`); +console.log(`✅ Bob sees ${bobThreads.length} thread`); +``` + +::: + Output: ```bash @@ -188,6 +357,7 @@ This means: ## 3. Add scoped authorization handlers {#scoped-authorization} +:::python The broad `@auth.on` handler matches on all [authorization events](../../concepts/auth.md#supported-resources). This is concise, but it means the contents of the `value` dict are not well-scoped, and the same user-level access control is applied to every resource. If you want to be more fine-grained, you can also control specific actions on resources. Update `src/security/auth.py` to add handlers for specific resource types: @@ -203,7 +373,7 @@ async def on_thread_create( value: Auth.types.on.threads.create.value, ): """Add owner when creating threads. - + This handler runs when creating new threads and does two things: 1. Sets metadata on the thread being created to track ownership 2. Returns a filter that ensures only the creator can access it @@ -215,8 +385,7 @@ async def on_thread_create( # This metadata is stored with the thread and persists metadata = value.setdefault("metadata", {}) metadata["owner"] = ctx.user.identity - - + # Return filter to restrict access to just the creator return {"owner": ctx.user.identity} @@ -226,7 +395,7 @@ async def on_thread_read( value: Auth.types.on.threads.read.value, ): """Only let users read their own threads. - + This handler runs on read operations. We don't need to set metadata since the thread already exists - we just need to return a filter to ensure users can only see their own threads. @@ -261,16 +430,88 @@ async def authorize_store(ctx: Auth.types.AuthContext, value: dict): assert namespace[0] == ctx.user.identity, "Not authorized" ``` +::: + +:::js +The broad `auth.on("*")` handler matches on all [authorization events](../../concepts/auth.md#supported-resources). This is concise, but it means the contents of the `value` object are not well-scoped, and the same user-level access control is applied to every resource. If you want to be more fine-grained, you can also control specific actions on resources. + +Update `src/security/auth.ts` to add handlers for specific resource types: + +```typescript +// Keep our previous handlers... + +import { Auth, HTTPException } from "@langchain/langgraph-sdk"; + +auth.on("threads:create", async ({ user, value }) => { + // Add owner when creating threads. + // This handler runs when creating new threads and does two things: + // 1. Sets metadata on the thread being created to track ownership + // 2. Returns a filter that ensures only the creator can access it + + // Example value: + // {thread_id: UUID('99b045bc-b90b-41a8-b882-dabc541cf740'), metadata: {}, if_exists: 'raise'} + + // Add owner metadata to the thread being created + // This metadata is stored with the thread and persists + const metadata = value.metadata || {}; + metadata.owner = user.identity; + value.metadata = metadata; + + // Return filter to restrict access to just the creator + return { owner: user.identity }; +}); + +auth.on("threads:read", async ({ user, value }) => { + // Only let users read their own threads. + // This handler runs on read operations. We don't need to set + // metadata since the thread already exists - we just need to + // return a filter to ensure users can only see their own threads. + return { owner: user.identity }; +}); + +auth.on("assistants", async ({ user, value }) => { + // For illustration purposes, we will deny all requests + // that touch the assistants resource + // Example value: + // { + // 'assistant_id': UUID('63ba56c3-b074-4212-96e2-cc333bbc4eb4'), + // 'graph_id': 'agent', + // 'config': {}, + // 'metadata': {}, + // 'name': 'Untitled' + // } + throw new HTTPException(403, "User lacks the required permissions."); +}); + +auth.on("store", async ({ user, value }) => { + // The "namespace" field for each store item is a tuple you can think of as the directory of an item. + const namespace: string[] = value.namespace; + if (namespace[0] !== user.identity) { + throw new Error("Not authorized"); + } +}); +``` + +::: + Notice that instead of one global handler, you now have specific handlers for: 1. Creating threads 2. Reading threads 3. Accessing assistants +:::python The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-specific-handlers)), while the last one (`@auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broadly scoped "`@auth.on`" handler. +::: + +:::js +The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-specific-handlers)), while the last one (`auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broadly scoped "`auth.on`" handler. +::: Try adding the following test code to your test file: +:::python + ```python # ... Same as before # Try creating an assistant. This should fail @@ -292,6 +533,38 @@ alice_thread = await alice.threads.create() print(f"✅ Alice created thread: {alice_thread['thread_id']}") ``` +::: + +:::js + +```typescript +// ... Same as before +// Try creating an assistant. This should fail +try { + await alice.assistants.create("agent"); + console.log("❌ Alice shouldn't be able to create assistants!"); +} catch (error) { + console.log("✅ Alice correctly denied access:", error); +} + +// Try searching for assistants. This also should fail +try { + await alice.assistants.search(); + console.log("❌ Alice shouldn't be able to search assistants!"); +} catch (error) { + console.log( + "✅ Alice correctly denied access to searching assistants:", + error + ); +} + +// Alice can still create threads +const aliceThread = await alice.threads.create(); +console.log(`✅ Alice created thread: ${aliceThread.thread_id}`); +``` + +::: + Output: ```bash @@ -302,7 +575,7 @@ For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/St ✅ Alice sees 1 thread ✅ Bob sees 1 thread ✅ Alice correctly denied access: -For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 +For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/50j0 ✅ Alice correctly denied access to searching assistants: ``` @@ -314,4 +587,9 @@ Now that you can control access to resources, you might want to: 1. Move on to [Connect an authentication provider](add_auth_server.md) to add real user accounts. 2. Read more about [authorization patterns](../../concepts/auth.md#authorization). -3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) for details about the interfaces and methods used in this tutorial. + +:::python 3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) for details about the interfaces and methods used in this tutorial. +::: + +:::js 3. Check out the [API reference](../../cloud/reference/sdk/js_sdk_ref.md#langgraph_sdk.auth.Auth) for details about the interfaces and methods used in this tutorial. +::: diff --git a/docs/docs/tutorials/langgraph-platform/local-server.md b/docs/docs/tutorials/langgraph-platform/local-server.md index 6be742224..2d581f2f6 100644 --- a/docs/docs/tutorials/langgraph-platform/local-server.md +++ b/docs/docs/tutorials/langgraph-platform/local-server.md @@ -10,57 +10,69 @@ Before you begin, ensure you have the following: ## 1. Install the LangGraph CLI -=== "Python server" +:::python - ```shell - # Python >= 3.11 is required. +```shell +# Python >= 3.11 is required. - pip install --upgrade "langgraph-cli[inmem]" - ``` +pip install --upgrade "langgraph-cli[inmem]" +``` -=== "Node server" +::: - ```shell - npx @langchain/langgraph-cli - ``` +:::js + +```shell +npx @langchain/langgraph-cli +``` + +::: ## 2. Create a LangGraph app 🌱 -Create a new app from the [`new-langgraph-project-python` template](https://github.com/langchain-ai/new-langgraph-project) or [`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. +:::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. -=== "Python server" - - ```shell - langgraph new path/to/your/app --template new-langgraph-project-python - ``` - -=== "Node server" - - ```shell - langgraph new path/to/your/app --template new-langgraph-project-js - ``` +```shell +langgraph new path/to/your/app --template new-langgraph-project-python +``` !!! tip "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 +langgraph new path/to/your/app --template new-langgraph-project-js +``` + +::: + ## 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 server" +:::python - ```shell - cd path/to/your/app - pip install -e . - ``` +```shell +cd path/to/your/app +pip install -e . +``` -=== "Node server" +::: - ```shell - cd path/to/your/app - yarn install - ``` +:::js + +```shell +cd path/to/your/app +npm install +``` + +::: ## 4. Create a `.env` file @@ -74,17 +86,21 @@ LANGSMITH_API_KEY=lsv2... Start the LangGraph API server locally: -=== "Python server" +:::python - ```shell - langgraph dev - ``` +```shell +langgraph dev +``` -=== "Node server" +::: - ```shell - npx @langchain/langgraph-cli dev - ``` +:::js + +```shell +npx @langchain/langgraph-cli dev +``` + +::: Sample output: @@ -120,6 +136,7 @@ For a LangGraph Server running on a custom host/port, update the baseURL paramet ## 7. Test the API +:::python === "Python SDK (async)" 1. Install the LangGraph Python SDK: @@ -185,7 +202,29 @@ For a LangGraph Server running on a custom host/port, update the baseURL paramet print("\n\n") ``` +=== "Rest API" + ```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 === "Javascript SDK" 1. Install the LangGraph JS SDK: @@ -242,6 +281,8 @@ For a LangGraph Server running on a custom host/port, update the baseURL paramet }" ``` +::: + ## Next steps Now that you have a LangGraph app running locally, take your journey further by exploring deployment and advanced features: @@ -249,5 +290,13 @@ Now that you have a LangGraph app running locally, take your journey further by - [Deployment quickstart](../../cloud/quick_start.md): Deploy your LangGraph app using LangGraph Platform. - [LangGraph Platform overview](../../concepts/langgraph_platform.md): Learn about foundational LangGraph Platform concepts. - [LangGraph Server API Reference](../../cloud/reference/api/api_ref.html): Explore the LangGraph Server API documentation. + +:::python + - [Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md): Explore the Python SDK API Reference. + ::: + +:::js + - [JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md): Explore the JS/TS SDK API Reference. + ::: diff --git a/docs/docs/tutorials/workflows.md b/docs/docs/tutorials/workflows.md index 8be5cf07b..c4d871c66 100644 --- a/docs/docs/tutorials/workflows.md +++ b/docs/docs/tutorials/workflows.md @@ -1,10 +1,11 @@ --- search: - boost: 2 + boost: 2 --- + # 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](https://python.langchain.com/docs/integrations/providers/anthropic/) `Building Effective Agents` blog post: +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. @@ -17,12 +18,13 @@ When building agents and workflows, LangGraph offers a number of benefits includ ## 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 + pip install langchain_core langchain-anthropic langgraph ``` Initialize an LLM @@ -43,12 +45,36 @@ _set_env("ANTHROPIC_API_KEY") llm = 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](https://python.langchain.com/docs/concepts/structured_outputs/) and [tool calling](https://python.langchain.com/docs/concepts/tool_calling/), as shown in this image from the Anthropic blog on `Building Effective Agents`: +:::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](./workflows/img/augmented_llm.png) +:::python ```python # Schema for structured output @@ -81,13 +107,64 @@ msg = llm_with_tools.invoke("What is 2 times 3?") 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. +In prompt chaining, each LLM call processes the output of the previous one. -As noted in the Anthropic blog on `Building Effective Agents`: +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. +> 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. @@ -95,6 +172,7 @@ As noted in the Anthropic blog on `Building Effective Agents`: === "Graph API" + :::python ```python from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END @@ -187,9 +265,94 @@ As noted in the Anthropic blog on `Building Effective Agents`: **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!"); + } + ``` + ::: === "Functional API" + :::python ```python from langgraph.func import entrypoint, task @@ -243,12 +406,64 @@ As noted in the Anthropic blog on `Building Effective Agents`: **LangSmith Trace** https://smith.langchain.com/public/332fa4fc-b6ca-416e-baa3-161625e69163/r + ::: -## Parallelization + :::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. +> 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. @@ -256,6 +471,7 @@ With parallelization, LLMs work simultaneously on a task: === "Graph API" + :::python ```python # Graph state class State(TypedDict): @@ -338,9 +554,72 @@ With parallelization, LLMs work simultaneously on a task: **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); + ``` + ::: === "Functional API" + :::python ```python @task def call_llm_1(topic: str): @@ -393,10 +672,63 @@ With parallelization, LLMs work simultaneously on a task: **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 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. @@ -404,9 +736,9 @@ Routing classifies an input and directs it to a followup task. As noted in the A ![routing.png](./workflows/img/routing.png) - === "Graph API" + :::python ```python from typing_extensions import Literal from langchain_core.messages import HumanMessage, SystemMessage @@ -527,9 +859,99 @@ Routing classifies an input and directs it to a followup task. As noted in the A **Examples** [Here](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/) is RAG workflow that routes questions. 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); + ``` + ::: === "Functional API" + :::python ```python from typing_extensions import Literal from pydantic import BaseModel @@ -604,20 +1026,87 @@ Routing classifies an input and directs it to a followup task. As noted in the A **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`: +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. +> 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](./workflows/img/worker.png) - === "Graph API" + :::python ```python from typing import Annotated, List import operator @@ -763,10 +1252,120 @@ With orchestrator-worker, an orchestrator breaks down a task and delegates each **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](../how-tos/map-reduce/) and [here](../concepts/low_level/#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); + ``` + ::: === "Functional API" + :::python ```python from typing import List @@ -848,19 +1447,75 @@ With orchestrator-worker, an orchestrator breaks down a task and delegates each **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: -> 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](./workflows/img/evaluator_optimizer.png) === "Graph API" + :::python ```python # Graph state class State(TypedDict): @@ -955,9 +1610,84 @@ In the evaluator-optimizer workflow, one LLM call generates a response while ano [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). [Here](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/) is a RAG workflow that grades answers for hallucinations or errors. 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); + ``` + ::: === "Functional API" + :::python ```python # Schema for structured output to use in evaluation class Feedback(BaseModel): @@ -1013,6 +1743,58 @@ In the evaluator-optimizer workflow, one LLM call generates a response while ano **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 @@ -1020,10 +1802,11 @@ Agents are typically implemented as an LLM performing actions (via tool-calling) > 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. +> 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](./workflows/img/agent.png) +:::python ```python from langchain_core.tools import tool @@ -1069,8 +1852,67 @@ 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); +``` + +::: + === "Graph API" + :::python ```python from langgraph.graph import MessagesState from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage @@ -1164,9 +2006,70 @@ llm_with_tools = llm.bind_tools(tools) **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}`); + } + ``` + ::: === "Functional API" + :::python ```python from langgraph.graph import add_messages from langchain_core.messages import ( @@ -1226,9 +2129,74 @@ llm_with_tools = llm.bind_tools(tools) **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`][langgraph.prebuilt.chat_agent_executor.create_react_agent] function): https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/ @@ -1254,6 +2222,28 @@ for m in messages["messages"]: **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`][] 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 @@ -1271,7 +2261,6 @@ LangGraph persistence layer supports conversational (short-term) memory and long 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.