| const fs = require('fs'); |
| const path = require('path'); |
| const { StructuredTool } = require('langchain/tools'); |
| const { zodToJsonSchema } = require('zod-to-json-schema'); |
| const { Calculator } = require('langchain/tools/calculator'); |
| const { |
| Tools, |
| ContentTypes, |
| imageGenTools, |
| actionDelimiter, |
| ImageVisionTool, |
| openapiToFunction, |
| validateAndParseOpenAPISpec, |
| } = require('librechat-data-provider'); |
| const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); |
| const { loadActionSets, createActionTool, domainParser } = require('./ActionService'); |
| const { recordUsage } = require('~/server/services/Threads'); |
| const { loadTools } = require('~/app/clients/tools/util'); |
| const { redactMessage } = require('~/config/parsers'); |
| const { sleep } = require('~/server/utils'); |
| const { logger } = require('~/config'); |
|
|
| const filteredTools = new Set([ |
| 'ChatTool.js', |
| 'CodeSherpa.js', |
| 'CodeSherpaTools.js', |
| 'E2BTools.js', |
| 'extractionChain.js', |
| ]); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) { |
| const filter = new Set([...adminFilter, ...filteredTools]); |
| const included = new Set(adminIncluded); |
| const tools = []; |
| |
| 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 StructuredTool)) { |
| continue; |
| } |
|
|
| if (included.size > 0 && !included.has(file)) { |
| 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; |
| } |
|
|
| const formattedTool = formatToOpenAIAssistantTool(toolInstance); |
| tools.push(formattedTool); |
| } |
|
|
| |
| const basicToolInstances = [new Calculator()]; |
| for (const toolInstance of basicToolInstances) { |
| const formattedTool = formatToOpenAIAssistantTool(toolInstance); |
| tools.push(formattedTool); |
| } |
|
|
| tools.push(ImageVisionTool); |
|
|
| return tools.reduce((map, tool) => { |
| map[tool.function.name] = tool; |
| return map; |
| }, {}); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function formatToOpenAIAssistantTool(tool) { |
| return { |
| type: Tools.function, |
| [Tools.function]: { |
| name: tool.name, |
| description: tool.description, |
| parameters: zodToJsonSchema(tool.schema), |
| }, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| const processVisionRequest = async (client, currentAction) => { |
| if (!client.visionPromise) { |
| return { |
| tool_call_id: currentAction.toolCallId, |
| output: 'No image details found.', |
| }; |
| } |
|
|
| |
| const completion = await client.visionPromise; |
| if (completion.usage) { |
| recordUsage({ |
| user: client.req.user.id, |
| model: client.req.body.model, |
| conversationId: (client.responseMessage ?? client.finalMessage).conversationId, |
| ...completion.usage, |
| }); |
| } |
| const output = completion?.choices?.[0]?.message?.content ?? 'No image details found.'; |
| return { |
| tool_call_id: currentAction.toolCallId, |
| output, |
| }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| async function processRequiredActions(client, requiredActions) { |
| logger.debug( |
| `[required actions] user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`, |
| requiredActions, |
| ); |
| const tools = requiredActions.map((action) => action.tool); |
| const loadedTools = await loadTools({ |
| user: client.req.user.id, |
| model: client.req.body.model ?? 'gpt-3.5-turbo-1106', |
| tools, |
| functions: true, |
| options: { |
| processFileURL, |
| req: client.req, |
| uploadImageBuffer, |
| openAIApiKey: client.apiKey, |
| fileStrategy: client.req.app.locals.fileStrategy, |
| returnMetadata: true, |
| }, |
| skipSpecs: true, |
| }); |
|
|
| const ToolMap = loadedTools.reduce((map, tool) => { |
| map[tool.name] = tool; |
| return map; |
| }, {}); |
|
|
| const promises = []; |
|
|
| |
| let actionSets = []; |
| let isActionTool = false; |
| const ActionToolMap = {}; |
| const ActionBuildersMap = {}; |
|
|
| for (let i = 0; i < requiredActions.length; i++) { |
| const currentAction = requiredActions[i]; |
| if (currentAction.tool === ImageVisionTool.function.name) { |
| promises.push(processVisionRequest(client, currentAction)); |
| continue; |
| } |
| let tool = ToolMap[currentAction.tool] ?? ActionToolMap[currentAction.tool]; |
|
|
| const handleToolOutput = async (output) => { |
| requiredActions[i].output = output; |
|
|
| |
| const toolCall = { |
| function: { |
| name: currentAction.tool, |
| arguments: JSON.stringify(currentAction.toolInput), |
| output, |
| }, |
| id: currentAction.toolCallId, |
| type: 'function', |
| progress: 1, |
| action: isActionTool, |
| }; |
|
|
| const toolCallIndex = client.mappedOrder.get(toolCall.id); |
|
|
| if (imageGenTools.has(currentAction.tool)) { |
| const imageOutput = output; |
| toolCall.function.output = `${currentAction.tool} displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.`; |
|
|
| |
| client.addContentData({ |
| [ContentTypes.TOOL_CALL]: toolCall, |
| index: toolCallIndex, |
| type: ContentTypes.TOOL_CALL, |
| }); |
|
|
| await sleep(500); |
|
|
| |
| const imageDetails = { |
| ...imageOutput, |
| ...currentAction.toolInput, |
| }; |
|
|
| const image_file = { |
| [ContentTypes.IMAGE_FILE]: imageDetails, |
| type: ContentTypes.IMAGE_FILE, |
| |
| index: toolCallIndex, |
| }; |
|
|
| client.addContentData(image_file); |
|
|
| |
| client.seenToolCalls && client.seenToolCalls.set(toolCall.id, toolCall); |
|
|
| return { |
| tool_call_id: currentAction.toolCallId, |
| output: toolCall.function.output, |
| }; |
| } |
|
|
| client.seenToolCalls && client.seenToolCalls.set(toolCall.id, toolCall); |
| client.addContentData({ |
| [ContentTypes.TOOL_CALL]: toolCall, |
| index: toolCallIndex, |
| type: ContentTypes.TOOL_CALL, |
| |
| |
| }); |
|
|
| return { |
| tool_call_id: currentAction.toolCallId, |
| output, |
| }; |
| }; |
|
|
| if (!tool) { |
| |
|
|
| if (!actionSets.length) { |
| actionSets = |
| (await loadActionSets({ |
| assistant_id: client.req.body.assistant_id, |
| })) ?? []; |
| } |
|
|
| let actionSet = null; |
| let currentDomain = ''; |
| for (let action of actionSets) { |
| const domain = await domainParser(client.req, action.metadata.domain, true); |
| if (currentAction.tool.includes(domain)) { |
| currentDomain = domain; |
| actionSet = action; |
| break; |
| } |
| } |
|
|
| if (!actionSet) { |
| |
| |
| continue; |
| } |
|
|
| let builders = ActionBuildersMap[actionSet.metadata.domain]; |
|
|
| if (!builders) { |
| const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec); |
| if (!validationResult.spec) { |
| throw new Error( |
| `Invalid spec: user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`, |
| ); |
| } |
| const { requestBuilders } = openapiToFunction(validationResult.spec); |
| ActionToolMap[actionSet.metadata.domain] = requestBuilders; |
| builders = requestBuilders; |
| } |
|
|
| const functionName = currentAction.tool.replace(`${actionDelimiter}${currentDomain}`, ''); |
|
|
| const requestBuilder = builders[functionName]; |
|
|
| if (!requestBuilder) { |
| |
| continue; |
| } |
|
|
| tool = createActionTool({ action: actionSet, requestBuilder }); |
| isActionTool = !!tool; |
| ActionToolMap[currentAction.tool] = tool; |
| } |
|
|
| if (currentAction.tool === 'calculator') { |
| currentAction.toolInput = currentAction.toolInput.input; |
| } |
|
|
| const handleToolError = (error) => { |
| logger.error( |
| `tool_call_id: ${currentAction.toolCallId} | Error processing tool ${currentAction.tool}`, |
| error, |
| ); |
| return { |
| tool_call_id: currentAction.toolCallId, |
| output: `Error processing tool ${currentAction.tool}: ${redactMessage(error.message, 256)}`, |
| }; |
| }; |
|
|
| try { |
| const promise = tool |
| ._call(currentAction.toolInput) |
| .then(handleToolOutput) |
| .catch(handleToolError); |
| promises.push(promise); |
| } catch (error) { |
| const toolOutputError = handleToolError(error); |
| promises.push(Promise.resolve(toolOutputError)); |
| } |
| } |
|
|
| return { |
| tool_outputs: await Promise.all(promises), |
| }; |
| } |
|
|
| module.exports = { |
| formatToOpenAIAssistantTool, |
| loadAndFormatTools, |
| processRequiredActions, |
| }; |
|
|