From 6859bc312d3ca6cf8fafe099e5dc27e40f8bda49 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 30 Jun 2025 10:57:28 -0400 Subject: [PATCH] Raw output unreviewed --- .../get-started/1-build-basic-chatbot.md | 196 ++++++++++- .../docs/tutorials/get-started/2-add-tools.md | 316 +++++++++++++++++- .../tutorials/get-started/3-add-memory.md | 257 +++++++++++++- 3 files changed, 755 insertions(+), 14 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 9235fa65f..06cb84abc 100644 --- a/docs/docs/tutorials/get-started/1-build-basic-chatbot.md +++ b/docs/docs/tutorials/get-started/1-build-basic-chatbot.md @@ -1,6 +1,6 @@ # Build a basic chatbot -In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let鈥檚 dive in! 馃専 +In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let's dive in! 馃専 ## Prerequisites @@ -13,9 +13,17 @@ tool-calling features, such as [OpenAI](https://platform.openai.com/api-keys), Install the required packages: +:::python ```bash pip install -U langgraph langsmith ``` +::: + +:::js +```bash +npm install @langchain/langgraph @langchain/core +``` +::: !!! tip @@ -27,6 +35,7 @@ Now you can create a basic chatbot using LangGraph. This chatbot will respond di Start by creating a `StateGraph`. A `StateGraph` object defines the structure of our chatbot as a "state machine". We'll add `nodes` to represent the llm and functions our chatbot can call and `edges` to specify how the bot should transition between these functions. +:::python ```python from typing import Annotated @@ -45,24 +54,45 @@ class State(TypedDict): graph_builder = StateGraph(State) ``` +::: + +:::js +```typescript +import { Annotation } from "@langchain/langgraph"; +import { StateGraph, START, END } from "@langchain/langgraph"; +import { BaseMessage } from "@langchain/core/messages"; + +const State = Annotation.Root({ + // Messages have the type "BaseMessage[]". The reducer function + // defines how this state key should be updated + // (in this case, it appends messages to the list, rather than overwriting them) + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), +}); + +const graphBuilder = new StateGraph(State); +``` +::: Our graph can now handle two key tasks: 1. Each `node` can receive the current `State` as input and output an update to the state. -2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function used with the `Annotated` syntax. +2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt reducer function. ------ !!! tip "Concept" - When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages). + When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a schema with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages). ## 3. Add a node -Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular Python functions. +Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular functions. Let's first select a chat model: +:::python {!snippets/chat_model_tabs.md!} +::: +:::js +```typescript +import { ChatOpenAI } from "@langchain/openai"; +// or import { ChatAnthropic } from "@langchain/anthropic"; + +const llm = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, +}); +``` +::: We can now incorporate the chat model into a simple node: +:::python ```python def chatbot(state: State): @@ -87,39 +130,84 @@ def chatbot(state: State): # the node is used. graph_builder.add_node("chatbot", chatbot) ``` +::: + +:::js +```typescript +const chatbot = async (state: typeof State.State) => { + return { messages: [await llm.invoke(state.messages)] }; +}; + +// The first argument is the unique node name +// The second argument is the function or object that will be called whenever +// the node is used. +graphBuilder.addNode("chatbot", chatbot); +``` +::: **Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key "messages". This is the basic pattern for all LangGraph node functions. +:::python The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state. +::: + +:::js +The reducer function in our `State` will append the LLM's response messages to whatever messages are already in the state. +::: ## 4. Add an `entry` point Add an `entry` point to tell the graph **where to start its work** each time it is run: +:::python ```python graph_builder.add_edge(START, "chatbot") ``` +::: + +:::js +```typescript +graphBuilder.addEdge(START, "chatbot"); +``` +::: ## 5. Add an `exit` point Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity. +:::python ```python graph_builder.add_edge("chatbot", END) ``` +::: + +:::js +```typescript +graphBuilder.addEdge("chatbot", END); +``` +::: + This tells the graph to terminate after running the chatbot node. ## 6. Compile the graph -Before running the graph, we'll need to compile it. We can do so by calling `compile()` -on the graph builder. This creates a `CompiledGraph` we can invoke on our state. +Before running the graph, we'll need to compile it. We can do so by calling `compile()` on the graph builder. This creates a `CompiledGraph` we can invoke on our state. +:::python ```python graph = graph_builder.compile() ``` +::: + +:::js +```typescript +const graph = graphBuilder.compile(); +``` +::: ## 7. Visualize the graph (optional) +:::python You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies. ```python @@ -131,10 +219,25 @@ except Exception: # This requires some extra dependencies and is optional pass ``` +::: + +:::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. + +```typescript +// Note: tslab only works inside a jupyter notebook. Don't worry about running this code yourself! +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)); +``` +::: ![basic chatbot diagram](basic-chatbot.png) - ## 8. Run the chatbot Now run the chatbot! @@ -143,6 +246,7 @@ Now run the chatbot! You can exit the chat loop at any time by typing `quit`, `exit`, or `q`. +:::python ```python def stream_graph_updates(user_input: str): for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}): @@ -164,16 +268,55 @@ while True: stream_graph_updates(user_input) break ``` +::: + +:::js +```typescript +import { HumanMessage } from "@langchain/core/messages"; + +async function streamGraphUpdates(userInput: string) { + const stream = await graph.stream({ + messages: [new HumanMessage(userInput)] + }); + + for await (const event of stream) { + for (const value of Object.values(event)) { + console.log("Assistant:", value.messages[value.messages.length - 1].content); + } + } +} + +// Example usage (in a real application, you'd implement input handling differently) +async function runChatbot() { + try { + const userInput = "What do you know about LangGraph?"; + console.log("User: " + userInput); + await streamGraphUpdates(userInput); + } catch (error) { + console.error("Error:", error); + } +} + +// Run the example +await runChatbot(); +``` +::: ``` Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions. +``` + +:::python +``` Goodbye! ``` +::: **Congratulations!** You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a [LangSmith Trace](https://smith.langchain.com/public/7527e308-9502-4894-b347-f34385740d5a/r) for the call above. Below is the full code for this tutorial: +:::python ```python from typing import Annotated @@ -206,9 +349,42 @@ graph_builder.add_edge(START, "chatbot") graph_builder.add_edge("chatbot", END) graph = graph_builder.compile() ``` +::: + +:::js +```typescript +import { Annotation } from "@langchain/langgraph"; +import { StateGraph, START, END } from "@langchain/langgraph"; +import { BaseMessage, HumanMessage } from "@langchain/core/messages"; +import { ChatOpenAI } from "@langchain/openai"; + +const State = Annotation.Root({ + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), +}); + +const graphBuilder = new StateGraph(State); + +const llm = new ChatOpenAI({ + model: "gpt-4o", + temperature: 0, +}); + +const chatbot = async (state: typeof State.State) => { + return { messages: [await llm.invoke(state.messages)] }; +}; + +// The first argument is the unique node name +// The second argument is the function or object that will be called whenever +// the node is used. +graphBuilder.addNode("chatbot", chatbot); +graphBuilder.addEdge(START, "chatbot"); +graphBuilder.addEdge("chatbot", END); +const graph = graphBuilder.compile(); +``` +::: ## Next steps -You may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll [add a web search tool](./2-add-tools.md) to expand the bot's knowledge and make it more capable. - - +You may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll [add a web search tool](./2-add-tools.md) to expand the bot's knowledge and make it more capable. \ No newline at end of file diff --git a/docs/docs/tutorials/get-started/2-add-tools.md b/docs/docs/tutorials/get-started/2-add-tools.md index 33accd9a8..b7e97a66c 100644 --- a/docs/docs/tutorials/get-started/2-add-tools.md +++ b/docs/docs/tutorials/get-started/2-add-tools.md @@ -10,19 +10,37 @@ To handle queries that your chatbot can't answer "from memory", integrate a web Before you start this tutorial, ensure you have the following: +:::python - An API key for the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/). +::: + +:::js +- An API key for the [Tavily Search Engine](https://docs.tavily.com/). +::: ## 1. Install the search engine +:::python Install the requirements to use the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/): ```bash pip install -U langchain-tavily ``` +::: + +:::js +Install the requirements to use the [Tavily Search Engine](https://docs.tavily.com/): + +```bash +npm install @langchain/community +``` +::: + ## 2. Configure your environment Configure your environment with your search engine API key: +:::python ```bash _set_env("TAVILY_API_KEY") ``` @@ -30,11 +48,19 @@ _set_env("TAVILY_API_KEY") ``` TAVILY_API_KEY: 路路路路路路路路 ``` +::: + +:::js +```typescript +process.env.TAVILY_API_KEY = "tvly-..."; +``` +::: ## 3. Define the tool Define the web search tool: +:::python ```python from langchain_tavily import TavilySearch @@ -42,9 +68,22 @@ tool = TavilySearch(max_results=2) tools = [tool] tool.invoke("What's a 'node' in LangGraph?") ``` +::: + +:::js +```typescript +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; + +const tool = new TavilySearchResults({ maxResults: 2 }); +const tools = [tool]; + +await tool.invoke({ query: "What's a 'node' in LangGraph?" }); +``` +::: The results are page summaries our chat bot can use to answer questions: +:::python ``` {'query': "What's a 'node' in LangGraph?", 'follow_up_questions': None, @@ -62,13 +101,27 @@ The results are page summaries our chat bot can use to answer questions: 'raw_content': None}], 'response_time': 1.38} ``` +::: + +:::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}]" +``` +::: ## 4. Define the graph +:::python For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bind_tools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine. +::: + +:::js +For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bindTools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine. +::: Let's first select our LLM: +:::python {!snippets/chat_model_tabs.md!} +::: + +:::js +```typescript +import { ChatOpenAI } from "@langchain/openai"; + +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); +``` +::: We can now incorporate it into a `StateGraph`: +:::python ```python hl_lines="15" from typing import Annotated @@ -103,11 +166,37 @@ def chatbot(state: State): graph_builder.add_node("chatbot", chatbot) ``` +::: + +:::js +```typescript +import { Annotation } from "@langchain/langgraph"; +import { BaseMessage } from "@langchain/core/messages"; + +const StateAnnotation = Annotation.Root({ + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), +}); + +const graphBuilder = new StateGraph(StateAnnotation); + +// 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 ```python import json @@ -143,6 +232,54 @@ class BasicToolNode: tool_node = BasicToolNode(tools=[tool]) graph_builder.add_node("tools", tool_node) ``` +::: + +:::js +```typescript +import { ToolMessage } from "@langchain/core/messages"; +import { isAIMessage } from "@langchain/core/messages"; + +class BasicToolNode { + private toolsByName: Record; + + constructor(tools: any[]) { + this.toolsByName = {}; + for (const tool of tools) { + this.toolsByName[tool.name] = tool; + } + } + + async invoke(inputs: Record) { + 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) { + throw new Error("Last message is not an AI message with tool calls"); + } + + const outputs = []; + for (const toolCall of message.tool_calls) { + const toolResult = await this.toolsByName[toolCall.name].invoke(toolCall.args); + outputs.push( + new ToolMessage({ + content: JSON.stringify(toolResult), + name: toolCall.name, + tool_call_id: toolCall.id, + }) + ); + } + return { messages: outputs }; + } +} + +const toolNode = new BasicToolNode([tool]); +graphBuilder.addNode("tools", toolNode.invoke.bind(toolNode)); +``` +::: !!! note @@ -154,10 +291,17 @@ With the tool node added, now you can define the `conditional_edges`. **Edges** route the control flow from one node to the next. **Conditional edges** start from a single node and usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next. -Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. +:::python +Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. +::: + +:::js +Next, define a router function called `routeTools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `addConditionalEdges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next. +::: The condition will route to `tools` if tool calls are present and `END` if not. Because the condition can return `END`, you do not need to explicitly set a `finish_point` this time. +:::python ```python def route_tools( state: State, @@ -194,6 +338,46 @@ graph_builder.add_edge("tools", "chatbot") graph_builder.add_edge(START, "chatbot") graph = graph_builder.compile() ``` +::: + +:::js +```typescript +import { END, START } from "@langchain/langgraph"; + +const routeTools = (state: typeof StateAnnotation.State) => { + /** + * Use in the conditional_edge to route to the ToolNode if the last message + * has tool calls. Otherwise, route to the end. + */ + const { messages } = state; + const lastMessage = messages[messages.length - 1]; + + if (isAIMessage(lastMessage) && lastMessage.tool_calls?.length) { + return "tools"; + } + 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 } +); + +// Any time a tool is called, we return to the chatbot to decide the next step +graphBuilder.addEdge("tools", "chatbot"); +graphBuilder.addEdge(START, "chatbot"); + +const graph = graphBuilder.compile(); +``` +::: !!! note @@ -201,6 +385,7 @@ graph = graph_builder.compile() ## 7. Visualize the graph (optional) +:::python You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies. ```python @@ -214,11 +399,31 @@ except Exception: ``` ![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. + +```typescript +import * as tslab from "tslab"; + +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"); +} +``` +::: ## 8. Ask the bot questions Now you can ask the chatbot questions outside its training data: +:::python ```python def stream_graph_updates(user_input: str): for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}): @@ -274,11 +479,71 @@ LangGraph appears to be a significant tool in the evolving landscape of LLM-base Goodbye! Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... ``` +::: + +:::js +```typescript +import { HumanMessage } from "@langchain/core/messages"; + +const streamGraphUpdates = async (userInput: string) => { + const stream = await graph.stream( + { messages: [new HumanMessage(userInput)] }, + { streamMode: "values" } + ); + + for await (const event of stream) { + const lastMessage = event.messages[event.messages.length - 1]; + console.log("Assistant:", lastMessage.content); + } +}; + +// Example usage +const userInput = "What do you know about LangGraph?"; +console.log("User:", userInput); +await streamGraphUpdates(userInput); +``` + +``` +User: What do you know about LangGraph? +Assistant: I'll search for the latest information about LangGraph for you. +Assistant: [{"title":"Introduction to LangGraph: A Beginner's Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","content":"..."}] +Assistant: Based on the search results, I can provide you with information about LangGraph: + +LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). Here are the key aspects: + +**Core Purpose:** +- LangGraph is specifically designed for creating agent and multi-agent workflows +- It provides a framework for defining, coordinating, and executing multiple LLM agents in a structured manner + +**Key Features:** +1. **Stateful Graph Architecture**: LangGraph revolves around a stateful graph where each node represents a step in computation, and the graph maintains state that is passed around and updated as the computation progresses + +2. **Conditional Edges**: It supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph + +3. **Cycles**: Unlike other LLM frameworks, LangGraph allows you to define flows that involve cycles, which is essential for most agentic architectures + +4. **Controllability**: It offers enhanced control over the application flow + +5. **Persistence**: The library provides ways to maintain state and persistence in LLM-based applications + +**Use Cases:** +- Conversational agents +- Complex task automation +- Custom LLM-backed experiences +- Multi-agent systems that perform complex tasks + +**Benefits:** +LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination, making it easier to build complex, production-ready features with LLMs. + +This makes LangGraph a significant tool in the evolving landscape of LLM-based application development. +``` +::: ## 9. Use prebuilts For ease of use, adjust your code to replace the following with LangGraph prebuilt components. These have built in functionality like parallel API execution. +:::python - `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode) - `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition) @@ -322,9 +587,54 @@ graph_builder.add_edge("tools", "chatbot") graph_builder.add_edge(START, "chatbot") graph = graph_builder.compile() ``` +::: -**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. 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). +:::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) + +```typescript +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { ChatOpenAI } from "@langchain/openai"; +import { Annotation } from "@langchain/langgraph"; +import { BaseMessage } from "@langchain/core/messages"; +import { StateGraph, START } from "@langchain/langgraph"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; + +const StateAnnotation = Annotation.Root({ + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), +}); + +const graphBuilder = new StateGraph(StateAnnotation); + +const tool = new TavilySearchResults({ maxResults: 2 }); +const tools = [tool]; + +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); +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); +// Any time a tool is called, we return to the chatbot to decide the next step +graphBuilder.addEdge("tools", "chatbot"); +graphBuilder.addEdge(START, "chatbot"); + +const graph = graphBuilder.compile(); +``` +::: + +**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 -The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](./3-add-memory.md) to address this. +The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](./3-add-memory.md) to address this. \ No newline at end of file diff --git a/docs/docs/tutorials/get-started/3-add-memory.md b/docs/docs/tutorials/get-started/3-add-memory.md index acd21c39d..8da3adae6 100644 --- a/docs/docs/tutorials/get-started/3-add-memory.md +++ b/docs/docs/tutorials/get-started/3-add-memory.md @@ -14,11 +14,21 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha Create a `MemorySaver` checkpointer: +:::python ``` python from langgraph.checkpoint.memory import MemorySaver memory = MemorySaver() ``` +::: + +:::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. @@ -26,6 +36,7 @@ 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 graph = graph_builder.compile(checkpointer=memory) ``` @@ -39,6 +50,28 @@ except Exception: # This requires some extra dependencies and is optional pass ``` +::: + +:::js +```typescript +const graph = graphBuilder.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 @@ -46,12 +79,21 @@ Now you can interact with your bot! 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: + :::python ```python user_input = "Hi there! My name is Will." @@ -73,6 +115,30 @@ 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? ``` + ::: + + :::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" } + ); + + for await (const event of events) { + const lastMessage = event.messages[event.messages.length - 1]; + console.log(`${lastMessage._getType()}: ${lastMessage.content}`); + } + ``` + + ``` + 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 @@ -82,6 +148,7 @@ Now you can interact with your bot! Ask a follow up question: +:::python ```python user_input = "Remember my name?" @@ -103,11 +170,36 @@ 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" } +); + +for await (const event of events2) { + const lastMessage = event.messages[event.messages.length - 1]; + console.log(`${lastMessage._getType()}: ${lastMessage.content}`); +} +``` + +``` +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. +``` +::: **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. 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( @@ -128,6 +220,29 @@ 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" +const events3 = await graph.stream( + { messages: [{ role: "user", content: userInput2 }] }, + // highlight-next-line + { 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}`); +} +``` + +``` +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? +``` +::: **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. @@ -135,6 +250,7 @@ I apologize, but I don't have any previous context or memory of your name. As an 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 @@ -147,6 +263,92 @@ 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 +```typescript +const snapshot = await graph.getState(config); +console.log(snapshot); +``` + +```typescript +{ + values: { + messages: [ + HumanMessage { + content: "Hi there! My name is Will.", + additional_kwargs: {}, + response_metadata: {}, + id: "8c1ca919-c553-4ebf-95d4-b59a2d61e078" + }, + 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 } + }, + HumanMessage { + content: "Remember my name?", + additional_kwargs: {}, + response_metadata: {}, + id: "daba7df6-ad75-4d6b-8057-745881cea1ca" + }, + 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 } + } + ] + }, + next: [], + config: { + configurable: { + thread_id: "1", + checkpoint_ns: "", + checkpoint_id: "1ef7d06e-93e0-6acc-8004-f2ac846575d2" + } + }, + metadata: { + source: "loop", + writes: { + chatbot: { + messages: [/* AIMessage */] + } + }, + step: 4, + parents: {} + }, + createdAt: "2024-09-27T19:30:10.820758+00:00", + parentConfig: { + configurable: { + thread_id: "1", + checkpoint_ns: "", + checkpoint_id: "1ef7d06e-859f-6206-8003-e1bd3c264b8f" + } + }, + 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) +``` +::: 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. @@ -157,13 +359,24 @@ Check out the code snippet below to review the graph from this tutorial: {!snippets/chat_model_tabs.md!} +:::python ```python hl_lines="36 37" from typing import Annotated @@ -203,7 +416,49 @@ graph_builder.set_entry_point("chatbot") memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory) ``` +::: + +:::js +```typescript hl_lines="34 35" +import { Annotation } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { BaseMessage } from "@langchain/core/messages"; +import { MemorySaver } from "@langchain/langgraph"; +import { StateGraph } from "@langchain/langgraph"; +import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; + +const StateAnnotation = Annotation.Root({ + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), +}); + +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 memory = new MemorySaver(); +const graph = graphBuilder.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. +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