From 50a9cc5fbc62c9f9d5defc8e0f9f5c098c46fa45 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Wed, 9 Jul 2025 16:38:14 +0200 Subject: [PATCH] Another pass at four pages of get started guide - Make sure that we're calling `getType` instead of deprecated `_getType()` - Make sure that we're referencing the proper camelCase symbols instead of snake_case symbols - Make sure that we're using proper links to the API reference - Make sure to chain calls of StateGraph methods - Make sure to use Zod state in all snippets --- .../get-started/1-build-basic-chatbot.md | 2 +- .../docs/tutorials/get-started/2-add-tools.md | 260 +++++++++------- .../tutorials/get-started/3-add-memory.md | 288 +++++++++--------- .../get-started/4-human-in-the-loop.md | 175 +++++++---- 4 files changed, 406 insertions(+), 319 deletions(-) diff --git a/docs/docs/tutorials/get-started/1-build-basic-chatbot.md b/docs/docs/tutorials/get-started/1-build-basic-chatbot.md index 9c295f6dd..b077c7650 100644 --- a/docs/docs/tutorials/get-started/1-build-basic-chatbot.md +++ b/docs/docs/tutorials/get-started/1-build-basic-chatbot.md @@ -300,7 +300,7 @@ except Exception: ::: :::js -You can visualize the graph using the `getGraph` method and one of the "draw" methods, like `drawMermaidPng`. The `draw` methods each require additional dependencies. +You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method. ```typescript import * as fs from "node:fs/promises"; diff --git a/docs/docs/tutorials/get-started/2-add-tools.md b/docs/docs/tutorials/get-started/2-add-tools.md index 1d960e377..1a4d33202 100644 --- a/docs/docs/tutorials/get-started/2-add-tools.md +++ b/docs/docs/tutorials/get-started/2-add-tools.md @@ -105,9 +105,9 @@ tool.invoke("What's a 'node' in LangGraph?") :::js ```typescript -import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { TavilySearch } from "@langchain/tavily"; -const tool = new TavilySearchResults({ maxResults: 2 }); +const tool = new TavilySearch({ maxResults: 2 }); const tools = [tool]; await tool.invoke({ query: "What's a 'node' in LangGraph?" }); @@ -141,8 +141,30 @@ The results are page summaries our chat bot can use to answer questions: :::js -``` -"[{\"title\":\"Introduction to LangGraph: A Beginner's Guide - Medium\",\"url\":\"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141\",\"content\":\"Stateful Graph: LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. We define nodes for classifying the input, handling greetings, and handling search queries. def classify_input_node(state): LangGraph is a versatile tool for building complex, stateful applications with LLMs. By understanding its core concepts and working through simple examples, beginners can start to leverage its power for their projects. Remember to pay attention to state management, conditional edges, and ensuring there are no dead-end nodes in your graph.\",\"score\":0.7065353,\"raw_content\":null},{\"title\":\"LangGraph Tutorial: What Is LangGraph and How to Use It?\",\"url\":\"https://www.datacamp.com/tutorial/langgraph-tutorial\",\"content\":\"LangGraph is a library within the LangChain ecosystem that provides a framework for defining, coordinating, and executing multiple LLM agents (or chains) in a structured and efficient manner. By managing the flow of data and the sequence of operations, LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination. Whether you need a chatbot that can handle various types of user requests or a multi-agent system that performs complex tasks, LangGraph provides the tools to build exactly what you need. LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.\",\"score\":0.5008063,\"raw_content\":null}]" +```json +{ + "query": "What's a 'node' in LangGraph?", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "url": "https://blog.langchain.dev/langgraph/", + "title": "LangGraph - LangChain Blog", + "content": "TL;DR: LangGraph is module built on top of LangChain to better enable creation of cyclical graphs, often needed for agent runtimes. This state is updated by nodes in the graph, which return operations to attributes of this state (in the form of a key-value store). After adding nodes, you can then add edges to create the graph. An example of this may be in the basic agent runtime, where we always want the model to be called after we call a tool. The state of this graph by default contains concepts that should be familiar to you if you've used LangChain agents: `input`, `chat_history`, `intermediate_steps` (and `agent_outcome` to represent the most recent agent outcome)", + "score": 0.7407191, + "raw_content": null + }, + { + "url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141", + "title": "Introduction to LangGraph: A Beginner's Guide - Medium", + "content": "* **Stateful Graph:** LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. Image 10: Introduction to AI Agent with LangChain and LangGraph: A Beginner’s Guide Image 18: How to build LLM Agent with LangGraph — StateGraph and Reducer Image 20: Simplest Graphs using LangGraph Framework Image 24: Building a ReAct Agent with Langgraph: A Step-by-Step Guide Image 28: Building an Agentic RAG with LangGraph: A Step-by-Step Guide", + "score": 0.65279555, + "raw_content": null + } + ], + "response_time": 1.34 +} ``` ::: @@ -174,9 +196,9 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest") :::js ```typescript -import { ChatOpenAI } from "@langchain/openai"; +import { ChatAnthropic } from "@langchain/anthropic"; -const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); +const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); ``` ::: @@ -212,36 +234,28 @@ graph_builder.add_node("chatbot", chatbot) :::js -```typescript -import { Annotation } from "@langchain/langgraph"; -import { BaseMessage } from "@langchain/core/messages"; +```typescript hl_lines="7-8" +import { StateGraph, MessagesZodState } from "@langchain/langgraph"; +import { z } from "zod"; -const StateAnnotation = Annotation.Root({ - messages: Annotation({ - reducer: (x, y) => x.concat(y), - }), -}); +const State = z.object({ messages: MessagesZodState.shape.messages }); -const graphBuilder = new StateGraph(StateAnnotation); +const chatbot = async (state: z.infer) => { + // Modification: tell the LLM which tools it can call + const llmWithTools = llm.bindTools(tools); -// Modification: tell the LLM which tools it can call -const llmWithTools = llm.bindTools(tools); - -const chatbot = async (state: typeof StateAnnotation.State) => { return { messages: [await llmWithTools.invoke(state.messages)] }; }; - -graphBuilder.addNode("chatbot", chatbot); ``` ::: ## 5. Create a function to run the tools -Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called`BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers. - :::python +Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers. + ```python import json @@ -278,62 +292,65 @@ tool_node = BasicToolNode(tools=[tool]) graph_builder.add_node("tools", tool_node) ``` +!!! note + + If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode). + ::: :::js +Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `"tools"` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's tool calling support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers. + ```typescript -import { ToolMessage } from "@langchain/core/messages"; -import { isAIMessage } from "@langchain/core/messages"; +import type { StructuredToolInterface } from "@langchain/core/tools"; +import { isAIMessage, ToolMessage } from "@langchain/core/messages"; -class BasicToolNode { - private toolsByName: Record; - - constructor(tools: any[]) { - this.toolsByName = {}; - for (const tool of tools) { - this.toolsByName[tool.name] = tool; - } +function createToolNode(tools: StructuredToolInterface[]) { + const toolByName: Record = {}; + for (const tool of tools) { + toolByName[tool.name] = tool; } - async invoke(inputs: Record) { + return async (inputs: z.infer) => { const { messages } = inputs; if (!messages || messages.length === 0) { throw new Error("No message found in input"); } - const message = messages[messages.length - 1]; - - if (!isAIMessage(message) || !message.tool_calls) { + const message = messages.at(-1); + if (!message || !isAIMessage(message) || !message.tool_calls) { throw new Error("Last message is not an AI message with tool calls"); } - const outputs = []; + const outputs: ToolMessage[] = []; for (const toolCall of message.tool_calls) { - const toolResult = await this.toolsByName[toolCall.name].invoke( - toolCall.args - ); + if (!toolCall.id) throw new Error("Tool call ID is required"); + + const tool = toolByName[toolCall.name]; + if (!tool) throw new Error(`Tool ${toolCall.name} not found`); + + const result = await tool.invoke(toolCall.args); + outputs.push( new ToolMessage({ - content: JSON.stringify(toolResult), + content: JSON.stringify(result), name: toolCall.name, tool_call_id: toolCall.id, }) ); } + return { messages: outputs }; - } + }; } - -const toolNode = new BasicToolNode([tool]); -graphBuilder.addNode("tools", toolNode.invoke.bind(toolNode)); ``` -::: - !!! note - If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode). + If you do not want to build this yourself in the future, you can use LangGraph's prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html). + +::: ## 6. Define the `conditional_edges` @@ -390,6 +407,10 @@ graph_builder.add_edge(START, "chatbot") graph = graph_builder.compile() ``` +!!! note + + You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise. + ::: :::js @@ -397,45 +418,48 @@ graph = graph_builder.compile() ```typescript import { END, START } from "@langchain/langgraph"; -const routeTools = (state: typeof StateAnnotation.State) => { +const routeTools = (state: z.infer) => { /** - * Use in the conditional_edge to route to the ToolNode if the last message - * has tool calls. Otherwise, route to the end. + * Use as conditional edge to route to the ToolNode if the last message + * has tool calls. */ - const { messages } = state; - const lastMessage = messages[messages.length - 1]; - - if (isAIMessage(lastMessage) && lastMessage.tool_calls?.length) { + const lastMessage = state.messages.at(-1); + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { return "tools"; } + + /** Otherwise, route to the end. */ return END; }; -// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if -// it is fine directly responding. This conditional routing defines the main agent loop. -graphBuilder.addConditionalEdges( - "chatbot", - routeTools, - // The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node - // It defaults to the identity function, but if you - // want to use a node named something else apart from "tools", - // You can update the value of the dictionary to something else - // e.g., "tools": "my_tools" - { tools: "tools", [END]: END } -); +const graph = new StateGraph(State) + .addNode("chatbot", chatbot) -// Any time a tool is called, we return to the chatbot to decide the next step -graphBuilder.addEdge("tools", "chatbot"); -graphBuilder.addEdge(START, "chatbot"); + // The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if + // it is fine directly responding. This conditional routing defines the main agent loop. + .addNode("tools", createToolNode(tools)) -const graph = graphBuilder.compile(); + // Start the graph with the chatbot + .addEdge(START, "chatbot") + + // The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if + // it is fine directly responding. + .addConditionalEdges("chatbot", routeTools, ["tools", END]) + + // Any time a tool is called, we need to return to the chatbot + .addEdge("tools", "chatbot") + .compile(); ``` -::: - !!! note - You can replace this with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) to be more concise. + You can replace this with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) to be more concise. + +::: ## 7. Visualize the graph (optional) @@ -452,28 +476,25 @@ except Exception: pass ``` -![chatbot-with-tools-diagram](chatbot-with-tools.png) ::: :::js -You can visualize the graph using the `getGraph` method and one of the "draw" methods, like `drawAscii` or `drawMermaidPng`. The `draw` methods each require additional dependencies. +You can visualize the graph using the `getGraph` method and render the graph with the `drawMermaidPng` method. ```typescript -import * as tslab from "tslab"; +import * as fs from "node:fs/promises"; -try { - const graphRepresentation = graph.getGraph(); - const image = await graphRepresentation.drawMermaidPng(); - const arrayBuffer = await image.arrayBuffer(); - await tslab.display.png(new Uint8Array(arrayBuffer)); -} catch (error) { - // This requires some extra dependencies and is optional - console.log("Could not render graph"); -} +const drawableGraph = await graph.getGraphAsync(); +const image = await drawableGraph.drawMermaidPng(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); + +await fs.writeFile("chatbot-with-tools.png", imageBuffer); ``` ::: +![chatbot-with-tools-diagram](chatbot-with-tools.png) + ## 8. Ask the bot questions Now you can ask the chatbot questions outside its training data: @@ -533,7 +554,6 @@ Assistant: Based on the search results, I can provide you with information about LangGraph appears to be a significant tool in the evolving landscape of LLM-based application development, offering developers new ways to create more complex, stateful, and interactive AI systems. Goodbye! -Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... ``` ::: @@ -541,24 +561,35 @@ Output is truncated. View as a scrollable element or open in a text editor. Adju :::js ```typescript -import { HumanMessage } from "@langchain/core/messages"; +import readline from "node:readline/promises"; -const streamGraphUpdates = async (userInput: string) => { +const prompt = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function generateText(content: string) { const stream = await graph.stream( - { messages: [new HumanMessage(userInput)] }, + { messages: [{ type: "human", content }] }, { streamMode: "values" } ); for await (const event of stream) { - const lastMessage = event.messages[event.messages.length - 1]; - console.log("Assistant:", lastMessage.content); - } -}; + const lastMessage = event.messages.at(-1); -// Example usage -const userInput = "What do you know about LangGraph?"; -console.log("User:", userInput); -await streamGraphUpdates(userInput); + if (lastMessage?.getType() === "ai" || lastMessage?.getType() === "tool") { + console.log(`Assistant: ${lastMessage?.text}`); + } + } +} + +while (true) { + const human = await prompt.question("User: "); + if (["quit", "exit", "q"].includes(human.trim())) break; + await generateText(human || "What do you know about LangGraph?"); +} + +prompt.close(); ``` ``` @@ -609,6 +640,14 @@ For ease of use, adjust your code to replace the following with LangGraph prebui {% include-markdown "../../../snippets/chat_model_tabs.md" %} + + ```python hl_lines="25 30" from typing import Annotated @@ -651,8 +690,8 @@ graph = graph_builder.compile() :::js -- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode) -- `routeTools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) +- `createToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html) +- `routeTools` is replaced with the prebuilt [toolsCondition](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.toolsCondition.html) ```typescript import { TavilySearch } from "@langchain/tavily"; @@ -661,21 +700,18 @@ import { StateGraph, START, MessagesZodState, END } from "@langchain/langgraph"; import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; import { z } from "zod"; -const State = z.object({ - messages: MessagesZodState.shape.messages, -}); +const State = z.object({ messages: MessagesZodState.shape.messages }); const tools = [new TavilySearch({ maxResults: 2 })]; const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools); const graph = new StateGraph(State) - .addNode("chatbot", async (state: z.infer) => { - return { messages: [await llm.invoke(state.messages)] }; - }) + .addNode("chatbot", async (state) => ({ + messages: [await llm.invoke(state.messages)], + })) .addNode("tools", new ToolNode(tools)) .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) - // Any time a tool is called, we return to the chatbot to decide the next step .addEdge("tools", "chatbot") .addEdge(START, "chatbot") .compile(); @@ -683,7 +719,13 @@ const graph = new StateGraph(State) ::: -**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. :::python To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r). ::: +**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. + +:::python + +To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r). + +::: ## Next steps diff --git a/docs/docs/tutorials/get-started/3-add-memory.md b/docs/docs/tutorials/get-started/3-add-memory.md index aa5e54ec6..cffc9cb47 100644 --- a/docs/docs/tutorials/get-started/3-add-memory.md +++ b/docs/docs/tutorials/get-started/3-add-memory.md @@ -2,7 +2,7 @@ The chatbot can now [use tools](./2-add-tools.md) to answer user questions, but it does not remember the context of previous interactions. This limits its ability to have coherent, multi-turn conversations. -LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off. +LangGraph solves this problem through **persistent checkpointing**. If you provide a `checkpointer` when compiling the graph and a `thread_id` when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same `thread_id`, the graph loads its saved state, allowing the chatbot to pick up where it left off. We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But first, let's add checkpointing to enable multi-turn conversations. @@ -15,19 +15,23 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha Create a `InMemorySaver` checkpointer: :::python -``` python + +```python from langgraph.checkpoint.memory import InMemorySaver memory = InMemorySaver() ``` + ::: :::js + ```typescript import { MemorySaver } from "@langchain/langgraph"; const memory = new MemorySaver(); ``` + ::: This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database. @@ -37,63 +41,53 @@ This is in-memory checkpointer, which is convenient for the tutorial. However, i Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node: :::python -``` python + +```python graph = graph_builder.compile(checkpointer=memory) ``` -``` python -from IPython.display import Image, display - -try: - display(Image(graph.get_graph().draw_mermaid_png())) -except Exception: - # This requires some extra dependencies and is optional - pass -``` ::: :::js -```typescript -const graph = graphBuilder.compile({ checkpointer: memory }); + +```typescript hl_lines="7" +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 }); ``` -```typescript -import * as tslab from "tslab"; - -try { - const drawableGraph = graph.getGraph(); - const image = await drawableGraph.drawMermaidPng(); - const arrayBuffer = await image.arrayBuffer(); - - await tslab.display.png(new Uint8Array(arrayBuffer)); -} catch (error) { - // This requires some extra dependencies and is optional - console.log("Could not render graph"); -} -``` ::: ## 3. Interact with your chatbot Now you can interact with your bot! -1. Pick a thread to use as the key for this conversation. +1. Pick a thread to use as the key for this conversation. :::python + ```python config = {"configurable": {"thread_id": "1"}} ``` + ::: :::js + ```typescript const config = { configurable: { thread_id: "1" } }; ``` + ::: -2. Call your chatbot: +2. Call your chatbot: :::python + ```python user_input = "Hi there! My name is Will." @@ -115,22 +109,26 @@ Now you can interact with your bot! Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss? ``` + + !!! note + + The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{'messages': []}`). + ::: :::js + ```typescript const userInput = "Hi there! My name is Will."; - // The config is the **second positional argument** to stream() or invoke()! const events = await graph.stream( - { messages: [{ role: "user", content: userInput }] }, - config, - { streamMode: "values" } + { messages: [{ type: "human", content: userInput }] }, + { configurable: { thread_id: "1" }, streamMode: "values" } ); - + for await (const event of events) { - const lastMessage = event.messages[event.messages.length - 1]; - console.log(`${lastMessage._getType()}: ${lastMessage.content}`); + const lastMessage = event.messages.at(-1); + console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`); } ``` @@ -138,17 +136,19 @@ Now you can interact with your bot! human: Hi there! My name is Will. ai: Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss? ``` + + !!! note + + The config was provided as the **second parameter** when calling our graph. It importantly is _not_ nested within the graph inputs (`{"messages": []}`). + ::: - !!! note - - The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{'messages': []}`). - ## 4. Ask a follow up question Ask a follow up question: :::python + ```python user_input = "Remember my name?" @@ -170,29 +170,30 @@ Remember my name? Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks. ``` + ::: :::js + ```typescript const userInput2 = "Remember my name?"; -// The config is the **second positional argument** to stream() or invoke()! const events2 = await graph.stream( - { messages: [{ role: "user", content: userInput2 }] }, - config, - { streamMode: "values" } + { messages: [{ type: "human", content: userInput2 }] }, + { configurable: { thread_id: "1" }, streamMode: "values" } ); for await (const event of events2) { - const lastMessage = event.messages[event.messages.length - 1]; - console.log(`${lastMessage._getType()}: ${lastMessage.content}`); + const lastMessage = event.messages.at(-1); + console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`); } ``` ``` human: Remember my name? -ai: Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks. +ai: Yes, your name is Will. How can I help you today? ``` + ::: **Notice** that we aren't using an external list for memory: it's all handled by the checkpointer! You can inspect the full execution in this [LangSmith trace](https://smith.langchain.com/public/29ba22b5-6d40-4fbe-8d27-b369e3329c84/r) to see what's going on. @@ -200,6 +201,7 @@ ai: Of course, I remember your name, Will. I always try to pay attention to impo Don't believe me? Try this using a different config. :::python + ```python # The only difference is we change the `thread_id` here to "2" instead of "1" events = graph.stream( @@ -220,37 +222,39 @@ Remember my name? I apologize, but I don't have any previous context or memory of your name. As an AI assistant, I don't retain information from past conversations. Each interaction starts fresh. Could you please tell me your name so I can address you properly in this conversation? ``` + ::: :::js -```typescript -// The only difference is we change the `thread_id` here to "2" instead of "1" + +```typescript hl_lines="3-4" const events3 = await graph.stream( - { messages: [{ role: "user", content: userInput2 }] }, - // highlight-next-line - { configurable: { thread_id: "2" } }, - { streamMode: "values" } + { messages: [{ type: "human", content: userInput2 }] }, + // The only difference is we change the `thread_id` here to "2" instead of "1" + { configurable: { thread_id: "2" }, streamMode: "values" } ); for await (const event of events3) { - const lastMessage = event.messages[event.messages.length - 1]; - console.log(`${lastMessage._getType()}: ${lastMessage.content}`); + const lastMessage = event.messages.at(-1); + console.log(`${lastMessage?.getType()}: ${lastMessage?.text}`); } ``` ``` human: Remember my name? -ai: I apologize, but I don't have any previous context or memory of your name. As an AI assistant, I don't retain information from past conversations. Each interaction starts fresh. Could you please tell me your name so I can address you properly in this conversation? +ai: I don't have the ability to remember personal information about users between interactions. However, I'm here to help you with any questions or topics you want to discuss! ``` + ::: **Notice** that the **only** change we've made is to modify the `thread_id` in the config. See this call's [LangSmith trace](https://smith.langchain.com/public/51a62351-2f0a-4058-91cc-9996c5561428/r) for comparison. ## 5. Inspect the state +:::python + By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `get_state(config)`. -:::python ```python snapshot = graph.get_state(config) snapshot @@ -263,12 +267,15 @@ StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Wi ``` snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next) ``` + ::: :::js + +By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `getState(config)`. + ```typescript -const snapshot = await graph.getState(config); -console.log(snapshot); +await graph.getState({ configurable: { thread_id: "1" } }); ``` ```typescript @@ -276,107 +283,89 @@ console.log(snapshot); values: { messages: [ HumanMessage { - content: "Hi there! My name is Will.", - additional_kwargs: {}, - response_metadata: {}, - id: "8c1ca919-c553-4ebf-95d4-b59a2d61e078" + "id": "32fabcef-b3b8-481f-8bcb-fd83399a5f8d", + "content": "Hi there! My name is Will.", + "additional_kwargs": {}, + "response_metadata": {} }, AIMessage { - content: "Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?", - additional_kwargs: {}, - response_metadata: { - id: "msg_01WTQebPhNwmMrmmWojJ9KXJ", - model: "claude-3-5-sonnet-20240620", - stop_reason: "end_turn", - stop_sequence: null, - usage: { input_tokens: 405, output_tokens: 32 } - }, - id: "run-58587b77-8c82-41e6-8a90-d62c444a261d-0", - usage_metadata: { input_tokens: 405, output_tokens: 32, total_tokens: 437 } + "id": "chatcmpl-BrPbTsCJbVqBvXWySlYoTJvM75Kv8", + "content": "Hello Will! How can I assist you today?", + "additional_kwargs": {}, + "response_metadata": {}, + "tool_calls": [], + "invalid_tool_calls": [] }, HumanMessage { - content: "Remember my name?", - additional_kwargs: {}, - response_metadata: {}, - id: "daba7df6-ad75-4d6b-8057-745881cea1ca" + "id": "561c3aad-f8fc-4fac-94a6-54269a220856", + "content": "Remember my name?", + "additional_kwargs": {}, + "response_metadata": {} }, AIMessage { - content: "Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.", - additional_kwargs: {}, - response_metadata: { - id: "msg_01E41KitY74HpENRgXx94vag", - model: "claude-3-5-sonnet-20240620", - stop_reason: "end_turn", - stop_sequence: null, - usage: { input_tokens: 444, output_tokens: 58 } - }, - id: "run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0", - usage_metadata: { input_tokens: 444, output_tokens: 58, total_tokens: 502 } + "id": "chatcmpl-BrPbU4BhhsUikGbW37hYuF5vvnnE2", + "content": "Yes, I remember your name, Will! How can I help you today?", + "additional_kwargs": {}, + "response_metadata": {}, + "tool_calls": [], + "invalid_tool_calls": [] } ] }, next: [], + tasks: [], + metadata: { + source: 'loop', + step: 4, + parents: {}, + thread_id: '1' + }, config: { configurable: { - thread_id: "1", - checkpoint_ns: "", - checkpoint_id: "1ef7d06e-93e0-6acc-8004-f2ac846575d2" + thread_id: '1', + checkpoint_id: '1f05cccc-9bb6-6270-8004-1d2108bcec77', + checkpoint_ns: '' } }, - metadata: { - source: "loop", - writes: { - chatbot: { - messages: [/* AIMessage */] - } - }, - step: 4, - parents: {} - }, - createdAt: "2024-09-27T19:30:10.820758+00:00", + createdAt: '2025-07-09T13:58:27.607Z', parentConfig: { configurable: { - thread_id: "1", - checkpoint_ns: "", - checkpoint_id: "1ef7d06e-859f-6206-8003-e1bd3c264b8f" + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1f05cccc-78fa-68d0-8003-ffb01a76b599' } - }, - tasks: [] + } } ``` ```typescript -console.log(snapshot.next); // (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next) +import * as assert from "node:assert"; + +// Since the graph ended this turn, `next` is empty. +// If you fetch a state from within a graph invocation, next tells which node will execute next) +assert.deepEqual(snapshot.next, []); ``` + ::: The snapshot above contains the current state values, corresponding config, and the `next` node to process. In our case, the graph has reached an `END` state, so `next` is empty. **Congratulations!** Your chatbot can now maintain conversation state across sessions thanks to LangGraph's checkpointing system. This opens up exciting possibilities for more natural, contextual interactions. LangGraph's checkpointing even handles **arbitrarily complex graph states**, which is much more expressive and powerful than simple chat memory. - + Check out the code snippet below to review the graph from this tutorial: +:::python + {% include-markdown "../../../snippets/chat_model_tabs.md" %} -:::python ```python hl_lines="36 37" from typing import Annotated @@ -416,49 +405,44 @@ graph_builder.set_entry_point("chatbot") memory = InMemorySaver() graph = graph_builder.compile(checkpointer=memory) ``` + ::: :::js -```typescript hl_lines="34 35" -import { Annotation } from "@langchain/langgraph"; + +```typescript hl_lines="16 26" +import { END, MessagesZodState, START } from "@langchain/langgraph"; import { ChatOpenAI } from "@langchain/openai"; -import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; -import { BaseMessage } from "@langchain/core/messages"; +import { TavilySearch } from "@langchain/tavily"; + import { MemorySaver } from "@langchain/langgraph"; import { StateGraph } from "@langchain/langgraph"; import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; +import { z } from "zod"; -const StateAnnotation = Annotation.Root({ - messages: Annotation({ - reducer: (x, y) => x.concat(y), - }), +const State = z.object({ + messages: MessagesZodState.shape.messages, }); -const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); - -const graphBuilder = new StateGraph(StateAnnotation); - -const tool = new TavilySearchResults({ maxResults: 2 }); -const tools = [tool]; -const llmWithTools = llm.bindTools(tools); - -const chatbot = async (state: typeof StateAnnotation.State) => { - return { messages: [await llmWithTools.invoke(state.messages)] }; -}; - -graphBuilder.addNode("chatbot", chatbot); - -const toolNode = new ToolNode(tools); -graphBuilder.addNode("tools", toolNode); - -graphBuilder.addConditionalEdges("chatbot", toolsCondition); -graphBuilder.addEdge("tools", "chatbot"); -graphBuilder.addEdge("__start__", "chatbot"); +const tools = [new TavilySearch({ maxResults: 2 })]; +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools(tools); const memory = new MemorySaver(); -const graph = graphBuilder.compile({ checkpointer: memory }); + +async function generateText(content: string) { + +const graph = new StateGraph(State) + .addNode("chatbot", async (state) => ({ + messages: [await llm.invoke(state.messages)], + })) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); ``` + ::: ## Next steps -In the next tutorial, you will [add human-in-the-loop to the chatbot](./4-human-in-the-loop.md) to handle situations where it may need guidance or verification before proceeding. \ No newline at end of file +In the next tutorial, you will [add human-in-the-loop to the chatbot](./4-human-in-the-loop.md) to handle situations where it may need guidance or verification before proceeding. diff --git a/docs/docs/tutorials/get-started/4-human-in-the-loop.md b/docs/docs/tutorials/get-started/4-human-in-the-loop.md index 10a132786..3fde8af1c 100644 --- a/docs/docs/tutorials/get-started/4-human-in-the-loop.md +++ b/docs/docs/tutorials/get-started/4-human-in-the-loop.md @@ -9,7 +9,7 @@ LangGraph's [persistence](../../concepts/persistence.md) layer supports **human- ::: :::js -`interrupt` is ergonomically similar to Node.js's built-in `prompt()` function, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). +`interrupt` is ergonomically similar to Node.js's built-in `readline.question()` function, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). ::: !!! note @@ -48,6 +48,8 @@ We can now incorporate it into our `StateGraph` with an additional tool: :::python +````python hl_lines="12 19 20 21 22 23" + ```python hl_lines="12 19 20 21 22 23" from typing import Annotated @@ -96,14 +98,19 @@ graph_builder.add_conditional_edges( ) graph_builder.add_edge("tools", "chatbot") graph_builder.add_edge(START, "chatbot") -``` +```` ::: :::js -```typescript hl_lines="12 19 20 21 22 23" +````typescript hl_lines="12 19 20 21 22 23" import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; + +```typescript hl_lines="1 7-19" +import { interrupt, MessagesZodState } from "@langchain/langgraph"; +import { ChatAnthropic } from "@langchain/anthropic"; +import { TavilySearch } from "@langchain/tavily"; import { tool } from "@langchain/core/tools"; import { z } from "zod"; @@ -119,6 +126,19 @@ import { ChatAnthropic } from "@langchain/anthropic"; import { Command, interrupt } from "@langchain/langgraph"; +const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + } +); const humanAssistance = tool( async ({ query }) => { const humanResponse = interrupt({ query }); @@ -133,14 +153,16 @@ const humanAssistance = tool( } ); -const searchTool = new TavilySearchResults({ maxResults: 2 }); +const searchTool = new TavilySearch({ maxResults: 2 }); const tools = [searchTool, humanAssistance]; -const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); -const llmWithTools = model.bindTools(tools); +const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", +}).bindTools(tools); -const chatbot = async (state: typeof MessagesAnnotation.State) => { +async function chatbot(state: z.infer) { const message = await llmWithTools.invoke(state.messages); + // Because we will be interrupting during tool execution, // we disable parallel tool calling to avoid repeating any // tool invocations when we resume. @@ -148,7 +170,7 @@ const chatbot = async (state: typeof MessagesAnnotation.State) => { throw new Error("Multiple tool calls not supported with interrupts"); } return { messages: [message] }; -}; +} const graphBuilder = new StateGraph(MessagesAnnotation).addNode( "chatbot", @@ -170,7 +192,7 @@ const shouldContinue = (state: typeof MessagesAnnotation.State) => { graphBuilder.addConditionalEdges("chatbot", shouldContinue); graphBuilder.addEdge("tools", "chatbot"); graphBuilder.addEdge(START, "chatbot"); -``` +```` ::: @@ -197,7 +219,13 @@ graph = graph_builder.compile(checkpointer=memory) ```typescript const memory = new MemorySaver(); -const graph = graphBuilder.compile({ checkpointer: memory }); +const graph = new StateGraph(MessagesZodState) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); ``` ::: @@ -223,13 +251,13 @@ except Exception: :::js ```typescript -import * as tslab from "tslab"; +import * as fs from "node:fs/promises"; -const drawableGraph = graph.getGraph(); +const drawableGraph = await graph.getGraphAsync(); const image = await drawableGraph.drawMermaidPng(); -const arrayBuffer = await image.arrayBuffer(); +const imageBuffer = new Uint8Array(await image.arrayBuffer()); -await tslab.display.png(new Uint8Array(arrayBuffer)); +await fs.writeFile("chatbot-with-tools.png", imageBuffer); ``` ::: @@ -284,14 +312,19 @@ const config = { const events = await graph.stream( { messages: [{ role: "user", content: userInput }] }, - config + { 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()}]: ${lastMessage.content}`); - if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) { + const lastMessage = event.messages.at(-1); + console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`); + + if ( + lastMessage && + isAIMessage(lastMessage) && + lastMessage.tool_calls?.length + ) { console.log("Tool calls:", lastMessage.tool_calls); } } @@ -300,14 +333,15 @@ for await (const event of events) { ``` [human]: I need some expert guidance for building an AI agent. Could you request assistance for me? -[ai]: Certainly! I'd be happy to request expert assistance for you regarding building an AI agent. To do this, I'll use the human_assistance function to relay your request. Let me do that for you now. +[ai]: I'll help you request human assistance for guidance on building an AI agent. Tool calls: [ { name: 'humanAssistance', args: { - query: 'A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?' + query: 'I would like expert guidance on building an AI agent. Could you please provide assistance with this topic?' }, - id: 'toolu_01ABUqneqnuHNuo1vhfDFQCW' + id: 'toolu_01Bpxc8rFVMhSaRosS6b85Ts', + type: 'tool_call' } ] ``` @@ -332,21 +366,25 @@ snapshot.next :::js ```typescript -const snapshot = await graph.getState(config); -console.log(snapshot.next); +const snapshot = await graph.getState({ configurable: { thread_id: "1" } }); +snapshot.next; ``` +```json +["tools"] ``` + ['tools'] -``` +```` ::: !!! info Additional information + :::python + Take a closer look at the `human_assistance` tool: - :::python ```python @tool def human_assistance(query: str) -> str: @@ -359,20 +397,26 @@ console.log(snapshot.next); ::: :::js - ```typescript - const humanAssistance = tool(async ({ query }) => { - const humanResponse = interrupt({ query }); - return humanResponse.data; - }, { - name: "humanAssistance", - description: "Request assistance from a human.", - schema: z.object({ - query: z.string().describe("Human readable question for the human") - }) - }); + + Take a closer look at the `humanAssistance` tool: + + ```typescript hl_lines="3" + const humanAssistance = tool( + async ({ query }) => { + const humanResponse = interrupt({ query }); + return humanResponse.data; + }, + { + name: "humanAssistance", + description: "Request assistance from a human.", + schema: z.object({ + query: z.string().describe("Human readable question for the human"), + }), + }, + ); ``` - Similar to JavaScript's built-in `prompt()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running. + Calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running. ::: ## 5. Resume execution @@ -380,6 +424,7 @@ console.log(snapshot.next); To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs. :::python + For this example, use a dict with a key `"data"`: ```python @@ -394,7 +439,7 @@ events = graph.stream(human_command, config, stream_mode="values") for event in events: if "messages" in event: event["messages"][-1].pretty_print() -``` +```` ``` ================================== Ai Message ================================== @@ -436,18 +481,21 @@ Output is truncated. View as a scrollable element or open in a text editor. Adju For this example, use an object with a key `"data"`: ```typescript -const humanResponse = +const humanResponse = ( "We, the experts are here to help! We'd recommend you check out LangGraph to build your agent." + " It's much more reliable and extensible than simple autonomous agents."; const humanCommand = new Command({ resume: { data: humanResponse } }); -const resumeEvents = await graph.stream(humanCommand, config); +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()}]: ${lastMessage.content}`); + const lastMessage = event.messages.at(-1); + console.log(`[${lastMessage?.getType()}]: ${lastMessage?.text}`); } } ``` @@ -478,7 +526,7 @@ The input has been received and processed as a tool message. Review this call's Check out the code snippet below to review the graph from this tutorial: :::python -{% include-markdown "../../../snippets/chat_model_tabs.md" %} +{!snippets/chat_model_tabs.md!} ```python from typing import Annotated @@ -534,20 +582,20 @@ graph = graph_builder.compile(checkpointer=memory) :::js ```typescript -import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; -import { tool } from "@langchain/core/tools"; -import { z } from "zod"; - -import { MemorySaver } from "@langchain/langgraph"; import { + interrupt, + MessagesZodState, StateGraph, + MemorySaver, START, END, - MessagesAnnotation, } from "@langchain/langgraph"; -import { ToolNode } from "@langchain/langgraph/prebuilt"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; +import { isAIMessage } from "@langchain/core/messages"; import { ChatAnthropic } from "@langchain/anthropic"; -import { Command, interrupt } from "@langchain/langgraph"; +import { TavilySearch } from "@langchain/tavily"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; const humanAssistance = tool( async ({ query }) => { @@ -563,18 +611,24 @@ const humanAssistance = tool( } ); -const searchTool = new TavilySearchResults({ maxResults: 2 }); +const searchTool = new TavilySearch({ maxResults: 2 }); const tools = [searchTool, humanAssistance]; -const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); -const llmWithTools = model.bindTools(tools); +const llmWithTools = new ChatAnthropic({ + model: "claude-3-5-sonnet-latest", +}).bindTools(tools); -const chatbot = async (state: typeof MessagesAnnotation.State) => { +const chatbot = async (state: z.infer) => { const message = await llmWithTools.invoke(state.messages); + + // Because we will be interrupting during tool execution, + // we disable parallel tool calling to avoid repeating any + // tool invocations when we resume. if (message.tool_calls && message.tool_calls.length > 1) { throw new Error("Multiple tool calls not supported with interrupts"); } - return { messages: [message] }; + + return { messages: message }; }; const graphBuilder = new StateGraph(MessagesAnnotation).addNode( @@ -599,7 +653,14 @@ graphBuilder.addEdge("tools", "chatbot"); graphBuilder.addEdge(START, "chatbot"); const memory = new MemorySaver(); -const graph = graphBuilder.compile({ checkpointer: memory }); + +const graph = new StateGraph(MessagesZodState) + .addNode("chatbot", chatbot) + .addNode("tools", new ToolNode(tools)) + .addConditionalEdges("chatbot", toolsCondition, ["tools", END]) + .addEdge("tools", "chatbot") + .addEdge(START, "chatbot") + .compile({ checkpointer: memory }); ``` :::