langchain
Version:
Typescript bindings for langchain
1 lines • 15.2 kB
Source Map (JSON)
{"version":3,"file":"llmToolSelector.cjs","names":["tools: StructuredToolInterface[]","z","BaseLanguageModel","options: LLMToolSelectorConfig","createMiddleware","request: ModelRequest<TState, TContext>","runtime: Runtime<LLMToolSelectorConfig>","lastUserMessage: HumanMessage | undefined","HumanMessage","initChatModel","response: { tools: string[] }","availableTools: StructuredToolInterface[]","validToolNames: string[]","selectedToolNames: string[]","invalidToolSelections: string[]"],"sources":["../../../src/agents/middleware/llmToolSelector.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\nimport { HumanMessage } from \"@langchain/core/messages\";\nimport type { StructuredToolInterface } from \"@langchain/core/tools\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\nimport type { ModelRequest } from \"../nodes/types.js\";\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"Your goal is to select the most relevant tools for answering the user's query.\";\n\n/**\n * Prepared inputs for tool selection.\n */\ninterface SelectionRequest {\n availableTools: StructuredToolInterface[];\n systemMessage: string;\n lastUserMessage: HumanMessage;\n model: BaseLanguageModel;\n validToolNames: string[];\n}\n\n/**\n * Create a structured output schema for tool selection.\n *\n * @param tools - Available tools to include in the schema.\n * @returns Zod schema where each tool name is a literal with its description.\n */\nfunction createToolSelectionResponse(tools: StructuredToolInterface[]) {\n if (!tools || tools.length === 0) {\n throw new Error(\"Invalid usage: tools must be non-empty\");\n }\n\n // Create a union of literals for each tool name\n const toolLiterals = tools.map((tool) => z.literal(tool.name));\n const toolEnum = z.union(\n toolLiterals as [\n z.ZodLiteral<string>,\n z.ZodLiteral<string>,\n ...z.ZodLiteral<string>[],\n ]\n );\n\n return z.object({\n tools: z\n .array(toolEnum)\n .describe(\"Tools to use. Place the most relevant tools first.\"),\n });\n}\n\n/**\n * Options for configuring the LLM Tool Selector middleware.\n */\nexport const LLMToolSelectorOptionsSchema = z.object({\n /**\n * The language model to use for tool selection (default: the provided model from the agent options).\n */\n model: z.string().or(z.instanceof(BaseLanguageModel)).optional(),\n /**\n * System prompt for the tool selection model.\n */\n systemPrompt: z.string().optional(),\n /**\n * Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n */\n maxTools: z.number().optional(),\n /**\n * Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n */\n alwaysInclude: z.array(z.string()).optional(),\n});\nexport type LLMToolSelectorConfig = InferInteropZodInput<\n typeof LLMToolSelectorOptionsSchema\n>;\n\n/**\n * Middleware for selecting tools using an LLM-based strategy.\n *\n * When an agent has many tools available, this middleware filters them down\n * to only the most relevant ones for the user's query. This reduces token usage\n * and helps the main model focus on the right tools.\n *\n * @param options - Configuration options for the middleware\n * @param options.model - The language model to use for tool selection (default: the provided model from the agent options).\n * @param options.systemPrompt - Instructions for the selection model.\n * @param options.maxTools - Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n * @param options.alwaysInclude - Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n *\n * @example\n * Limit to 3 tools:\n * ```ts\n * import { llmToolSelectorMiddleware } from \"langchain/agents/middleware\";\n *\n * const middleware = llmToolSelectorMiddleware({ maxTools: 3 });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [tool1, tool2, tool3, tool4, tool5],\n * middleware: [middleware],\n * });\n * ```\n *\n * @example\n * Use a smaller model for selection:\n * ```ts\n * const middleware = llmToolSelectorMiddleware({\n * model: \"openai:gpt-4o-mini\",\n * maxTools: 2\n * });\n * ```\n */\nexport function llmToolSelectorMiddleware(\n options: LLMToolSelectorConfig\n): ReturnType<typeof createMiddleware> {\n return createMiddleware({\n name: \"LLMToolSelector\",\n contextSchema: LLMToolSelectorOptionsSchema,\n async wrapModelCall(request, handler) {\n const selectionRequest = await prepareSelectionRequest(\n request,\n options,\n request.runtime\n );\n if (!selectionRequest) {\n return handler(request);\n }\n\n // Create dynamic response model with union of literal tool names\n const toolSelectionSchema = createToolSelectionResponse(\n selectionRequest.availableTools\n );\n const structuredModel =\n await selectionRequest.model.withStructuredOutput?.(\n toolSelectionSchema\n );\n\n const response = await structuredModel?.invoke([\n { role: \"system\", content: selectionRequest.systemMessage },\n selectionRequest.lastUserMessage,\n ]);\n\n // Response should be an object with a tools array\n if (!response || typeof response !== \"object\" || !(\"tools\" in response)) {\n throw new Error(\n `Expected object response with tools array, got ${typeof response}`\n );\n }\n\n return handler(\n processSelectionResponse(\n response as { tools: string[] },\n selectionRequest.availableTools,\n selectionRequest.validToolNames,\n request,\n options\n )\n );\n },\n });\n}\n\n/**\n * Prepare inputs for tool selection.\n *\n * @param request - The model request to process.\n * @param options - Configuration options.\n * @param runtime - Runtime context.\n * @returns SelectionRequest with prepared inputs, or null if no selection is needed.\n */\nasync function prepareSelectionRequest<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig,\n runtime: Runtime<LLMToolSelectorConfig>\n): Promise<SelectionRequest | undefined> {\n const model = runtime.context.model ?? options.model;\n const maxTools = runtime.context.maxTools ?? options.maxTools;\n const alwaysInclude =\n runtime.context.alwaysInclude ?? options.alwaysInclude ?? [];\n const systemPrompt =\n runtime.context.systemPrompt ??\n options.systemPrompt ??\n DEFAULT_SYSTEM_PROMPT;\n\n /**\n * If no tools available, return null\n */\n if (!request.tools || request.tools.length === 0) {\n return undefined;\n }\n\n /**\n * Filter to only StructuredToolInterface instances (exclude provider-specific tool dicts)\n */\n const baseTools = request.tools.filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n );\n\n /**\n * Validate that alwaysInclude tools exist\n */\n if (alwaysInclude.length > 0) {\n const availableToolNames = new Set(baseTools.map((tool) => tool.name));\n const missingTools = alwaysInclude.filter(\n (name) => !availableToolNames.has(name)\n );\n if (missingTools.length > 0) {\n throw new Error(\n `Tools in alwaysInclude not found in request: ${missingTools.join(\n \", \"\n )}. ` +\n `Available tools: ${Array.from(availableToolNames).sort().join(\", \")}`\n );\n }\n }\n\n /**\n * Separate tools that are always included from those available for selection\n */\n const availableTools = baseTools.filter(\n (tool) => !alwaysInclude.includes(tool.name)\n );\n\n /**\n * If no tools available for selection, return null\n */\n if (availableTools.length === 0) {\n return undefined;\n }\n\n let systemMessage = systemPrompt;\n /**\n * If there's a maxTools limit, append instructions to the system prompt\n */\n if (maxTools !== undefined) {\n systemMessage +=\n `\\nIMPORTANT: List the tool names in order of relevance, ` +\n `with the most relevant first. ` +\n `If you exceed the maximum number of tools, ` +\n `only the first ${maxTools} will be used.`;\n }\n\n /**\n * Get the last user message from the conversation history\n */\n let lastUserMessage: HumanMessage | undefined;\n for (const message of request.messages) {\n if (HumanMessage.isInstance(message)) {\n lastUserMessage = message;\n }\n }\n\n if (!lastUserMessage) {\n throw new Error(\"No user message found in request messages\");\n }\n\n const modelInstance = !model\n ? (request.model as BaseLanguageModel)\n : typeof model === \"string\"\n ? await initChatModel(model)\n : model;\n\n const validToolNames = availableTools.map((tool) => tool.name);\n\n return {\n availableTools,\n systemMessage,\n lastUserMessage,\n model: modelInstance,\n validToolNames,\n };\n}\n\n/**\n * Process the selection response and return filtered ModelRequest.\n *\n * @param response - The structured output response from the model.\n * @param availableTools - Tools available for selection.\n * @param validToolNames - Valid tool names that can be selected.\n * @param request - Original model request.\n * @param options - Configuration options.\n * @returns Modified ModelRequest with filtered tools.\n */\nfunction processSelectionResponse<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n response: { tools: string[] },\n availableTools: StructuredToolInterface[],\n validToolNames: string[],\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig\n): ModelRequest<TState, TContext> {\n const maxTools = options.maxTools;\n const alwaysInclude = options.alwaysInclude ?? [];\n\n const selectedToolNames: string[] = [];\n const invalidToolSelections: string[] = [];\n\n for (const toolName of response.tools) {\n if (!validToolNames.includes(toolName)) {\n invalidToolSelections.push(toolName);\n continue;\n }\n\n /**\n * Only add if not already selected and within maxTools limit\n */\n if (\n !selectedToolNames.includes(toolName) &&\n (maxTools === undefined || selectedToolNames.length < maxTools)\n ) {\n selectedToolNames.push(toolName);\n }\n }\n\n if (invalidToolSelections.length > 0) {\n throw new Error(\n `Model selected invalid tools: ${invalidToolSelections.join(\", \")}`\n );\n }\n\n /**\n * Filter tools based on selection\n */\n const selectedTools = availableTools.filter((tool) =>\n selectedToolNames.includes(tool.name)\n );\n\n /**\n * Append always-included tools\n */\n const alwaysIncludedTools = (request.tools ?? []).filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n typeof tool.name === \"string\" &&\n alwaysInclude.includes(tool.name)\n );\n selectedTools.push(...alwaysIncludedTools);\n\n /**\n * Also preserve any provider-specific tool dicts from the original request\n */\n const providerTools = (request.tools ?? []).filter(\n (tool) =>\n !(\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n )\n );\n\n return {\n ...request,\n tools: [...selectedTools, ...providerTools],\n };\n}\n"],"mappings":"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"}