UNPKG

@langchain/langgraph

Version:
309 lines (308 loc) 11.7 kB
const require_constants = require("../constants.cjs"); const require_errors = require("../errors.cjs"); const require_utils = require("../utils.cjs"); let _langchain_core_messages = require("@langchain/core/messages"); //#region src/prebuilt/tool_node.ts const isBaseMessageArray = (input) => Array.isArray(input) && input.every(_langchain_core_messages.isBaseMessage); const isMessagesState = (input) => typeof input === "object" && input != null && "messages" in input && isBaseMessageArray(input.messages); const isSendInput = (input) => typeof input === "object" && input != null && "lg_tool_call" in input; /** * A node that runs the tools requested in the last AIMessage. It can be used * either in StateGraph with a "messages" key or in MessageGraph. If multiple * tool calls are requested, they will be run in parallel. The output will be * a list of ToolMessages, one for each tool call. * * @example * ```ts * import { ToolNode } from "@langchain/langgraph/prebuilt"; * import { tool } from "@langchain/core/tools"; * import { z } from "zod"; * import { AIMessage } from "@langchain/core/messages"; * * const getWeather = tool((input) => { * if (["sf", "san francisco"].includes(input.location.toLowerCase())) { * return "It's 60 degrees and foggy."; * } else { * return "It's 90 degrees and sunny."; * } * }, { * name: "get_weather", * description: "Call to get the current weather.", * schema: z.object({ * location: z.string().describe("Location to get the weather for."), * }), * }); * * const tools = [getWeather]; * const toolNode = new ToolNode(tools); * * const messageWithSingleToolCall = new AIMessage({ * content: "", * tool_calls: [ * { * name: "get_weather", * args: { location: "sf" }, * id: "tool_call_id", * type: "tool_call", * } * ] * }) * * await toolNode.invoke({ messages: [messageWithSingleToolCall] }); * // Returns tool invocation responses as: * // { messages: ToolMessage[] } * ``` * * @example * ```ts * import { * StateGraph, * MessagesAnnotation, * } from "@langchain/langgraph"; * import { ToolNode } from "@langchain/langgraph/prebuilt"; * import { tool } from "@langchain/core/tools"; * import { z } from "zod"; * import { ChatAnthropic } from "@langchain/anthropic"; * * const getWeather = tool((input) => { * if (["sf", "san francisco"].includes(input.location.toLowerCase())) { * return "It's 60 degrees and foggy."; * } else { * return "It's 90 degrees and sunny."; * } * }, { * name: "get_weather", * description: "Call to get the current weather.", * schema: z.object({ * location: z.string().describe("Location to get the weather for."), * }), * }); * * const tools = [getWeather]; * const modelWithTools = new ChatAnthropic({ * model: "claude-3-haiku-20240307", * temperature: 0 * }).bindTools(tools); * * const toolNodeForGraph = new ToolNode(tools) * * const shouldContinue = (state: typeof MessagesAnnotation.State) => { * const { messages } = state; * const lastMessage = messages[messages.length - 1]; * if ("tool_calls" in lastMessage && Array.isArray(lastMessage.tool_calls) && lastMessage.tool_calls?.length) { * return "tools"; * } * return "__end__"; * } * * const callModel = async (state: typeof MessagesAnnotation.State) => { * const { messages } = state; * const response = await modelWithTools.invoke(messages); * return { messages: response }; * } * * const graph = new StateGraph(MessagesAnnotation) * .addNode("agent", callModel) * .addNode("tools", toolNodeForGraph) * .addEdge("__start__", "agent") * .addConditionalEdges("agent", shouldContinue) * .addEdge("tools", "agent") * .compile(); * * const inputs = { * messages: [{ role: "user", content: "what is the weather in SF?" }], * }; * * const stream = await graph.stream(inputs, { * streamMode: "values", * }); * * for await (const { messages } of stream) { * console.log(messages); * } * // Returns the messages in the state at each step of execution * ``` * * ### Accessing graph state and runtime context from tools * * Tools executed by a `ToolNode` only receive the arguments produced by the * model. To give a tool access to the surrounding graph state or other runtime * context, read them from the {@link ToolRuntime} that is passed as the * second argument to every tool: * * - `runtime.state` — the input the `ToolNode` was invoked with. When the * `ToolNode` runs as a graph node (e.g. inside `createReactAgent`), this is * the current graph state. This works in any runtime, including web browsers, * because it does not rely on `node:async_hooks`/`AsyncLocalStorage`. * - `runtime.config`, `runtime.context`, `runtime.store`, etc. — other * run-scoped values. * * @example * ```ts * import { ToolNode } from "@langchain/langgraph/prebuilt"; * import { StateGraph, MessagesZodState } from "@langchain/langgraph"; * import { tool, type ToolRuntime } from "@langchain/core/tools"; * import { z } from "zod"; * * // Define the graph state with a Zod schema. The extra `userId` key becomes * // part of the state that the ToolNode forwards to its tools via `runtime.state`. * const AgentState = z.object({ * ...MessagesZodState.shape, * userId: z.string(), * }); * * const getUserInfo = tool( * async (_input, runtime: ToolRuntime<typeof AgentState>) => { * // Read the current graph state directly from the second argument. * const userId = runtime.state.userId; * return userId === "user_123" ? "User is John Smith" : "Unknown user"; * }, * { * name: "get_user_info", * description: "Look up user info.", * schema: z.object({}), * } * ); * * // Wire the ToolNode into a StateGraph that uses `AgentState`. Because the * // node runs with the graph state as its input, the tool can read `userId`. * const graph = new StateGraph(AgentState) * .addNode("tools", new ToolNode([getUserInfo])) * .addEdge("__start__", "tools") * .compile(); * * await graph.invoke({ messages: [...], userId: "user_123" }); * ``` */ var ToolNode = class extends require_utils.RunnableCallable { tools; handleToolErrors = true; trace = false; constructor(tools, options) { const { name, tags, handleToolErrors } = options ?? {}; super({ name, tags, func: (input, config) => this.run(input, config) }); this.tools = tools; this.handleToolErrors = handleToolErrors ?? this.handleToolErrors; } async runTool(call, config, state) { const tool = this.tools.find((tool) => tool.name === call.name); try { if (tool === void 0) throw new Error(`Tool "${call.name}" not found.`); const toolCall = { ...call, type: "tool_call" }; const runtime = { ...config, state, toolCallId: call.id ?? "", config, context: config.context, store: config.store ?? null, writer: config.writer ?? config.configurable?.writer ?? null }; const output = await tool.invoke(toolCall, runtime); if ((0, _langchain_core_messages.isBaseMessage)(output) && output.getType() === "tool" || require_constants.isCommand(output)) return output; return new _langchain_core_messages.ToolMessage({ status: "success", name: tool.name, content: typeof output === "string" ? output : JSON.stringify(output), tool_call_id: call.id }); } catch (e) { if (!this.handleToolErrors) throw e; if (require_errors.isGraphInterrupt(e)) throw e; return new _langchain_core_messages.ToolMessage({ status: "error", content: `Error: ${e.message}\n Please fix your mistakes.`, name: call.name, tool_call_id: call.id ?? "" }); } } async run(input, config) { let outputs; if (isSendInput(input)) { const { lg_tool_call: toolCall, ...state } = input; outputs = [await this.runTool(toolCall, config, state)]; } else { let messages; if (isBaseMessageArray(input)) messages = input; else if (isMessagesState(input)) messages = input.messages; else throw new Error("ToolNode only accepts BaseMessage[] or { messages: BaseMessage[] } as input."); const toolMessageIds = new Set(messages.filter((msg) => msg.getType() === "tool").map((msg) => msg.tool_call_id)); let aiMessage; for (let i = messages.length - 1; i >= 0; i -= 1) { const message = messages[i]; if ((0, _langchain_core_messages.isAIMessage)(message)) { aiMessage = message; break; } } if (aiMessage == null || !(0, _langchain_core_messages.isAIMessage)(aiMessage)) throw new Error("ToolNode only accepts AIMessages as input."); outputs = await Promise.all(aiMessage.tool_calls?.filter((call) => call.id == null || !toolMessageIds.has(call.id)).map((call) => this.runTool(call, config, input)) ?? []); } if (!outputs.some(require_constants.isCommand)) return Array.isArray(input) ? outputs : { messages: outputs }; const combinedOutputs = []; let parentCommand = null; for (const output of outputs) if (require_constants.isCommand(output)) if (output.graph === require_constants.Command.PARENT && Array.isArray(output.goto) && output.goto.every((send) => require_constants._isSend(send))) if (parentCommand) parentCommand.goto.push(...output.goto); else parentCommand = new require_constants.Command({ graph: require_constants.Command.PARENT, goto: output.goto }); else combinedOutputs.push(output); else combinedOutputs.push(Array.isArray(input) ? [output] : { messages: [output] }); if (parentCommand) combinedOutputs.push(parentCommand); return combinedOutputs; } }; /** * A conditional edge function that determines whether to route to a tools node or end the graph. * * This function is designed to be used as a conditional edge in a LangGraph state graph to implement * the common pattern of checking if an AI message contains tool calls that need to be executed. * * @param state - The current state of the graph, which can be either: * - An array of `BaseMessage` objects, where the last message is checked for tool calls * - A state object conforming to `MessagesAnnotation.State`, which contains a `messages` array * * @returns A string indicating the next node to route to: * - `"tools"` - If the last message contains tool calls that need to be executed * - `END` - If there are no tool calls, indicating the graph should terminate * * @example * ```typescript * import { StateGraph, MessagesAnnotation, END, START } from "@langchain/langgraph"; * import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt"; * * const graph = new StateGraph(MessagesAnnotation) * .addNode("agent", agentNode) * .addNode("tools", new ToolNode([searchTool, calculatorTool])) * .addEdge(START, "agent") * .addConditionalEdges("agent", toolsCondition, ["tools", END]) * .addEdge("tools", "agent") * .compile(); * ``` * * @remarks * The function checks the last message in the state for the presence of `tool_calls`. * If the message is an `AIMessage` with one or more tool calls, it returns `"tools"`, * indicating that the graph should route to a tools node (typically a `ToolNode`) to * execute those tool calls. Otherwise, it returns `END` to terminate the graph execution. * * This is a common pattern in agentic workflows where an AI model decides whether to * use tools or provide a final response. */ function toolsCondition(state) { const message = Array.isArray(state) ? state[state.length - 1] : state.messages[state.messages.length - 1]; if (message !== void 0 && "tool_calls" in message && (message.tool_calls?.length ?? 0) > 0) return "tools"; else return require_constants.END; } //#endregion exports.ToolNode = ToolNode; exports.toolsCondition = toolsCondition; //# sourceMappingURL=tool_node.cjs.map