diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index e42a35b1ec..f776f7f23b 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1,11 +1,7 @@ -const fs = require('fs'); -const path = require('path'); const { sleep } = require('@librechat/agents'); const { getToolkitKey } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); -const { zodToJsonSchema } = require('zod-to-json-schema'); -const { Calculator } = require('@langchain/community/tools/calculator'); -const { tool: toolFn, Tool, DynamicStructuredTool } = require('@langchain/core/tools'); +const { tool: toolFn, DynamicStructuredTool } = require('@langchain/core/tools'); const { Tools, Constants, @@ -26,153 +22,14 @@ const { loadActionSets, domainParser, } = require('./ActionService'); -const { - createOpenAIImageTools, - createYouTubeTools, - manifestToolMap, - toolkits, -} = require('~/app/clients/tools'); const { getEndpointsConfig, getCachedTools, getAppConfig } = require('~/server/services/Config'); const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); +const { manifestToolMap, toolkits } = require('~/app/clients/tools/manifest'); const { createOnSearchResults } = require('~/server/services/Tools/search'); const { isActionDomainAllowed } = require('~/server/services/domains'); const { recordUsage } = require('~/server/services/Threads'); const { loadTools } = require('~/app/clients/tools/util'); const { redactMessage } = require('~/config/parsers'); - -/** - * Loads and formats tools from the specified tool directory. - * - * The directory is scanned for JavaScript files, excluding any files in the filter set. - * For each file, it attempts to load the file as a module and instantiate a class, if it's a subclass of `StructuredTool`. - * Each tool instance is then formatted to be compatible with the OpenAI Assistant. - * Additionally, instances of LangChain Tools are included in the result. - * - * @param {object} params - The parameters for the function. - * @param {string} params.directory - The directory path where the tools are located. - * @param {string} [params.fileStrategy] - The file storage strategy. - * @param {string} [params.imageOutputType] - The image output type configuration. - * @param {Array} [params.adminFilter=[]] - Array of admin-defined tool keys to exclude from loading. - * @param {Array} [params.adminIncluded=[]] - Array of admin-defined tool keys to include from loading. - * @returns {Record} An object mapping each tool's plugin key to its instance. - */ -function loadAndFormatTools({ - directory, - fileStrategy, - imageOutputType, - adminFilter = [], - adminIncluded = [], -}) { - const filter = new Set([...adminFilter]); - const included = new Set(adminIncluded); - const tools = []; - /* Structured Tools Directory */ - const files = fs.readdirSync(directory); - - if (included.size > 0 && adminFilter.length > 0) { - logger.warn( - 'Both `includedTools` and `filteredTools` are defined; `filteredTools` will be ignored.', - ); - } - - for (const file of files) { - const filePath = path.join(directory, file); - if (!file.endsWith('.js') || (filter.has(file) && included.size === 0)) { - continue; - } - - let ToolClass = null; - try { - ToolClass = require(filePath); - } catch (error) { - logger.error(`[loadAndFormatTools] Error loading tool from ${filePath}:`, error); - continue; - } - - if (!ToolClass || !(ToolClass.prototype instanceof Tool)) { - continue; - } - - let toolInstance = null; - try { - toolInstance = new ToolClass({ override: true }); - } catch (error) { - logger.error( - `[loadAndFormatTools] Error initializing \`${file}\` tool; if it requires authentication, is the \`override\` field configured?`, - error, - ); - continue; - } - - if (!toolInstance) { - continue; - } - - if (filter.has(toolInstance.name) && included.size === 0) { - continue; - } - - if (included.size > 0 && !included.has(file) && !included.has(toolInstance.name)) { - continue; - } - - const formattedTool = formatToOpenAIAssistantTool(toolInstance); - tools.push(formattedTool); - } - - /** Basic Tools & Toolkits; schema: { input: string } */ - const openAIImageTools = createOpenAIImageTools({ - override: true, - imageOutputType, - fileStrategy, - }); - const basicToolInstances = [ - new Calculator(), - ...openAIImageTools, - ...createYouTubeTools({ override: true }), - ]; - for (const toolInstance of basicToolInstances) { - const formattedTool = formatToOpenAIAssistantTool(toolInstance); - let toolName = formattedTool[Tools.function].name; - toolName = getToolkitKey({ toolkits, toolName }) ?? toolName; - if (filter.has(toolName) && included.size === 0) { - continue; - } - - if (included.size > 0 && !included.has(toolName)) { - continue; - } - tools.push(formattedTool); - } - - tools.push(ImageVisionTool); - - return tools.reduce((map, tool) => { - map[tool.function.name] = tool; - return map; - }, {}); -} - -/** - * Formats a `StructuredTool` instance into a format that is compatible - * with OpenAI's ChatCompletionFunctions. It uses the `zodToJsonSchema` - * function to convert the schema of the `StructuredTool` into a JSON - * schema, which is then used as the parameters for the OpenAI function. - * - * @param {StructuredTool} tool - The StructuredTool to format. - * @returns {FunctionTool} The OpenAI Assistant Tool. - */ -function formatToOpenAIAssistantTool(tool) { - return { - type: Tools.function, - [Tools.function]: { - name: tool.name, - description: tool.description, - parameters: zodToJsonSchema(tool.schema), - }, - }; -} - /** * Processes the required actions by calling the appropriate tools and returning the outputs. * @param {OpenAIClient} client - OpenAI or StreamRunManager Client. @@ -737,7 +594,5 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) module.exports = { getToolkitKey, loadAgentTools, - loadAndFormatTools, processRequiredActions, - formatToOpenAIAssistantTool, }; diff --git a/api/server/services/start/tools.js b/api/server/services/start/tools.js new file mode 100644 index 0000000000..684430db59 --- /dev/null +++ b/api/server/services/start/tools.js @@ -0,0 +1,132 @@ +const fs = require('fs'); +const path = require('path'); +const { Tool } = require('@langchain/core/tools'); +const { logger } = require('@librechat/data-schemas'); +const { zodToJsonSchema } = require('zod-to-json-schema'); +const { Tools, ImageVisionTool } = require('librechat-data-provider'); +const { Calculator } = require('@langchain/community/tools/calculator'); +const { getToolkitKey, oaiToolkit, ytToolkit } = require('@librechat/api'); +const { toolkits } = require('~/app/clients/tools/manifest'); + +/** + * Loads and formats tools from the specified tool directory. + * + * The directory is scanned for JavaScript files, excluding any files in the filter set. + * For each file, it attempts to load the file as a module and instantiate a class, if it's a subclass of `StructuredTool`. + * Each tool instance is then formatted to be compatible with the OpenAI Assistant. + * Additionally, instances of LangChain Tools are included in the result. + * + * @param {object} params - The parameters for the function. + * @param {string} params.directory - The directory path where the tools are located. + * @param {Array} [params.adminFilter=[]] - Array of admin-defined tool keys to exclude from loading. + * @param {Array} [params.adminIncluded=[]] - Array of admin-defined tool keys to include from loading. + * @returns {Record} An object mapping each tool's plugin key to its instance. + */ +function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) { + const filter = new Set([...adminFilter]); + const included = new Set(adminIncluded); + const tools = []; + /* Structured Tools Directory */ + const files = fs.readdirSync(directory); + + if (included.size > 0 && adminFilter.length > 0) { + logger.warn( + 'Both `includedTools` and `filteredTools` are defined; `filteredTools` will be ignored.', + ); + } + + for (const file of files) { + const filePath = path.join(directory, file); + if (!file.endsWith('.js') || (filter.has(file) && included.size === 0)) { + continue; + } + + let ToolClass = null; + try { + ToolClass = require(filePath); + } catch (error) { + logger.error(`[loadAndFormatTools] Error loading tool from ${filePath}:`, error); + continue; + } + + if (!ToolClass || !(ToolClass.prototype instanceof Tool)) { + continue; + } + + let toolInstance = null; + try { + toolInstance = new ToolClass({ override: true }); + } catch (error) { + logger.error( + `[loadAndFormatTools] Error initializing \`${file}\` tool; if it requires authentication, is the \`override\` field configured?`, + error, + ); + continue; + } + + if (!toolInstance) { + continue; + } + + if (filter.has(toolInstance.name) && included.size === 0) { + continue; + } + + if (included.size > 0 && !included.has(file) && !included.has(toolInstance.name)) { + continue; + } + + const formattedTool = formatToOpenAIAssistantTool(toolInstance); + tools.push(formattedTool); + } + + const basicToolInstances = [ + new Calculator(), + ...Object.values(oaiToolkit), + ...Object.values(ytToolkit), + ]; + for (const toolInstance of basicToolInstances) { + const formattedTool = formatToOpenAIAssistantTool(toolInstance); + let toolName = formattedTool[Tools.function].name; + toolName = getToolkitKey({ toolkits, toolName }) ?? toolName; + if (filter.has(toolName) && included.size === 0) { + continue; + } + + if (included.size > 0 && !included.has(toolName)) { + continue; + } + tools.push(formattedTool); + } + + tools.push(ImageVisionTool); + + return tools.reduce((map, tool) => { + map[tool.function.name] = tool; + return map; + }, {}); +} + +/** + * Formats a `StructuredTool` instance into a format that is compatible + * with OpenAI's ChatCompletionFunctions. It uses the `zodToJsonSchema` + * function to convert the schema of the `StructuredTool` into a JSON + * schema, which is then used as the parameters for the OpenAI function. + * + * @param {StructuredTool} tool - The StructuredTool to format. + * @returns {FunctionTool} The OpenAI Assistant Tool. + */ +function formatToOpenAIAssistantTool(tool) { + return { + type: Tools.function, + [Tools.function]: { + name: tool.name, + description: tool.description, + parameters: zodToJsonSchema(tool.schema), + }, + }; +} + +module.exports = { + loadAndFormatTools, +};