diff --git a/docs/docs/agents/multi-agent.md b/docs/docs/agents/multi-agent.md index b378141cc..d96996a9c 100644 --- a/docs/docs/agents/multi-agent.md +++ b/docs/docs/agents/multi-agent.md @@ -367,13 +367,13 @@ To implement handoffs with `createReactAgent`, you need to: 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) - // ... - ``` + ```typescript + import { StateGraph, MessagesZodState } from "@langchain/langgraph"; + const multiAgentGraph = new StateGraph(MessagesZodState) + .addNode("flight_assistant", flightAssistant) + .addNode("hotel_assistant", hotelAssistant) + // ... + ``` ::: @@ -619,7 +619,8 @@ for await (const chunk of multiAgentGraph.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. - ::: + +::: !!! Note diff --git a/docs/docs/concepts/mcp.md b/docs/docs/concepts/mcp.md index 276f39ae3..a0ea447be 100644 --- a/docs/docs/concepts/mcp.md +++ b/docs/docs/concepts/mcp.md @@ -6,7 +6,14 @@ Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph: +:::python ```bash pip install langchain-mcp-adapters ``` +::: +:::js +```bash +npm install @langchain/mcp-adapters +``` +::: \ No newline at end of file diff --git a/docs/docs/how-tos/graph-api.md b/docs/docs/how-tos/graph-api.md index bbea68dbb..bf0af9610 100644 --- a/docs/docs/how-tos/graph-api.md +++ b/docs/docs/how-tos/graph-api.md @@ -4,11 +4,21 @@ This guide demonstrates the basics of LangGraph's Graph API. It walks through [s ## Setup +:::python Install `langgraph`: ```bash pip install -U langgraph ``` +::: + +:::js +Install `langgraph`: + +```bash +npm install @langchain/langgraph +``` +::: !!! tip "Set up LangSmith for better debugging" @@ -23,14 +33,19 @@ Here we show how to define and update [state](../concepts/low_level.md#state) in ### Define state +:::python [State](../concepts/low_level.md#state) in LangGraph can be a `TypedDict`, `Pydantic` model, or dataclass. Below we will use `TypedDict`. See [this section](#use-pydantic-models-for-graph-state) for detail on using Pydantic. +::: + +:::js +[State](../concepts/low_level.md#state) in LangGraph can be defined using Zod schemas. Below we will use Zod. See [this section](#alternative-state-definitions) for detail on using alternative approaches. +::: By default, graphs will have the same input and output schema, and the state determines that schema. See [this section](#define-input-and-output-schemas) for how to define distinct input and output schemas. -:::python -Let's consider a simple example using [messages](../concepts/low_level.md#messagesstate). This represents a versatile formulation of state for many LLM applications. See our [concepts page](../concepts/low_level.md#working-with-messages-in-graph-state) for more detail. -::: +Let's consider a simple example using [messages](../concepts/low_level.md#working-with-messages-in-graph-state). This represents a versatile formulation of state for many LLM applications. See our [concepts page](../concepts/low_level.md#working-with-messages-in-graph-state) for more detail. +:::python ```python from langchain_core.messages import AnyMessage from typing_extensions import TypedDict @@ -41,9 +56,25 @@ class State(TypedDict): ``` This state tracks a list of [message](https://python.langchain.com/docs/concepts/messages/) objects, as well as an extra integer field. +::: + +:::js +```typescript +import { BaseMessage } from "@langchain/core/messages"; +import { z } from "zod"; + +const State = z.object({ + messages: z.array(z.custom()), + extraField: z.number(), +}); +``` + +This state tracks a list of [message](https://js.langchain.com/docs/concepts/messages/) objects, as well as an extra integer field. +::: ### Update state +:::python Let's build an example graph with a single node. Our [node](../concepts/low_level.md#nodes) is just a Python function that reads our graph's state and makes updates to it. The first argument to this function will always be the state: ```python @@ -56,11 +87,29 @@ def node(state: State): ``` This node simply appends a message to our message list, and populates an extra field. +::: + +:::js +Let's build an example graph with a single node. Our [node](../concepts/low_level.md#nodes) is just a TypeScript function that reads our graph's state and makes updates to it. The first argument to this function will always be the state: + +```typescript +import { AIMessage } from "@langchain/core/messages"; + +const node = (state: z.infer) => { + const messages = state.messages; + const newMessage = new AIMessage("Hello!"); + return { messages: messages.concat([newMessage]), extraField: 10 }; +}; +``` + +This node simply appends a message to our message list, and populates an extra field. +::: !!! important Nodes should return updates to the state directly, instead of mutating the state. +:::python Let's next define a simple graph containing this node. We use [StateGraph](../concepts/low_level.md#stategraph) to define a graph that operates on this state. We then use [add_node](../concepts/low_level.md#nodes) populate our graph. ```python @@ -71,9 +120,24 @@ builder.add_node(node) builder.set_entry_point("node") graph = builder.compile() ``` +::: + +:::js +Let's next define a simple graph containing this node. We use [StateGraph](../concepts/low_level.md#stategraph) to define a graph that operates on this state. We then use [addNode](../concepts/low_level.md#nodes) populate our graph. + +```typescript +import { StateGraph } from "@langchain/langgraph"; + +const graph = new StateGraph(State) + .addNode("node", node) + .addEdge("__start__", "node") + .compile(); +``` +::: LangGraph provides built-in utilities for visualizing your graph. Let's inspect our graph. See [this section](#visualize-your-graph) for detail on visualization. +:::python ```python from IPython.display import Image, display @@ -81,9 +145,23 @@ display(Image(graph.get_graph().draw_mermaid_png())) ``` ![Simple graph with single node](assets/graph_api_image_1.png) +::: + +:::js +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` +::: In this case, our graph just executes a single node. Let's proceed with a simple invocation: +:::python ```python from langchain_core.messages import HumanMessage @@ -94,12 +172,27 @@ result ``` {'messages': [HumanMessage(content='Hi'), AIMessage(content='Hello!')], 'extra_field': 10} ``` +::: + +:::js +```typescript +import { HumanMessage } from "@langchain/core/messages"; + +const result = await graph.invoke({ messages: [new HumanMessage("Hi")], extraField: 0 }); +console.log(result); +``` + +``` +{ messages: [HumanMessage { content: 'Hi' }, AIMessage { content: 'Hello!' }], extraField: 10 } +``` +::: Note that: - We kicked off invocation by updating a single key of the state. - We receive the entire state in the invocation result. +:::python For convenience, we frequently inspect the content of [message objects](https://python.langchain.com/docs/concepts/messages/) via pretty-print: ```python @@ -115,11 +208,28 @@ Hi Hello! ``` +::: + +:::js +For convenience, we frequently inspect the content of [message objects](https://js.langchain.com/docs/concepts/messages/) via logging: + +```typescript +for (const message of result.messages) { + console.log(`${message.getType()}: ${message.content}`); +} +``` + +``` +human: Hi +ai: Hello! +``` +::: ### Process state updates with reducers Each key in the state can have its own independent [reducer](../concepts/low_level.md#reducers) function, which controls how updates from nodes are applied. If no reducer function is explicitly specified then it is assumed that all updates to the key should override it. +:::python For `TypedDict` state schemas, we can define reducers by annotating the corresponding field of the state with a reducer function. In the earlier example, our node updated the `"messages"` key in the state by appending a message to it. Below, we add a reducer to this key, such that updates are automatically appended: @@ -145,7 +255,35 @@ def node(state: State): # highlight-next-line return {"messages": [new_message], "extra_field": 10} ``` +::: +:::js +For Zod state schemas, we can define reducers by using the special `.langgraph.reducer()` method on the schema field. + +In the earlier example, our node updated the `"messages"` key in the state by appending a message to it. Below, we add a reducer to this key, such that updates are automatically appended: + +```typescript +import "@langchain/langgraph/zod"; + +const State = z.object({ + // highlight-next-line + messages: z.array(z.custom()).langgraph.reducer((x, y) => x.concat(y)), + extraField: z.number(), +}); +``` + +Now our node can be simplified: + +```typescript +const node = (state: z.infer) => { + const newMessage = new AIMessage("Hello!"); + // highlight-next-line + return { messages: [newMessage], extraField: 10 }; +}; +``` +::: + +:::python ```python from langgraph.graph import START @@ -165,6 +303,29 @@ Hi Hello! ``` +::: + +:::js +```typescript +import { START } from "@langchain/langgraph"; + +const graph = new StateGraph(State) + .addNode("node", node) + .addEdge(START, "node") + .compile(); + +const result = await graph.invoke({ messages: [new HumanMessage("Hi")] }); + +for (const message of result.messages) { + console.log(`${message.getType()}: ${message.content}`); +} +``` + +``` +human: Hi +ai: Hello! +``` +::: #### MessagesState @@ -173,6 +334,7 @@ In practice, there are additional considerations for updating lists of messages: - We may wish to update an existing message in the state. - We may want to accept short-hands for [message formats](../concepts/low_level.md#using-messages-in-your-graph), such as [OpenAI format](https://python.langchain.com/docs/concepts/messages/#openai-format). +:::python LangGraph includes a built-in reducer `add_messages` that handles these considerations: ```python @@ -217,6 +379,55 @@ from langgraph.graph import MessagesState class State(MessagesState): extra_field: int ``` +::: + +:::js +LangGraph includes a built-in `MessagesZodState` that handles these considerations: + +```typescript +import { MessagesZodState } from "@langchain/langgraph"; + +const State = z.object({ + // highlight-next-line + messages: MessagesZodState.shape.messages, + extraField: z.number(), +}); + +const graph = new StateGraph(State) + .addNode("node", (state) => { + const newMessage = new AIMessage("Hello!"); + return { messages: [newMessage], extraField: 10 }; + }) + .addEdge(START, "node") + .compile(); +``` + +```typescript +// highlight-next-line +const inputMessage = { role: "user", content: "Hi" }; + +const result = await graph.invoke({ messages: [inputMessage] }); + +for (const message of result.messages) { + console.log(`${message.getType()}: ${message.content}`); +} +``` + +``` +human: Hi +ai: Hello! +``` + +This is a versatile representation of state for applications involving [chat models](https://js.langchain.com/docs/concepts/chat_models/). LangGraph includes this pre-built `MessagesZodState` for convenience, so that we can have: + +```typescript +import { MessagesZodState } from "@langchain/langgraph"; + +const State = MessagesZodState.extend({ + extraField: z.number(), +}); +``` +::: ### Define input and output schemas @@ -226,6 +437,7 @@ When distinct schemas are specified, an internal schema will still be used for c Below, we'll see how to define distinct input and output schema. +:::python ```python from langgraph.graph import StateGraph, START, END from typing_extensions import TypedDict @@ -261,6 +473,48 @@ print(graph.invoke({"question": "hi"})) ``` {'answer': 'bye'} ``` +::: + +:::js +```typescript +import { StateGraph, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +// Define the schema for the input +const InputState = z.object({ + question: z.string(), +}); + +// Define the schema for the output +const OutputState = z.object({ + answer: z.string(), +}); + +// Define the overall schema, combining both input and output +const OverallState = InputState.merge(OutputState); + +// Build the graph with input and output schemas specified +const graph = new StateGraph({ + input: InputState, + output: OutputState, + state: OverallState, +}) + .addNode("answerNode", (state) => { + // Example answer and an extra key + return { answer: "bye", question: state.question }; + }) + .addEdge(START, "answerNode") + .addEdge("answerNode", END) + .compile(); + +// Invoke the graph with an input and print the result +console.log(await graph.invoke({ question: "hi" })); +``` + +``` +{ answer: 'bye' } +``` +::: Notice that the output of invoke only includes the output schema. @@ -270,6 +524,7 @@ In some cases, you may want nodes to exchange information that is crucial for in Below, we'll create an example sequential graph consisting of three nodes (node_1, node_2 and node_3), where private data is passed between the first two steps (node_1 and node_2), while the third step (node_3) only has access to the public overall state. +:::python ```python from langgraph.graph import StateGraph, START, END from typing_extensions import TypedDict @@ -334,6 +589,87 @@ Entered node `node_3`: Output of graph invocation: {'a': 'set by node_3'} ``` +::: + +:::js +```typescript +import { StateGraph, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +// The overall state of the graph (this is the public state shared across nodes) +const OverallState = z.object({ + a: z.string(), +}); + +// Output from node1 contains private data that is not part of the overall state +const Node1Output = z.object({ + privateData: z.string(), +}); + +// The private data is only shared between node1 and node2 +const node1 = (state: z.infer): z.infer => { + const output = { privateData: "set by node1" }; + console.log(`Entered node 'node1':\n\tInput: ${JSON.stringify(state)}.\n\tReturned: ${JSON.stringify(output)}`); + return output; +}; + +// Node 2 input only requests the private data available after node1 +const Node2Input = z.object({ + privateData: z.string(), +}); + +const node2 = (state: z.infer): z.infer => { + const output = { a: "set by node2" }; + console.log(`Entered node 'node2':\n\tInput: ${JSON.stringify(state)}.\n\tReturned: ${JSON.stringify(output)}`); + return output; +}; + +// Node 3 only has access to the overall state (no access to private data from node1) +const node3 = (state: z.infer): z.infer => { + const output = { a: "set by node3" }; + console.log(`Entered node 'node3':\n\tInput: ${JSON.stringify(state)}.\n\tReturned: ${JSON.stringify(output)}`); + return output; +}; + +// Connect nodes in a sequence +// node2 accepts private data from node1, whereas +// node3 does not see the private data. +const graph = new StateGraph({ + state: OverallState, + nodes: { + node1: { action: node1, output: Node1Output }, + node2: { action: node2, input: Node2Input }, + node3: { action: node3 }, + } +}) + .addEdge(START, "node1") + .addEdge("node1", "node2") + .addEdge("node2", "node3") + .addEdge("node3", END) + .compile(); + +// Invoke the graph with the initial state +const response = await graph.invoke({ a: "set at start" }); + +console.log(`\nOutput of graph invocation: ${JSON.stringify(response)}`); +``` + +``` +Entered node 'node1': + Input: {"a":"set at start"}. + Returned: {"privateData":"set by node1"} +Entered node 'node2': + Input: {"privateData":"set by node1"}. + Returned: {"a":"set by node2"} +Entered node 'node3': + Input: {"a":"set by node2"}. + Returned: {"a":"set by node3"} + +Output of graph invocation: {"a":"set by node3"} +``` +::: + +:::python ### Use Pydantic models for graph state @@ -513,6 +849,35 @@ See below for additional features of Pydantic model state: for i, msg in enumerate(output_model.messages): print(f"Message {i}: {type(msg).__name__} - {msg.content}") ``` +::: + +:::js +### Alternative state definitions + +While Zod schemas are the recommended approach, LangGraph also supports other ways to define state schemas: + +```typescript +import { BaseMessage } from "@langchain/core/messages"; +import { StateGraph } from "@langchain/langgraph"; + +interface WorkflowChannelsState { + messages: BaseMessage[]; + question: string; + answer: string; +} + +const workflowWithChannels = new StateGraph({ + channels: { + messages: { + reducer: (currentState, updateValue) => currentState.concat(updateValue), + default: () => [], + }, + question: null, + answer: null, + }, +}); +``` +::: ## Add runtime configuration @@ -526,6 +891,7 @@ To add runtime configuration: See below for a simple example: +:::python ```python from langgraph.graph import END, StateGraph, START from langgraph.runtime import Runtime @@ -569,9 +935,56 @@ print(graph.invoke({}, context={"my_runtime_value": "b"})) {'my_state_value': 1} {'my_state_value': 2} ``` +::: + +:::js +```typescript +import { StateGraph, END, START } from "@langchain/langgraph"; +import { RunnableConfig } from "@langchain/core/runnables"; +import { z } from "zod"; + +// 1. Specify config schema +const ConfigurableSchema = z.object({ + myRuntimeValue: z.string(), +}); + +// 2. Define a graph that accesses the config in a node +const State = z.object({ + myStateValue: z.number(), +}); + +const graph = new StateGraph(State) + .addNode("node", (state, config) => { + // highlight-next-line + if (config?.configurable?.myRuntimeValue === "a") { + return { myStateValue: 1 }; + // highlight-next-line + } else if (config?.configurable?.myRuntimeValue === "b") { + return { myStateValue: 2 }; + } else { + throw new Error("Unknown values."); + } + }) + .addEdge(START, "node") + .addEdge("node", END) + .compile(); + +// 3. Pass in configuration at runtime: +// highlight-next-line +console.log(await graph.invoke({}, { configurable: { myRuntimeValue: "a" } })); +// highlight-next-line +console.log(await graph.invoke({}, { configurable: { myRuntimeValue: "b" } })); +``` + +``` +{ myStateValue: 1 } +{ myStateValue: 2 } +``` +::: ??? example "Extended example: specifying LLM at runtime" + :::python Below we demonstrate a practical example in which we configure what LLM to use at runtime. We will use both OpenAI and Anthropic models. ```python @@ -617,9 +1030,60 @@ print(graph.invoke({}, context={"my_runtime_value": "b"})) claude-3-5-haiku-20241022 gpt-4.1-mini-2025-04-14 ``` + ::: + + :::js + Below we demonstrate a practical example in which we configure what LLM to use at runtime. We will use both OpenAI and Anthropic models. + + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { MessagesZodState, StateGraph, START, END } from "@langchain/langgraph"; + import { RunnableConfig } from "@langchain/core/runnables"; + import { z } from "zod"; + + const ConfigSchema = z.object({ + modelProvider: z.string().default("anthropic"), + }); + + const MODELS = { + anthropic: new ChatAnthropic({ model: "claude-3-5-haiku-latest" }), + openai: new ChatOpenAI({ model: "gpt-4o-mini" }), + }; + + const graph = new StateGraph(MessagesZodState) + .addNode("model", async (state, config) => { + const modelProvider = config?.configurable?.modelProvider || "anthropic"; + const model = MODELS[modelProvider as keyof typeof MODELS]; + const response = await model.invoke(state.messages); + return { messages: [response] }; + }) + .addEdge(START, "model") + .addEdge("model", END) + .compile(); + + // Usage + const inputMessage = { role: "user", content: "hi" }; + // With no configuration, uses default (Anthropic) + const response1 = await graph.invoke({ messages: [inputMessage] }); + // Or, can set OpenAI + const response2 = await graph.invoke( + { messages: [inputMessage] }, + { configurable: { modelProvider: "openai" } } + ); + + console.log(response1.messages.at(-1)?.response_metadata?.model); + console.log(response2.messages.at(-1)?.response_metadata?.model); + ``` + ``` + claude-3-5-haiku-20241022 + gpt-4o-mini-2024-07-18 + ``` + ::: ??? example "Extended example: specifying model and system message at runtime" + :::python Below we demonstrate a practical example in which we configure two parameters: the LLM and system message to use at runtime. ```python @@ -670,11 +1134,74 @@ print(graph.invoke({}, context={"my_runtime_value": "b"})) Ciao! Come posso aiutarti oggi? ``` + ::: + + :::js + Below we demonstrate a practical example in which we configure two parameters: the LLM and system message to use at runtime. + + ```typescript + import { ChatOpenAI } from "@langchain/openai"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { SystemMessage } from "@langchain/core/messages"; + import { MessagesZodState, StateGraph, START, END } from "@langchain/langgraph"; + import { z } from "zod"; + + const ConfigSchema = z.object({ + modelProvider: z.string().default("anthropic"), + systemMessage: z.string().optional(), + }); + + const MODELS = { + anthropic: new ChatAnthropic({ model: "claude-3-5-haiku-latest" }), + openai: new ChatOpenAI({ model: "gpt-4o-mini" }), + }; + + const graph = new StateGraph(MessagesZodState) + .addNode("model", async (state, config) => { + const modelProvider = config?.configurable?.modelProvider || "anthropic"; + const systemMessage = config?.configurable?.systemMessage; + + const model = MODELS[modelProvider as keyof typeof MODELS]; + let messages = state.messages; + + if (systemMessage) { + messages = [new SystemMessage(systemMessage), ...messages]; + } + + const response = await model.invoke(messages); + return { messages: [response] }; + }) + .addEdge(START, "model") + .addEdge("model", END) + .compile(); + + // Usage + const inputMessage = { role: "user", content: "hi" }; + const response = await graph.invoke( + { messages: [inputMessage] }, + { + configurable: { + modelProvider: "openai", + systemMessage: "Respond in Italian." + } + } + ); + + for (const message of response.messages) { + console.log(`${message.getType()}: ${message.content}`); + } + ``` + ``` + human: hi + ai: Ciao! Come posso aiutarti oggi? + ``` + ::: ## Add retry policies There are many use cases where you may wish for your node to have a custom retry policy, for example if you are calling an API, querying a database, or calling an LLM, etc. LangGraph lets you add retry policies to nodes. +:::python To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node: ```python @@ -703,9 +1230,29 @@ By default, the `retry_on` parameter uses the `default_retry_on` function, which - `OSError` In addition, for exceptions from popular http request libraries such as `requests` and `httpx` it only retries on 5xx status codes. +::: + +:::js +To configure a retry policy, pass the `retryPolicy` parameter to the [addNode](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retryPolicy` parameter takes in a `RetryPolicy` object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node: + +```typescript +import { RetryPolicy } from "@langchain/langgraph"; + +const graph = new StateGraph(State) + .addNode("nodeName", nodeFunction, { retryPolicy: {} }) + .compile(); +``` + +By default, the retry policy retries on any exception except for the following: + +- `TypeError` +- `SyntaxError` +- `ReferenceError` +::: ??? example "Extended example: customizing retry policies" + :::python Consider an example in which we are reading from a SQL database. Below we pass two different retry policies to nodes: ```python @@ -741,6 +1288,59 @@ In addition, for exceptions from popular http request libraries such as `request builder.add_edge("query_database", END) graph = builder.compile() ``` + ::: + + :::js + Consider an example in which we are reading from a SQL database. Below we pass two different retry policies to nodes: + + ```typescript + import Database from "better-sqlite3"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, END, MessagesZodState } from "@langchain/langgraph"; + import { AIMessage } from "@langchain/core/messages"; + import { z } from "zod"; + + // Create an in-memory database + const db: typeof Database.prototype = new Database(":memory:"); + + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" }); + + const callModel = async (state: z.infer) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }; + + const queryDatabase = async (state: z.infer) => { + const queryResult: string = JSON.stringify( + db.prepare("SELECT * FROM Artist LIMIT 10;").all(), + ); + + return { messages: [new AIMessage({ content: "queryResult" })] }; + }; + + const workflow = new StateGraph(MessagesZodState) + // Define the two nodes we will cycle between + .addNode("call_model", callModel, { retryPolicy: { maxAttempts: 5 } }) + .addNode("query_database", queryDatabase, { + retryPolicy: { + retryOn: (e: any): boolean => { + if (e instanceof Database.SqliteError) { + // Retry on "SQLITE_BUSY" error + return e.code === "SQLITE_BUSY"; + } + return false; // Don't retry on other errors + }, + }, + }) + .addEdge(START, "call_model") + .addEdge("call_model", "query_database") + .addEdge("query_database", END); + + const graph = workflow.compile(); + ``` + ::: + +:::python ## Add node caching @@ -765,6 +1365,7 @@ from langgraph.cache.memory import InMemoryCache graph = builder.compile(cache=InMemoryCache()) ``` +::: ## Create a sequence of steps @@ -777,6 +1378,7 @@ Here we demonstrate how to construct a simple sequence of steps. We will show: 1. How to build a sequential graph 2. Built-in short-hand for constructing similar graphs. +:::python To add a sequence of nodes, we use the `.add_node` and `.add_edge` methods of our [graph](../concepts/low_level.md#stategraph): ```python @@ -801,6 +1403,23 @@ We can also use the built-in shorthand `.add_sequence`: builder = StateGraph(State).add_sequence([step_1, step_2, step_3]) builder.add_edge(START, "step_1") ``` +::: + +:::js +To add a sequence of nodes, we use the `.addNode` and `.addEdge` methods of our [graph](../concepts/low_level.md#stategraph): + +```typescript +import { START, StateGraph } from "@langchain/langgraph"; + +const builder = new StateGraph(State) + .addNode("step1", step1) + .addNode("step2", step2) + .addNode("step3", step3) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3"); +``` +::: ??? info "Why split application steps into a sequence with LangGraph?" LangGraph makes it easy to add an underlying persistence layer to your application. @@ -823,6 +1442,7 @@ Let's first define our [state](../concepts/low_level.md#state). This governs the In our case, we will just keep track of two values: +:::python ```python from typing_extensions import TypedDict @@ -830,7 +1450,20 @@ class State(TypedDict): value_1: str value_2: int ``` +::: +:::js +```typescript +import { z } from "zod"; + +const State = z.object({ + value1: z.string(), + value2: z.number(), +}); +``` +::: + +:::python Our [nodes](../concepts/low_level.md#nodes) are just Python functions that read our graph's state and make updates to it. The first argument to this function will always be the state: ```python @@ -844,6 +1477,26 @@ def step_2(state: State): def step_3(state: State): return {"value_2": 10} ``` +::: + +:::js +Our [nodes](../concepts/low_level.md#nodes) are just TypeScript functions that read our graph's state and make updates to it. The first argument to this function will always be the state: + +```typescript +const step1 = (state: z.infer) => { + return { value1: "a" }; +}; + +const step2 = (state: z.infer) => { + const currentValue1 = state.value1; + return { value1: `${currentValue1} b` }; +}; + +const step3 = (state: z.infer) => { + return { value2: 10 }; +}; +``` +::: !!! note @@ -855,7 +1508,6 @@ Finally, we define the graph. We use [StateGraph](../concepts/low_level.md#state :::python We will then use [add_node](../concepts/low_level.md#messagesstate) and [add_edge](../concepts/low_level.md#edges) to populate our graph and define its control flow. -::: ```python from langgraph.graph import START, StateGraph @@ -872,7 +1524,26 @@ builder.add_edge(START, "step_1") builder.add_edge("step_1", "step_2") builder.add_edge("step_2", "step_3") ``` +::: +:::js +We will then use [addNode](../concepts/low_level.md#nodes) and [addEdge](../concepts/low_level.md#edges) to populate our graph and define its control flow. + +```typescript +import { START, StateGraph } from "@langchain/langgraph"; + +const graph = new StateGraph(State) + .addNode("step1", step1) + .addNode("step2", step2) + .addNode("step3", step3) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .compile(); +``` +::: + +:::python !!! tip "Specifying custom names" You can specify custom names for nodes using `.add_node`: @@ -880,9 +1551,23 @@ builder.add_edge("step_2", "step_3") ```python builder.add_node("my_node", step_1) ``` +::: + +:::js +!!! tip "Specifying custom names" + + You can specify custom names for nodes using `.addNode`: + + ```typescript + const graph = new StateGraph(State) + .addNode("myNode", step1) + .compile(); + ``` +::: Note that: +:::python - `.add_edge` takes the names of nodes, which for functions defaults to `node.__name__`. - We must specify the entry point of the graph. For this we add an edge with the [START node](../concepts/low_level.md#start-node). - The graph halts when there are no more nodes to execute. @@ -892,9 +1577,19 @@ We next [compile](../concepts/low_level.md#compiling-your-graph) our graph. This ```python graph = builder.compile() ``` +::: + +:::js +- `.addEdge` takes the names of nodes, which for functions defaults to `node.name`. +- We must specify the entry point of the graph. For this we add an edge with the [START node](../concepts/low_level.md#start-node). +- The graph halts when there are no more nodes to execute. + +We next [compile](../concepts/low_level.md#compiling-your-graph) our graph. This provides a few basic checks on the structure of the graph (e.g., identifying orphaned nodes). If we were adding persistence to our application via a [checkpointer](../concepts/persistence.md), it would also be passed in here. +::: LangGraph provides built-in utilities for visualizing your graph. Let's inspect our sequence. See [this guide](#visualize-your-graph) for detail on visualization. +:::python ```python from IPython.display import Image, display @@ -902,9 +1597,23 @@ display(Image(graph.get_graph().draw_mermaid_png())) ``` ![Sequence of steps graph](assets/graph_api_image_2.png) +::: + +:::js +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` +::: Let's proceed with a simple invocation: +:::python ```python graph.invoke({"value_1": "c"}) ``` @@ -912,6 +1621,18 @@ graph.invoke({"value_1": "c"}) ``` {'value_1': 'a b', 'value_2': 10} ``` +::: + +:::js +```typescript +const result = await graph.invoke({ value1: "c" }); +console.log(result); +``` + +``` +{ value1: 'a b', value2: 10 } +``` +::: Note that: @@ -920,6 +1641,7 @@ Note that: - The second node updated the value. - The third node populated a different value. +:::python !!! tip "Built-in shorthand" `langgraph>=0.2.46` includes a built-in short-hand `add_sequence` for adding node sequences. You can compile the same graph as follows: @@ -933,6 +1655,7 @@ Note that: graph.invoke({"value_1": "c"}) ``` +::: ## Create branches @@ -942,6 +1665,7 @@ Parallel execution of nodes is essential to speed up overall graph operation. La In this example, we fan out from `Node A` to `B and C` and then fan in to `D`. With our state, [we specify the reducer add operation](https://langchain-ai.github.io/langgraph/concepts/low_level.md#reducers). This will combine or accumulate values for the specific key in the State, rather than simply overwriting the existing value. For lists, this means concatenating the new list with the existing list. See the above section on [state reducers](#process-state-updates-with-reducers) for more detail on updating state with reducers. +:::python ```python import operator from typing import Annotated, Any @@ -981,7 +1705,55 @@ builder.add_edge("c", "d") builder.add_edge("d", END) graph = builder.compile() ``` +::: +:::js +```typescript +import "@langchain/langgraph/zod"; +import { StateGraph, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + // The reducer makes this append-only + aggregate: z.array(z.string()).langgraph.reducer((x, y) => x.concat(y)), +}); + +const nodeA = (state: z.infer) => { + console.log(`Adding "A" to ${state.aggregate}`); + return { aggregate: ["A"] }; +}; + +const nodeB = (state: z.infer) => { + console.log(`Adding "B" to ${state.aggregate}`); + return { aggregate: ["B"] }; +}; + +const nodeC = (state: z.infer) => { + console.log(`Adding "C" to ${state.aggregate}`); + return { aggregate: ["C"] }; +}; + +const nodeD = (state: z.infer) => { + console.log(`Adding "D" to ${state.aggregate}`); + return { aggregate: ["D"] }; +}; + +const graph = new StateGraph(State) + .addNode("a", nodeA) + .addNode("b", nodeB) + .addNode("c", nodeC) + .addNode("d", nodeD) + .addEdge(START, "a") + .addEdge("a", "b") + .addEdge("a", "c") + .addEdge("b", "d") + .addEdge("c", "d") + .addEdge("d", END) + .compile(); +``` +::: + +:::python ```python from IPython.display import Image, display @@ -989,9 +1761,23 @@ display(Image(graph.get_graph().draw_mermaid_png())) ``` ![Parallel execution graph](assets/graph_api_image_3.png) +::: + +:::js +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` +::: With the reducer, you can see that the values added in each node are accumulated. +:::python ```python graph.invoke({"aggregate": []}, {"configurable": {"thread_id": "foo"}}) ``` @@ -1002,6 +1788,24 @@ Adding "B" to ['A'] Adding "C" to ['A'] Adding "D" to ['A', 'B', 'C'] ``` +::: + +:::js +```typescript +const result = await graph.invoke({ + aggregate: [], +}); +console.log(result); +``` + +``` +Adding "A" to [] +Adding "B" to ['A'] +Adding "C" to ['A'] +Adding "D" to ['A', 'B', 'C'] +{ aggregate: ['A', 'B', 'C', 'D'] } +``` +::: !!! note @@ -1022,6 +1826,8 @@ Adding "D" to ['A', 'B', 'C'] Together, these let you perform parallel execution and fully control exception handling. +:::python + ### Defer node execution Deferring node execution is useful when you want to delay the execution of a node until all other pending tasks are completed. This is particularly relevant when branches have different lengths, which is common in workflows like map-reduce flows. @@ -1096,9 +1902,11 @@ Adding "D" to ['A', 'B', 'C', 'B_2'] ``` In the above example, nodes `"b"` and `"c"` are executed concurrently in the same superstep. We set `defer=True` on node `d` so it will not execute until all pending tasks are finished. In this case, this means that `"d"` waits to execute until the entire `"b"` branch is finished. +::: ### Conditional branching +:::python If your fan-out should vary at runtime based on the state, you can use [add_conditional_edges](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.StateGraph.add_conditional_edges) to select one or more paths using the graph state. See example below, where node `a` generates a state update that determines the following node. ```python @@ -1163,22 +1971,108 @@ Adding "A" to [] Adding "C" to ['A'] {'aggregate': ['A', 'C'], 'which': 'c'} ``` +::: + +:::js +If your fan-out should vary at runtime based on the state, you can use [addConditionalEdges](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.StateGraph.addConditionalEdges) to select one or more paths using the graph state. See example below, where node `a` generates a state update that determines the following node. + +```typescript +import "@langchain/langgraph/zod"; +import { StateGraph, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + aggregate: z.array(z.string()).langgraph.reducer((x, y) => x.concat(y)), + // Add a key to the state. We will set this key to determine + // how we branch. + which: z.string().langgraph.reducer((x, y) => y ?? x), +}); + +const nodeA = (state: z.infer) => { + console.log(`Adding "A" to ${state.aggregate}`); + // highlight-next-line + return { aggregate: ["A"], which: "c" }; +}; + +const nodeB = (state: z.infer) => { + console.log(`Adding "B" to ${state.aggregate}`); + return { aggregate: ["B"] }; +}; + +const nodeC = (state: z.infer) => { + console.log(`Adding "C" to ${state.aggregate}`); + return { aggregate: ["C"] }; +}; + +const conditionalEdge = (state: z.infer): "b" | "c" => { + // Fill in arbitrary logic here that uses the state + // to determine the next node + return state.which as "b" | "c"; +}; + +// highlight-next-line +const graph = new StateGraph(State) + .addNode("a", nodeA) + .addNode("b", nodeB) + .addNode("c", nodeC) + .addEdge(START, "a") + .addEdge("b", END) + .addEdge("c", END) + .addConditionalEdges("a", conditionalEdge) + .compile(); +``` + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` + +```typescript +const result = await graph.invoke({ aggregate: [] }); +console.log(result); +``` + +``` +Adding "A" to [] +Adding "C" to ['A'] +{ aggregate: ['A', 'C'], which: 'c' } +``` +::: !!! tip Your conditional edges can route to multiple destination nodes. For example: + :::python ```python def route_bc_or_cd(state: State) -> Sequence[str]: if state["which"] == "cd": return ["c", "d"] return ["b", "c"] ``` + ::: + + :::js + ```typescript + const routeBcOrCd = (state: z.infer): string[] => { + if (state.which === "cd") { + return ["c", "d"]; + } + return ["b", "c"]; + }; + ``` + ::: ## Map-Reduce and the Send API LangGraph supports map-reduce and other advanced branching patterns using the Send API. Here is an example of how to use it: +:::python ```python from langgraph.graph import StateGraph, START, END from langgraph.types import Send @@ -1241,6 +2135,78 @@ for step in graph.stream({"topic": "animals"}): {'generate_joke': {'jokes': ['Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice.']}} {'best_joke': {'best_selected_joke': 'penguins'}} ``` +::: + +:::js +```typescript +import "@langchain/langgraph/zod"; +import { StateGraph, START, END, Send } from "@langchain/langgraph"; +import { z } from "zod"; + +const OverallState = z.object({ + topic: z.string(), + subjects: z.array(z.string()), + jokes: z.array(z.string()).langgraph.reducer((x, y) => x.concat(y)), + bestSelectedJoke: z.string(), +}); + +const generateTopics = (state: z.infer) => { + return { subjects: ["lions", "elephants", "penguins"] }; +}; + +const generateJoke = (state: { subject: string }) => { + const jokeMap: Record = { + lions: "Why don't lions like fast food? Because they can't catch it!", + elephants: "Why don't elephants use computers? They're afraid of the mouse!", + penguins: "Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice." + }; + return { jokes: [jokeMap[state.subject]] }; +}; + +const continueToJokes = (state: z.infer) => { + return state.subjects.map((subject) => new Send("generateJoke", { subject })); +}; + +const bestJoke = (state: z.infer) => { + return { bestSelectedJoke: "penguins" }; +}; + +const graph = new StateGraph(OverallState) + .addNode("generateTopics", generateTopics) + .addNode("generateJoke", generateJoke) + .addNode("bestJoke", bestJoke) + .addEdge(START, "generateTopics") + .addConditionalEdges("generateTopics", continueToJokes) + .addEdge("generateJoke", "bestJoke") + .addEdge("bestJoke", END) + .compile(); +``` + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` + +```typescript +// Call the graph: here we call it to generate a list of jokes +for await (const step of await graph.stream({ topic: "animals" })) { + console.log(step); +} +``` + +``` +{ generateTopics: { subjects: [ 'lions', 'elephants', 'penguins' ] } } +{ generateJoke: { jokes: [ "Why don't lions like fast food? Because they can't catch it!" ] } } +{ generateJoke: { jokes: [ "Why don't elephants use computers? They're afraid of the mouse!" ] } } +{ generateJoke: { jokes: [ "Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice." ] } } +{ bestJoke: { bestSelectedJoke: 'penguins' } } +``` +::: ## Create and control loops @@ -1256,6 +2222,7 @@ Let's consider a simple graph with a loop to better understand how these mechani When creating a loop, you can include a conditional edge that specifies a termination condition: +:::python ```python builder = StateGraph(State) builder.add_node(a) @@ -1272,9 +2239,31 @@ builder.add_conditional_edges("a", route) builder.add_edge("b", "a") graph = builder.compile() ``` +::: -To control the recursion limit, specify `"recursion_limit"` in the config. This will raise a `GraphRecursionError`, which you can catch and handle: +:::js +```typescript +const graph = new StateGraph(State) + .addNode("a", nodeA) + .addNode("b", nodeB) + .addEdge(START, "a") + .addConditionalEdges("a", route) + .addEdge("b", "a") + .compile(); +const route = (state: z.infer): "b" | typeof END => { + if (terminationCondition(state)) { + return END; + } else { + return "b"; + } +}; +``` +::: + +To control the recursion limit, specify `"recursionLimit"` in the config. This will raise a `GraphRecursionError`, which you can catch and handle: + +:::python ```python from langgraph.errors import GraphRecursionError @@ -1283,9 +2272,25 @@ try: except GraphRecursionError: print("Recursion Error") ``` +::: + +:::js +```typescript +import { GraphRecursionError } from "@langchain/langgraph"; + +try { + await graph.invoke(inputs, { recursionLimit: 3 }); +} catch (error) { + if (error instanceof GraphRecursionError) { + console.log("Recursion Error"); + } +} +``` +::: Let's define a graph with a simple loop. Note that we use a conditional edge to implement a termination condition. +:::python ```python import operator from typing import Annotated, Literal @@ -1329,13 +2334,65 @@ display(Image(graph.get_graph().draw_mermaid_png())) ``` ![Simple loop graph](assets/graph_api_image_3.png) +::: -This architecture is similar to a [ReAct agent](../agents/overview.md) in which node `"a"` is a tool-calling model, and node `"b"` represents the tools. +:::js +```typescript +import "@langchain/langgraph/zod"; +import { StateGraph, START, END } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + // The reducer makes this append-only + aggregate: z.array(z.string()).langgraph.reducer((x, y) => x.concat(y)), +}); + +const nodeA = (state: z.infer) => { + console.log(`Node A sees ${state.aggregate}`); + return { aggregate: ["A"] }; +}; + +const nodeB = (state: z.infer) => { + console.log(`Node B sees ${state.aggregate}`); + return { aggregate: ["B"] }; +}; + +// Define edges +const route = (state: z.infer): "b" | typeof END => { + if (state.aggregate.length < 7) { + return "b"; + } else { + return END; + } +}; + +const graph = new StateGraph(State) + .addNode("a", nodeA) + .addNode("b", nodeB) + .addEdge(START, "a") + .addConditionalEdges("a", route) + .addEdge("b", "a") + .compile(); +``` + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` +::: + +This architecture is similar to a [React agent](../agents/overview.md) in which node `"a"` is a tool-calling model, and node `"b"` represents the tools. In our `route` conditional edge, we specify that we should end after the `"aggregate"` list in the state passes a threshold length. Invoking the graph, we see that we alternate between nodes `"a"` and `"b"` before terminating once we reach the termination condition. +:::python ```python graph.invoke({"aggregate": []}) ``` @@ -1349,11 +2406,31 @@ Node A sees ['A', 'B', 'A', 'B'] Node B sees ['A', 'B', 'A', 'B', 'A'] Node A sees ['A', 'B', 'A', 'B', 'A', 'B'] ``` +::: + +:::js +```typescript +const result = await graph.invoke({ aggregate: [] }); +console.log(result); +``` + +``` +Node A sees [] +Node B sees ['A'] +Node A sees ['A', 'B'] +Node B sees ['A', 'B', 'A'] +Node A sees ['A', 'B', 'A', 'B'] +Node B sees ['A', 'B', 'A', 'B', 'A'] +Node A sees ['A', 'B', 'A', 'B', 'A', 'B'] +{ aggregate: ['A', 'B', 'A', 'B', 'A', 'B', 'A'] } +``` +::: ### Impose a recursion limit In some applications, we may not have a guarantee that we will reach a given termination condition. In these cases, we can set the graph's [recursion limit](../concepts/low_level.md#recursion-limit). This will raise a `GraphRecursionError` after a given number of [supersteps](../concepts/low_level.md#graphs). We can then catch and handle this exception: +:::python ```python from langgraph.errors import GraphRecursionError @@ -1371,7 +2448,33 @@ Node D sees ['A', 'B'] Node A sees ['A', 'B', 'C', 'D'] Recursion Error ``` +::: +:::js +```typescript +import { GraphRecursionError } from "@langchain/langgraph"; + +try { + await graph.invoke({ aggregate: [] }, { recursionLimit: 4 }); +} catch (error) { + if (error instanceof GraphRecursionError) { + console.log("Recursion Error"); + } +} +``` + +``` +Node A sees [] +Node B sees ['A'] +Node A sees ['A', 'B'] +Node B sees ['A', 'B', 'A'] +Node A sees ['A', 'B', 'A', 'B'] +Recursion Error +``` +::: + + +:::python ??? example "Extended example: return state on hitting recursion limit" Instead of raising `GraphRecursionError`, we can introduce a new key to the state that keeps track of the number of steps remaining until reaching the recursion limit. We can then use this key to determine if we should end the run. @@ -1424,7 +2527,9 @@ Recursion Error Node A sees ['A', 'B'] {'aggregate': ['A', 'B', 'A']} ``` +::: +:::python ??? example "Extended example: loops with branches" To better understand how the recursion limit works, let's consider a more complex example. Below we implement a loop, but one step fans out into two nodes: @@ -1529,10 +2634,13 @@ Recursion Error Node A sees ['A', 'B', 'C', 'D'] Recursion Error ``` +::: + +:::python ## Async -Using the [async](https://docs.python.org/3/library/asyncio.html) programming paradigm can produce significant performance improvements when running [IO-bound](https://en.wikipedia.org/wiki/I/O_bound) code concurrently (e.g., making concurrent API requests to a chat model provider). +Using the async programming paradigm can produce significant performance improvements when running [IO-bound](https://en.wikipedia.org/wiki/I/O_bound) code concurrently (e.g., making concurrent API requests to a chat model provider). To convert a `sync` implementation of the graph to an `async` implementation, you will need to: @@ -1572,10 +2680,13 @@ result = await graph.ainvoke({"messages": [input_message]}) # (3)! See the [streaming guide](./streaming.md) for examples of streaming with async. +::: + ## Combine control flow and state updates with `Command` It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [Command](../reference/types.md#langgraph.types.Command) object from node functions: +:::python ```python def my_node(state: State) -> Command[Literal["my_other_node"]]: return Command( @@ -1585,9 +2696,26 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]: goto="my_other_node" ) ``` +::: + +:::js +```typescript +import { Command } from "@langchain/langgraph"; + +const myNode = (state: State): Command => { + return new Command({ + // state update + update: { foo: "bar" }, + // control flow + goto: "myOtherNode" + }); +}; +``` +::: We show an end-to-end example below. Let's create a simple graph with 3 nodes: A, B and C. We will first execute node A, and then decide whether to go to Node B or Node C next based on the output of node A. +:::python ```python import random from typing_extensions import TypedDict, Literal @@ -1661,11 +2789,92 @@ graph.invoke({"foo": ""}) Called A Called C ``` +::: + +:::js +```typescript +import { StateGraph, START, Command } from "@langchain/langgraph"; +import { z } from "zod"; + +// Define graph state +const State = z.object({ + foo: z.string(), +}); + +// Define the nodes + +const nodeA = (state: z.infer): Command => { + console.log("Called A"); + const value = Math.random() > 0.5 ? "b" : "c"; + // this is a replacement for a conditional edge function + const goto = value === "b" ? "nodeB" : "nodeC"; + + // note how Command allows you to BOTH update the graph state AND route to the next node + return new Command({ + // this is the state update + update: { foo: value }, + // this is a replacement for an edge + goto, + }); +}; + +const nodeB = (state: z.infer) => { + console.log("Called B"); + return { foo: state.foo + "b" }; +}; + +const nodeC = (state: z.infer) => { + console.log("Called C"); + return { foo: state.foo + "c" }; +}; +``` + +We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../concepts/low_level.md#conditional-edges) for routing! This is because control flow is defined with `Command` inside `nodeA`. + +```typescript +const graph = new StateGraph(State) + .addNode("nodeA", nodeA, { + ends: ["nodeB", "nodeC"], + }) + .addNode("nodeB", nodeB) + .addNode("nodeC", nodeC) + .addEdge(START, "nodeA") + .compile(); +``` + +!!! important + + You might have noticed that we used `ends` to specify which nodes `nodeA` can navigate to. This is necessary for the graph rendering and tells LangGraph that `nodeA` can navigate to `nodeB` and `nodeC`. + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` + +If we run the graph multiple times, we'd see it take different paths (A -> B or A -> C) based on the random choice in node A. + +```typescript +const result = await graph.invoke({ foo: "" }); +console.log(result); +``` + +``` +Called A +Called C +{ foo: 'cc' } +``` +::: ### Navigate to a node in a parent graph If you are using [subgraphs](../concepts/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 ```python def my_node(state: State) -> Command[Literal["my_other_node"]]: return Command( @@ -1674,13 +2883,27 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]: graph=Command.PARENT ) ``` +::: -Let's demonstrate this using the above example. We'll do so by changing `node_a` in the above example into a single-node graph that we'll add as a subgraph to our parent graph. +:::js +```typescript +const myNode = (state: State): Command => { + return new Command({ + update: { foo: "bar" }, + goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph + graph: Command.PARENT + }); +}; +``` +::: + +Let's demonstrate this using the above example. We'll do so by changing `nodeA` in the above example into a single-node graph that we'll add as a subgraph to our 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](../concepts/low_level.md#schema), you **must** define a [reducer](../concepts/low_level.md#reducers) for the key you're updating in the parent graph state. See the example below. +:::python ```python import operator from typing_extensions import Annotated @@ -1741,11 +2964,80 @@ graph.invoke({"foo": ""}) Called A Called C ``` +::: + +:::js +```typescript +import "@langchain/langgraph/zod"; +import { StateGraph, START, Command } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + // NOTE: we define a reducer here + // highlight-next-line + foo: z.string().langgraph.reducer((x, y) => x + y), +}); + +const nodeA = (state: z.infer) => { + console.log("Called A"); + const value = Math.random() > 0.5 ? "nodeB" : "nodeC"; + + // note how Command allows you to BOTH update the graph state AND route to the next node + return new Command({ + update: { foo: "a" }, + goto: value, + // this tells LangGraph to navigate to nodeB or nodeC in the parent graph + // NOTE: this will navigate to the closest parent graph relative to the subgraph + // highlight-next-line + graph: Command.PARENT, + }); +}; + +const subgraph = new StateGraph(State) + .addNode("nodeA", nodeA, { ends: ["nodeB", "nodeC"] }) + .addEdge(START, "nodeA") + .compile(); + +const nodeB = (state: z.infer) => { + console.log("Called B"); + // NOTE: since we've defined a reducer, we don't need to manually append + // new characters to existing 'foo' value. instead, reducer will append these + // automatically + // highlight-next-line + return { foo: "b" }; +}; + +const nodeC = (state: z.infer) => { + console.log("Called C"); + // highlight-next-line + return { foo: "c" }; +}; + +const graph = new StateGraph(State) + .addNode("subgraph", subgraph, { ends: ["nodeB", "nodeC"] }) + .addNode("nodeB", nodeB) + .addNode("nodeC", nodeC) + .addEdge(START, "subgraph") + .compile(); +``` + +```typescript +const result = await graph.invoke({ foo: "" }); +console.log(result); +``` + +``` +Called A +Called C +{ foo: 'ac' } +``` +::: ### Use inside tools A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool: +:::python ```python @tool def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig): @@ -1760,6 +3052,40 @@ def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: R } ) ``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { Command } from "@langchain/langgraph"; +import { RunnableConfig } from "@langchain/core/runnables"; +import { z } from "zod"; + +const lookupUserInfo = tool( + async (input, config: RunnableConfig) => { + const userId = config.configurable?.userId; + const userInfo = getUserInfo(userId); + return new Command({ + update: { + // update the state keys + userInfo: userInfo, + // update the message history + messages: [{ + role: "tool", + content: "Successfully looked up user information", + tool_call_id: config.toolCall.id + }] + } + }); + }, + { + name: "lookupUserInfo", + description: "Use this to look up user information to better assist them with their questions.", + schema: z.object({}), + } +); +``` +::: !!! important @@ -1771,7 +3097,10 @@ If you are using tools that update state via `Command`, we recommend using prebu Here we demonstrate how to visualize the graphs you create. -You can visualize any arbitrary [Graph](https://langchain-ai.github.io/langgraph/reference/graphs/), including [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.state.StateGraph). Let's have some fun by drawing fractals :). +You can visualize any arbitrary [Graph](https://langchain-ai.github.io/langgraph/reference/graphs/), including [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.state.StateGraph). + +:::python +Let's have some fun by drawing fractals :). ```python import random @@ -1826,11 +3155,44 @@ def build_fractal_graph(max_level: int): app = build_fractal_graph(3) ``` +::: + +:::js +Let's create a simple example graph to demonstrate visualization. + +```typescript +import { StateGraph, START, END } from "@langchain/langgraph"; +import { MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = MessagesZodState.extend({ + value: z.number(), +}); + +const app = new StateGraph(State) + .addNode("node1", (state) => { + return { value: state.value + 1 }; + }) + .addNode("node2", (state) => { + return { value: state.value * 2 }; + }) + .addEdge(START, "node1") + .addConditionalEdges("node1", (state) => { + if (state.value < 10) { + return "node2"; + } + return END; + }) + .addEdge("node2", "node1") + .compile(); +``` +::: ### Mermaid We can also convert a graph class into Mermaid syntax. +:::python ```python print(app.get_graph().draw_mermaid()) ``` @@ -1865,9 +3227,34 @@ graph TD; classDef first fill-opacity:0 classDef last fill:#bfb6fc ``` +::: + +:::js +```typescript +const drawableGraph = await app.getGraphAsync(); +console.log(drawableGraph.drawMermaid()); +``` + +``` +%%{init: {'flowchart': {'curve': 'linear'}}}%% +graph TD; + __start__([

__start__

]):::first + node1(node1) + node2(node2) + __end__([

__end__

]):::last + __start__ --> node1; + node1 -.-> node2; + node1 -.-> __end__; + node2 --> node1; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc +``` +::: ### PNG +:::python If preferred, we could render the Graph into a `.png`. Here we could use three options: - Using Mermaid.ink API (does not require additional packages) @@ -1919,3 +3306,18 @@ except ImportError: "You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt" ) ``` +::: + +:::js +If preferred, we could render the Graph into a `.png`. This uses the Mermaid.ink API to generate the diagram. + +```typescript +import * as fs from "node:fs/promises"; + +const drawableGraph = await app.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("graph.png", imageBuffer); +``` +::: \ No newline at end of file diff --git a/docs/docs/how-tos/multi_agent.md b/docs/docs/how-tos/multi_agent.md index e7296f9fd..8f33099e6 100644 --- a/docs/docs/how-tos/multi_agent.md +++ b/docs/docs/how-tos/multi_agent.md @@ -22,6 +22,7 @@ To set up communication between the agents in a multi-agent system you can use [ To implement handoffs, you can return `Command` objects from your agent nodes or tools: +:::python ```python from typing import Annotated from langchain_core.tools import tool, InjectedToolCallId @@ -73,25 +74,109 @@ def create_handoff_tool(*, agent_name: str, description: str | None = None): commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls] return commands ``` +::: + +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { Command, MessagesZodState } 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) => { + // (1)! + const state = config.state; + const toolCallId = config.toolCall.id; + + const toolMessage = { + role: "tool" as const, + content: `Successfully transferred to ${agentName}`, + name: name, + tool_call_id: toolCallId, + }; + + return new Command({ + // (3)! + goto: agentName, + // (4)! + update: { messages: [...state.messages, toolMessage] }, + // (5)! + graph: Command.PARENT, + }); + }, + { + name, + description: toolDescription, + schema: z.object({}), + } + ); +} +``` + +1. Access the [state](../concepts/low_level.md#state) of the agent that is calling the handoff tool through the `config` parameter. +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. + +!!! tip + + If you want to use tools that return `Command`, you can either use prebuilt @[`create_react_agent`][create_react_agent] / @[`ToolNode`][ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.: + + ```typescript + const callTools = async (state) => { + // ... + const commands = await Promise.all( + toolCalls.map(toolCall => toolsByName[toolCall.name].invoke(toolCall)) + ); + return commands; + }; + ``` +::: !!! Important This handoff implementation assumes that: - - each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs) - - each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function: + - each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs) + - each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function: - ```python - def call_hotel_assistant(state): - # return agent's final response, - # excluding inner monologue - response = hotel_assistant.invoke(state) - # highlight-next-line - return {"messages": response["messages"][-1]} - ``` + :::python + ```python + def call_hotel_assistant(state): + # return agent's final response, + # excluding inner monologue + response = hotel_assistant.invoke(state) + # highlight-next-line + return {"messages": response["messages"][-1]} + ``` + ::: + + :::js + ```typescript + const callHotelAssistant = async (state) => { + // return agent's final response, + // excluding inner monologue + const response = await hotelAssistant.invoke(state); + // highlight-next-line + return { messages: [response.messages.at(-1)] }; + }; + ``` + ::: ### Control agent inputs +:::python You can use the @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent: ```python @@ -129,6 +214,63 @@ def create_task_description_handoff_tool( return handoff_tool ``` +::: + +:::js +You can use the @[`Send()`][Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent: + +```typescript +import { tool } from "@langchain/core/tools"; +import { Command, Send, MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; + +function createTaskDescriptionHandoffTool({ + agentName, + description, +}: { + agentName: string; + description?: string; +}) { + const name = `transfer_to_${agentName}`; + const toolDescription = description || `Ask ${agentName} for help.`; + + return tool( + async ( + { taskDescription }, + config + ) => { + const state = config.state; + + const taskDescriptionMessage = { + role: "user" as const, + content: taskDescription, + }; + const agentInput = { + ...state, + messages: [taskDescriptionMessage], + }; + + return new Command({ + // highlight-next-line + goto: [new Send(agentName, agentInput)], + graph: Command.PARENT, + }); + }, + { + name, + description: toolDescription, + schema: z.object({ + taskDescription: z + .string() + .describe( + "Description of what the next agent should do, including all of the relevant context." + ), + }), + } + ); +} +``` +::: See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.md#4-create-delegation-tasks) example for a full example of using @[`Send()`][Send] in handoffs. @@ -136,6 +278,7 @@ See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.md#4- You can use handoffs in any agents built with LangGraph. We recommend using the prebuilt [agent](../agents/overview.md) or [`ToolNode`](./tool-calling.md#toolnode), as they natively support handoffs tools returning `Command`. Below is an example of how you can implement a multi-agent system for booking travel using handoffs: +:::python ```python from langgraph.prebuilt import create_react_agent from langgraph.graph import StateGraph, START, MessagesState @@ -176,9 +319,65 @@ multi_agent_graph = ( .compile() ) ``` +::: + +:::js +```typescript +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { StateGraph, START, MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; + +function createHandoffTool({ + agentName, + description, +}: { + agentName: string; + description?: string; +}) { + // same implementation as above + // ... + return new Command(/* ... */); +} + +// Handoffs +const transferToHotelAssistant = createHandoffTool({ + agentName: "hotel_assistant", +}); +const transferToFlightAssistant = createHandoffTool({ + agentName: "flight_assistant", +}); + +// Define agents +const flightAssistant = createReactAgent({ + llm: model, + // highlight-next-line + tools: [/* ... */, transferToHotelAssistant], + // highlight-next-line + name: "flight_assistant", +}); + +const hotelAssistant = createReactAgent({ + llm: model, + // highlight-next-line + tools: [/* ... */, transferToFlightAssistant], + // highlight-next-line + name: "hotel_assistant", +}); + +// Define multi-agent graph +const multiAgentGraph = new StateGraph(MessagesZodState) + // highlight-next-line + .addNode("flight_assistant", flightAssistant) + // highlight-next-line + .addNode("hotel_assistant", hotelAssistant) + .addEdge(START, "flight_assistant") + .compile(); +``` +::: ??? example "Full example: Multi-agent system for booking travel" + :::python ```python from typing import Annotated from langchain_core.messages import convert_to_messages @@ -323,6 +522,183 @@ multi_agent_graph = ( 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 { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { StateGraph, START, MessagesZodState, Command } from "@langchain/langgraph"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { isBaseMessage } from "@langchain/core/messages"; + import { z } from "zod"; + + // We'll use a helper to render the streamed agent outputs nicely + const prettyPrintMessages = (update: Record) => { + // Handle tuple case with namespace + if (Array.isArray(update)) { + const [ns, updateData] = update; + // Skip parent graph updates in the printouts + if (ns.length === 0) { + return; + } + + const graphId = ns[ns.length - 1].split(":")[0]; + console.log(`Update from subgraph ${graphId}:\n`); + update = updateData; + } + + for (const [nodeName, updateValue] of Object.entries(update)) { + console.log(`Update from node ${nodeName}:\n`); + + const messages = updateValue.messages || []; + for (const message of messages) { + if (isBaseMessage(message)) { + const textContent = + typeof message.content === "string" + ? message.content + : JSON.stringify(message.content); + console.log(`${message.getType()}: ${textContent}`); + } + } + console.log("\n"); + } + }; + + function createHandoffTool({ + agentName, + description, + }: { + agentName: string; + description?: string; + }) { + const name = `transfer_to_${agentName}`; + const toolDescription = description || `Transfer to ${agentName}`; + + return tool( + async (_, config) => { + // highlight-next-line + const state = config.state; // (1)! + const toolCallId = config.toolCall.id; + + const toolMessage = { + role: "tool" as const, + content: `Successfully transferred to ${agentName}`, + name: name, + tool_call_id: toolCallId, + }; + + return new Command({ + // highlight-next-line + goto: agentName, // (3)! + // highlight-next-line + update: { messages: [...state.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 }) => { + return `Successfully booked a stay at ${hotelName}.`; + }, + { + name: "book_hotel", + description: "Book a hotel", + schema: z.object({ + hotelName: z.string(), + }), + } + ); + + const bookFlight = tool( + async ({ fromAirport, toAirport }) => { + return `Successfully booked a flight from ${fromAirport} to ${toAirport}.`; + }, + { + name: "book_flight", + description: "Book a flight", + schema: z.object({ + fromAirport: z.string(), + toAirport: z.string(), + }), + } + ); + + const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", + }); + + // Define agents + const flightAssistant = createReactAgent({ + llm: model, + // highlight-next-line + tools: [bookFlight, transferToHotelAssistant], + prompt: "You are a flight booking assistant", + // highlight-next-line + name: "flight_assistant", + }); + + const hotelAssistant = createReactAgent({ + llm: model, + // highlight-next-line + tools: [bookHotel, transferToFlightAssistant], + prompt: "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 + const stream = await multiAgentGraph.stream( + { + messages: [ + { + role: "user", + content: "book a flight from BOS to JFK and a stay at McKittrick Hotel", + }, + ], + }, + // highlight-next-line + { subgraphs: true } + ); + + for await (const chunk of stream) { + prettyPrintMessages(chunk); + } + ``` + + 1. Access agent's state + 2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs. + 3. Name of the agent or node to hand off to. + 4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state. + 5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph. + ::: ## Multi-turn conversation @@ -333,6 +709,7 @@ The agents can then be implemented as nodes in a graph that executes agent steps 1. **Wait for user input** to continue the conversation, or 2. **Route to another agent** (or back to itself, such as in a loop) via a [handoff](#handoffs) +:::python ```python def human(state) -> Command[Literal["agent", "another_agent"]]: """A node for collecting user input.""" @@ -360,6 +737,44 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]: else: return Command(goto="human") # Go to human node ``` +::: + +:::js +```typescript +import { interrupt, Command } from "@langchain/langgraph"; + +function human(state: MessagesState): Command { + const userInput: string = interrupt("Ready for user input."); + + // Determine the active agent + const activeAgent = /* ... */; + + return new Command({ + update: { + messages: [{ + role: "human", + content: userInput, + }] + }, + goto: activeAgent, + }); +} + +function agent(state: MessagesState): Command { + // The condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc. + const goto = getNextAgent(/* ... */); // 'agent' / 'anotherAgent' + + if (goto) { + return new Command({ + goto, + update: { myStateKey: "myStateValue" } + }); + } + + return new Command({ goto: "human" }); +} +``` +::: ??? example "Full example: multi-agent system for travel recommendations" @@ -370,6 +785,7 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]: * travel_advisor: can help with travel destination recommendations. Can ask hotel_advisor for help. * hotel_advisor: can help with hotel recommendations. Can ask travel_advisor for help. + :::python ```python from langchain_anthropic import ChatAnthropic from langgraph.graph import MessagesState, StateGraph, START @@ -571,10 +987,267 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]: Would you like more specific information about any of these activities or would you like to know about other options in the area? ``` + ::: + + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, MessagesZodState, Command, interrupt, MemorySaver } from "@langchain/langgraph"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; + import { tool } from "@langchain/core/tools"; + import { z } from "zod"; + + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); + + const MultiAgentState = MessagesZodState.extend({ + lastActiveAgent: z.string().optional(), + }); + + // Define travel advisor tools + const getTravelRecommendations = tool( + async () => { + // Placeholder implementation + return "Based on current trends, I recommend visiting Japan, Portugal, or New Zealand."; + }, + { + name: "get_travel_recommendations", + description: "Get current travel destination recommendations", + schema: z.object({}), + } + ); + + const makeHandoffTool = (agentName: string) => { + return tool( + async (_, config) => { + const state = config.state; + const toolCallId = config.toolCall.id; + + const toolMessage = { + role: "tool" as const, + content: `Successfully transferred to ${agentName}`, + name: `transfer_to_${agentName}`, + tool_call_id: toolCallId, + }; + + return new Command({ + goto: agentName, + update: { messages: [...state.messages, toolMessage] }, + graph: Command.PARENT, + }); + }, + { + name: `transfer_to_${agentName}`, + description: `Transfer to ${agentName}`, + schema: z.object({}), + } + ); + }; + + const travelAdvisorTools = [ + getTravelRecommendations, + makeHandoffTool("hotel_advisor"), + ]; + + const travelAdvisor = createReactAgent({ + llm: model, + tools: travelAdvisorTools, + prompt: [ + "You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). ", + "If you need hotel recommendations, ask 'hotel_advisor' for help. ", + "You MUST include human-readable response before transferring to another agent." + ].join("") + }); + + const callTravelAdvisor = async ( + state: z.infer + ): Promise => { + const response = await travelAdvisor.invoke(state); + const update = { ...response, lastActiveAgent: "travel_advisor" }; + return new Command({ update, goto: "human" }); + }; + + // Define hotel advisor tools + const getHotelRecommendations = tool( + async () => { + // Placeholder implementation + return "I recommend the Ritz-Carlton for luxury stays or boutique hotels for unique experiences."; + }, + { + name: "get_hotel_recommendations", + description: "Get hotel recommendations for destinations", + schema: z.object({}), + } + ); + + const hotelAdvisorTools = [ + getHotelRecommendations, + makeHandoffTool("travel_advisor"), + ]; + + const hotelAdvisor = createReactAgent({ + llm: model, + tools: hotelAdvisorTools, + prompt: [ + "You are a hotel expert that can provide hotel recommendations for a given destination. ", + "If you need help picking travel destinations, ask 'travel_advisor' for help.", + "You MUST include human-readable response before transferring to another agent." + ].join("") + }); + + const callHotelAdvisor = async ( + state: z.infer + ): Promise => { + const response = await hotelAdvisor.invoke(state); + const update = { ...response, lastActiveAgent: "hotel_advisor" }; + return new Command({ update, goto: "human" }); + }; + + const humanNode = async ( + state: z.infer + ): Promise => { + const userInput: string = interrupt("Ready for user input."); + const activeAgent = state.lastActiveAgent || "travel_advisor"; + + return new Command({ + update: { + messages: [ + { + role: "human", + content: userInput, + } + ] + }, + goto: activeAgent, + }); + }; + + const builder = new StateGraph(MultiAgentState) + .addNode("travel_advisor", callTravelAdvisor) + .addNode("hotel_advisor", callHotelAdvisor) + .addNode("human", humanNode) + .addEdge(START, "travel_advisor"); + + const checkpointer = new MemorySaver(); + const graph = builder.compile({ checkpointer }); + ``` + + Let's test a multi turn conversation with this application. + + ```typescript + import { v4 as uuidv4 } from "uuid"; + import { Command } from "@langchain/langgraph"; + + const threadConfig = { configurable: { thread_id: uuidv4() } }; + + const inputs = [ + // 1st round of conversation + { + messages: [ + { role: "user", content: "i wanna go somewhere warm in the caribbean" } + ] + }, + // Since we're using `interrupt`, we'll need to resume using the Command primitive. + // 2nd round of conversation + new Command({ + resume: "could you recommend a nice hotel in one of the areas and tell me which area it is." + }), + // 3rd round of conversation + new Command({ + resume: "i like the first one. could you recommend something to do near the hotel?" + }), + ]; + + for (const [idx, userInput] of inputs.entries()) { + console.log(); + console.log(`--- Conversation Turn ${idx + 1} ---`); + console.log(); + console.log(`User: ${JSON.stringify(userInput)}`); + console.log(); + + for await (const update of await graph.stream( + userInput, + { ...threadConfig, streamMode: "updates" } + )) { + for (const [nodeId, value] of Object.entries(update)) { + if (value?.messages?.length) { + const lastMessage = value.messages.at(-1); + if (lastMessage?.getType?.() === "ai") { + console.log(`${nodeId}: ${lastMessage.content}`); + } + } + } + } + } + ``` + + ``` + --- Conversation Turn 1 --- + + User: {"messages":[{"role":"user","content":"i wanna go somewhere warm in the caribbean"}]} + + travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known as "One Happy Island" and offers: + - Year-round warm weather with consistent temperatures around 82°F (28°C) + - Beautiful white sand beaches like Eagle Beach and Palm Beach + - Clear turquoise waters perfect for swimming and snorkeling + - Minimal rainfall and location outside the hurricane belt + - A blend of Caribbean and Dutch culture + - Great dining options and nightlife + - Various water sports and activities + + Would you like me to get some specific hotel recommendations in Aruba for your stay? I can transfer you to our hotel advisor who can help with accommodations. + + --- Conversation Turn 2 --- + + User: Command { resume: 'could you recommend a nice hotel in one of the areas and tell me which area it is.' } + + hotel_advisor: Based on the recommendations, I can suggest two excellent options: + + 1. The Ritz-Carlton, Aruba - Located in Palm Beach + - This luxury resort is situated in the vibrant Palm Beach area + - Known for its exceptional service and amenities + - Perfect if you want to be close to dining, shopping, and entertainment + - Features multiple restaurants, a casino, and a world-class spa + - Located on a pristine stretch of Palm Beach + + 2. Bucuti & Tara Beach Resort - Located in Eagle Beach + - An adults-only boutique resort on Eagle Beach + - Known for being more intimate and peaceful + - Award-winning for its sustainability practices + - Perfect for a romantic getaway or peaceful vacation + - Located on one of the most beautiful beaches in the Caribbean + + Would you like more specific information about either of these properties or their locations? + + --- Conversation Turn 3 --- + + User: Command { resume: 'i like the first one. could you recommend something to do near the hotel?' } + + travel_advisor: Near the Ritz-Carlton in Palm Beach, here are some highly recommended activities: + + 1. Visit the Palm Beach Plaza Mall - Just a short walk from the hotel, featuring shopping, dining, and entertainment + 2. Try your luck at the Stellaris Casino - It's right in the Ritz-Carlton + 3. Take a sunset sailing cruise - Many depart from the nearby pier + 4. Visit the California Lighthouse - A scenic landmark just north of Palm Beach + 5. Enjoy water sports at Palm Beach: + - Jet skiing + - Parasailing + - Snorkeling + - Stand-up paddleboarding + + Would you like more specific information about any of these activities or would you like to know about other options in the area? + ``` + ::: ## Prebuilt implementations LangGraph comes with prebuilt implementations of two of the most popular multi-agent architectures: +:::python - [supervisor](../agents/multi-agent.md#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent systems. -- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems. \ No newline at end of file +- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems. +::: + +:::js +- [supervisor](../agents/multi-agent.md#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-js) library to create a supervisor multi-agent systems. +- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-js) library to create a swarm multi-agent systems. +::: \ No newline at end of file diff --git a/docs/docs/how-tos/subgraph.md b/docs/docs/how-tos/subgraph.md index 9ad820f73..71aeb6d0f 100644 --- a/docs/docs/how-tos/subgraph.md +++ b/docs/docs/how-tos/subgraph.md @@ -9,11 +9,20 @@ When adding subgraphs, you need to define how the parent graph and the subgraph ## Setup +:::python ```bash pip install -U langgraph ``` +::: + +:::js +```bash +npm install @langchain/langgraph +``` +::: !!! tip "Set up LangSmith for LangGraph development" + Sign up for [LangSmith](https://smith.langchain.com) to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com). ## Shared state schemas @@ -22,6 +31,7 @@ A common case is for the parent graph and subgraph to communicate over a shared If your subgraph shares state keys with the parent graph, you can follow these steps to add it to your graph: +:::python 1. Define the subgraph workflow (`subgraph_builder` in the example below) and compile it 2. Pass compiled subgraph to the `.add_node` method when defining the parent graph workflow @@ -49,9 +59,41 @@ builder.add_node("node_1", subgraph) builder.add_edge(START, "node_1") graph = builder.compile() ``` +::: + +:::js +1. Define the subgraph workflow (`subgraphBuilder` in the example below) and compile it +2. Pass compiled subgraph to the `.addNode` method when defining the parent graph workflow + +```typescript +import { StateGraph, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const State = z.object({ + foo: z.string(), +}); + +// Subgraph +const subgraphBuilder = new StateGraph(State) + .addNode("subgraphNode1", (state) => { + return { foo: "hi! " + state.foo }; + }) + .addEdge(START, "subgraphNode1"); + +const subgraph = subgraphBuilder.compile(); + +// Parent graph +const builder = new StateGraph(State) + .addNode("node1", subgraph) + .addEdge(START, "node1"); + +const graph = builder.compile(); +``` +::: ??? example "Full example: shared state schemas" + :::python ```python from typing_extensions import TypedDict from langgraph.graph.state import StateGraph, START @@ -101,6 +143,61 @@ graph = builder.compile() {'node_1': {'foo': 'hi! foo'}} {'node_2': {'foo': 'hi! foobar'}} ``` + ::: + + :::js + ```typescript + import { StateGraph, START } from "@langchain/langgraph"; + import { z } from "zod"; + + // Define subgraph + const SubgraphState = z.object({ + foo: z.string(), // (1)! + bar: z.string(), // (2)! + }); + + const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("subgraphNode1", (state) => { + return { bar: "bar" }; + }) + .addNode("subgraphNode2", (state) => { + // note that this node is using a state key ('bar') that is only available in the subgraph + // and is sending update on the shared state key ('foo') + return { foo: state.foo + state.bar }; + }) + .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" })) { + console.log(chunk); + } + ``` + + 3. This key is shared with the parent graph state + 4. This key is private to the `SubgraphState` and is not visible to the parent graph + + ``` + { node1: { foo: 'hi! foo' } } + { node2: { foo: 'hi! foobar' } } + ``` + ::: ## Different state schemas @@ -108,6 +205,7 @@ For more complex systems you might want to define subgraphs that have a **comple If that's the case for your application, you need to define a node **function that invokes the subgraph**. This function needs to transform the input (parent) state to the subgraph state before invoking the subgraph, and transform the results back to the parent state before returning the state update from the node. +:::python ```python from typing_extensions import TypedDict from langgraph.graph.state import StateGraph, START @@ -142,9 +240,48 @@ graph = builder.compile() 1. Transform the state to the subgraph state 2. Transform response back to the parent state +::: + +:::js +```typescript +import { StateGraph, START } from "@langchain/langgraph"; +import { z } from "zod"; + +const SubgraphState = z.object({ + bar: z.string(), +}); + +// Subgraph +const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("subgraphNode1", (state) => { + return { bar: "hi! " + state.bar }; + }) + .addEdge(START, "subgraphNode1"); + +const subgraph = subgraphBuilder.compile(); + +// Parent graph +const State = z.object({ + foo: z.string(), +}); + +const builder = new StateGraph(State) + .addNode("node1", async (state) => { + const subgraphOutput = await subgraph.invoke({ bar: state.foo }); // (1)! + return { foo: subgraphOutput.bar }; // (2)! + }) + .addEdge(START, "node1"); + +const graph = builder.compile(); +``` + +1. Transform the state to the subgraph state +2. Transform response back to the parent state +::: ??? example "Full example: different state schemas" + :::python ```python from typing_extensions import TypedDict from langgraph.graph.state import StateGraph, START @@ -200,11 +337,74 @@ graph = builder.compile() (('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'grandchild_2': {'bar': 'hi! foobaz'}}) ((), {'node_2': {'foo': 'hi! foobaz'}}) ``` + ::: + + :::js + ```typescript + import { StateGraph, START } from "@langchain/langgraph"; + import { z } from "zod"; + + // Define subgraph + const SubgraphState = z.object({ + // note that none of these keys are shared with the parent graph state + bar: z.string(), + baz: z.string(), + }); + + const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("subgraphNode1", (state) => { + return { baz: "baz" }; + }) + .addNode("subgraphNode2", (state) => { + return { bar: state.bar + state.baz }; + }) + .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", async (state) => { + const response = await subgraph.invoke({ bar: state.foo }); // (1)! + return { foo: response.bar }; // (2)! + }) + .addEdge(START, "node1") + .addEdge("node1", "node2"); + + const graph = builder.compile(); + + for await (const chunk of await graph.stream( + { foo: "foo" }, + { subgraphs: true } + )) { + console.log(chunk); + } + ``` + + 3. Transform the state to the subgraph state + 4. Transform response back to the parent state + + ``` + [[], { node1: { foo: 'hi! foo' } }] + [['node2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7'], { subgraphNode1: { baz: 'baz' } }] + [['node2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7'], { subgraphNode2: { bar: 'hi! foobaz' } }] + [[], { node2: { foo: 'hi! foobaz' } }] + ``` + ::: ??? example "Full example: different state schemas (two levels of subgraphs)" This is an example with two levels of subgraphs: parent -> child -> grandchild. + :::python ```python # Grandchild graph from typing_extensions import TypedDict @@ -288,14 +488,102 @@ graph = builder.compile() ((), {'child': {'my_key': 'hi Bob, how are you today?'}}) ((), {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}}) ``` + ::: + + :::js + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { z } from "zod"; + + // Grandchild graph + const GrandChildState = z.object({ + myGrandchildKey: z.string(), + }); + + const grandchild = new StateGraph(GrandChildState) + .addNode("grandchild1", (state) => { + // NOTE: child or parent keys will not be accessible here + return { myGrandchildKey: state.myGrandchildKey + ", how are you" }; + }) + .addEdge(START, "grandchild1") + .addEdge("grandchild1", END); + + const grandchildGraph = grandchild.compile(); + + // Child graph + const ChildState = z.object({ + myChildKey: z.string(), + }); + + const child = new StateGraph(ChildState) + .addNode("child1", async (state) => { + // NOTE: parent or grandchild keys won't be accessible here + const grandchildGraphInput = { myGrandchildKey: state.myChildKey }; // (1)! + const grandchildGraphOutput = await grandchildGraph.invoke(grandchildGraphInput); + return { myChildKey: grandchildGraphOutput.myGrandchildKey + " today?" }; // (2)! + }) // (3)! + .addEdge(START, "child1") + .addEdge("child1", END); + + const childGraph = child.compile(); + + // Parent graph + const ParentState = z.object({ + myKey: z.string(), + }); + + const parent = new StateGraph(ParentState) + .addNode("parent1", (state) => { + // NOTE: child or grandchild keys won't be accessible here + return { myKey: "hi " + state.myKey }; + }) + .addNode("child", async (state) => { + const childGraphInput = { myChildKey: state.myKey }; // (4)! + const childGraphOutput = await childGraph.invoke(childGraphInput); + return { myKey: childGraphOutput.myChildKey }; // (5)! + }) // (6)! + .addNode("parent2", (state) => { + return { myKey: state.myKey + " bye!" }; + }) + .addEdge(START, "parent1") + .addEdge("parent1", "child") + .addEdge("child", "parent2") + .addEdge("parent2", END); + + const parentGraph = parent.compile(); + + for await (const chunk of await parentGraph.stream( + { myKey: "Bob" }, + { subgraphs: true } + )) { + console.log(chunk); + } + ``` + + 7. We're transforming the state from the child state channels (`myChildKey`) to the grandchild state channels (`myGrandchildKey`) + 8. We're transforming the state from the grandchild state channels (`myGrandchildKey`) back to the child state channels (`myChildKey`) + 9. We're passing a function here instead of just compiled graph (`grandchildGraph`) + 10. We're transforming the state from the parent state channels (`myKey`) to the child state channels (`myChildKey`) + 11. We're transforming the state from the child state channels (`myChildKey`) back to the parent state channels (`myKey`) + 12. We're passing a function here instead of just a compiled graph (`childGraph`) + + ``` + [[], { parent1: { myKey: 'hi Bob' } }] + [['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child1:781bb3b1-3971-84ce-810b-acf819a03f9c'], { grandchild1: { myGrandchildKey: 'hi Bob, how are you' } }] + [['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b'], { child1: { myChildKey: 'hi Bob, how are you today?' } }] + [[], { child: { myKey: 'hi Bob, how are you today?' } }] + [[], { parent2: { myKey: 'hi Bob, how are you today? bye!' } }] + ``` + ::: ## Add persistence You only need to **provide the checkpointer when compiling the parent graph**. LangGraph will automatically propagate the checkpointer to the child subgraphs. +:::python ```python from langgraph.graph import START, StateGraph -from langgraph.checkpoint.memory import InMemorySaver +from langgraph.checkpoint.memory import MemorySaver from typing_extensions import TypedDict class State(TypedDict): @@ -317,20 +605,66 @@ builder = StateGraph(State) builder.add_node("node_1", subgraph) builder.add_edge(START, "node_1") -checkpointer = InMemorySaver() +checkpointer = MemorySaver() graph = builder.compile(checkpointer=checkpointer) ``` +::: -If you want the subgraph to **have its own memory**, you can compile it `with checkpointer=True`. This is useful in [multi-agent](../concepts/multi_agent.md) systems, if you want agents to keep track of their internal message histories: +:::js +```typescript +import { StateGraph, START, MemorySaver } from "@langchain/langgraph"; +import { z } from "zod"; +const State = z.object({ + foo: z.string(), +}); + +// Subgraph +const subgraphBuilder = new StateGraph(State) + .addNode("subgraphNode1", (state) => { + return { foo: state.foo + "bar" }; + }) + .addEdge(START, "subgraphNode1"); + +const subgraph = subgraphBuilder.compile(); + +// Parent graph +const builder = new StateGraph(State) + .addNode("node1", subgraph) + .addEdge(START, "node1"); + +const checkpointer = new MemorySaver(); +const graph = builder.compile({ checkpointer }); +``` +::: + +If you want the subgraph to **have its own memory**, you can compile it with the appropriate checkpointer option. This is useful in [multi-agent](../concepts/multi_agent.md) systems, if you want agents to keep track of their internal message histories: + +:::python ```python subgraph_builder = StateGraph(...) subgraph = subgraph_builder.compile(checkpointer=True) ``` +::: + +:::js +```typescript +const subgraphBuilder = new StateGraph(...) +const subgraph = subgraphBuilder.compile({ checkpointer: true }); +``` +::: ## View subgraph state -When you enable [persistence](../concepts/persistence.md), you can [inspect the graph state](../concepts/persistence.md#checkpoints) (checkpoint) via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`. +When you enable [persistence](../concepts/persistence.md), you can [inspect the graph state](../concepts/persistence.md#checkpoints) (checkpoint) via the appropriate method. To view the subgraph state, you can use the subgraphs option. + +:::python +You can inspect the graph state via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`. +::: + +:::js +You can inspect the graph state via `graph.getState(config)`. To view the subgraph state, you can use `graph.getState(config, { subgraphs: true })`. +::: !!! important "Available **only** when interrupted" @@ -338,9 +672,10 @@ When you enable [persistence](../concepts/persistence.md), you can [inspect the ??? example "View interrupted subgraph state" + :::python ```python from langgraph.graph import START, StateGraph - from langgraph.checkpoint.memory import InMemorySaver + from langgraph.checkpoint.memory import MemorySaver from langgraph.types import interrupt, Command from typing_extensions import TypedDict @@ -365,7 +700,7 @@ When you enable [persistence](../concepts/persistence.md), you can [inspect the builder.add_node("node_1", subgraph) builder.add_edge(START, "node_1") - checkpointer = InMemorySaver() + checkpointer = MemorySaver() graph = builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "1"}} @@ -379,11 +714,53 @@ When you enable [persistence](../concepts/persistence.md), you can [inspect the ``` 1. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state. + ::: + + :::js + ```typescript + import { StateGraph, START, MemorySaver, interrupt, Command } from "@langchain/langgraph"; + import { z } from "zod"; + + const State = z.object({ + foo: z.string(), + }); + + // Subgraph + const subgraphBuilder = new StateGraph(State) + .addNode("subgraphNode1", (state) => { + const value = interrupt("Provide value:"); + return { foo: state.foo + value }; + }) + .addEdge(START, "subgraphNode1"); + + const subgraph = subgraphBuilder.compile(); + + // Parent graph + const builder = new StateGraph(State) + .addNode("node1", subgraph) + .addEdge(START, "node1"); + + const checkpointer = new MemorySaver(); + const graph = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + + await graph.invoke({ foo: "" }, config); + const parentState = await graph.getState(config); + const subgraphState = (await graph.getState(config, { subgraphs: true })).tasks[0].state; // (1)! + + // resume the subgraph + await graph.invoke(new Command({ resume: "bar" }), config); + ``` + + 2. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state. + ::: ## Stream subgraph outputs -To include outputs from subgraphs in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. +To include outputs from subgraphs in the streamed outputs, you can set the subgraphs option in the stream method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. +:::python ```python for chunk in graph.stream( {"foo": "foo"}, @@ -394,9 +771,27 @@ for chunk in graph.stream( ``` 1. Set `subgraphs=True` to stream outputs from subgraphs. +::: + +:::js +```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 "Stream from subgraphs" + :::python ```python from typing_extensions import TypedDict from langgraph.graph.state import StateGraph, START @@ -450,4 +845,66 @@ for chunk in graph.stream( (('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_1': {'bar': 'bar'}}) (('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_2': {'foo': 'hi! foobar'}}) ((), {'node_2': {'foo': 'hi! foobar'}}) - \ No newline at end of file + ``` + ::: + + :::js + ```typescript + import { StateGraph, START } from "@langchain/langgraph"; + import { z } from "zod"; + + // Define subgraph + const SubgraphState = z.object({ + foo: z.string(), + bar: z.string(), + }); + + const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("subgraphNode1", (state) => { + return { bar: "bar" }; + }) + .addNode("subgraphNode2", (state) => { + // note that this node is using a state key ('bar') that is only available in the subgraph + // and is sending update on the shared state key ('foo') + return { foo: state.foo + state.bar }; + }) + .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); + } + ``` + + 2. Set `subgraphs: true` to stream outputs from subgraphs. + + ``` + [[], { node1: { foo: 'hi! foo' } }] + [['node2:e58e5673-a661-ebb0-70d4-e298a7fc28b7'], { subgraphNode1: { bar: 'bar' } }] + [['node2:e58e5673-a661-ebb0-70d4-e298a7fc28b7'], { subgraphNode2: { foo: 'hi! foobar' } }] + [[], { node2: { foo: 'hi! foobar' } }] + ``` + ::: \ No newline at end of file