@langgraph-js/pro
Version:
The Pro SDK for LangGraph - seamlessly integrate your AI agents with frontend interfaces and build complex AI workflows
114 lines (98 loc) • 4.57 kB
text/typescript
import { z } from "zod";
import { ToolMessage } from "@langchain/core/messages";
import { DynamicTool, StructuredToolInterface, tool } from "@langchain/core/tools";
import { RunnableToolLike } from "@langchain/core/runnables";
import { AnnotationRoot, MessagesAnnotation, Command, CompiledStateGraph, getCurrentTaskInput } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
const WHITESPACE_RE = /\s+/g;
const METADATA_KEY_HANDOFF_DESTINATION = "__handoff_destination";
function _normalizeAgentName(agentName: string): string {
/**
* Normalize an agent name to be used inside the tool name.
*/
return agentName.trim().replace(WHITESPACE_RE, "_").toLowerCase();
}
interface CreateHandoffToolParams {
agentName: string;
description?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateState?: (state: any) => Record<string, any>;
}
// type guard
function isDynamicTool(tool: StructuredToolInterface | DynamicTool | RunnableToolLike): tool is DynamicTool {
return "schema" in tool && "name" in tool && "description" in tool && "responseFormat" in tool;
}
const createHandoffTool = ({ agentName, description, updateState }: CreateHandoffToolParams) => {
/**
* Create a tool that can handoff control to the requested agent.
*
* @param agentName - The name of the agent to handoff control to, i.e.
* the name of the agent node in the multi-agent graph.
* Agent names should be simple, clear and unique, preferably in snake_case,
* although you are only limited to the names accepted by LangGraph
* nodes as well as the tool names accepted by LLM providers
* (the tool name will look like this: `transfer_to_<agent_name>`).
* @param description - Optional description for the handoff tool.
* @param updateState - Optional function to customize state updates during handoff.
*/
const toolName = `transfer_to_${_normalizeAgentName(agentName)}`;
const toolDescription = description || `Ask agent '${agentName}' for help`;
const handoffTool = tool(
async (_, config) => {
/**
* Ask another agent for help.
*/
const toolMessage = new ToolMessage({
content: `Successfully transferred to ${agentName}`,
name: toolName,
tool_call_id: config.toolCall.id,
});
// inject the current agent state
const state = getCurrentTaskInput() as (typeof MessagesAnnotation)["State"];
// Base update object containing essential state updates
const baseUpdate = {
messages: state.messages.concat(toolMessage),
activeAgent: agentName,
};
// Merge custom updates with base updates if updateState function is provided
const finalUpdate = updateState ? { ...baseUpdate, ...updateState(state) } : baseUpdate;
return new Command({
goto: agentName,
graph: Command.PARENT,
update: finalUpdate,
});
},
{
name: toolName,
schema: z.object({}),
description: toolDescription,
}
);
handoffTool.metadata = { [METADATA_KEY_HANDOFF_DESTINATION]: agentName };
return handoffTool;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getHandoffDestinations = <AnnotationRootT extends AnnotationRoot<any>>(
agent: CompiledStateGraph<AnnotationRootT["State"], AnnotationRootT["Update"], string, AnnotationRootT["spec"], AnnotationRootT["spec"]>,
toolNodeName: string = "tools"
): string[] => {
/**
* Get a list of destinations from agent's handoff tools.
*
* @param agent - The compiled state graph
* @param toolNodeName - The name of the tool node in the graph
*/
const { nodes } = agent.getGraph();
if (!(toolNodeName in nodes)) {
return [];
}
const toolNode = nodes[toolNodeName].data;
if (!toolNode || !("tools" in toolNode) || !toolNode.tools) {
return [];
}
const { tools } = toolNode as ToolNode;
return tools
.filter((tool): tool is DynamicTool => isDynamicTool(tool) && tool.metadata !== undefined && METADATA_KEY_HANDOFF_DESTINATION in tool.metadata)
.map((tool) => tool.metadata![METADATA_KEY_HANDOFF_DESTINATION] as string);
};
export { createHandoffTool, getHandoffDestinations, METADATA_KEY_HANDOFF_DESTINATION };