Update last message

This commit is contained in:
Tat Dat Duong
2025-07-28 14:52:10 -07:00
committed by Hunter Lovell
parent 50a9cc5fbc
commit 0711fccd44
2 changed files with 428 additions and 499 deletions
@@ -11,6 +11,7 @@ In this tutorial, you will add additional fields to the state to define complex
Update the chatbot to research the birthday of an entity by adding `name` and `birthday` keys to the state:
:::python
```python
from typing import Annotated
@@ -26,23 +27,24 @@ class State(TypedDict):
# highlight-next-line
birthday: str
```
:::
:::js
```typescript
import { Annotation } from "@langchain/langgraph";
import type { BaseMessage } from "@langchain/core/messages";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
```typescript
import { MessagesZodState } from "@langchain/langgraph";
import { z } from "zod";
const State = z.object({
messages: MessagesZodState.shape.messages,
// highlight-next-line
name: Annotation<string>,
name: z.string(),
// highlight-next-line
birthday: Annotation<string>,
birthday: z.string(),
});
```
:::
Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer.
@@ -50,9 +52,10 @@ Adding this information to the state makes it easily accessible by other graph n
## 2. Update the state inside the tool
:::python
Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
``` python
```python
from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, tool
@@ -95,69 +98,72 @@ def human_assistance(
# We return a Command object in the tool to update our state.
return Command(update=state_update)
```
:::
:::js
Now, populate the state keys inside of the `humanAssistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
```typescript
import { tool } from "@langchain/core/tools";
import { ToolMessage } from "@langchain/core/messages";
import { z } from "zod";
import { Command, interrupt } from "@langchain/langgraph";
const humanAssistance = tool(async (input, config) => {
const { name, birthday } = input;
// Note that because we are generating a ToolMessage for a state update, we
// generally require the ID of the corresponding tool call. This is available
// in the tool's config.
const toolCallId = config?.toolCall?.id;
const humanResponse = await interrupt({
question: "Is this correct?",
name: name,
birthday: birthday,
});
const humanAssistance = tool(
async (input, config) => {
// Note that because we are generating a ToolMessage for a state update,
// we generally require the ID of the corresponding tool call.
// This is available in the tool's config.
const toolCallId = config?.toolCall?.id as string | undefined;
if (!toolCallId) throw new Error("Tool call ID is required");
let verifiedName: string;
let verifiedBirthday: string;
let response: string;
const humanResponse = await interrupt({
question: "Is this correct?",
name: input.name,
birthday: input.birthday,
});
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
verifiedName = name;
verifiedBirthday = birthday;
response = "Correct";
} else {
// Otherwise, receive information from the human reviewer.
verifiedName = humanResponse.name || name;
verifiedBirthday = humanResponse.birthday || birthday;
response = `Made a correction: ${JSON.stringify(humanResponse)}`;
// We explicitly update the state with a ToolMessage inside the tool.
const stateUpdate = (() => {
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
return {
name: input.name,
birthday: input.birthday,
messages: [
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
],
};
}
// Otherwise, receive information from the human reviewer.
return {
name: humanResponse.name || input.name,
birthday: humanResponse.birthday || input.birthday,
messages: [
new ToolMessage({
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
tool_call_id: toolCallId,
}),
],
};
})();
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
}
// This time we explicitly update the state with a ToolMessage inside
// the tool.
const stateUpdate = {
name: verifiedName,
birthday: verifiedBirthday,
messages: [new ToolMessage({
content: response,
tool_call_id: toolCallId!,
})],
};
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
}, {
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
});
);
```
:::
The rest of the graph stays the same.
@@ -183,38 +189,50 @@ for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```typescript
const userInput = (
import { isAIMessage } from "@langchain/core/messages";
const userInput =
"Can you look up when LangGraph was released? " +
"When you have the answer, use the humanAssistance tool for review."
);
const config = { configurable: { thread_id: "1" } };
"When you have the answer, use the humanAssistance tool for review.";
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ ...config, streamMode: "values" }
{ configurable: { thread_id: "1" }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
if (lastMessage.tool_calls?.length) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool Calls:");
lastMessage.tool_calls.forEach((call: any) => {
for (const call of lastMessage.tool_calls) {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
});
}
}
}
}
```
:::
```
@@ -257,6 +275,7 @@ We've hit the `interrupt` in the `humanAssistance` tool again.
The chatbot failed to identify the correct date, so supply it with information:
:::python
```python
human_command = Command(
resume={
@@ -270,9 +289,11 @@ for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
@@ -283,23 +304,37 @@ const humanCommand = new Command({
},
});
const resumeEvents = await graph.stream(humanCommand, { ...config, streamMode: "values" });
const resumeEvents = await graph.stream(humanCommand, {
configurable: { thread_id: "1" },
streamMode: "values",
});
for await (const event of resumeEvents) {
if ("messages" in event) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
if (lastMessage.tool_calls?.length) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
if (
lastMessage &&
isAIMessage(lastMessage) &&
lastMessage.tool_calls?.length
) {
console.log("Tool Calls:");
lastMessage.tool_calls.forEach((call: any) => {
for (const call of lastMessage.tool_calls) {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
});
}
}
}
}
```
:::
```
@@ -332,6 +367,7 @@ It's worth noting that LangGraph had been in development and use for some time b
Note that these fields are now reflected in the state:
:::python
```python
snapshot = graph.get_state(config)
@@ -341,21 +377,25 @@ snapshot = graph.get_state(config)
```
{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}
```
:::
:::js
```typescript
const snapshot = await graph.getState(config);
const relevantState = Object.fromEntries(
Object.entries(snapshot.values).filter(([k]) => ["name", "birthday"].includes(k))
Object.entries(snapshot.values).filter(([k]) =>
["name", "birthday"].includes(k)
)
);
console.log(relevantState);
```
```
{ name: 'LangGraph', birthday: 'Jan 17, 2024' }
```
:::
This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information).
@@ -365,7 +405,7 @@ This makes them easily accessible to downstream nodes (e.g., a node that further
:::python
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`:
``` python
```python
graph.update_state(config, {"name": "LangGraph (library)"})
```
@@ -374,16 +414,20 @@ graph.update_state(config, {"name": "LangGraph (library)"})
'checkpoint_ns': '',
'checkpoint_id': '1efd4ec5-cf69-6352-8006-9278f1730162'}}
```
:::
:::js
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`:
```typescript
await graph.updateState(config, { name: "LangGraph (library)" });
await graph.updateState(
{ configurable: { thread_id: "1" } },
{ name: "LangGraph (library)" }
);
```
```
```typescript
{
configurable: {
thread_id: '1',
@@ -392,6 +436,7 @@ await graph.updateState(config, { name: "LangGraph (library)" });
}
}
```
:::
## 6. View the new value
@@ -399,7 +444,7 @@ await graph.updateState(config, { name: "LangGraph (library)" });
:::python
If you call `graph.get_state`, you can see the new value is reflected:
``` python
```python
snapshot = graph.get_state(config)
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
@@ -408,6 +453,7 @@ snapshot = graph.get_state(config)
```
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
```
:::
:::js
@@ -417,14 +463,16 @@ If you call `graph.getState`, you can see the new value is reflected:
const updatedSnapshot = await graph.getState(config);
const updatedRelevantState = Object.fromEntries(
Object.entries(updatedSnapshot.values).filter(([k]) => ["name", "birthday"].includes(k))
Object.entries(updatedSnapshot.values).filter(([k]) =>
["name", "birthday"].includes(k)
)
);
console.log(updatedRelevantState);
```
```
```typescript
{ name: 'LangGraph (library)', birthday: 'Jan 17, 2024' }
```
:::
Manual state updates will [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to [control human-in-the-loop workflows](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). Use of the `interrupt` function is generally recommended instead, as it allows data to be transmitted in a human-in-the-loop interaction independently of state updates.
@@ -433,6 +481,8 @@ Manual state updates will [generate a trace](https://smith.langchain.com/public/
Check out the code snippet below to review the graph from this tutorial:
:::python
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
<!---
@@ -443,7 +493,6 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::python
```python
from typing import Annotated
@@ -517,109 +566,112 @@ graph_builder.add_edge(START, "chatbot")
memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import {
Command,
interrupt,
MessagesZodState,
MemorySaver,
StateGraph,
END,
START,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { tool } from "@langchain/core/tools";
import { TavilySearch } from "@langchain/tavily";
import { ToolMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { Annotation } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph";
import { StateGraph, START } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { Command, interrupt } from "@langchain/langgraph";
const model = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
const State = z.object({
messages: MessagesZodState.shape.messages,
name: z.string(),
birthday: z.string(),
});
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
name: Annotation<string>,
birthday: Annotation<string>,
});
const humanAssistance = tool(
async (input, config) => {
// Note that because we are generating a ToolMessage for a state update, we
// generally require the ID of the corresponding tool call. This is available
// in the tool's config.
const toolCallId = config?.toolCall?.id as string | undefined;
if (!toolCallId) throw new Error("Tool call ID is required");
const humanAssistance = tool(async (input, config) => {
const { name, birthday } = input;
const toolCallId = config?.toolCall?.id;
const humanResponse = await interrupt({
question: "Is this correct?",
name: name,
birthday: birthday,
});
const humanResponse = await interrupt({
question: "Is this correct?",
name: input.name,
birthday: input.birthday,
});
let verifiedName: string;
let verifiedBirthday: string;
let response: string;
// We explicitly update the state with a ToolMessage inside the tool.
const stateUpdate = (() => {
// If the information is correct, update the state as-is.
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
return {
name: input.name,
birthday: input.birthday,
messages: [
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
],
};
}
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
verifiedName = name;
verifiedBirthday = birthday;
response = "Correct";
} else {
verifiedName = humanResponse.name || name;
verifiedBirthday = humanResponse.birthday || birthday;
response = `Made a correction: ${JSON.stringify(humanResponse)}`;
// Otherwise, receive information from the human reviewer.
return {
name: humanResponse.name || input.name,
birthday: humanResponse.birthday || input.birthday,
messages: [
new ToolMessage({
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
tool_call_id: toolCallId,
}),
],
};
})();
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
},
{
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
}
);
const stateUpdate = {
name: verifiedName,
birthday: verifiedBirthday,
messages: [new ToolMessage({
content: response,
tool_call_id: toolCallId!,
})],
};
const searchTool = new TavilySearch({ maxResults: 2 });
return new Command({ update: stateUpdate });
}, {
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string().describe("The name of the entity"),
birthday: z.string().describe("The birthday/release date of the entity"),
}),
});
const searchTool = new TavilySearchResults({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = model.bindTools(tools);
const chatbot = async (state: typeof StateAnnotation.State) => {
const message = await llmWithTools.invoke(state.messages);
return { messages: [message] };
};
const graphBuilder = new StateGraph(StateAnnotation);
graphBuilder.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
const shouldContinue = (state: typeof StateAnnotation.State) => {
const messages = state.messages;
const lastMessage = messages[messages.length - 1];
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
return "tools";
}
return "__end__";
};
graphBuilder.addConditionalEdges("chatbot", shouldContinue);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const llmWithTools = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
}).bindTools(tools);
const memory = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer: memory });
const chatbot = async (state: z.infer<typeof State>) => {
const message = await llmWithTools.invoke(state.messages);
return { messages: message };
};
const graph = new StateGraph(State)
.addNode("chatbot", chatbot)
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
:::
## Next steps
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
+207 -330
View File
@@ -4,7 +4,7 @@ In a typical chatbot workflow, the user interacts with the bot one or more times
What if you want a user to be able to start from a previous response and explore a different outcome? Or what if you want users to be able to rewind your chatbot's work to fix mistakes or try a different strategy, something that is common in applications like autonomous software engineers?
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
You can create these types of experiences using LangGraph's built-in **time travel** functionality.
!!! note
@@ -20,9 +20,10 @@ Rewind your graph by fetching a checkpoint using the graph's `get_state_history`
Rewind your graph by fetching a checkpoint using the graph's `getStateHistory` method. You can then resume execution at this previous point in time.
:::
:::python
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
:::python
<!---
```python
from langchain.chat_models import init_chat_model
@@ -70,184 +71,41 @@ graph_builder.add_edge(START, "chatbot")
memory = InMemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
// process.env.OPENAI_API_KEY = "sk_...";
// Optional, add tracing in LangSmith
// process.env.LANGCHAIN_API_KEY = "ls__...";
// process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_TRACING_V2 = "true";
process.env.LANGCHAIN_PROJECT = "Time Travel: LangGraphJS";
```
```typescript
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { BaseMessage } from "@langchain/core/messages";
import {
MessagesAnnotation,
StateGraph,
START,
Command,
interrupt,
MemorySaver
END,
MessagesZodState,
MemorySaver,
} from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { TavilySearch } from "@langchain/tavily";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const model = new ChatOpenAI({ model: "gpt-4o" });
const State = z.object({ messages: MessagesZodState.shape.messages });
const tools = [new TavilySearch({ maxResults: 2 })];
const llmWithTools = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools);
const memory = new MemorySaver();
/**
* Call LLM with structured output to get a natural language response as well as a target agent (node) to go to next.
* @param messages list of messages to pass to the LLM
* @param targetAgentNodes list of the node names of the target agents to navigate to
*/
function callLlm(messages: BaseMessage[], targetAgentNodes: string[]) {
// define the schema for the structured output:
// - model's text response (`response`)
// - name of the node to go to next (or 'finish')
const outputSchema = z.object({
response: z.string().describe("A human readable response to the original question. Does not need to be a final response. Will be streamed back to the user."),
goto: z.enum(["finish", ...targetAgentNodes]).describe("The next agent to call, or 'finish' if the user's query has been resolved. Must be one of the specified values."),
})
return model.withStructuredOutput(outputSchema, { name: "Response" }).invoke(messages)
}
async function travelAdvisor(
state: typeof MessagesAnnotation.State
): Promise<Command> {
const systemPrompt =
"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). " +
"If you need specific sightseeing recommendations, ask 'sightseeingAdvisor' for help. " +
"If you need hotel recommendations, ask 'hotelAdvisor' for help. " +
"If you have enough information to respond to the user, return 'finish'. " +
"Never mention other agents by name.";
const messages = [{"role": "system", "content": systemPrompt}, ...state.messages] as BaseMessage[];
const targetAgentNodes = ["sightseeingAdvisor", "hotelAdvisor"];
const response = await callLlm(messages, targetAgentNodes);
const aiMsg = {"role": "ai", "content": response.response, "name": "travelAdvisor"};
let goto = response.goto;
if (goto === "finish") {
goto = "human";
}
return new Command({goto, update: { "messages": [aiMsg] } });
}
async function sightseeingAdvisor(
state: typeof MessagesAnnotation.State
): Promise<Command> {
const systemPrompt =
"You are a travel expert that can provide specific sightseeing recommendations for a given destination. " +
"If you need general travel help, go to 'travelAdvisor' for help. " +
"If you need hotel recommendations, go to 'hotelAdvisor' for help. " +
"If you have enough information to respond to the user, return 'finish'. " +
"Never mention other agents by name.";
const messages = [{"role": "system", "content": systemPrompt}, ...state.messages] as BaseMessage[];
const targetAgentNodes = ["travelAdvisor", "hotelAdvisor"];
const response = await callLlm(messages, targetAgentNodes);
const aiMsg = {"role": "ai", "content": response.response, "name": "sightseeingAdvisor"};
let goto = response.goto;
if (goto === "finish") {
goto = "human";
}
return new Command({ goto, update: {"messages": [aiMsg] } });
}
async function hotelAdvisor(
state: typeof MessagesAnnotation.State
): Promise<Command> {
const systemPrompt =
"You are a travel expert that can provide hotel recommendations for a given destination. " +
"If you need general travel help, ask 'travelAdvisor' for help. " +
"If you need specific sightseeing recommendations, ask 'sightseeingAdvisor' for help. " +
"If you have enough information to respond to the user, return 'finish'. " +
"Never mention other agents by name.";
const messages = [{"role": "system", "content": systemPrompt}, ...state.messages] as BaseMessage[];
const targetAgentNodes = ["travelAdvisor", "sightseeingAdvisor"];
const response = await callLlm(messages, targetAgentNodes);
const aiMsg = {"role": "ai", "content": response.response, "name": "hotelAdvisor"};
let goto = response.goto;
if (goto === "finish") {
goto = "human";
}
return new Command({ goto, update: {"messages": [aiMsg] } });
}
function humanNode(
state: typeof MessagesAnnotation.State
): Command {
const userInput: string = interrupt("Ready for user input.");
let activeAgent: string | undefined = undefined;
// Look up the active agent
for (let i = state.messages.length - 1; i >= 0; i--) {
if (state.messages[i].name) {
activeAgent = state.messages[i].name;
break;
}
}
if (!activeAgent) {
throw new Error("Could not determine the active agent.");
}
return new Command({
goto: activeAgent,
update: {
"messages": [
{
"role": "human",
"content": userInput,
}
]
}
});
}
const builder = new StateGraph(MessagesAnnotation)
.addNode("travelAdvisor", travelAdvisor, {
ends: ["sightseeingAdvisor", "hotelAdvisor"]
})
.addNode("sightseeingAdvisor", sightseeingAdvisor, {
ends: ["human", "travelAdvisor", "hotelAdvisor"]
})
.addNode("hotelAdvisor", hotelAdvisor, {
ends: ["human", "travelAdvisor", "sightseeingAdvisor"]
})
// This adds a node to collect human input, which will route
// back to the active agent.
.addNode("human", humanNode, {
ends: ["hotelAdvisor", "sightseeingAdvisor", "travelAdvisor", "human"]
})
// We'll always start with a general travel advisor.
.addEdge(START, "travelAdvisor")
const checkpointer = new MemorySaver()
const graph = builder.compile({ checkpointer })
const graph = new StateGraph(State)
.addNode("chatbot", async (state) => ({
messages: [await llmWithTools.invoke(state.messages)],
}))
.addNode("tools", new ToolNode(tools))
.addConditionalEdges("chatbot", toolsCondition, ["tools", END])
.addEdge("tools", "chatbot")
.addEdge(START, "chatbot")
.compile({ checkpointer: memory });
```
```typescript
import * as tslab from "tslab";
const drawableGraph = graph.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
```
:::
## 2. Add steps
@@ -255,7 +113,8 @@ await tslab.display.png(new Uint8Array(arrayBuffer));
Add steps to your graph. Every step will be checkpointed in its state history:
:::python
``` python
```python
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{
@@ -363,140 +222,132 @@ Great idea! Building an autonomous agent with LangGraph is definitely an excitin
Building an autonomous agent is an iterative process, so be prepared to refine and improve your agent over time. Good luck with your project! If you need any more specific information as you progress, feel free to ask.
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
import { Command } from "@langchain/langgraph";
import { v4 as uuidv4 } from "uuid";
const threadConfig = { configurable: { thread_id: uuidv4() }, streamMode: "values" as const };
const inputs = [
// 1st round of conversation
{
messages: [
{ role: "user", content: "i wanna go somewhere warm in the caribbean" }
]
},
// Since we're using `interrupt`, we'll need to resume using the Command primitive.
// 2nd round of conversation
new Command({
resume: "could you recommend a nice hotel in one of the areas and tell me which area it is."
}),
// Third round of conversation
new Command({ resume: "could you recommend something to do near the hotel?" }),
]
import { randomUUID } from "node:crypto";
const threadId = randomUUID();
let iter = 0;
for await (const userInput of inputs) {
iter += 1;
console.log(`\n--- Conversation Turn ${iter} ---\n`);
console.log(`User: ${JSON.stringify(userInput)}\n`);
for await (const update of await graph.stream(userInput, threadConfig)) {
const lastMessage = update.messages ? update.messages[update.messages.length - 1] : undefined;
if (lastMessage && lastMessage._getType() === "ai") {
console.log(`${lastMessage.name}: ${lastMessage.content}`)
for (const userInput of [
"I'm learning LangGraph. Could you do some research on it for me?",
"Ya that's helpful. Maybe I'll build an autonomous agent with it!",
]) {
iter += 1;
console.log(`\n--- Conversation Turn ${iter} ---\n`);
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ configurable: { thread_id: threadId }, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages.at(-1);
console.log(
"=".repeat(32),
`${lastMessage?.getType()} Message`,
"=".repeat(32)
);
console.log(lastMessage?.text);
}
}
}
```
```
--- Conversation Turn 1 ---
User: {"messages":[{"role":"user","content":"i wanna go somewhere warm in the caribbean"}]}
================================ human Message ================================
I'm learning LangGraph.js. Could you do some research on it for me?
================================ ai Message ================================
I'll search for information about LangGraph.js for you.
================================ tool Message ================================
{
"query": "LangGraph.js framework TypeScript langchain what is it tutorial guide",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://techcommunity.microsoft.com/blog/educatordeveloperblog/an-absolute-beginners-guide-to-langgraph-js/4212496",
"title": "An Absolute Beginner's Guide to LangGraph.js",
"content": "(...)",
"score": 0.79369855,
"raw_content": null
},
{
"url": "https://langchain-ai.github.io/langgraphjs/",
"title": "LangGraph.js",
"content": "(...)",
"score": 0.78154784,
"raw_content": null
}
],
"response_time": 2.37
}
================================ ai Message ================================
Let me provide you with an overview of LangGraph.js based on the search results:
travelAdvisor: The Caribbean has so many wonderful warm destinations to choose from! Here are some fantastic options:
LangGraph.js is a JavaScript/TypeScript library that's part of the LangChain ecosystem, specifically designed for creating and managing complex LLM (Large Language Model) based workflows. Here are the key points about LangGraph.js:
**Popular Caribbean Destinations:**
- **Barbados** - Beautiful beaches, friendly locals, and great weather year-round
- **Jamaica** - Vibrant culture, stunning beaches, and amazing food
- **Aruba** - Consistently sunny with gorgeous white sand beaches
- **Bahamas** - Crystal clear waters and easy access from the US
- **Costa Rica** - While technically Central America, offers Caribbean coastline with lush rainforests
- **Puerto Rico** - No passport needed for US citizens, rich history and culture
- **Dominican Republic** - Great value with beautiful resorts and beaches
Each destination offers something unique - some are better for nightlife and culture (like Jamaica), others for pure relaxation (like Aruba), and some for adventure activities (like Costa Rica).
What kind of experience are you looking for? Are you interested in:
- Pure beach relaxation
- Cultural experiences and local cuisine
- Adventure activities like snorkeling or hiking
- Vibrant nightlife
- Family-friendly activities
This will help me give you a more targeted recommendation!
1. Purpose:
- It's a low-level orchestration framework for building controllable agents
- Particularly useful for creating agentic workflows where LLMs decide the course of action based on current state
- Helps model workflows as graphs with nodes and edges
(...)
--- Conversation Turn 2 ---
User: {"resume":"could you recommend a nice hotel in one of the areas and tell me which area it is."}
================================ human Message ================================
Ya that's helpful. Maybe I'll build an autonomous agent with it!
================================ ai Message ================================
Let me search for specific information about building autonomous agents with LangGraph.js.
================================ tool Message ================================
{
"query": "how to build autonomous agents with LangGraph.js examples tutorial react agent",
"follow_up_questions": null,
"answer": null,
"images": [],
"results": [
{
"url": "https://ai.google.dev/gemini-api/docs/langgraph-example",
"title": "ReAct agent from scratch with Gemini 2.5 and LangGraph",
"content": "(...)",
"score": 0.7602419,
"raw_content": null
},
{
"url": "https://www.youtube.com/watch?v=ZfjaIshGkmk",
"title": "Build Autonomous AI Agents with ReAct and LangGraph Tools",
"content": "(...)",
"score": 0.7471924,
"raw_content": null
}
],
"response_time": 1.98
}
================================ ai Message ================================
Based on the search results, I can provide you with a practical overview of how to build an autonomous agent with LangGraph.js. Here's what you need to know:
hotelAdvisor: I'd be happy to recommend a fantastic hotel! Let me suggest one of the Caribbean's most beautiful destinations:
1. Basic Structure for Building an Agent:
- LangGraph.js provides a ReAct (Reason + Act) pattern implementation
- The basic components include:
- State management for conversation history
- Nodes for different actions
- Edges for decision-making flow
- Tools for specific functionalities
**The Ocean Club, A Four Seasons Resort - Paradise Island, Bahamas**
(...)
**Location:** Paradise Island, Nassau, Bahamas
This is an absolutely stunning luxury resort that offers the perfect Caribbean experience. Here's why it's exceptional:
**Hotel Highlights:**
- Pristine white sand beaches with crystal-clear turquoise waters
- Beautifully landscaped gardens designed by the same architect who created Versailles
- Multiple pools including an adults-only pool
- World-class spa and fitness facilities
- Several excellent restaurants on-site
- Easy access to Atlantis Paradise Island next door for additional dining and entertainment
**The Area - Paradise Island:**
- Connected to Nassau by bridge, so you can easily explore the capital city
- Home to beautiful beaches like Cove Beach and Paradise Beach
- Close to great snorkeling and diving spots
- Duty-free shopping in Nassau
- Rich history and colonial architecture to explore
- Easy airport access (Nassau Airport is about 30 minutes away)
The Bahamas is perfect for a warm Caribbean getaway - it's consistently sunny, the water is incredibly beautiful, and there's a nice mix of relaxation and activities available. Plus, if you're coming from the US, no passport is required for citizens!
Would you like more details about this hotel or would you prefer recommendations for a different Caribbean destination?
--- Conversation Turn 3 ---
User: {"resume":"could you recommend something to do near the hotel?"}
sightseeingAdvisor: Excellent choice with The Ocean Club! Paradise Island and Nassau offer wonderful activities right near your hotel. Here are some fantastic things to do:
**On Paradise Island (walking distance/very close):**
- **Atlantis Paradise Island** - Right next door! Explore their massive aquariums, water slides, and marine exhibits even if you're not staying there
- **Paradise Beach** - One of the most beautiful beaches in the Caribbean, perfect for swimming and sunbathing
- **Cove Beach** - A more secluded, adults-oriented beach area
- **Golf at Ocean Club Golf Course** - Championship golf course designed by Tom Weiskopf
**Short trip to Nassau (15-20 minutes):**
- **Swimming with Dolphins** - Several locations offer this incredible experience
- **Straw Market** - Famous local market for souvenirs, crafts, and local goods
- **Queen's Staircase** - Historic 66-step staircase carved by slaves, beautiful and historically significant
- **Fort Charlotte** - Historic British colonial fort with great views and cannons
- **Junkanoo Beach** - Popular local beach with great vibes
- **Bay Street** - Main shopping and dining street with duty-free stores
**Water Activities Near the Hotel:**
- **Snorkeling and Diving** - Crystal clear waters with coral reefs nearby
- **Deep Sea Fishing** - Excellent fishing charters available
- **Boat Tours** - Island hopping, sunset cruises, or swimming with stingrays
- **Kayaking** - Explore the beautiful coastline
**Day Trips:**
- **Pig Beach** - Famous swimming with pigs experience (full day excursion)
- **Exuma Cays** - Stunning natural beauty and marine life
The great thing about this location is you have both luxury resort amenities and easy access to authentic Bahamian culture and adventures!
```
:::
## 3. Replay the full state history
@@ -504,7 +355,8 @@ The great thing about this location is you have both luxury resort amenities and
Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred.
:::python
``` python
```python
to_replay = None
for state in graph.get_state_history(config):
print("Num Messages: ", len(state.values["messages"]), "Next: ", state.next)
@@ -536,42 +388,57 @@ Num Messages: 1 Next: ('chatbot',)
Num Messages: 0 Next: ('__start__',)
--------------------------------------------------------------------------------
```
:::
:::js
```typescript
let toReplay = null;
for await (const state of graph.getStateHistory(threadConfig)) {
console.log(`Num Messages: ${state.values.messages.length}, Next: ${JSON.stringify(state.next)}`);
console.log("-".repeat(80));
if (state.values.messages.length === 6) {
// We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
toReplay = state;
}
import type { StateSnapshot } from "@langchain/langgraph";
let toReplay: StateSnapshot | undefined;
for await (const state of graph.getStateHistory({
configurable: { thread_id: threadId },
})) {
console.log(
`Num Messages: ${state.values.messages.length}, Next: ${JSON.stringify(
state.next
)}`
);
console.log("-".repeat(80));
if (state.values.messages.length === 6) {
// We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
toReplay = state;
}
}
```
```
Num Messages: 8, Next: []
Num Messages: 8 Next: []
--------------------------------------------------------------------------------
Num Messages: 7, Next: ["human"]
Num Messages: 7 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 6, Next: ["sightseeingAdvisor"]
Num Messages: 6 Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 5, Next: ["human"]
Num Messages: 5 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 4, Next: ["hotelAdvisor"]
Num Messages: 4 Next: ["__start__"]
--------------------------------------------------------------------------------
Num Messages: 3, Next: ["human"]
Num Messages: 4 Next: []
--------------------------------------------------------------------------------
Num Messages: 2, Next: ["travelAdvisor"]
Num Messages: 3 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 1, Next: ["__start__"]
Num Messages: 2 Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 1 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 0 Next: ["__start__"]
--------------------------------------------------------------------------------
```
:::
Checkpoints are saved for every step of the graph. This __spans invocations__ so you can rewind across a full thread's history.
Checkpoints are saved for every step of the graph. This **spans invocations** so you can rewind across a full thread's history.
## Resume from a checkpoint
@@ -580,10 +447,11 @@ Resume from the `to_replay` state, which is after the `chatbot` node in the seco
:::
:::js
Resume from the `to_replay` state, which is after a specific node in one of the graph invocations. Resuming from this point will call the next scheduled node.
Resume from the `toReplay` state, which is after a specific node in one of the graph invocations. Resuming from this point will call the next scheduled node.
:::
:::python
```python
print(to_replay.next)
print(to_replay.config)
@@ -593,32 +461,36 @@ print(to_replay.config)
('tools',)
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}
```
:::
:::js
```typescript
console.log(toReplay.next);
console.log(toReplay.config);
```
```
["sightseeingAdvisor"]
["tools"]
{
configurable: {
thread_id: "dcf6a444-3094-41c9-ba7d-9f4c7b99b5a0",
thread_id: "1",
checkpoint_ns: "",
checkpoint_id: "1eff9b04-63e7-672c-8010-5fb09def85f8"
checkpoint_id: "1efd43e3-0c1f-6c4e-8006-891877d65740"
}
}
```
:::
## 4. Load a state from a moment-in-time
:::python
The checkpoint's `to_replay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
:::python
``` python
```python
# The `checkpoint_id` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.
for event in graph.stream(None, to_replay.config, stream_mode="values"):
if "messages" in event:
@@ -655,51 +527,56 @@ Would you like more information on any specific aspect of building your autonomo
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
The graph resumed execution from the `action` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
:::
:::js
The checkpoint's `toReplay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
```typescript
// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer.
for await (const event of await graph.stream(null, { ...toReplay.config, streamMode: "values" as const })) {
const lastMessage = event.messages ? event.messages[event.messages.length - 1] : undefined;
if (lastMessage && lastMessage._getType() === "ai") {
console.log(`${lastMessage.name}: ${lastMessage.content}`)
}
for await (const event of await graph.stream(null, {
...toReplay?.config,
streamMode: "values",
})) {
const lastMessage = event.messages?.at(-1);
if (lastMessage && lastMessage.getType() === "ai") {
console.log(`${lastMessage.getType()}: ${lastMessage.text}`);
}
}
```
```
sightseeingAdvisor: Excellent choice with The Ocean Club! Paradise Island and Nassau offer wonderful activities right near your hotel. Here are some fantastic things to do:
================================== Ai Message ==================================
**On Paradise Island (walking distance/very close):**
- **Atlantis Paradise Island** - Right next door! Explore their massive aquariums, water slides, and marine exhibits even if you're not staying there
- **Paradise Beach** - One of the most beautiful beaches in the Caribbean, perfect for swimming and sunbathing
- **Cove Beach** - A more secluded, adults-oriented beach area
- **Golf at Ocean Club Golf Course** - Championship golf course designed by Tom Weiskopf
[{'text': "That's an exciting idea! Building an autonomous agent with LangGraph is indeed a great application of this technology. LangGraph is particularly well-suited for creating complex, multi-step AI workflows, which is perfect for autonomous agents. Let me gather some more specific information about using LangGraph for building autonomous agents.", 'type': 'text'}, {'id': 'toolu_01QWNHhUaeeWcGXvA4eHT7Zo', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
Tool Calls:
tavily_search_results_json (toolu_01QWNHhUaeeWcGXvA4eHT7Zo)
Call ID: toolu_01QWNHhUaeeWcGXvA4eHT7Zo
Args:
query: Building autonomous agents with LangGraph examples and tutorials
================================= Tool Message =================================
Name: tavily_search_results_json
**Short trip to Nassau (15-20 minutes):**
- **Swimming with Dolphins** - Several locations offer this incredible experience
- **Straw Market** - Famous local market for souvenirs, crafts, and local goods
- **Queen's Staircase** - Historic 66-step staircase carved by slaves, beautiful and historically significant
- **Fort Charlotte** - Historic British colonial fort with great views and cannons
- **Junkanoo Beach** - Popular local beach with great vibes
- **Bay Street** - Main shopping and dining street with duty-free stores
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
================================== Ai Message ==================================
**Water Activities Near the Hotel:**
- **Snorkeling and Diving** - Crystal clear waters with coral reefs nearby
- **Deep Sea Fishing** - Excellent fishing charters available
- **Boat Tours** - Island hopping, sunset cruises, or swimming with stingrays
- **Kayaking** - Explore the beautiful coastline
Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:
**Day Trips:**
- **Pig Beach** - Famous swimming with pigs experience (full day excursion)
- **Exuma Cays** - Stunning natural beauty and marine life
1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task.
The great thing about this location is you have both luxury resort amenities and easy access to authentic Bahamian culture and adventures!
2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the "brain" of your agent, making decisions and generating responses.
3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order.
...
Remember, building an autonomous agent is an iterative process. Start simple and gradually increase complexity as you become more comfortable with LangGraph and its capabilities.
Would you like more information on any specific aspect of building your autonomous agent with LangGraph?
```
The graph resumed execution from the `sightseeingAdvisor` node. You can tell this is the case since the first value printed above is the response from our sightseeing advisor agent.
The graph resumed execution from the `tools` node. You can tell this is the case since the first value printed above is the response from our search engine tool.
:::
**Congratulations!** You've now used time-travel checkpoint traversal in LangGraph. Being able to rewind and explore alternative paths opens up a world of possibilities for debugging, experimentation, and interactive applications.
@@ -710,4 +587,4 @@ Take your LangGraph journey further by exploring deployment and advanced feature
- **[LangGraph Server quickstart](../../tutorials/langgraph-platform/local-server.md)**: Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- **[LangGraph Platform quickstart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Platform.
- **[LangGraph Platform concepts](../../concepts/langgraph_platform.md)**: Understand the foundational concepts of the LangGraph Platform.
- **[LangGraph Platform concepts](../../concepts/langgraph_platform.md)**: Understand the foundational concepts of the LangGraph Platform.