diff --git a/docs/docs/tutorials/get-started/5-customize-state.md b/docs/docs/tutorials/get-started/5-customize-state.md index 5bfc6dd05..7b2b0515f 100644 --- a/docs/docs/tutorials/get-started/5-customize-state.md +++ b/docs/docs/tutorials/get-started/5-customize-state.md @@ -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({ - 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, + name: z.string(), // highlight-next-line - birthday: Annotation, + 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" %} -:::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({ - reducer: (x, y) => x.concat(y), - }), - name: Annotation, - birthday: Annotation, -}); +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) => { + 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). \ No newline at end of file +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). diff --git a/docs/docs/tutorials/get-started/6-time-travel.md b/docs/docs/tutorials/get-started/6-time-travel.md index 605a6581c..76fa60502 100644 --- a/docs/docs/tutorials/get-started/6-time-travel.md +++ b/docs/docs/tutorials/get-started/6-time-travel.md @@ -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