mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-08 02:37:52 +02:00
feat: add docs translations (#5552)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com> Co-authored-by: Tat Dat Duong <david@duong.cz>
This commit is contained in:
co-authored by
Eugene Yurtsev
Tat Dat Duong
parent
72e418e4d0
commit
d59091672f
@@ -10,6 +10,8 @@ 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,13 +28,34 @@ class State(TypedDict):
|
||||
birthday: str
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { MessagesZodState } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
messages: MessagesZodState.shape.messages,
|
||||
// highlight-next-line
|
||||
name: z.string(),
|
||||
// highlight-next-line
|
||||
birthday: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer.
|
||||
|
||||
## 2. Update the state inside the tool
|
||||
|
||||
:::python
|
||||
|
||||
Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../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
|
||||
|
||||
@@ -76,10 +99,78 @@ def human_assistance(
|
||||
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 { Command, interrupt } from "@langchain/langgraph";
|
||||
|
||||
const humanAssistance = tool(
|
||||
async (input, config) => {
|
||||
// Note that because we are generating a ToolMessage for a state update,
|
||||
// we generally require the ID of the corresponding tool call.
|
||||
// This is available in the tool's config.
|
||||
const toolCallId = config?.toolCall?.id as string | undefined;
|
||||
if (!toolCallId) throw new Error("Tool call ID is required");
|
||||
|
||||
const humanResponse = await interrupt({
|
||||
question: "Is this correct?",
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
});
|
||||
|
||||
// We explicitly update the state with a ToolMessage inside the tool.
|
||||
const stateUpdate = (() => {
|
||||
// If the information is correct, update the state as-is.
|
||||
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
|
||||
return {
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, receive information from the human reviewer.
|
||||
return {
|
||||
name: humanResponse.name || input.name,
|
||||
birthday: humanResponse.birthday || input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({
|
||||
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
|
||||
tool_call_id: toolCallId,
|
||||
}),
|
||||
],
|
||||
};
|
||||
})();
|
||||
|
||||
// We return a Command object in the tool to update our state.
|
||||
return new Command({ update: stateUpdate });
|
||||
},
|
||||
{
|
||||
name: "humanAssistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
name: z.string().describe("The name of the entity"),
|
||||
birthday: z.string().describe("The birthday/release date of the entity"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The rest of the graph stays the same.
|
||||
|
||||
## 3. Prompt the chatbot
|
||||
|
||||
:::python
|
||||
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
|
||||
|
||||
```python
|
||||
@@ -99,6 +190,51 @@ for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
|
||||
|
||||
```typescript
|
||||
import { isAIMessage } from "@langchain/core/messages";
|
||||
|
||||
const userInput =
|
||||
"Can you look up when LangGraph was released? " +
|
||||
"When you have the answer, use the humanAssistance tool for review.";
|
||||
|
||||
const events = await graph.stream(
|
||||
{ messages: [{ role: "user", content: userInput }] },
|
||||
{ configurable: { thread_id: "1" }, streamMode: "values" }
|
||||
);
|
||||
|
||||
for await (const event of events) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
console.log(
|
||||
"=".repeat(32),
|
||||
`${lastMessage?.getType()} Message`,
|
||||
"=".repeat(32)
|
||||
);
|
||||
console.log(lastMessage?.text);
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
isAIMessage(lastMessage) &&
|
||||
lastMessage.tool_calls?.length
|
||||
) {
|
||||
console.log("Tool Calls:");
|
||||
for (const call of lastMessage.tool_calls) {
|
||||
console.log(` ${call.name} (${call.id})`);
|
||||
console.log(` Args: ${JSON.stringify(call.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```
|
||||
================================ Human Message =================================
|
||||
|
||||
@@ -126,12 +262,20 @@ Tool Calls:
|
||||
birthday: 2023-01-01
|
||||
```
|
||||
|
||||
:::python
|
||||
We've hit the `interrupt` in the `human_assistance` tool again.
|
||||
:::
|
||||
|
||||
:::js
|
||||
We've hit the `interrupt` in the `humanAssistance` tool again.
|
||||
:::
|
||||
|
||||
## 4. Add human assistance
|
||||
|
||||
The chatbot failed to identify the correct date, so supply it with information:
|
||||
|
||||
:::python
|
||||
|
||||
```python
|
||||
human_command = Command(
|
||||
resume={
|
||||
@@ -146,6 +290,53 @@ for event in events:
|
||||
event["messages"][-1].pretty_print()
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import { Command } from "@langchain/langgraph";
|
||||
|
||||
const humanCommand = new Command({
|
||||
resume: {
|
||||
name: "LangGraph",
|
||||
birthday: "Jan 17, 2024",
|
||||
},
|
||||
});
|
||||
|
||||
const resumeEvents = await graph.stream(humanCommand, {
|
||||
configurable: { thread_id: "1" },
|
||||
streamMode: "values",
|
||||
});
|
||||
|
||||
for await (const event of resumeEvents) {
|
||||
if ("messages" in event) {
|
||||
const lastMessage = event.messages.at(-1);
|
||||
|
||||
console.log(
|
||||
"=".repeat(32),
|
||||
`${lastMessage?.getType()} Message`,
|
||||
"=".repeat(32)
|
||||
);
|
||||
console.log(lastMessage?.text);
|
||||
|
||||
if (
|
||||
lastMessage &&
|
||||
isAIMessage(lastMessage) &&
|
||||
lastMessage.tool_calls?.length
|
||||
) {
|
||||
console.log("Tool Calls:");
|
||||
for (const call of lastMessage.tool_calls) {
|
||||
console.log(` ${call.name} (${call.id})`);
|
||||
console.log(` Args: ${JSON.stringify(call.args)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```
|
||||
================================== Ai Message ==================================
|
||||
|
||||
@@ -175,6 +366,8 @@ 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)
|
||||
|
||||
@@ -185,13 +378,34 @@ 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)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
```
|
||||
{ name: 'LangGraph', birthday: 'Jan 17, 2024' }
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information).
|
||||
|
||||
## 5. Manually update the state
|
||||
|
||||
:::python
|
||||
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`:
|
||||
|
||||
``` python
|
||||
```python
|
||||
graph.update_state(config, {"name": "LangGraph (library)"})
|
||||
```
|
||||
|
||||
@@ -201,11 +415,36 @@ graph.update_state(config, {"name": "LangGraph (library)"})
|
||||
'checkpoint_id': '1efd4ec5-cf69-6352-8006-9278f1730162'}}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`:
|
||||
|
||||
```typescript
|
||||
await graph.updateState(
|
||||
{ configurable: { thread_id: "1" } },
|
||||
{ name: "LangGraph (library)" }
|
||||
);
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
configurable: {
|
||||
thread_id: '1',
|
||||
checkpoint_ns: '',
|
||||
checkpoint_id: '1efd4ec5-cf69-6352-8006-9278f1730162'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 6. View the new value
|
||||
|
||||
:::python
|
||||
If you call `graph.get_state`, you can see the new value is reflected:
|
||||
|
||||
``` python
|
||||
```python
|
||||
snapshot = graph.get_state(config)
|
||||
|
||||
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
|
||||
@@ -215,12 +454,35 @@ snapshot = graph.get_state(config)
|
||||
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
If you call `graph.getState`, you can see the new value is reflected:
|
||||
|
||||
```typescript
|
||||
const updatedSnapshot = await graph.getState(config);
|
||||
|
||||
const updatedRelevantState = Object.fromEntries(
|
||||
Object.entries(updatedSnapshot.values).filter(([k]) =>
|
||||
["name", "birthday"].includes(k)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
```typescript
|
||||
{ name: 'LangGraph (library)', birthday: 'Jan 17, 2024' }
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Manual state updates will [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to [control human-in-the-loop workflows](../../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.
|
||||
|
||||
**Congratulations!** You've added custom keys to the state to facilitate a more complex workflow, and learned how to generate state updates from inside tools.
|
||||
|
||||
Check out the code snippet below to review the graph from this tutorial:
|
||||
|
||||
:::python
|
||||
|
||||
{% include-markdown "../../../snippets/chat_model_tabs.md" %}
|
||||
|
||||
<!---
|
||||
@@ -305,7 +567,111 @@ memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::js
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Command,
|
||||
interrupt,
|
||||
MessagesZodState,
|
||||
MemorySaver,
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
} from "@langchain/langgraph";
|
||||
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearch } from "@langchain/tavily";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const State = z.object({
|
||||
messages: MessagesZodState.shape.messages,
|
||||
name: z.string(),
|
||||
birthday: z.string(),
|
||||
});
|
||||
|
||||
const humanAssistance = tool(
|
||||
async (input, config) => {
|
||||
// Note that because we are generating a ToolMessage for a state update, we
|
||||
// generally require the ID of the corresponding tool call. This is available
|
||||
// in the tool's config.
|
||||
const toolCallId = config?.toolCall?.id as string | undefined;
|
||||
if (!toolCallId) throw new Error("Tool call ID is required");
|
||||
|
||||
const humanResponse = await interrupt({
|
||||
question: "Is this correct?",
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
});
|
||||
|
||||
// We explicitly update the state with a ToolMessage inside the tool.
|
||||
const stateUpdate = (() => {
|
||||
// If the information is correct, update the state as-is.
|
||||
if (humanResponse.correct?.toLowerCase().startsWith("y")) {
|
||||
return {
|
||||
name: input.name,
|
||||
birthday: input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({ content: "Correct", tool_call_id: toolCallId }),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, receive information from the human reviewer.
|
||||
return {
|
||||
name: humanResponse.name || input.name,
|
||||
birthday: humanResponse.birthday || input.birthday,
|
||||
messages: [
|
||||
new ToolMessage({
|
||||
content: `Made a correction: ${JSON.stringify(humanResponse)}`,
|
||||
tool_call_id: toolCallId,
|
||||
}),
|
||||
],
|
||||
};
|
||||
})();
|
||||
|
||||
// We return a Command object in the tool to update our state.
|
||||
return new Command({ update: stateUpdate });
|
||||
},
|
||||
{
|
||||
name: "humanAssistance",
|
||||
description: "Request assistance from a human.",
|
||||
schema: z.object({
|
||||
name: z.string().describe("The name of the entity"),
|
||||
birthday: z.string().describe("The birthday/release date of the entity"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const searchTool = new TavilySearch({ maxResults: 2 });
|
||||
|
||||
const tools = [searchTool, humanAssistance];
|
||||
const llmWithTools = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
}).bindTools(tools);
|
||||
|
||||
const memory = new MemorySaver();
|
||||
|
||||
const chatbot = async (state: z.infer<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).
|
||||
|
||||
Reference in New Issue
Block a user