LibreChat/api/server/services/start/tools.js
Danny Avila 1b79e0b785
🧬 chore: Align LibreChat With Agents LangChain Upgrade (#12922)
* 🔧 chore: Update dependencies in package-lock.json and package.json

- Bump version of @librechat/agents to 3.1.75-dev.0 in multiple package.json files.
- Upgrade various AWS SDK and Smithy dependencies to their latest versions in package-lock.json for improved stability and performance.

* 🔧 chore: Update AWS SDK and Smithy dependencies in package-lock.json

- Bump version of @aws-sdk/client-bedrock-runtime to 3.1041.0 and update related dependencies for improved performance and stability.
- Upgrade various AWS SDK and Smithy packages to their latest versions, ensuring compatibility and enhanced functionality.

* chore: Align LibreChat with agents LangChain upgrade

- Route LangChain imports through @librechat/agents facade exports
- Update @librechat/agents to 3.1.75-dev.1 and remove direct LangChain deps
- Normalize nullable agent model params and API key override typing
- Update Google thinking config typing for newer LangChain packages
- Refresh targeted audit-related dependency overrides

* chore: Add Jest types for API specs

* test: Fix LangChain upgrade CI specs

* test: Exercise agents env facade

* fix: Clean up TS preview diagnostics

* fix: Address Codex review feedback
2026-05-03 12:46:01 -04:00

143 lines
4.7 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { Calculator } = require('@librechat/agents');
const { logger } = require('@librechat/data-schemas');
const { zodToJsonSchema } = require('zod-to-json-schema');
const { Tool } = require('@librechat/agents/langchain/tools');
const { Tools, ImageVisionTool } = require('librechat-data-provider');
const { getToolkitKey, oaiToolkit, geminiToolkit } = 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<string>} [params.adminFilter=[]] - Array of admin-defined tool keys to exclude from loading.
* @param {Array<string>} [params.adminIncluded=[]] - Array of admin-defined tool keys to include from loading.
* @returns {Record<string, FunctionTool>} 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(geminiToolkit),
];
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;
}, {});
}
/**
* Checks if a schema is a Zod schema by looking for the _def property
* @param {unknown} schema - The schema to check
* @returns {boolean} True if it's a Zod schema
*/
function isZodSchema(schema) {
return schema && typeof schema === 'object' && '_def' in schema;
}
/**
* 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.
* If the schema is already a JSON schema, it is used directly.
*
* @param {StructuredTool} tool - The StructuredTool to format.
* @returns {FunctionTool} The OpenAI Assistant Tool.
*/
function formatToOpenAIAssistantTool(tool) {
const parameters = isZodSchema(tool.schema) ? zodToJsonSchema(tool.schema) : tool.schema;
return {
type: Tools.function,
[Tools.function]: {
name: tool.name,
description: tool.description,
parameters,
},
};
}
module.exports = {
loadAndFormatTools,
};