diff --git a/docs/Makefile b/docs/Makefile index 95d256241..a0e564afe 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,8 +15,8 @@ build-prebuilt: fi uv run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md --language python -build-docs: build-prebuilt - uv run python -m mkdocs build --clean -f mkdocs.yml --strict +build-docs: build-typedoc build-prebuilt + TARGET_LANGUAGE=python uv run python -m mkdocs build --clean -f mkdocs.yml --strict llms-text: uv run python -m _scripts.generate_llms_text docs/llms-full.txt 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 cf1c5ba4a..10a132786 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 @@ -2,7 +2,15 @@ Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended. -LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). +LangGraph's [persistence](../../concepts/persistence.md) layer supports **human-in-the-loop** workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [`interrupt`](../../how-tos/human_in_the_loop/add-human-in-the-loop.md) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/low_level.md#command). + +:::python +`interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../how-tos/human_in_the_loop/add-human-in-the-loop.md). +::: + +:::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). +::: !!! note @@ -14,6 +22,7 @@ Starting with the existing code from the [Add memory to the chatbot](./3-add-mem Let's first select a chat model: +:::python {% include-markdown "../../../snippets/chat_model_tabs.md" %} +::: + +:::js + +```typescript +// Add your API key here +process.env.ANTHROPIC_API_KEY = "YOUR_API_KEY"; +``` + +::: + We can now incorporate it into our `StateGraph` with an additional tool: -``` python hl_lines="12 19 20 21 22 23" +:::python + +```python hl_lines="12 19 20 21 22 23" from typing import Annotated from langchain_tavily import TavilySearch @@ -76,6 +98,82 @@ graph_builder.add_edge("tools", "chatbot") graph_builder.add_edge(START, "chatbot") ``` +::: + +:::js + +```typescript hl_lines="12 19 20 21 22 23" +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { tool } from "@langchain/core/tools"; +import { z } from "zod"; + +import { MemorySaver } from "@langchain/langgraph"; +import { + StateGraph, + START, + END, + MessagesAnnotation, +} from "@langchain/langgraph"; +import { ToolNode } from "@langchain/langgraph/prebuilt"; +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 searchTool = new TavilySearchResults({ maxResults: 2 }); +const tools = [searchTool, humanAssistance]; + +const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); +const llmWithTools = model.bindTools(tools); + +const chatbot = async (state: typeof MessagesAnnotation.State) => { + 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] }; +}; + +const graphBuilder = new StateGraph(MessagesAnnotation).addNode( + "chatbot", + chatbot +); + +const toolNode = new ToolNode(tools); +graphBuilder.addNode("tools", toolNode); + +const shouldContinue = (state: typeof MessagesAnnotation.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"); +``` + +::: + !!! tip For more information and examples of human-in-the-loop workflows, see [Human-in-the-loop](../../concepts/human_in_the_loop.md). @@ -84,17 +182,33 @@ graph_builder.add_edge(START, "chatbot") We compile the graph with a checkpointer, as before: +:::python + ```python memory = InMemorySaver() graph = graph_builder.compile(checkpointer=memory) ``` +::: + +:::js + +```typescript +const memory = new MemorySaver(); + +const graph = graphBuilder.compile({ checkpointer: memory }); +``` + +::: + ## 3. Visualize the graph (optional) Visualizing the graph, you get the same layout as before – just with the added tool! -``` python +:::python + +```python from IPython.display import Image, display try: @@ -104,12 +218,30 @@ except Exception: pass ``` +::: + +:::js + +```typescript +import * as tslab from "tslab"; + +const drawableGraph = graph.getGraph(); +const image = await drawableGraph.drawMermaidPng(); +const arrayBuffer = await image.arrayBuffer(); + +await tslab.display.png(new Uint8Array(arrayBuffer)); +``` + +::: + ![chatbot-with-tools-diagram](chatbot-with-tools.png) ## 4. Prompt the chatbot Now, prompt the chatbot with a question that will engage the new `human_assistance` tool: +:::python + ```python user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?" config = {"configurable": {"thread_id": "1"}} @@ -138,8 +270,54 @@ Tool Calls: query: A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic? ``` +::: + +:::js + +```typescript +const userInput = + "I need some expert guidance for building an AI agent. Could you request assistance for me?"; +const config = { + configurable: { thread_id: "1" }, + streamMode: "values" as const, +}; + +const events = await graph.stream( + { messages: [{ role: "user", content: userInput }] }, + config +); + +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) { + console.log("Tool calls:", lastMessage.tool_calls); + } + } +} +``` + +``` +[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. +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?' + }, + id: 'toolu_01ABUqneqnuHNuo1vhfDFQCW' + } +] +``` + +::: + The chatbot generated a tool call, but then execution has been interrupted. If you inspect the graph state, you see that it stopped at the tools node: +:::python + ```python snapshot = graph.get_state(config) snapshot.next @@ -149,10 +327,26 @@ snapshot.next ('tools',) ``` +::: + +:::js + +```typescript +const snapshot = await graph.getState(config); +console.log(snapshot.next); +``` + +``` +['tools'] +``` + +::: + !!! info Additional information Take a closer look at the `human_assistance` tool: + :::python ```python @tool def human_assistance(query: str) -> str: @@ -162,12 +356,33 @@ snapshot.next ``` Similar to Python's built-in `input()` 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 Python kernel is running. + ::: + + :::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") + }) + }); + ``` + + 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. + ::: ## 5. Resume execution -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. For this example, use a dict with a key `"data"`: +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 +:::python +For this example, use a dict with a key `"data"`: + +```python human_response = ( "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." @@ -215,12 +430,54 @@ If you'd like more specific information about LangGraph or have any questions ab Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings... ``` +::: + +:::js +For this example, use an object with a key `"data"`: + +```typescript +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); + +for await (const event of resumeEvents) { + if ("messages" in event) { + const lastMessage = event.messages[event.messages.length - 1]; + console.log(`[${lastMessage._getType()}]: ${lastMessage.content}`); + } +} +``` + +``` +[tool]: 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. +[ai]: Thank you for your patience. I've received some expert advice regarding your request for guidance on building an AI agent. Here's what the experts have suggested: + +The experts recommend that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents. + +LangGraph is likely a framework or library designed specifically for creating AI agents with advanced capabilities. Here are a few points to consider based on this recommendation: + +1. Reliability: The experts emphasize that LangGraph is more reliable than simpler autonomous agent approaches. This could mean it has better stability, error handling, or consistent performance. + +2. Extensibility: LangGraph is described as more extensible, which suggests that it probably offers a flexible architecture that allows you to easily add new features or modify existing ones as your agent's requirements evolve. + +3. Advanced capabilities: Given that it's recommended over "simple autonomous agents," LangGraph likely provides more sophisticated tools and techniques for building complex AI agents. + +... +``` + +::: + The input has been received and processed as a tool message. Review this call's [LangSmith trace](https://smith.langchain.com/public/9f0f87e3-56a7-4dde-9c76-b71675624e91/r) to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that our chatbot can continue where it left off. **Congratulations!** You've used an `interrupt` to add human-in-the-loop execution to your chatbot, allowing for human oversight and intervention when needed. This opens up the potential UIs you can create with your AI systems. Since you have already added a **checkpointer**, as long as the underlying persistence layer is running, the graph can be paused **indefinitely** and resumed at any time as if nothing had happened. Check out the code snippet below to review the graph from this tutorial: +:::python {% include-markdown "../../../snippets/chat_model_tabs.md" %} ```python @@ -272,6 +529,81 @@ memory = InMemorySaver() 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 { + StateGraph, + START, + END, + MessagesAnnotation, +} from "@langchain/langgraph"; +import { ToolNode } from "@langchain/langgraph/prebuilt"; +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 searchTool = new TavilySearchResults({ maxResults: 2 }); +const tools = [searchTool, humanAssistance]; + +const model = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" }); +const llmWithTools = model.bindTools(tools); + +const chatbot = async (state: typeof MessagesAnnotation.State) => { + const message = await llmWithTools.invoke(state.messages); + if (message.tool_calls && message.tool_calls.length > 1) { + throw new Error("Multiple tool calls not supported with interrupts"); + } + return { messages: [message] }; +}; + +const graphBuilder = new StateGraph(MessagesAnnotation).addNode( + "chatbot", + chatbot +); + +const toolNode = new ToolNode(tools); +graphBuilder.addNode("tools", toolNode); + +const shouldContinue = (state: typeof MessagesAnnotation.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 memory = new MemorySaver(); +const graph = graphBuilder.compile({ checkpointer: memory }); +``` + +::: + ## Next steps -So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md). \ No newline at end of file +So far, the tutorial examples have relied on a simple state with one entry: a list of messages. You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can [add additional fields to the state](./5-customize-state.md). diff --git a/docs/docs/tutorials/get-started/5-customize-state.md b/docs/docs/tutorials/get-started/5-customize-state.md index 8be1a9246..5bfc6dd05 100644 --- a/docs/docs/tutorials/get-started/5-customize-state.md +++ b/docs/docs/tutorials/get-started/5-customize-state.md @@ -10,6 +10,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 @@ -25,11 +26,30 @@ 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), + }), + // highlight-next-line + name: Annotation, + // highlight-next-line + birthday: Annotation, +}); +``` +::: Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer. ## 2. Update the state inside the tool +:::python Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool. ``` python @@ -75,11 +95,76 @@ 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, + }); + + let verifiedName: string; + let verifiedBirthday: string; + let response: string; + + // 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)}`; + } + + // 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. ## 3. Prompt the chatbot +:::python Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields. ```python @@ -98,6 +183,39 @@ 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 = ( + "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" } }; + +const events = await graph.stream( + { messages: [{ role: "user", content: userInput }] }, + { ...config, 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) { + console.log("Tool Calls:"); + lastMessage.tool_calls.forEach((call: any) => { + console.log(` ${call.name} (${call.id})`); + console.log(` Args: ${JSON.stringify(call.args)}`); + }); + } + } +} +``` +::: ``` ================================ Human Message ================================= @@ -126,12 +244,19 @@ Tool Calls: birthday: 2023-01-01 ``` +:::python We've hit the `interrupt` in the `human_assistance` tool again. +::: + +:::js +We've hit the `interrupt` in the `humanAssistance` tool again. +::: ## 4. Add human assistance The chatbot failed to identify the correct date, so supply it with information: +:::python ```python human_command = Command( resume={ @@ -145,6 +270,37 @@ for event in events: if "messages" in event: event["messages"][-1].pretty_print() ``` +::: + +:::js +```typescript +import { Command } from "@langchain/langgraph"; + +const humanCommand = new Command({ + resume: { + name: "LangGraph", + birthday: "Jan 17, 2024", + }, +}); + +const resumeEvents = await graph.stream(humanCommand, { ...config, 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) { + console.log("Tool Calls:"); + lastMessage.tool_calls.forEach((call: any) => { + console.log(` ${call.name} (${call.id})`); + console.log(` Args: ${JSON.stringify(call.args)}`); + }); + } + } +} +``` +::: ``` ================================== Ai Message ================================== @@ -175,6 +331,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) @@ -184,11 +341,28 @@ 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)) +); +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). ## 5. Manually update the state +:::python LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`: ``` python @@ -200,9 +374,29 @@ 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)" }); +``` + +``` +{ + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1efd4ec5-cf69-6352-8006-9278f1730162' + } +} +``` +::: ## 6. View the new value +:::python If you call `graph.get_state`, you can see the new value is reflected: ``` python @@ -214,6 +408,24 @@ snapshot = graph.get_state(config) ``` {'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'} ``` +::: + +:::js +If you call `graph.getState`, you can see the new value is reflected: + +```typescript +const updatedSnapshot = await graph.getState(config); + +const updatedRelevantState = Object.fromEntries( + Object.entries(updatedSnapshot.values).filter(([k]) => ["name", "birthday"].includes(k)) +); +console.log(updatedRelevantState); +``` + +``` +{ 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. @@ -231,6 +443,7 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest") ``` --> +:::python ```python from typing import Annotated @@ -304,8 +517,109 @@ graph_builder.add_edge(START, "chatbot") memory = InMemorySaver() graph = graph_builder.compile(checkpointer=memory) ``` +::: + +:::js +```typescript +import { ChatAnthropic } from "@langchain/anthropic"; +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { tool } from "@langchain/core/tools"; +import { ToolMessage } from "@langchain/core/messages"; +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 StateAnnotation = Annotation.Root({ + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), + name: Annotation, + birthday: Annotation, +}); + +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, + }); + + let verifiedName: string; + let verifiedBirthday: string; + let response: string; + + 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)}`; + } + + const stateUpdate = { + name: verifiedName, + birthday: verifiedBirthday, + messages: [new ToolMessage({ + content: response, + tool_call_id: toolCallId!, + })], + }; + + 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 memory = new MemorySaver(); +const graph = graphBuilder.compile({ checkpointer: memory }); +``` +::: ## Next steps -There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md). - +There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md). \ No newline at end of file