@langchain/langgraph
Version:
309 lines (308 loc) • 11.7 kB
JavaScript
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