| | const { logger } = require('@librechat/data-schemas'); |
| | const { |
| | EnvVar, |
| | Calculator, |
| | createSearchTool, |
| | createCodeExecutionTool, |
| | } = require('@librechat/agents'); |
| | const { |
| | checkAccess, |
| | createSafeUser, |
| | mcpToolPattern, |
| | loadWebSearchAuth, |
| | } = require('@librechat/api'); |
| | const { |
| | Tools, |
| | Constants, |
| | Permissions, |
| | EToolResources, |
| | PermissionTypes, |
| | replaceSpecialVars, |
| | } = require('librechat-data-provider'); |
| | const { |
| | availableTools, |
| | manifestToolMap, |
| | |
| | GoogleSearchAPI, |
| | |
| | DALLE3, |
| | FluxAPI, |
| | OpenWeather, |
| | StructuredSD, |
| | StructuredACS, |
| | TraversaalSearch, |
| | StructuredWolfram, |
| | createYouTubeTools, |
| | TavilySearchResults, |
| | createOpenAIImageTools, |
| | } = require('../'); |
| | const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); |
| | const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch'); |
| | const { getUserPluginAuthValue } = require('~/server/services/PluginService'); |
| | const { createMCPTool, createMCPTools } = require('~/server/services/MCP'); |
| | const { loadAuthValues } = require('~/server/services/Tools/credentials'); |
| | const { getMCPServerTools } = require('~/server/services/Config'); |
| | const { getRoleByName } = require('~/models/Role'); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const validateTools = async (user, tools = []) => { |
| | try { |
| | const validToolsSet = new Set(tools); |
| | const availableToolsToValidate = availableTools.filter((tool) => |
| | validToolsSet.has(tool.pluginKey), |
| | ); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const validateCredentials = async (authField, toolName) => { |
| | const fields = authField.split('||'); |
| | for (const field of fields) { |
| | const adminAuth = process.env[field]; |
| | if (adminAuth && adminAuth.length > 0) { |
| | return; |
| | } |
| |
|
| | let userAuth = null; |
| | try { |
| | userAuth = await getUserPluginAuthValue(user, field); |
| | } catch (err) { |
| | if (field === fields[fields.length - 1] && !userAuth) { |
| | throw err; |
| | } |
| | } |
| | if (userAuth && userAuth.length > 0) { |
| | return; |
| | } |
| | } |
| |
|
| | validToolsSet.delete(toolName); |
| | }; |
| |
|
| | for (const tool of availableToolsToValidate) { |
| | if (!tool.authConfig || tool.authConfig.length === 0) { |
| | continue; |
| | } |
| |
|
| | for (const auth of tool.authConfig) { |
| | await validateCredentials(auth.authField, tool.pluginKey); |
| | } |
| | } |
| |
|
| | return Array.from(validToolsSet.values()); |
| | } catch (err) { |
| | logger.error('[validateTools] There was a problem validating tools', err); |
| | throw new Error(err); |
| | } |
| | }; |
| |
|
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => { |
| | return async function () { |
| | const authValues = await loadAuthValues({ userId, authFields }); |
| | return new ToolConstructor({ ...options, ...authValues, userId }); |
| | }; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | const getAuthFields = (toolKey) => { |
| | return manifestToolMap[toolKey]?.authConfig.map((auth) => auth.authField) ?? []; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const loadTools = async ({ |
| | user, |
| | agent, |
| | model, |
| | signal, |
| | endpoint, |
| | userMCPAuthMap, |
| | tools = [], |
| | options = {}, |
| | functions = true, |
| | returnMap = false, |
| | webSearch, |
| | fileStrategy, |
| | imageOutputType, |
| | }) => { |
| | const toolConstructors = { |
| | flux: FluxAPI, |
| | calculator: Calculator, |
| | google: GoogleSearchAPI, |
| | open_weather: OpenWeather, |
| | wolfram: StructuredWolfram, |
| | 'stable-diffusion': StructuredSD, |
| | 'azure-ai-search': StructuredACS, |
| | traversaal_search: TraversaalSearch, |
| | tavily_search_results_json: TavilySearchResults, |
| | }; |
| |
|
| | const customConstructors = { |
| | youtube: async (_toolContextMap) => { |
| | const authFields = getAuthFields('youtube'); |
| | const authValues = await loadAuthValues({ userId: user, authFields }); |
| | return createYouTubeTools(authValues); |
| | }, |
| | image_gen_oai: async (toolContextMap) => { |
| | const authFields = getAuthFields('image_gen_oai'); |
| | const authValues = await loadAuthValues({ userId: user, authFields }); |
| | const imageFiles = options.tool_resources?.[EToolResources.image_edit]?.files ?? []; |
| | let toolContext = ''; |
| | for (let i = 0; i < imageFiles.length; i++) { |
| | const file = imageFiles[i]; |
| | if (!file) { |
| | continue; |
| | } |
| | if (i === 0) { |
| | toolContext = |
| | 'Image files provided in this request (their image IDs listed in order of appearance) available for image editing:'; |
| | } |
| | toolContext += `\n\t- ${file.file_id}`; |
| | if (i === imageFiles.length - 1) { |
| | toolContext += `\n\nInclude any you need in the \`image_ids\` array when calling \`${EToolResources.image_edit}_oai\`. You may also include previously referenced or generated image IDs.`; |
| | } |
| | } |
| | if (toolContext) { |
| | toolContextMap.image_edit_oai = toolContext; |
| | } |
| | return createOpenAIImageTools({ |
| | ...authValues, |
| | isAgent: !!agent, |
| | req: options.req, |
| | imageOutputType, |
| | fileStrategy, |
| | imageFiles, |
| | }); |
| | }, |
| | }; |
| |
|
| | const requestedTools = {}; |
| |
|
| | if (functions === true) { |
| | toolConstructors.dalle = DALLE3; |
| | } |
| |
|
| | |
| | const imageGenOptions = { |
| | isAgent: !!agent, |
| | req: options.req, |
| | fileStrategy, |
| | processFileURL: options.processFileURL, |
| | returnMetadata: options.returnMetadata, |
| | uploadImageBuffer: options.uploadImageBuffer, |
| | }; |
| |
|
| | const toolOptions = { |
| | flux: imageGenOptions, |
| | dalle: imageGenOptions, |
| | 'stable-diffusion': imageGenOptions, |
| | }; |
| |
|
| | |
| | const toolContextMap = {}; |
| | const requestedMCPTools = {}; |
| |
|
| | for (const tool of tools) { |
| | if (tool === Tools.execute_code) { |
| | requestedTools[tool] = async () => { |
| | const authValues = await loadAuthValues({ |
| | userId: user, |
| | authFields: [EnvVar.CODE_API_KEY], |
| | }); |
| | const codeApiKey = authValues[EnvVar.CODE_API_KEY]; |
| | const { files, toolContext } = await primeCodeFiles( |
| | { |
| | ...options, |
| | agentId: agent?.id, |
| | }, |
| | codeApiKey, |
| | ); |
| | if (toolContext) { |
| | toolContextMap[tool] = toolContext; |
| | } |
| | const CodeExecutionTool = createCodeExecutionTool({ |
| | user_id: user, |
| | files, |
| | ...authValues, |
| | }); |
| | CodeExecutionTool.apiKey = codeApiKey; |
| | return CodeExecutionTool; |
| | }; |
| | continue; |
| | } else if (tool === Tools.file_search) { |
| | requestedTools[tool] = async () => { |
| | const { files, toolContext } = await primeSearchFiles({ |
| | ...options, |
| | agentId: agent?.id, |
| | }); |
| | if (toolContext) { |
| | toolContextMap[tool] = toolContext; |
| | } |
| |
|
| | |
| | let fileCitations; |
| | if (fileCitations == null && options.req?.user != null) { |
| | try { |
| | fileCitations = await checkAccess({ |
| | user: options.req.user, |
| | permissionType: PermissionTypes.FILE_CITATIONS, |
| | permissions: [Permissions.USE], |
| | getRoleByName, |
| | }); |
| | } catch (error) { |
| | logger.error('[handleTools] FILE_CITATIONS permission check failed:', error); |
| | fileCitations = false; |
| | } |
| | } |
| |
|
| | return createFileSearchTool({ |
| | userId: user, |
| | files, |
| | entity_id: agent?.id, |
| | fileCitations, |
| | }); |
| | }; |
| | continue; |
| | } else if (tool === Tools.web_search) { |
| | const result = await loadWebSearchAuth({ |
| | userId: user, |
| | loadAuthValues, |
| | webSearchConfig: webSearch, |
| | }); |
| | const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {}; |
| | requestedTools[tool] = async () => { |
| | toolContextMap[tool] = `# \`${tool}\`: |
| | Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} |
| | |
| | **Execute immediately without preface.** After search, provide a brief summary addressing the query directly, then structure your response with clear Markdown formatting (## headers, lists, tables). Cite sources properly, tailor tone to query type, and provide comprehensive details. |
| | |
| | **CITATION FORMAT - UNICODE ESCAPE SEQUENCES ONLY:** |
| | Use these EXACT escape sequences (copy verbatim): \\ue202 (before each anchor), \\ue200 (group start), \\ue201 (group end), \\ue203 (highlight start), \\ue204 (highlight end) |
| | |
| | Anchor pattern: \\ue202turn{N}{type}{index} where N=turn number, type=search|news|image|ref, index=0,1,2... |
| | |
| | **Examples (copy these exactly):** |
| | - Single: "Statement.\\ue202turn0search0" |
| | - Multiple: "Statement.\\ue202turn0search0\\ue202turn0news1" |
| | - Group: "Statement. \\ue200\\ue202turn0search0\\ue202turn0news1\\ue201" |
| | - Highlight: "\\ue203Cited text.\\ue204\\ue202turn0search0" |
| | - Image: "See photo\\ue202turn0image0." |
| | |
| | **CRITICAL:** Output escape sequences EXACTLY as shown. Do NOT substitute with † or other symbols. Place anchors AFTER punctuation. Cite every non-obvious fact/quote. NEVER use markdown links, [1], footnotes, or HTML tags.`.trim(); |
| | return createSearchTool({ |
| | ...result.authResult, |
| | onSearchResults, |
| | onGetHighlights, |
| | logger, |
| | }); |
| | }; |
| | continue; |
| | } else if (tool && mcpToolPattern.test(tool)) { |
| | const [toolName, serverName] = tool.split(Constants.mcp_delimiter); |
| | if (toolName === Constants.mcp_server) { |
| | |
| | continue; |
| | } |
| | if (serverName && options.req?.config?.mcpConfig?.[serverName] == null) { |
| | logger.warn( |
| | `MCP server "${serverName}" for "${toolName}" tool is not configured${agent?.id != null && agent.id ? ` but attached to "${agent.id}"` : ''}`, |
| | ); |
| | continue; |
| | } |
| | if (toolName === Constants.mcp_all) { |
| | requestedMCPTools[serverName] = [ |
| | { |
| | type: 'all', |
| | serverName, |
| | }, |
| | ]; |
| | continue; |
| | } |
| |
|
| | requestedMCPTools[serverName] = requestedMCPTools[serverName] || []; |
| | requestedMCPTools[serverName].push({ |
| | type: 'single', |
| | toolKey: tool, |
| | serverName, |
| | }); |
| | continue; |
| | } |
| |
|
| | if (customConstructors[tool]) { |
| | requestedTools[tool] = async () => customConstructors[tool](toolContextMap); |
| | continue; |
| | } |
| |
|
| | if (toolConstructors[tool]) { |
| | const options = toolOptions[tool] || {}; |
| | const toolInstance = loadToolWithAuth( |
| | user, |
| | getAuthFields(tool), |
| | toolConstructors[tool], |
| | options, |
| | ); |
| | requestedTools[tool] = toolInstance; |
| | continue; |
| | } |
| | } |
| |
|
| | if (returnMap) { |
| | return requestedTools; |
| | } |
| |
|
| | const toolPromises = []; |
| | for (const tool of tools) { |
| | const validTool = requestedTools[tool]; |
| | if (validTool) { |
| | toolPromises.push( |
| | validTool().catch((error) => { |
| | logger.error(`Error loading tool ${tool}:`, error); |
| | return null; |
| | }), |
| | ); |
| | } |
| | } |
| |
|
| | const loadedTools = (await Promise.all(toolPromises)).flatMap((plugin) => plugin || []); |
| | const mcpToolPromises = []; |
| | |
| | let index = -1; |
| | const failedMCPServers = new Set(); |
| | const safeUser = createSafeUser(options.req?.user); |
| | for (const [serverName, toolConfigs] of Object.entries(requestedMCPTools)) { |
| | index++; |
| | |
| | let availableTools; |
| | for (const config of toolConfigs) { |
| | try { |
| | if (failedMCPServers.has(serverName)) { |
| | continue; |
| | } |
| | const mcpParams = { |
| | index, |
| | signal, |
| | user: safeUser, |
| | userMCPAuthMap, |
| | res: options.res, |
| | model: agent?.model ?? model, |
| | serverName: config.serverName, |
| | provider: agent?.provider ?? endpoint, |
| | }; |
| |
|
| | if (config.type === 'all' && toolConfigs.length === 1) { |
| | |
| | mcpToolPromises.push( |
| | createMCPTools(mcpParams).catch((error) => { |
| | logger.error(`Error loading ${serverName} tools:`, error); |
| | return null; |
| | }), |
| | ); |
| | continue; |
| | } |
| | if (!availableTools) { |
| | try { |
| | availableTools = await getMCPServerTools(safeUser.id, serverName); |
| | } catch (error) { |
| | logger.error(`Error fetching available tools for MCP server ${serverName}:`, error); |
| | } |
| | } |
| |
|
| | |
| | const mcpTool = |
| | config.type === 'all' |
| | ? await createMCPTools(mcpParams) |
| | : await createMCPTool({ |
| | ...mcpParams, |
| | availableTools, |
| | toolKey: config.toolKey, |
| | }); |
| |
|
| | if (Array.isArray(mcpTool)) { |
| | loadedTools.push(...mcpTool); |
| | } else if (mcpTool) { |
| | loadedTools.push(mcpTool); |
| | } else { |
| | failedMCPServers.add(serverName); |
| | logger.warn( |
| | `MCP tool creation failed for "${config.toolKey}", server may be unavailable or unauthenticated.`, |
| | ); |
| | } |
| | } catch (error) { |
| | logger.error(`Error loading MCP tool for server ${serverName}:`, error); |
| | } |
| | } |
| | } |
| | loadedTools.push(...(await Promise.all(mcpToolPromises)).flatMap((plugin) => plugin || [])); |
| | return { loadedTools, toolContextMap }; |
| | }; |
| |
|
| | module.exports = { |
| | loadToolWithAuth, |
| | validateTools, |
| | loadTools, |
| | }; |
| |
|