mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-12 12:47:53 +02:00
Related Linear ticket: https://linear.app/langchain/issue/DOC-51/add-js-translations-for-remaining-pages --------- Co-authored-by: Brody Klapko <brody@langchain.dev>
3323 lines
100 KiB
Markdown
3323 lines
100 KiB
Markdown
# How to use the graph API
|
|
|
|
This guide demonstrates the basics of LangGraph's Graph API. It walks through [state](#define-and-update-state), as well as composing common graph structures such as [sequences](#create-a-sequence-of-steps), [branches](#create-branches), and [loops](#create-and-control-loops). It also covers LangGraph's control features, including the [Send API](#map-reduce-and-the-send-api) for map-reduce workflows and the [Command API](#combine-control-flow-and-state-updates-with-command) for combining state updates with "hops" across nodes.
|
|
|
|
## Setup
|
|
|
|
:::python
|
|
Install `langgraph`:
|
|
|
|
```bash
|
|
pip install -U langgraph
|
|
```
|
|
:::
|
|
|
|
:::js
|
|
Install `langgraph`:
|
|
|
|
```bash
|
|
npm install @langchain/langgraph
|
|
```
|
|
:::
|
|
|
|
!!! tip "Set up LangSmith for better debugging"
|
|
|
|
Sign up for [LangSmith](https://smith.langchain.com) to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started in the [docs](https://docs.smith.langchain.com).
|
|
|
|
## Define and update state
|
|
|
|
Here we show how to define and update [state](../concepts/low_level.md#state) in LangGraph. We will demonstrate:
|
|
|
|
1. How to use state to define a graph's [schema](../concepts/low_level.md#schema)
|
|
2. How to use [reducers](../concepts/low_level.md#reducers) to control how state updates are processed.
|
|
|
|
### 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.
|
|
|
|
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
|
|
|
|
class State(TypedDict):
|
|
messages: list[AnyMessage]
|
|
extra_field: int
|
|
```
|
|
|
|
This state tracks a list of [message](https://python.langchain.com/docs/concepts/messages/) objects, as well as an extra integer field.
|
|
:::
|
|
|
|
:::js
|
|
```typescript
|
|
import { BaseMessage } from "@langchain/core/messages";
|
|
import { z } from "zod";
|
|
|
|
const State = z.object({
|
|
messages: z.array(z.custom<BaseMessage>()),
|
|
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
|
|
from langchain_core.messages import AIMessage
|
|
|
|
def node(state: State):
|
|
messages = state["messages"]
|
|
new_message = AIMessage("Hello!")
|
|
return {"messages": messages + [new_message], "extra_field": 10}
|
|
```
|
|
|
|
This node simply appends a message to our message list, and populates an extra field.
|
|
:::
|
|
|
|
:::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<typeof State>) => {
|
|
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
|
|
from langgraph.graph import StateGraph
|
|
|
|
builder = StateGraph(State)
|
|
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
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_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
|
|
|
|
result = graph.invoke({"messages": [HumanMessage("Hi")]})
|
|
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
|
|
for message in result["messages"]:
|
|
message.pretty_print()
|
|
```
|
|
|
|
```
|
|
================================ Human Message ================================
|
|
|
|
Hi
|
|
================================== Ai Message ==================================
|
|
|
|
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:
|
|
|
|
```python
|
|
from typing_extensions import Annotated
|
|
|
|
def add(left, right):
|
|
"""Can also import `add` from the `operator` built-in."""
|
|
return left + right
|
|
|
|
class State(TypedDict):
|
|
# highlight-next-line
|
|
messages: Annotated[list[AnyMessage], add]
|
|
extra_field: int
|
|
```
|
|
|
|
Now our node can be simplified:
|
|
|
|
```python
|
|
def node(state: State):
|
|
new_message = AIMessage("Hello!")
|
|
# highlight-next-line
|
|
return {"messages": [new_message], "extra_field": 10}
|
|
```
|
|
:::
|
|
|
|
:::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<BaseMessage>()).langgraph.reducer((x, y) => x.concat(y)),
|
|
extraField: z.number(),
|
|
});
|
|
```
|
|
|
|
Now our node can be simplified:
|
|
|
|
```typescript
|
|
const node = (state: z.infer<typeof State>) => {
|
|
const newMessage = new AIMessage("Hello!");
|
|
// highlight-next-line
|
|
return { messages: [newMessage], extraField: 10 };
|
|
};
|
|
```
|
|
:::
|
|
|
|
:::python
|
|
```python
|
|
from langgraph.graph import START
|
|
|
|
graph = StateGraph(State).add_node(node).add_edge(START, "node").compile()
|
|
|
|
result = graph.invoke({"messages": [HumanMessage("Hi")]})
|
|
|
|
for message in result["messages"]:
|
|
message.pretty_print()
|
|
```
|
|
|
|
```
|
|
================================ Human Message ================================
|
|
|
|
Hi
|
|
================================== Ai Message ==================================
|
|
|
|
Hello!
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
|
|
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
|
|
from langgraph.graph.message import add_messages
|
|
|
|
class State(TypedDict):
|
|
# highlight-next-line
|
|
messages: Annotated[list[AnyMessage], add_messages]
|
|
extra_field: int
|
|
|
|
def node(state: State):
|
|
new_message = AIMessage("Hello!")
|
|
return {"messages": [new_message], "extra_field": 10}
|
|
|
|
graph = StateGraph(State).add_node(node).set_entry_point("node").compile()
|
|
```
|
|
|
|
```python
|
|
# highlight-next-line
|
|
input_message = {"role": "user", "content": "Hi"}
|
|
|
|
result = graph.invoke({"messages": [input_message]})
|
|
|
|
for message in result["messages"]:
|
|
message.pretty_print()
|
|
```
|
|
|
|
```
|
|
================================ Human Message ================================
|
|
|
|
Hi
|
|
================================== Ai Message ==================================
|
|
|
|
Hello!
|
|
```
|
|
|
|
This is a versatile representation of state for applications involving [chat models](https://python.langchain.com/docs/concepts/chat_models/). LangGraph includes a pre-built `MessagesState` for convenience, so that we can have:
|
|
|
|
```python
|
|
from langgraph.graph import MessagesState
|
|
|
|
class State(MessagesState):
|
|
extra_field: int
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
|
|
By default, `StateGraph` operates with a single schema, and all nodes are expected to communicate using that schema. However, it's also possible to define distinct input and output schemas for a graph.
|
|
|
|
When distinct schemas are specified, an internal schema will still be used for communication between nodes. The input schema ensures that the provided input matches the expected structure, while the output schema filters the internal data to return only the relevant information according to the defined output schema.
|
|
|
|
Below, we'll see how to define distinct input and output schema.
|
|
|
|
:::python
|
|
```python
|
|
from langgraph.graph import StateGraph, START, END
|
|
from typing_extensions import TypedDict
|
|
|
|
# Define the schema for the input
|
|
class InputState(TypedDict):
|
|
question: str
|
|
|
|
# Define the schema for the output
|
|
class OutputState(TypedDict):
|
|
answer: str
|
|
|
|
# Define the overall schema, combining both input and output
|
|
class OverallState(InputState, OutputState):
|
|
pass
|
|
|
|
# Define the node that processes the input and generates an answer
|
|
def answer_node(state: InputState):
|
|
# Example answer and an extra key
|
|
return {"answer": "bye", "question": state["question"]}
|
|
|
|
# Build the graph with input and output schemas specified
|
|
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
|
|
builder.add_node(answer_node) # Add the answer node
|
|
builder.add_edge(START, "answer_node") # Define the starting edge
|
|
builder.add_edge("answer_node", END) # Define the ending edge
|
|
graph = builder.compile() # Compile the graph
|
|
|
|
# Invoke the graph with an input and print the result
|
|
print(graph.invoke({"question": "hi"}))
|
|
```
|
|
|
|
```
|
|
{'answer': 'bye'}
|
|
```
|
|
:::
|
|
|
|
:::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.
|
|
|
|
### Pass private state between nodes
|
|
|
|
In some cases, you may want nodes to exchange information that is crucial for intermediate logic but doesn't need to be part of the main schema of the graph. This private data is not relevant to the overall input/output of the graph and should only be shared between certain nodes.
|
|
|
|
Below, we'll create an example sequential graph consisting of three nodes (node_1, node_2 and node_3), where private data is passed between the first two steps (node_1 and node_2), while the third step (node_3) only has access to the public overall state.
|
|
|
|
:::python
|
|
```python
|
|
from langgraph.graph import StateGraph, START, END
|
|
from typing_extensions import TypedDict
|
|
|
|
# The overall state of the graph (this is the public state shared across nodes)
|
|
class OverallState(TypedDict):
|
|
a: str
|
|
|
|
# Output from node_1 contains private data that is not part of the overall state
|
|
class Node1Output(TypedDict):
|
|
private_data: str
|
|
|
|
# The private data is only shared between node_1 and node_2
|
|
def node_1(state: OverallState) -> Node1Output:
|
|
output = {"private_data": "set by node_1"}
|
|
print(f"Entered node `node_1`:\n\tInput: {state}.\n\tReturned: {output}")
|
|
return output
|
|
|
|
# Node 2 input only requests the private data available after node_1
|
|
class Node2Input(TypedDict):
|
|
private_data: str
|
|
|
|
def node_2(state: Node2Input) -> OverallState:
|
|
output = {"a": "set by node_2"}
|
|
print(f"Entered node `node_2`:\n\tInput: {state}.\n\tReturned: {output}")
|
|
return output
|
|
|
|
# Node 3 only has access to the overall state (no access to private data from node_1)
|
|
def node_3(state: OverallState) -> OverallState:
|
|
output = {"a": "set by node_3"}
|
|
print(f"Entered node `node_3`:\n\tInput: {state}.\n\tReturned: {output}")
|
|
return output
|
|
|
|
# Connect nodes in a sequence
|
|
# node_2 accepts private data from node_1, whereas
|
|
# node_3 does not see the private data.
|
|
builder = StateGraph(OverallState).add_sequence([node_1, node_2, node_3])
|
|
builder.add_edge(START, "node_1")
|
|
graph = builder.compile()
|
|
|
|
# Invoke the graph with the initial state
|
|
response = graph.invoke(
|
|
{
|
|
"a": "set at start",
|
|
}
|
|
)
|
|
|
|
print()
|
|
print(f"Output of graph invocation: {response}")
|
|
```
|
|
|
|
```
|
|
Entered node `node_1`:
|
|
Input: {'a': 'set at start'}.
|
|
Returned: {'private_data': 'set by node_1'}
|
|
Entered node `node_2`:
|
|
Input: {'private_data': 'set by node_1'}.
|
|
Returned: {'a': 'set by node_2'}
|
|
Entered node `node_3`:
|
|
Input: {'a': 'set by node_2'}.
|
|
Returned: {'a': 'set by 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<typeof OverallState>): z.infer<typeof Node1Output> => {
|
|
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<typeof Node2Input>): z.infer<typeof OverallState> => {
|
|
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<typeof OverallState>): z.infer<typeof OverallState> => {
|
|
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
|
|
|
|
A [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.StateGraph) accepts a `state_schema` argument on initialization that specifies the "shape" of the state that the nodes in the graph can access and update.
|
|
|
|
In our examples, we typically use a python-native `TypedDict` or [`dataclass`](https://docs.python.org/3/library/dataclasses.html) for `state_schema`, but `state_schema` can be any [type](https://docs.python.org/3/library/stdtypes.html#type-objects).
|
|
|
|
Here, we'll see how a [Pydantic BaseModel](https://docs.pydantic.dev/latest/api/base_model/) can be used for `state_schema` to add run-time validation on **inputs**.
|
|
|
|
!!! note "Known Limitations"
|
|
|
|
- Currently, the output of the graph will **NOT** be an instance of a pydantic model.
|
|
- Run-time validation only occurs on inputs into nodes, not on the outputs.
|
|
- The validation error trace from pydantic does not show which node the error arises in.
|
|
- Pydantic's recursive validation can be slow. For performance-sensitive applications, you may want to consider using a `dataclass` instead.
|
|
|
|
```python
|
|
from langgraph.graph import StateGraph, START, END
|
|
from typing_extensions import TypedDict
|
|
from pydantic import BaseModel
|
|
|
|
# The overall state of the graph (this is the public state shared across nodes)
|
|
class OverallState(BaseModel):
|
|
a: str
|
|
|
|
def node(state: OverallState):
|
|
return {"a": "goodbye"}
|
|
|
|
# Build the state graph
|
|
builder = StateGraph(OverallState)
|
|
builder.add_node(node) # node_1 is the first node
|
|
builder.add_edge(START, "node") # Start the graph with node_1
|
|
builder.add_edge("node", END) # End the graph after node_1
|
|
graph = builder.compile()
|
|
|
|
# Test the graph with a valid input
|
|
graph.invoke({"a": "hello"})
|
|
```
|
|
|
|
Invoke the graph with an **invalid** input
|
|
|
|
```python
|
|
try:
|
|
graph.invoke({"a": 123}) # Should be a string
|
|
except Exception as e:
|
|
print("An exception was raised because `a` is an integer rather than a string.")
|
|
print(e)
|
|
```
|
|
|
|
```
|
|
An exception was raised because `a` is an integer rather than a string.
|
|
1 validation error for OverallState
|
|
a
|
|
Input should be a valid string [type=string_type, input_value=123, input_type=int]
|
|
For further information visit https://errors.pydantic.dev/2.9/v/string_type
|
|
```
|
|
|
|
See below for additional features of Pydantic model state:
|
|
|
|
??? example "Serialization Behavior"
|
|
|
|
When using Pydantic models as state schemas, it's important to understand how serialization works, especially when:
|
|
- Passing Pydantic objects as inputs
|
|
- Receiving outputs from the graph
|
|
- Working with nested Pydantic models
|
|
|
|
Let's see these behaviors in action.
|
|
|
|
```python
|
|
from langgraph.graph import StateGraph, START, END
|
|
from pydantic import BaseModel
|
|
|
|
class NestedModel(BaseModel):
|
|
value: str
|
|
|
|
class ComplexState(BaseModel):
|
|
text: str
|
|
count: int
|
|
nested: NestedModel
|
|
|
|
def process_node(state: ComplexState):
|
|
# Node receives a validated Pydantic object
|
|
print(f"Input state type: {type(state)}")
|
|
print(f"Nested type: {type(state.nested)}")
|
|
# Return a dictionary update
|
|
return {"text": state.text + " processed", "count": state.count + 1}
|
|
|
|
# Build the graph
|
|
builder = StateGraph(ComplexState)
|
|
builder.add_node("process", process_node)
|
|
builder.add_edge(START, "process")
|
|
builder.add_edge("process", END)
|
|
graph = builder.compile()
|
|
|
|
# Create a Pydantic instance for input
|
|
input_state = ComplexState(text="hello", count=0, nested=NestedModel(value="test"))
|
|
print(f"Input object type: {type(input_state)}")
|
|
|
|
# Invoke graph with a Pydantic instance
|
|
result = graph.invoke(input_state)
|
|
print(f"Output type: {type(result)}")
|
|
print(f"Output content: {result}")
|
|
|
|
# Convert back to Pydantic model if needed
|
|
output_model = ComplexState(**result)
|
|
print(f"Converted back to Pydantic: {type(output_model)}")
|
|
```
|
|
|
|
??? example "Runtime Type Coercion"
|
|
|
|
Pydantic performs runtime type coercion for certain data types. This can be helpful but also lead to unexpected behavior if you're not aware of it.
|
|
|
|
```python
|
|
from langgraph.graph import StateGraph, START, END
|
|
from pydantic import BaseModel
|
|
|
|
class CoercionExample(BaseModel):
|
|
# Pydantic will coerce string numbers to integers
|
|
number: int
|
|
# Pydantic will parse string booleans to bool
|
|
flag: bool
|
|
|
|
def inspect_node(state: CoercionExample):
|
|
print(f"number: {state.number} (type: {type(state.number)})")
|
|
print(f"flag: {state.flag} (type: {type(state.flag)})")
|
|
return {}
|
|
|
|
builder = StateGraph(CoercionExample)
|
|
builder.add_node("inspect", inspect_node)
|
|
builder.add_edge(START, "inspect")
|
|
builder.add_edge("inspect", END)
|
|
graph = builder.compile()
|
|
|
|
# Demonstrate coercion with string inputs that will be converted
|
|
result = graph.invoke({"number": "42", "flag": "true"})
|
|
|
|
# This would fail with a validation error
|
|
try:
|
|
graph.invoke({"number": "not-a-number", "flag": "true"})
|
|
except Exception as e:
|
|
print(f"\nExpected validation error: {e}")
|
|
```
|
|
|
|
??? example "Working with Message Models"
|
|
|
|
When working with LangChain message types in your state schema, there are important considerations for serialization. You should use `AnyMessage` (rather than `BaseMessage`) for proper serialization/deserialization when using message objects over the wire.
|
|
|
|
```python
|
|
from langgraph.graph import StateGraph, START, END
|
|
from pydantic import BaseModel
|
|
from langchain_core.messages import HumanMessage, AIMessage, AnyMessage
|
|
from typing import List
|
|
|
|
class ChatState(BaseModel):
|
|
messages: List[AnyMessage]
|
|
context: str
|
|
|
|
def add_message(state: ChatState):
|
|
return {"messages": state.messages + [AIMessage(content="Hello there!")]}
|
|
|
|
builder = StateGraph(ChatState)
|
|
builder.add_node("add_message", add_message)
|
|
builder.add_edge(START, "add_message")
|
|
builder.add_edge("add_message", END)
|
|
graph = builder.compile()
|
|
|
|
# Create input with a message
|
|
initial_state = ChatState(
|
|
messages=[HumanMessage(content="Hi")], context="Customer support chat"
|
|
)
|
|
|
|
result = graph.invoke(initial_state)
|
|
print(f"Output: {result}")
|
|
|
|
# Convert back to Pydantic model to see message types
|
|
output_model = ChatState(**result)
|
|
for i, msg in enumerate(output_model.messages):
|
|
print(f"Message {i}: {type(msg).__name__} - {msg.content}")
|
|
```
|
|
:::
|
|
|
|
:::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<WorkflowChannelsState>({
|
|
channels: {
|
|
messages: {
|
|
reducer: (currentState, updateValue) => currentState.concat(updateValue),
|
|
default: () => [],
|
|
},
|
|
question: null,
|
|
answer: null,
|
|
},
|
|
});
|
|
```
|
|
:::
|
|
|
|
## Add runtime configuration
|
|
|
|
Sometimes you want to be able to configure your graph when calling it. For example, you might want to be able to specify what LLM or system prompt to use at runtime, _without polluting the graph state with these parameters_.
|
|
|
|
To add runtime configuration:
|
|
|
|
1. Specify a schema for your configuration
|
|
2. Add the configuration to the function signature for nodes or conditional edges
|
|
3. Pass the configuration into the graph.
|
|
|
|
See below for a simple example:
|
|
|
|
:::python
|
|
```python
|
|
from langgraph.graph import END, StateGraph, START
|
|
from langgraph.runtime import Runtime
|
|
from typing_extensions import TypedDict
|
|
|
|
# 1. Specify config schema
|
|
class ContextSchema(TypedDict):
|
|
my_runtime_value: str
|
|
|
|
# 2. Define a graph that accesses the config in a node
|
|
class State(TypedDict):
|
|
my_state_value: str
|
|
|
|
# highlight-next-line
|
|
def node(state: State, runtime: Runtime[ContextSchema]):
|
|
# highlight-next-line
|
|
if runtime.context["my_runtime_value"] == "a":
|
|
return {"my_state_value": 1}
|
|
# highlight-next-line
|
|
elif runtime.context["my_runtime_value"] == "b":
|
|
return {"my_state_value": 2}
|
|
else:
|
|
raise ValueError("Unknown values.")
|
|
|
|
# highlight-next-line
|
|
builder = StateGraph(State, context_schema=ContextSchema)
|
|
builder.add_node(node)
|
|
builder.add_edge(START, "node")
|
|
builder.add_edge("node", END)
|
|
|
|
graph = builder.compile()
|
|
|
|
# 3. Pass in configuration at runtime:
|
|
# highlight-next-line
|
|
print(graph.invoke({}, context={"my_runtime_value": "a"}))
|
|
# highlight-next-line
|
|
print(graph.invoke({}, context={"my_runtime_value": "b"}))
|
|
```
|
|
|
|
```
|
|
{'my_state_value': 1}
|
|
{'my_state_value': 2}
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
from dataclasses import dataclass
|
|
|
|
from langchain.chat_models import init_chat_model
|
|
from langgraph.graph import MessagesState, END, StateGraph, START
|
|
from langgraph.runtime import Runtime
|
|
from typing_extensions import TypedDict
|
|
|
|
@dataclass
|
|
class ContextSchema:
|
|
model_provider: str = "anthropic"
|
|
|
|
MODELS = {
|
|
"anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"),
|
|
"openai": init_chat_model("openai:gpt-4.1-mini"),
|
|
}
|
|
|
|
def call_model(state: MessagesState, runtime: Runtime[ContextSchema]):
|
|
model = MODELS[runtime.context.model_provider]
|
|
response = model.invoke(state["messages"])
|
|
return {"messages": [response]}
|
|
|
|
builder = StateGraph(MessagesState, context_schema=ContextSchema)
|
|
builder.add_node("model", call_model)
|
|
builder.add_edge(START, "model")
|
|
builder.add_edge("model", END)
|
|
|
|
graph = builder.compile()
|
|
|
|
# Usage
|
|
input_message = {"role": "user", "content": "hi"}
|
|
# With no configuration, uses default (Anthropic)
|
|
response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
|
|
# Or, can set OpenAI
|
|
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
|
|
|
|
print(response_1.response_metadata["model_name"])
|
|
print(response_2.response_metadata["model_name"])
|
|
```
|
|
```
|
|
claude-3-5-haiku-20241022
|
|
gpt-4.1-mini-2025-04-14
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
from langchain.chat_models import init_chat_model
|
|
from langchain_core.messages import SystemMessage
|
|
from langgraph.graph import END, MessagesState, StateGraph, START
|
|
from langgraph.runtime import Runtime
|
|
from typing_extensions import TypedDict
|
|
|
|
@dataclass
|
|
class ContextSchema:
|
|
model_provider: str = "anthropic"
|
|
system_message: str | None = None
|
|
|
|
MODELS = {
|
|
"anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"),
|
|
"openai": init_chat_model("openai:gpt-4.1-mini"),
|
|
}
|
|
|
|
def call_model(state: MessagesState, runtime: Runtime[ContextSchema]):
|
|
model = MODELS[runtime.context.model_provider]
|
|
messages = state["messages"]
|
|
if (system_message := runtime.context.system_message):
|
|
messages = [SystemMessage(system_message)] + messages
|
|
response = model.invoke(messages)
|
|
return {"messages": [response]}
|
|
|
|
builder = StateGraph(MessagesState, context_schema=ContextSchema)
|
|
builder.add_node("model", call_model)
|
|
builder.add_edge(START, "model")
|
|
builder.add_edge("model", END)
|
|
|
|
graph = builder.compile()
|
|
|
|
# Usage
|
|
input_message = {"role": "user", "content": "hi"}
|
|
response = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai", "system_message": "Respond in Italian."})
|
|
for message in response["messages"]:
|
|
message.pretty_print()
|
|
```
|
|
```
|
|
================================ Human Message ================================
|
|
|
|
hi
|
|
================================== Ai Message ==================================
|
|
|
|
Ciao! Come posso aiutarti oggi?
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
from langgraph.pregel import RetryPolicy
|
|
|
|
builder.add_node(
|
|
"node_name",
|
|
node_function,
|
|
retry_policy=RetryPolicy(),
|
|
)
|
|
```
|
|
|
|
By default, the `retry_on` parameter uses the `default_retry_on` function, which retries on any exception except for the following:
|
|
|
|
- `ValueError`
|
|
- `TypeError`
|
|
- `ArithmeticError`
|
|
- `ImportError`
|
|
- `LookupError`
|
|
- `NameError`
|
|
- `SyntaxError`
|
|
- `RuntimeError`
|
|
- `ReferenceError`
|
|
- `StopIteration`
|
|
- `StopAsyncIteration`
|
|
- `OSError`
|
|
|
|
In addition, for exceptions from popular http request libraries such as `requests` and `httpx` it only retries on 5xx status codes.
|
|
:::
|
|
|
|
:::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
|
|
import sqlite3
|
|
from typing_extensions import TypedDict
|
|
from langchain.chat_models import init_chat_model
|
|
from langgraph.graph import END, MessagesState, StateGraph, START
|
|
from langgraph.pregel import RetryPolicy
|
|
from langchain_community.utilities import SQLDatabase
|
|
from langchain_core.messages import AIMessage
|
|
|
|
db = SQLDatabase.from_uri("sqlite:///:memory:")
|
|
model = init_chat_model("anthropic:claude-3-5-haiku-latest")
|
|
|
|
def query_database(state: MessagesState):
|
|
query_result = db.run("SELECT * FROM Artist LIMIT 10;")
|
|
return {"messages": [AIMessage(content=query_result)]}
|
|
|
|
def call_model(state: MessagesState):
|
|
response = model.invoke(state["messages"])
|
|
return {"messages": [response]}
|
|
|
|
# Define a new graph
|
|
builder = StateGraph(MessagesState)
|
|
builder.add_node(
|
|
"query_database",
|
|
query_database,
|
|
retry_policy=RetryPolicy(retry_on=sqlite3.OperationalError),
|
|
)
|
|
builder.add_node("model", call_model, retry_policy=RetryPolicy(max_attempts=5))
|
|
builder.add_edge(START, "model")
|
|
builder.add_edge("model", "query_database")
|
|
builder.add_edge("query_database", END)
|
|
graph = builder.compile()
|
|
```
|
|
:::
|
|
|
|
:::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<typeof MessagesZodState>) => {
|
|
const response = await model.invoke(state.messages);
|
|
return { messages: [response] };
|
|
};
|
|
|
|
const queryDatabase = async (state: z.infer<typeof MessagesZodState>) => {
|
|
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
|
|
|
|
Node caching is useful in cases where you want to avoid repeating operations, like when doing something expensive (either in terms of time or cost). LangGraph lets you add individualized caching policies to nodes in a graph.
|
|
|
|
To configure a cache policy, pass the `cache_policy` parameter to the [add_node](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.state.StateGraph.add_node) function. In the following example, a [`CachePolicy`](https://langchain-ai.github.io/langgraph/reference/types/?h=cachepolicy#langgraph.types.CachePolicy) object is instantiated with a time to live of 120 seconds and the default `key_func` generator. Then it is associated with a node:
|
|
|
|
```python
|
|
from langgraph.types import CachePolicy
|
|
|
|
builder.add_node(
|
|
"node_name",
|
|
node_function,
|
|
cache_policy=CachePolicy(ttl=120),
|
|
)
|
|
```
|
|
|
|
Then, to enable node-level caching for a graph, set the `cache` argument when compiling the graph. The example below uses `InMemoryCache` to set up a graph with in-memory cache, but `SqliteCache` is also available.
|
|
|
|
```python
|
|
from langgraph.cache.memory import InMemoryCache
|
|
|
|
graph = builder.compile(cache=InMemoryCache())
|
|
```
|
|
:::
|
|
|
|
## Create a sequence of steps
|
|
|
|
!!! info "Prerequisites"
|
|
|
|
This guide assumes familiarity with the above section on [state](#define-and-update-state).
|
|
|
|
Here we demonstrate how to construct a simple sequence of steps. We will show:
|
|
|
|
1. How to build a sequential graph
|
|
2. Built-in short-hand for constructing similar graphs.
|
|
|
|
:::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
|
|
from langgraph.graph import START, StateGraph
|
|
|
|
builder = StateGraph(State)
|
|
|
|
# Add nodes
|
|
builder.add_node(step_1)
|
|
builder.add_node(step_2)
|
|
builder.add_node(step_3)
|
|
|
|
# Add edges
|
|
builder.add_edge(START, "step_1")
|
|
builder.add_edge("step_1", "step_2")
|
|
builder.add_edge("step_2", "step_3")
|
|
```
|
|
|
|
We can also use the built-in shorthand `.add_sequence`:
|
|
|
|
```python
|
|
builder = StateGraph(State).add_sequence([step_1, step_2, step_3])
|
|
builder.add_edge(START, "step_1")
|
|
```
|
|
:::
|
|
|
|
:::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.
|
|
This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern:
|
|
|
|
- How state updates are [checkpointed](../concepts/persistence.md)
|
|
- How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows
|
|
- How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features
|
|
|
|
They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized
|
|
and debugged using [LangGraph Studio](../concepts/langgraph_studio.md).
|
|
|
|
Let's demonstrate an end-to-end example. We will create a sequence of three steps:
|
|
|
|
1. Populate a value in a key of the state
|
|
2. Update the same value
|
|
3. Populate a different value
|
|
|
|
Let's first define our [state](../concepts/low_level.md#state). This governs the [schema of the graph](../concepts/low_level.md#schema), and can also specify how to apply updates. See [this section](#process-state-updates-with-reducers) for more detail.
|
|
|
|
In our case, we will just keep track of two values:
|
|
|
|
:::python
|
|
```python
|
|
from typing_extensions import TypedDict
|
|
|
|
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
|
|
def step_1(state: State):
|
|
return {"value_1": "a"}
|
|
|
|
def step_2(state: State):
|
|
current_value_1 = state["value_1"]
|
|
return {"value_1": f"{current_value_1} b"}
|
|
|
|
def step_3(state: State):
|
|
return {"value_2": 10}
|
|
```
|
|
:::
|
|
|
|
:::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<typeof State>) => {
|
|
return { value1: "a" };
|
|
};
|
|
|
|
const step2 = (state: z.infer<typeof State>) => {
|
|
const currentValue1 = state.value1;
|
|
return { value1: `${currentValue1} b` };
|
|
};
|
|
|
|
const step3 = (state: z.infer<typeof State>) => {
|
|
return { value2: 10 };
|
|
};
|
|
```
|
|
:::
|
|
|
|
!!! note
|
|
|
|
Note that when issuing updates to the state, each node can just specify the value of the key it wishes to update.
|
|
|
|
By default, this will **overwrite** the value of the corresponding key. You can also use [reducers](../concepts/low_level.md#reducers) to control how updates are processed— for example, you can append successive updates to a key instead. See [this section](#process-state-updates-with-reducers) for more detail.
|
|
|
|
Finally, we define the graph. We use [StateGraph](../concepts/low_level.md#stategraph) to define a graph that operates on this 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
|
|
|
|
builder = StateGraph(State)
|
|
|
|
# Add nodes
|
|
builder.add_node(step_1)
|
|
builder.add_node(step_2)
|
|
builder.add_node(step_3)
|
|
|
|
# Add edges
|
|
builder.add_edge(START, "step_1")
|
|
builder.add_edge("step_1", "step_2")
|
|
builder.add_edge("step_2", "step_3")
|
|
```
|
|
:::
|
|
|
|
:::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`:
|
|
|
|
```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.
|
|
|
|
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.
|
|
|
|
```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
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_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"})
|
|
```
|
|
|
|
```
|
|
{'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:
|
|
|
|
- We kicked off invocation by providing a value for a single state key. We must always provide a value for at least one key.
|
|
- The value we passed in was overwritten by the first node.
|
|
- The second node updated the value.
|
|
- The third node populated a different value.
|
|
|
|
:::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:
|
|
|
|
```python
|
|
# highlight-next-line
|
|
builder = StateGraph(State).add_sequence([step_1, step_2, step_3])
|
|
builder.add_edge(START, "step_1")
|
|
|
|
graph = builder.compile()
|
|
|
|
graph.invoke({"value_1": "c"})
|
|
```
|
|
:::
|
|
|
|
## Create branches
|
|
|
|
Parallel execution of nodes is essential to speed up overall graph operation. LangGraph offers native support for parallel execution of nodes, which can significantly enhance the performance of graph-based workflows. This parallelization is achieved through fan-out and fan-in mechanisms, utilizing both standard edges and [conditional_edges](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.MessageGraph.add_conditional_edges). Below are some examples showing how to add create branching dataflows that work for you.
|
|
|
|
### Run graph nodes in parallel
|
|
|
|
In this example, we fan out from `Node A` to `B and C` and then fan in to `D`. With our state, [we specify the reducer add operation](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
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
|
|
class State(TypedDict):
|
|
# The operator.add reducer fn makes this append-only
|
|
aggregate: Annotated[list, operator.add]
|
|
|
|
def a(state: State):
|
|
print(f'Adding "A" to {state["aggregate"]}')
|
|
return {"aggregate": ["A"]}
|
|
|
|
def b(state: State):
|
|
print(f'Adding "B" to {state["aggregate"]}')
|
|
return {"aggregate": ["B"]}
|
|
|
|
def c(state: State):
|
|
print(f'Adding "C" to {state["aggregate"]}')
|
|
return {"aggregate": ["C"]}
|
|
|
|
def d(state: State):
|
|
print(f'Adding "D" to {state["aggregate"]}')
|
|
return {"aggregate": ["D"]}
|
|
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
builder.add_node(c)
|
|
builder.add_node(d)
|
|
builder.add_edge(START, "a")
|
|
builder.add_edge("a", "b")
|
|
builder.add_edge("a", "c")
|
|
builder.add_edge("b", "d")
|
|
builder.add_edge("c", "d")
|
|
builder.add_edge("d", END)
|
|
graph = builder.compile()
|
|
```
|
|
:::
|
|
|
|
:::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<typeof State>) => {
|
|
console.log(`Adding "A" to ${state.aggregate}`);
|
|
return { aggregate: ["A"] };
|
|
};
|
|
|
|
const nodeB = (state: z.infer<typeof State>) => {
|
|
console.log(`Adding "B" to ${state.aggregate}`);
|
|
return { aggregate: ["B"] };
|
|
};
|
|
|
|
const nodeC = (state: z.infer<typeof State>) => {
|
|
console.log(`Adding "C" to ${state.aggregate}`);
|
|
return { aggregate: ["C"] };
|
|
};
|
|
|
|
const nodeD = (state: z.infer<typeof State>) => {
|
|
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
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_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"}})
|
|
```
|
|
|
|
```
|
|
Adding "A" to []
|
|
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
|
|
|
|
In the above example, nodes `"b"` and `"c"` are executed concurrently in the same [superstep](../concepts/low_level.md#graphs). Because they are in the same step, node `"d"` executes after both `"b"` and `"c"` are finished.
|
|
|
|
Importantly, updates from a parallel superstep may not be ordered consistently. If you need a consistent, predetermined ordering of updates from a parallel superstep, you should write the outputs to a separate field in the state together with a value with which to order them.
|
|
|
|
??? note "Exception handling?"
|
|
|
|
LangGraph executes nodes within [supersteps](../concepts/low_level.md#graphs), meaning that while parallel branches are executed in parallel, the entire superstep is **transactional**. If any of these branches raises an exception, **none** of the updates are applied to the state (the entire superstep errors).
|
|
|
|
Importantly, when using a [checkpointer](../concepts/persistence.md), results from successful nodes within a superstep are saved, and don't repeat when resumed.
|
|
|
|
If you have error-prone (perhaps want to handle flakey API calls), LangGraph provides two ways to address this:
|
|
|
|
1. You can write regular python code within your node to catch and handle exceptions.
|
|
2. You can set a **[retry_policy](../reference/types.md#langgraph.types.RetryPolicy)** to direct the graph to retry nodes that raise certain types of exceptions. Only failing branches are retried, so you needn't worry about performing redundant work.
|
|
|
|
Together, these let you perform parallel execution and fully control exception handling.
|
|
|
|
:::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.
|
|
|
|
The above example showed how to fan-out and fan-in when each path was only one step. But what if one branch had more than one step? Let's add a node `"b_2"` in the `"b"` branch:
|
|
|
|
```python
|
|
import operator
|
|
from typing import Annotated, Any
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
|
|
class State(TypedDict):
|
|
# The operator.add reducer fn makes this append-only
|
|
aggregate: Annotated[list, operator.add]
|
|
|
|
def a(state: State):
|
|
print(f'Adding "A" to {state["aggregate"]}')
|
|
return {"aggregate": ["A"]}
|
|
|
|
def b(state: State):
|
|
print(f'Adding "B" to {state["aggregate"]}')
|
|
return {"aggregate": ["B"]}
|
|
|
|
def b_2(state: State):
|
|
print(f'Adding "B_2" to {state["aggregate"]}')
|
|
return {"aggregate": ["B_2"]}
|
|
|
|
def c(state: State):
|
|
print(f'Adding "C" to {state["aggregate"]}')
|
|
return {"aggregate": ["C"]}
|
|
|
|
def d(state: State):
|
|
print(f'Adding "D" to {state["aggregate"]}')
|
|
return {"aggregate": ["D"]}
|
|
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
builder.add_node(b_2)
|
|
builder.add_node(c)
|
|
# highlight-next-line
|
|
builder.add_node(d, defer=True)
|
|
builder.add_edge(START, "a")
|
|
builder.add_edge("a", "b")
|
|
builder.add_edge("a", "c")
|
|
builder.add_edge("b", "b_2")
|
|
builder.add_edge("b_2", "d")
|
|
builder.add_edge("c", "d")
|
|
builder.add_edge("d", END)
|
|
graph = builder.compile()
|
|
```
|
|
|
|
```python
|
|
from IPython.display import Image, display
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
|
|
```python
|
|
graph.invoke({"aggregate": []})
|
|
```
|
|
|
|
```
|
|
Adding "A" to []
|
|
Adding "B" to ['A']
|
|
Adding "C" to ['A']
|
|
Adding "B_2" to ['A', 'B', 'C']
|
|
Adding "D" to ['A', 'B', 'C', 'B_2']
|
|
```
|
|
|
|
In the above example, nodes `"b"` and `"c"` are executed concurrently in the same superstep. We set `defer=True` on node `d` so it will not execute until all pending tasks are finished. In this case, this means that `"d"` waits to execute until the entire `"b"` branch is finished.
|
|
:::
|
|
|
|
### Conditional branching
|
|
|
|
:::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
|
|
import operator
|
|
from typing import Annotated, Literal, Sequence
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
|
|
class State(TypedDict):
|
|
aggregate: Annotated[list, operator.add]
|
|
# Add a key to the state. We will set this key to determine
|
|
# how we branch.
|
|
which: str
|
|
|
|
def a(state: State):
|
|
print(f'Adding "A" to {state["aggregate"]}')
|
|
# highlight-next-line
|
|
return {"aggregate": ["A"], "which": "c"}
|
|
|
|
def b(state: State):
|
|
print(f'Adding "B" to {state["aggregate"]}')
|
|
return {"aggregate": ["B"]}
|
|
|
|
def c(state: State):
|
|
print(f'Adding "C" to {state["aggregate"]}')
|
|
return {"aggregate": ["C"]}
|
|
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
builder.add_node(c)
|
|
builder.add_edge(START, "a")
|
|
builder.add_edge("b", END)
|
|
builder.add_edge("c", END)
|
|
|
|
def conditional_edge(state: State) -> Literal["b", "c"]:
|
|
# Fill in arbitrary logic here that uses the state
|
|
# to determine the next node
|
|
return state["which"]
|
|
|
|
# highlight-next-line
|
|
builder.add_conditional_edges("a", conditional_edge)
|
|
|
|
graph = builder.compile()
|
|
```
|
|
|
|
```python
|
|
from IPython.display import Image, display
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
|
|
```python
|
|
result = graph.invoke({"aggregate": []})
|
|
print(result)
|
|
```
|
|
|
|
```
|
|
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<typeof State>) => {
|
|
console.log(`Adding "A" to ${state.aggregate}`);
|
|
// highlight-next-line
|
|
return { aggregate: ["A"], which: "c" };
|
|
};
|
|
|
|
const nodeB = (state: z.infer<typeof State>) => {
|
|
console.log(`Adding "B" to ${state.aggregate}`);
|
|
return { aggregate: ["B"] };
|
|
};
|
|
|
|
const nodeC = (state: z.infer<typeof State>) => {
|
|
console.log(`Adding "C" to ${state.aggregate}`);
|
|
return { aggregate: ["C"] };
|
|
};
|
|
|
|
const conditionalEdge = (state: z.infer<typeof State>): "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<typeof State>): 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
|
|
from typing_extensions import TypedDict, Annotated
|
|
import operator
|
|
|
|
class OverallState(TypedDict):
|
|
topic: str
|
|
subjects: list[str]
|
|
jokes: Annotated[list[str], operator.add]
|
|
best_selected_joke: str
|
|
|
|
def generate_topics(state: OverallState):
|
|
return {"subjects": ["lions", "elephants", "penguins"]}
|
|
|
|
def generate_joke(state: OverallState):
|
|
joke_map = {
|
|
"lions": "Why don't lions like fast food? Because they can't catch it!",
|
|
"elephants": "Why don't elephants use computers? They're afraid of the mouse!",
|
|
"penguins": "Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice."
|
|
}
|
|
return {"jokes": [joke_map[state["subject"]]]}
|
|
|
|
def continue_to_jokes(state: OverallState):
|
|
return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
|
|
|
|
def best_joke(state: OverallState):
|
|
return {"best_selected_joke": "penguins"}
|
|
|
|
builder = StateGraph(OverallState)
|
|
builder.add_node("generate_topics", generate_topics)
|
|
builder.add_node("generate_joke", generate_joke)
|
|
builder.add_node("best_joke", best_joke)
|
|
builder.add_edge(START, "generate_topics")
|
|
builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
|
|
builder.add_edge("generate_joke", "best_joke")
|
|
builder.add_edge("best_joke", END)
|
|
builder.add_edge("generate_topics", END)
|
|
graph = builder.compile()
|
|
```
|
|
|
|
```python
|
|
from IPython.display import Image, display
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
|
|
```python
|
|
# Call the graph: here we call it to generate a list of jokes
|
|
for step in graph.stream({"topic": "animals"}):
|
|
print(step)
|
|
```
|
|
|
|
```
|
|
{'generate_topics': {'subjects': ['lions', 'elephants', 'penguins']}}
|
|
{'generate_joke': {'jokes': ["Why don't lions like fast food? Because they can't catch it!"]}}
|
|
{'generate_joke': {'jokes': ["Why don't elephants use computers? They're afraid of the mouse!"]}}
|
|
{'generate_joke': {'jokes': ['Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice.']}}
|
|
{'best_joke': {'best_selected_joke': 'penguins'}}
|
|
```
|
|
:::
|
|
|
|
:::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<typeof OverallState>) => {
|
|
return { subjects: ["lions", "elephants", "penguins"] };
|
|
};
|
|
|
|
const generateJoke = (state: { subject: string }) => {
|
|
const jokeMap: Record<string, string> = {
|
|
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<typeof OverallState>) => {
|
|
return state.subjects.map((subject) => new Send("generateJoke", { subject }));
|
|
};
|
|
|
|
const bestJoke = (state: z.infer<typeof OverallState>) => {
|
|
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
|
|
|
|
When creating a graph with a loop, we require a mechanism for terminating execution. This is most commonly done by adding a [conditional edge](../concepts/low_level.md#conditional-edges) that routes to the [END](../concepts/low_level.md#end-node) node once we reach some termination condition.
|
|
|
|
You can also set the graph recursion limit when invoking or streaming the graph. The recursion limit sets the number of [supersteps](../concepts/low_level.md#graphs) that the graph is allowed to execute before it raises an error. Read more about the concept of recursion limits [here](../concepts/low_level.md#recursion-limit).
|
|
|
|
Let's consider a simple graph with a loop to better understand how these mechanisms work.
|
|
|
|
!!! tip
|
|
|
|
To return the last value of your state instead of receiving a recursion limit error, see the [next section](#impose-a-recursion-limit).
|
|
|
|
When creating a loop, you can include a conditional edge that specifies a termination condition:
|
|
|
|
:::python
|
|
```python
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
|
|
def route(state: State) -> Literal["b", END]:
|
|
if termination_condition(state):
|
|
return END
|
|
else:
|
|
return "b"
|
|
|
|
builder.add_edge(START, "a")
|
|
builder.add_conditional_edges("a", route)
|
|
builder.add_edge("b", "a")
|
|
graph = builder.compile()
|
|
```
|
|
:::
|
|
|
|
:::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<typeof State>): "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
|
|
|
|
try:
|
|
graph.invoke(inputs, {"recursion_limit": 3})
|
|
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
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
|
|
class State(TypedDict):
|
|
# The operator.add reducer fn makes this append-only
|
|
aggregate: Annotated[list, operator.add]
|
|
|
|
def a(state: State):
|
|
print(f'Node A sees {state["aggregate"]}')
|
|
return {"aggregate": ["A"]}
|
|
|
|
def b(state: State):
|
|
print(f'Node B sees {state["aggregate"]}')
|
|
return {"aggregate": ["B"]}
|
|
|
|
# Define nodes
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
|
|
# Define edges
|
|
def route(state: State) -> Literal["b", END]:
|
|
if len(state["aggregate"]) < 7:
|
|
return "b"
|
|
else:
|
|
return END
|
|
|
|
builder.add_edge(START, "a")
|
|
builder.add_conditional_edges("a", route)
|
|
builder.add_edge("b", "a")
|
|
graph = builder.compile()
|
|
```
|
|
|
|
```python
|
|
from IPython.display import Image, display
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
:::
|
|
|
|
:::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<typeof State>) => {
|
|
console.log(`Node A sees ${state.aggregate}`);
|
|
return { aggregate: ["A"] };
|
|
};
|
|
|
|
const nodeB = (state: z.infer<typeof State>) => {
|
|
console.log(`Node B sees ${state.aggregate}`);
|
|
return { aggregate: ["B"] };
|
|
};
|
|
|
|
// Define edges
|
|
const route = (state: z.infer<typeof State>): "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": []})
|
|
```
|
|
|
|
```
|
|
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']
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
|
|
try:
|
|
graph.invoke({"aggregate": []}, {"recursion_limit": 4})
|
|
except GraphRecursionError:
|
|
print("Recursion Error")
|
|
```
|
|
|
|
```
|
|
Node A sees []
|
|
Node B sees ['A']
|
|
Node C sees ['A', 'B']
|
|
Node D sees ['A', 'B']
|
|
Node A sees ['A', 'B', 'C', 'D']
|
|
Recursion Error
|
|
```
|
|
:::
|
|
|
|
:::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.
|
|
|
|
LangGraph implements a special `RemainingSteps` annotation. Under the hood, it creates a `ManagedValue` channel -- a state channel that will exist for the duration of our graph run and no longer.
|
|
|
|
```python
|
|
import operator
|
|
from typing import Annotated, Literal
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
from langgraph.managed.is_last_step import RemainingSteps
|
|
|
|
class State(TypedDict):
|
|
aggregate: Annotated[list, operator.add]
|
|
remaining_steps: RemainingSteps
|
|
|
|
def a(state: State):
|
|
print(f'Node A sees {state["aggregate"]}')
|
|
return {"aggregate": ["A"]}
|
|
|
|
def b(state: State):
|
|
print(f'Node B sees {state["aggregate"]}')
|
|
return {"aggregate": ["B"]}
|
|
|
|
# Define nodes
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
|
|
# Define edges
|
|
def route(state: State) -> Literal["b", END]:
|
|
if state["remaining_steps"] <= 2:
|
|
return END
|
|
else:
|
|
return "b"
|
|
|
|
builder.add_edge(START, "a")
|
|
builder.add_conditional_edges("a", route)
|
|
builder.add_edge("b", "a")
|
|
graph = builder.compile()
|
|
|
|
# Test it out
|
|
result = graph.invoke({"aggregate": []}, {"recursion_limit": 4})
|
|
print(result)
|
|
```
|
|
```
|
|
Node A sees []
|
|
Node B sees ['A']
|
|
Node A sees ['A', 'B']
|
|
{'aggregate': ['A', 'B', 'A']}
|
|
```
|
|
:::
|
|
|
|
:::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:
|
|
|
|
```python
|
|
import operator
|
|
from typing import Annotated, Literal
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
|
|
class State(TypedDict):
|
|
aggregate: Annotated[list, operator.add]
|
|
|
|
def a(state: State):
|
|
print(f'Node A sees {state["aggregate"]}')
|
|
return {"aggregate": ["A"]}
|
|
|
|
def b(state: State):
|
|
print(f'Node B sees {state["aggregate"]}')
|
|
return {"aggregate": ["B"]}
|
|
|
|
def c(state: State):
|
|
print(f'Node C sees {state["aggregate"]}')
|
|
return {"aggregate": ["C"]}
|
|
|
|
def d(state: State):
|
|
print(f'Node D sees {state["aggregate"]}')
|
|
return {"aggregate": ["D"]}
|
|
|
|
# Define nodes
|
|
builder = StateGraph(State)
|
|
builder.add_node(a)
|
|
builder.add_node(b)
|
|
builder.add_node(c)
|
|
builder.add_node(d)
|
|
|
|
# Define edges
|
|
def route(state: State) -> Literal["b", END]:
|
|
if len(state["aggregate"]) < 7:
|
|
return "b"
|
|
else:
|
|
return END
|
|
|
|
builder.add_edge(START, "a")
|
|
builder.add_conditional_edges("a", route)
|
|
builder.add_edge("b", "c")
|
|
builder.add_edge("b", "d")
|
|
builder.add_edge(["c", "d"], "a")
|
|
graph = builder.compile()
|
|
```
|
|
|
|
```python
|
|
from IPython.display import Image, display
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
|
|
This graph looks complex, but can be conceptualized as loop of [supersteps](../concepts/low_level.md#graphs):
|
|
|
|
1. Node A
|
|
2. Node B
|
|
3. Nodes C and D
|
|
4. Node A
|
|
5. ...
|
|
|
|
We have a loop of four supersteps, where nodes C and D are executed concurrently.
|
|
|
|
Invoking the graph as before, we see that we complete two full "laps" before hitting the termination condition:
|
|
|
|
```python
|
|
result = graph.invoke({"aggregate": []})
|
|
```
|
|
```
|
|
Node A sees []
|
|
Node B sees ['A']
|
|
Node D sees ['A', 'B']
|
|
Node C sees ['A', 'B']
|
|
Node A sees ['A', 'B', 'C', 'D']
|
|
Node B sees ['A', 'B', 'C', 'D', 'A']
|
|
Node D sees ['A', 'B', 'C', 'D', 'A', 'B']
|
|
Node C sees ['A', 'B', 'C', 'D', 'A', 'B']
|
|
Node A sees ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']
|
|
```
|
|
|
|
However, if we set the recursion limit to four, we only complete one lap because each lap is four supersteps:
|
|
|
|
```python
|
|
from langgraph.errors import GraphRecursionError
|
|
|
|
try:
|
|
result = graph.invoke({"aggregate": []}, {"recursion_limit": 4})
|
|
except GraphRecursionError:
|
|
print("Recursion Error")
|
|
```
|
|
```
|
|
Node A sees []
|
|
Node B sees ['A']
|
|
Node C sees ['A', 'B']
|
|
Node D sees ['A', 'B']
|
|
Node A sees ['A', 'B', 'C', 'D']
|
|
Recursion Error
|
|
```
|
|
:::
|
|
|
|
:::python
|
|
|
|
## Async
|
|
|
|
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:
|
|
|
|
1. Update `nodes` use `async def` instead of `def`.
|
|
2. Update the code inside to use `await` appropriately.
|
|
3. Invoke the graph with `.ainvoke` or `.astream` as desired.
|
|
|
|
Because many LangChain objects implement the [Runnable Protocol](https://python.langchain.com/docs/expression_language/interface/) which has `async` variants of all the `sync` methods it's typically fairly quick to upgrade a `sync` graph to an `async` graph.
|
|
|
|
See example below. To demonstrate async invocations of underlying LLMs, we will include a chat model:
|
|
|
|
{% include-markdown "../../snippets/chat_model_tabs.md" %}
|
|
|
|
```python
|
|
from langchain.chat_models import init_chat_model
|
|
from langgraph.graph import MessagesState, StateGraph
|
|
|
|
# highlight-next-line
|
|
async def node(state: MessagesState): # (1)!
|
|
# highlight-next-line
|
|
new_message = await llm.ainvoke(state["messages"]) # (2)!
|
|
return {"messages": [new_message]}
|
|
|
|
builder = StateGraph(MessagesState).add_node(node).set_entry_point("node")
|
|
graph = builder.compile()
|
|
|
|
input_message = {"role": "user", "content": "Hello"}
|
|
# highlight-next-line
|
|
result = await graph.ainvoke({"messages": [input_message]}) # (3)!
|
|
```
|
|
|
|
1. Declare nodes to be async functions.
|
|
2. Use async invocations when available within the node.
|
|
3. Use async invocations on the graph object itself.
|
|
|
|
!!! tip "Async streaming"
|
|
|
|
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(
|
|
# state update
|
|
update={"foo": "bar"},
|
|
# control flow
|
|
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
|
|
from langgraph.graph import StateGraph, START
|
|
from langgraph.types import Command
|
|
|
|
# Define graph state
|
|
class State(TypedDict):
|
|
foo: str
|
|
|
|
# Define the nodes
|
|
|
|
def node_a(state: State) -> Command[Literal["node_b", "node_c"]]:
|
|
print("Called A")
|
|
value = random.choice(["b", "c"])
|
|
# this is a replacement for a conditional edge function
|
|
if value == "b":
|
|
goto = "node_b"
|
|
else:
|
|
goto = "node_c"
|
|
|
|
# note how Command allows you to BOTH update the graph state AND route to the next node
|
|
return Command(
|
|
# this is the state update
|
|
update={"foo": value},
|
|
# this is a replacement for an edge
|
|
goto=goto,
|
|
)
|
|
|
|
def node_b(state: State):
|
|
print("Called B")
|
|
return {"foo": state["foo"] + "b"}
|
|
|
|
def node_c(state: State):
|
|
print("Called C")
|
|
return {"foo": state["foo"] + "c"}
|
|
```
|
|
|
|
We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../concepts/low_level.md#conditional-edges) for routing! This is because control flow is defined with `Command` inside `node_a`.
|
|
|
|
```python
|
|
builder = StateGraph(State)
|
|
builder.add_edge(START, "node_a")
|
|
builder.add_node(node_a)
|
|
builder.add_node(node_b)
|
|
builder.add_node(node_c)
|
|
# NOTE: there are no edges between nodes A, B and C!
|
|
|
|
graph = builder.compile()
|
|
```
|
|
|
|
!!! important
|
|
|
|
You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal["node_b", "node_c"]]`. This is necessary for the graph rendering and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`.
|
|
|
|
```python
|
|
from IPython.display import display, Image
|
|
|
|
display(Image(graph.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
|
|
If we run the graph multiple times, we'd see it take different paths (A -> B or A -> C) based on the random choice in node A.
|
|
|
|
```python
|
|
graph.invoke({"foo": ""})
|
|
```
|
|
|
|
```
|
|
Called A
|
|
Called C
|
|
```
|
|
:::
|
|
|
|
:::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<typeof State>): 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<typeof State>) => {
|
|
console.log("Called B");
|
|
return { foo: state.foo + "b" };
|
|
};
|
|
|
|
const nodeC = (state: z.infer<typeof State>) => {
|
|
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(
|
|
update={"foo": "bar"},
|
|
goto="other_subgraph", # where `other_subgraph` is a node in the parent graph
|
|
graph=Command.PARENT
|
|
)
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
|
|
class State(TypedDict):
|
|
# NOTE: we define a reducer here
|
|
# highlight-next-line
|
|
foo: Annotated[str, operator.add]
|
|
|
|
def node_a(state: State):
|
|
print("Called A")
|
|
value = random.choice(["a", "b"])
|
|
# this is a replacement for a conditional edge function
|
|
if value == "a":
|
|
goto = "node_b"
|
|
else:
|
|
goto = "node_c"
|
|
|
|
# note how Command allows you to BOTH update the graph state AND route to the next node
|
|
return Command(
|
|
update={"foo": value},
|
|
goto=goto,
|
|
# this tells LangGraph to navigate to node_b or node_c in the parent graph
|
|
# NOTE: this will navigate to the closest parent graph relative to the subgraph
|
|
# highlight-next-line
|
|
graph=Command.PARENT,
|
|
)
|
|
|
|
subgraph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile()
|
|
|
|
def node_b(state: State):
|
|
print("Called B")
|
|
# NOTE: since we've defined a reducer, we don't need to manually append
|
|
# new characters to existing 'foo' value. instead, reducer will append these
|
|
# automatically (via operator.add)
|
|
# highlight-next-line
|
|
return {"foo": "b"}
|
|
|
|
def node_c(state: State):
|
|
print("Called C")
|
|
# highlight-next-line
|
|
return {"foo": "c"}
|
|
|
|
builder = StateGraph(State)
|
|
builder.add_edge(START, "subgraph")
|
|
builder.add_node("subgraph", subgraph)
|
|
builder.add_node(node_b)
|
|
builder.add_node(node_c)
|
|
|
|
graph = builder.compile()
|
|
```
|
|
|
|
```python
|
|
graph.invoke({"foo": ""})
|
|
```
|
|
|
|
```
|
|
Called A
|
|
Called C
|
|
```
|
|
:::
|
|
|
|
:::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<typeof State>) => {
|
|
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<typeof State>) => {
|
|
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<typeof State>) => {
|
|
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):
|
|
"""Use this to look up user information to better assist them with their questions."""
|
|
user_info = get_user_info(config.get("configurable", {}).get("user_id"))
|
|
return Command(
|
|
update={
|
|
# update the state keys
|
|
"user_info": user_info,
|
|
# update the message history
|
|
"messages": [ToolMessage("Successfully looked up user information", tool_call_id=tool_call_id)]
|
|
}
|
|
)
|
|
```
|
|
:::
|
|
|
|
:::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
|
|
|
|
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
|
|
|
|
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`](../reference/agents.md#langgraph.prebuilt.tool_node.ToolNode) which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from the node.
|
|
|
|
## Visualize your graph
|
|
|
|
Here we demonstrate how to visualize the graphs you create.
|
|
|
|
You can visualize any arbitrary [Graph](https://langchain-ai.github.io/langgraph/reference/graphs/), including [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs.md#langgraph.graph.state.StateGraph).
|
|
|
|
:::python
|
|
Let's have some fun by drawing fractals :).
|
|
|
|
```python
|
|
import random
|
|
from typing import Annotated, Literal
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph import StateGraph, START, END
|
|
from langgraph.graph.message import add_messages
|
|
|
|
class State(TypedDict):
|
|
messages: Annotated[list, add_messages]
|
|
|
|
class MyNode:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
def __call__(self, state: State):
|
|
return {"messages": [("assistant", f"Called node {self.name}")]}
|
|
|
|
def route(state) -> Literal["entry_node", "__end__"]:
|
|
if len(state["messages"]) > 10:
|
|
return "__end__"
|
|
return "entry_node"
|
|
|
|
def add_fractal_nodes(builder, current_node, level, max_level):
|
|
if level > max_level:
|
|
return
|
|
# Number of nodes to create at this level
|
|
num_nodes = random.randint(1, 3) # Adjust randomness as needed
|
|
for i in range(num_nodes):
|
|
nm = ["A", "B", "C"][i]
|
|
node_name = f"node_{current_node}_{nm}"
|
|
builder.add_node(node_name, MyNode(node_name))
|
|
builder.add_edge(current_node, node_name)
|
|
# Recursively add more nodes
|
|
r = random.random()
|
|
if r > 0.2 and level + 1 < max_level:
|
|
add_fractal_nodes(builder, node_name, level + 1, max_level)
|
|
elif r > 0.05:
|
|
builder.add_conditional_edges(node_name, route, node_name)
|
|
else:
|
|
# End
|
|
builder.add_edge(node_name, "__end__")
|
|
|
|
def build_fractal_graph(max_level: int):
|
|
builder = StateGraph(State)
|
|
entry_point = "entry_node"
|
|
builder.add_node(entry_point, MyNode(entry_point))
|
|
builder.add_edge(START, entry_point)
|
|
add_fractal_nodes(builder, entry_point, 1, max_level)
|
|
# Optional: set a finish point if required
|
|
builder.add_edge(entry_point, END) # or any specific node
|
|
return builder.compile()
|
|
|
|
app = build_fractal_graph(3)
|
|
```
|
|
:::
|
|
|
|
:::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())
|
|
```
|
|
|
|
```
|
|
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
|
graph TD;
|
|
__start__([<p>__start__</p>]):::first
|
|
entry_node(entry_node)
|
|
node_entry_node_A(node_entry_node_A)
|
|
node_entry_node_B(node_entry_node_B)
|
|
node_node_entry_node_B_A(node_node_entry_node_B_A)
|
|
node_node_entry_node_B_B(node_node_entry_node_B_B)
|
|
node_node_entry_node_B_C(node_node_entry_node_B_C)
|
|
__end__([<p>__end__</p>]):::last
|
|
__start__ --> entry_node;
|
|
entry_node --> __end__;
|
|
entry_node --> node_entry_node_A;
|
|
entry_node --> node_entry_node_B;
|
|
node_entry_node_B --> node_node_entry_node_B_A;
|
|
node_entry_node_B --> node_node_entry_node_B_B;
|
|
node_entry_node_B --> node_node_entry_node_B_C;
|
|
node_entry_node_A -.-> entry_node;
|
|
node_entry_node_A -.-> __end__;
|
|
node_node_entry_node_B_A -.-> entry_node;
|
|
node_node_entry_node_B_A -.-> __end__;
|
|
node_node_entry_node_B_B -.-> entry_node;
|
|
node_node_entry_node_B_B -.-> __end__;
|
|
node_node_entry_node_B_C -.-> entry_node;
|
|
node_node_entry_node_B_C -.-> __end__;
|
|
classDef default fill:#f2f0ff,line-height:1.2
|
|
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__([<p>__start__</p>]):::first
|
|
node1(node1)
|
|
node2(node2)
|
|
__end__([<p>__end__</p>]):::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)
|
|
- Using Mermaid + Pyppeteer (requires `pip install pyppeteer`)
|
|
- Using graphviz (which requires `pip install graphviz`)
|
|
|
|
**Using Mermaid.Ink**
|
|
|
|
By default, `draw_mermaid_png()` uses Mermaid.Ink's API to generate the diagram.
|
|
|
|
```python
|
|
from IPython.display import Image, display
|
|
from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles
|
|
|
|
display(Image(app.get_graph().draw_mermaid_png()))
|
|
```
|
|
|
|

|
|
|
|
**Using Mermaid + Pyppeteer**
|
|
|
|
```python
|
|
import nest_asyncio
|
|
|
|
nest_asyncio.apply() # Required for Jupyter Notebook to run async functions
|
|
|
|
display(
|
|
Image(
|
|
app.get_graph().draw_mermaid_png(
|
|
curve_style=CurveStyle.LINEAR,
|
|
node_colors=NodeStyles(first="#ffdfba", last="#baffc9", default="#fad7de"),
|
|
wrap_label_n_words=9,
|
|
output_file_path=None,
|
|
draw_method=MermaidDrawMethod.PYPPETEER,
|
|
background_color="white",
|
|
padding=10,
|
|
)
|
|
)
|
|
)
|
|
```
|
|
|
|
**Using Graphviz**
|
|
|
|
```python
|
|
try:
|
|
display(Image(app.get_graph().draw_png()))
|
|
except ImportError:
|
|
print(
|
|
"You likely need to install dependencies for pygraphviz, see more here https://github.com/pygraphviz/pygraphviz/blob/main/INSTALL.txt"
|
|
)
|
|
```
|
|
:::
|
|
|
|
:::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);
|
|
```
|
|
::: |