UNPKG

jorel

Version:

A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.

357 lines (356 loc) 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LlmToolKit = void 0; const shared_1 = require("../shared"); const llm_tool_1 = require("./llm-tool"); const utilities_1 = require("./utilities"); /** * A toolkit for managing one or more LLM tools. */ class LlmToolKit { /** * Allow access from the instance as well */ get utilities() { return LlmToolKit.utilities; } constructor(tools, config = {}) { this.tools = tools.map((tool) => (tool instanceof llm_tool_1.LlmTool ? tool : new llm_tool_1.LlmTool(tool))); this.allowParallelCalls = config.allowParallelCalls ?? true; } /** * Whether the toolkit has any tools */ get hasTools() { return this.tools.length > 0; } /** * Get all tools as LlmFunction objects */ get asLlmFunctions() { if (this.tools.length === 0) return undefined; return this.tools.map((tool) => tool.asLLmFunction); } /** * Deserialize strings * @param input */ static deserialize(input, strict = false) { const raw = (input ?? "").trim(); if (strict || raw.startsWith("{") || raw.startsWith("[")) { return JSON.parse(raw, shared_1.dateReviver); } const match = raw.match(/```(?:json|json5|jsonc|application\/json)?\s*([\s\S]*?)\s*```/i); if (match) { const inner = (match[1] || "").trim(); if (inner.startsWith("{") || inner.startsWith("[")) { return JSON.parse(inner, shared_1.dateReviver); } } return JSON.parse(raw, shared_1.dateReviver); } /** * Serialize objects * @param input */ static serialize(input) { return JSON.stringify(input); } /** * Create a new toolkit with only selected (allowed) tools * @param allowedToolIds */ withAllowedToolsOnly(allowedToolIds) { return new LlmToolKit(this.tools.filter((tool) => allowedToolIds.includes(tool.name)), { allowParallelCalls: this.allowParallelCalls, }); } /** * Get the next tool call that requires processing * @param input */ getNextToolCall(input) { const toolCall = input.find((call) => call.executionState === "pending" || call.executionState === "inProgress") || null; if (!toolCall) return null; const tool = this.getTool(toolCall.request.function.name); if (!tool) { throw new Error(`Tool not found: ${toolCall.request.function.name}`); } return { toolCall, tool }; } /** * Register one or more tools * @param tools */ registerTools(tools) { for (const tool of tools) { if (this.tools.find((t) => t.name === tool.name)) { throw new Error(`A tool with name ${tool.name} already exists`); } } this.tools.push(...tools.map((tool) => (tool instanceof llm_tool_1.LlmTool ? tool : new llm_tool_1.LlmTool(tool)))); } /** * Register a new tool * @param tool Tool or tool configuration */ registerTool(tool) { if (this.tools.find((t) => t.name === tool.name)) { throw new Error(`A tool with name ${tool.name} already exists`); } this.tools.push(tool instanceof llm_tool_1.LlmTool ? tool : new llm_tool_1.LlmTool(tool)); } /** * Unregister a tool * @param id Tool name */ unregisterTool(id) { const index = this.tools.findIndex((tool) => tool.name === id); if (index === -1) throw new Error(`Tool not found: ${id}`); if (this.tools[index].type === "transfer" || this.tools[index].type === "subTask") { throw new Error(`Cannot unregister tool "${id}". ${this.tools[index].type} tools cannot be unregistered.`); } this.tools.splice(index, 1); } /** * Get a tool by name * @param id Tool name */ getTool(id) { return this.tools.find((tool) => tool.name === id) ?? null; } /** * Classify what type of tool calls are present. This implementation goes beyond the basic classification inside * the toolkit utilities to provide more detailed information, such as pending transfers and missing executors * which require access to the tool instances * @param toolCalls */ classifyToolCalls(toolCalls) { if (toolCalls.some((call) => call.approvalState === "requiresApproval")) { return "approvalPending"; } if (toolCalls.some((call) => { const tool = this.getTool(call.request.function.name); if (!tool) { throw new Error(`Tool not found: ${call.request.function.name}`); } return (tool.type === "functionDefinition" && (call.executionState === "pending" || call.executionState === "inProgress")); })) { return "missingExecutor"; } if (toolCalls.some((call) => { const tool = this.getTool(call.request.function.name); if (!tool) { throw new Error(`Tool not found: ${call.request.function.name}`); } return ((tool.type === "transfer" || tool.type === "subTask") && (call.executionState === "pending" || call.executionState === "inProgress")); })) { return "transferPending"; } if (toolCalls.some((call) => call.executionState === "pending" || call.executionState === "inProgress")) { return "executionPending"; } return "completed"; } /** * Process a single tool call and return the updated tool call * @param toolCall * @param config * @returns Object containing the updated tool call and a boolean indicating whether the tool call was handled * If the tool call was not handled, it requires additional processing (e.g. approval or delegation) */ async processToolCall(toolCall, config) { const { id, request, approvalState, executionState } = toolCall; if (approvalState === "requiresApproval") { return { toolCall, handled: false }; } if (executionState === "completed") { return { toolCall, handled: true }; } else if (executionState === "error" && !config?.retryFailed) { return { toolCall, handled: true }; } else if (executionState === "inProgress") { return { toolCall, handled: false }; } if (approvalState === "rejected") { return { toolCall: { id, request, approvalState: "rejected", executionState: "completed", result: { error: "Tool call was rejected by user", }, error: null, }, handled: true, }; } const tool = this.tools.find((tool) => tool.name === request.function.name); if (!tool) { return { toolCall: { id, request, approvalState, executionState: "error", result: null, error: { message: `Tool not found: ${request.function.name}`, type: "ToolNotFoundError", numberOfAttempts: toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, lastAttempt: new Date(), }, }, handled: true, }; } if (tool.type !== "function") { return { toolCall, handled: false }; } try { const result = await tool.execute(request.function.arguments, { context: config?.context, secureContext: config?.secureContext, }); return { toolCall: { id, request, approvalState, executionState: "completed", result, error: null, }, handled: true, }; } catch (_error) { const error = _error instanceof Error ? _error : new Error("Unknown error"); return { toolCall: { id, request, approvalState, executionState: "error", result: null, error: { message: _error instanceof Error ? error.message : `Unable to execute tool: ${request.function.name}`, type: _error instanceof Error ? error.name : "ToolExecutionError", numberOfAttempts: toolCall.error ? toolCall.error.numberOfAttempts + 1 : 1, lastAttempt: new Date(), }, }, handled: true, }; } } /** * Process tool calls * * This method will execute the tools and return the results. * All tool calls must be approved or rejected before processing. * * @param input Object containing tool calls (e.g. llm "assistant_with_tools" message) * @param config * @returns Object containing tool calls with results * @throws Error if a tool is not found or if any tool calls still require approval */ async processCalls(input, config) { let errors = 0; let calls = 0; if (!input.toolCalls) return input; const classification = this.classifyToolCalls(input.toolCalls); if (classification === "transferPending") { throw new Error("Transfer tools cannot be processed by this method"); } const toolCalls = []; for (const call of input.toolCalls) { if (call.executionState === "completed" || (call.executionState === "error" && !config?.retryFailed)) { toolCalls.push(call); } else if (call.executionState === "cancelled") { toolCalls.push(call); } else if (errors >= (config?.maxErrors ?? 5)) { const message = "Too many tool call errors"; toolCalls.push({ ...call, executionState: "cancelled", result: null, error: { message, type: "ToolExecutionError", numberOfAttempts: call.error ? call.error.numberOfAttempts + 1 : 1, lastAttempt: new Date(), }, }); } else if (calls >= (config?.maxCalls ?? 8)) { const message = "Too many tool calls"; toolCalls.push({ ...call, executionState: "cancelled", result: null, error: { message, type: "ToolExecutionError", numberOfAttempts: call.error ? call.error.numberOfAttempts + 1 : 1, lastAttempt: new Date(), }, }); } else if (classification === "missingExecutor") { const message = "Unable to execute tool"; toolCalls.push({ ...call, executionState: "cancelled", result: null, error: { message, type: "ToolExecutionError", numberOfAttempts: call.error ? call.error.numberOfAttempts + 1 : 1, lastAttempt: new Date(), }, }); } else { // Check for abort signal before processing each tool call if (config?.abortSignal?.aborted) { toolCalls.push({ ...call, executionState: "cancelled", result: null, error: { message: "Request was aborted", type: "ToolExecutionError", numberOfAttempts: call.error ? call.error.numberOfAttempts + 1 : 1, lastAttempt: new Date(), }, }); continue; } const { toolCall } = await this.processToolCall(call, config); toolCalls.push(toolCall); if (toolCall.error) { errors++; } calls++; } } return { ...input, toolCalls, }; } } exports.LlmToolKit = LlmToolKit; LlmToolKit.utilities = new utilities_1.LlmToolKitUtilities();