diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index e480dd492..ea730e340 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -1,7 +1,13 @@ const { SerpAPI } = require('@langchain/community/tools/serpapi'); const { Calculator } = require('@langchain/community/tools/calculator'); -const { createCodeExecutionTool, EnvVar } = require('@librechat/agents'); -const { Tools, Constants, EToolResources } = require('librechat-data-provider'); +const { EnvVar, createCodeExecutionTool, createSearchTool } = require('@librechat/agents'); +const { + Tools, + Constants, + EToolResources, + loadWebSearchAuth, + replaceSpecialVars, +} = require('librechat-data-provider'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { availableTools, @@ -138,7 +144,6 @@ const loadTools = async ({ agent, model, endpoint, - useSpecs, tools = [], options = {}, functions = true, @@ -263,6 +268,37 @@ const loadTools = async ({ return createFileSearchTool({ req: options.req, files, entity_id: agent?.id }); }; continue; + } else if (tool === Tools.web_search) { + const webSearchConfig = options?.req?.app?.locals?.webSearch; + const result = await loadWebSearchAuth({ + userId: user, + loadAuthValues, + webSearchConfig, + }); + const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {}; + requestedTools[tool] = async () => { + // const { files, toolContext } = await primeSearchFiles(options); + // if (toolContext) { + // toolContextMap[tool] = toolContext; + // } + toolContextMap[tool] = `# \`${tool}\`: +Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} +1. **Execute immediately without preface** when using \`${tool}\`. +2. **After the search, begin with a brief summary** that directly addresses the query without headers or explaining your process. +3. **Structure your response clearly** using Markdown formatting (Level 2 headers for sections, lists for multiple points, tables for comparisons). +4. **Cite sources properly** according to the citation anchor format, utilizing group anchors when appropriate. +5. **Tailor your approach to the query type** (academic, news, coding, etc.) while maintaining an expert, journalistic, unbiased tone. +6. **Provide comprehensive information** with specific details, examples, and as much relevant context as possible from search results. +7. **Avoid moralizing language.** +`.trim(); + return createSearchTool({ + ...result.authResult, + onSearchResults, + onGetHighlights, + logger, + }); + }; + continue; } else if (tool && appTools[tool] && mcpToolPattern.test(tool)) { requestedTools[tool] = async () => createMCPTool({ diff --git a/api/models/Agent.js b/api/models/Agent.js index c52c364a6..f0dd3b53b 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -60,12 +60,17 @@ const loadEphemeralAgent = ({ req, agent_id, endpoint, model_parameters: _m }) = const { model, ...model_parameters } = _m; /** @type {Record} */ const availableTools = req.app.locals.availableTools; - const mcpServers = new Set(req.body.ephemeralAgent?.mcp); + /** @type {TEphemeralAgent | null} */ + const ephemeralAgent = req.body.ephemeralAgent; + const mcpServers = new Set(ephemeralAgent?.mcp); /** @type {string[]} */ const tools = []; - if (req.body.ephemeralAgent?.execute_code === true) { + if (ephemeralAgent?.execute_code === true) { tools.push(Tools.execute_code); } + if (ephemeralAgent?.web_search === true) { + tools.push(Tools.web_search); + } if (mcpServers.size > 0) { for (const toolName of Object.keys(availableTools)) { diff --git a/api/package.json b/api/package.json index d96e763e3..80b245533 100644 --- a/api/package.json +++ b/api/package.json @@ -48,7 +48,7 @@ "@langchain/google-genai": "^0.2.8", "@langchain/google-vertexai": "^0.2.8", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.317", + "@librechat/agents": "^2.4.35", "@librechat/data-schemas": "*", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 1ed2c4741..571c45455 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -1,4 +1,10 @@ -const { FileSources } = require('librechat-data-provider'); +const { + Tools, + Constants, + FileSources, + webSearchKeys, + extractWebSearchEnvVars, +} = require('librechat-data-provider'); const { Balance, getFiles, @@ -83,7 +89,6 @@ const deleteUserFiles = async (req) => { const updateUserPluginsController = async (req, res) => { const { user } = req; const { pluginKey, action, auth, isEntityTool } = req.body; - let authService; try { if (!isEntityTool) { const userPluginsService = await updateUserPluginsService(user, pluginKey, action); @@ -95,32 +100,55 @@ const updateUserPluginsController = async (req, res) => { } } - if (auth) { - const keys = Object.keys(auth); - const values = Object.values(auth); - if (action === 'install' && keys.length > 0) { - for (let i = 0; i < keys.length; i++) { - authService = await updateUserPluginAuth(user.id, keys[i], pluginKey, values[i]); - if (authService instanceof Error) { - logger.error('[authService]', authService); - const { status, message } = authService; - res.status(status).send({ message }); - } + if (auth == null) { + return res.status(200).send(); + } + + let keys = Object.keys(auth); + if (keys.length === 0 && pluginKey !== Tools.web_search) { + return res.status(200).send(); + } + const values = Object.values(auth); + + /** @type {number} */ + let status = 200; + /** @type {string} */ + let message; + /** @type {IPluginAuth | Error} */ + let authService; + + if (pluginKey === Tools.web_search) { + /** @type {TCustomConfig['webSearch']} */ + const webSearchConfig = req.app.locals?.webSearch; + keys = extractWebSearchEnvVars({ + keys: action === 'install' ? keys : webSearchKeys, + config: webSearchConfig, + }); + } + + if (action === 'install') { + for (let i = 0; i < keys.length; i++) { + authService = await updateUserPluginAuth(user.id, keys[i], pluginKey, values[i]); + if (authService instanceof Error) { + logger.error('[authService]', authService); + ({ status, message } = authService); } } - if (action === 'uninstall' && keys.length > 0) { - for (let i = 0; i < keys.length; i++) { - authService = await deleteUserPluginAuth(user.id, keys[i]); - if (authService instanceof Error) { - logger.error('[authService]', authService); - const { status, message } = authService; - res.status(status).send({ message }); - } + } else if (action === 'uninstall') { + for (let i = 0; i < keys.length; i++) { + authService = await deleteUserPluginAuth(user.id, keys[i]); + if (authService instanceof Error) { + logger.error('[authService]', authService); + ({ status, message } = authService); } } } - res.status(200).send(); + if (status === 200) { + return res.status(status).send(); + } + + res.status(status).send({ message }); } catch (err) { logger.error('[updateUserPluginsController]', err); return res.status(500).json({ message: 'Something went wrong.' }); diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 3f507f7d0..cedfc6bd6 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -237,6 +237,30 @@ function createToolEndCallback({ req, res, artifactPromises }) { return; } + if (output.artifact[Tools.web_search]) { + artifactPromises.push( + (async () => { + const name = `${output.name}_${output.tool_call_id}_${nanoid()}`; + const attachment = { + name, + type: Tools.web_search, + messageId: metadata.run_id, + toolCallId: output.tool_call_id, + conversationId: metadata.thread_id, + [Tools.web_search]: { ...output.artifact[Tools.web_search] }, + }; + if (!res.headersSent) { + return attachment; + } + res.write(`event: attachment\ndata: ${JSON.stringify(attachment)}\n\n`); + return attachment; + })().catch((error) => { + logger.error('Error processing artifact content:', error); + return null; + }), + ); + } + if (output.artifact.content) { /** @type {FormattedContent[]} */ const content = output.artifact.content; diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index a3484f650..31fd56930 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -39,9 +39,6 @@ const BaseClient = require('~/app/clients/BaseClient'); const { logger, sendEvent } = require('~/config'); const { createRun } = require('./run'); -/** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */ -/** @typedef {import('@langchain/core/runnables').RunnableConfig} RunnableConfig */ - /** * @param {ServerRequest} req * @param {Agent} agent @@ -543,7 +540,7 @@ class AgentClient extends BaseClient { } async chatCompletion({ payload, abortController = null }) { - /** @type {Partial & { version: 'v1' | 'v2'; run_id?: string; streamMode: string }} */ + /** @type {Partial} */ let config; /** @type {ReturnType} */ let run; diff --git a/api/server/controllers/tools.js b/api/server/controllers/tools.js index b37b6fcb8..254ecb4f9 100644 --- a/api/server/controllers/tools.js +++ b/api/server/controllers/tools.js @@ -6,6 +6,7 @@ const { Permissions, ToolCallTypes, PermissionTypes, + loadWebSearchAuth, } = require('librechat-data-provider'); const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); const { processCodeOutput } = require('~/server/services/Files/Code/process'); @@ -24,6 +25,36 @@ const toolAccessPermType = { [Tools.execute_code]: PermissionTypes.RUN_CODE, }; +/** + * Verifies web search authentication, ensuring each category has at least + * one fully authenticated service. + * + * @param {ServerRequest} req - The request object + * @param {ServerResponse} res - The response object + * @returns {Promise} A promise that resolves when the function has completed + */ +const verifyWebSearchAuth = async (req, res) => { + try { + const userId = req.user.id; + /** @type {TCustomConfig['webSearch']} */ + const webSearchConfig = req.app.locals?.webSearch || {}; + const result = await loadWebSearchAuth({ + userId, + loadAuthValues, + webSearchConfig, + throwError: false, + }); + + return res.status(200).json({ + authenticated: result.authenticated, + authTypes: result.authTypes, + }); + } catch (error) { + console.error('Error in verifyWebSearchAuth:', error); + return res.status(500).json({ message: error.message }); + } +}; + /** * @param {ServerRequest} req - The request object, containing information about the HTTP request. * @param {ServerResponse} res - The response object, used to send back the desired HTTP response. @@ -32,6 +63,9 @@ const toolAccessPermType = { const verifyToolAuth = async (req, res) => { try { const { toolId } = req.params; + if (toolId === Tools.web_search) { + return await verifyWebSearchAuth(req, res); + } const authFields = fieldsMap[toolId]; if (!authFields) { res.status(404).json({ message: 'Tool not found' }); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 583453fe4..e34497688 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -85,6 +85,26 @@ router.get('/', async function (req, res) { bundlerURL: process.env.SANDPACK_BUNDLER_URL, staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL, }; + /** @type {TCustomConfig['webSearch']} */ + const webSearchConfig = req.app.locals.webSearch; + if ( + webSearchConfig != null && + (webSearchConfig.searchProvider || + webSearchConfig.scraperType || + webSearchConfig.rerankerType) + ) { + payload.webSearch = {}; + } + + if (webSearchConfig?.searchProvider) { + payload.webSearch.searchProvider = webSearchConfig.searchProvider; + } + if (webSearchConfig?.scraperType) { + payload.webSearch.scraperType = webSearchConfig.scraperType; + } + if (webSearchConfig?.rerankerType) { + payload.webSearch.rerankerType = webSearchConfig.rerankerType; + } if (ldap) { payload.ldap = ldap; diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 5f119e67a..f1a6c0768 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -1,9 +1,10 @@ const { FileSources, - EModelEndpoint, loadOCRConfig, processMCPEnv, + EModelEndpoint, getConfigDefaults, + loadWebSearchConfig, } = require('librechat-data-provider'); const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks'); const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants'); @@ -35,6 +36,7 @@ const AppService = async (app) => { const configDefaults = getConfigDefaults(); const ocr = loadOCRConfig(config.ocr); + const webSearch = loadWebSearchConfig(config.webSearch); const filteredTools = config.filteredTools; const includedTools = config.includedTools; const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy; @@ -79,6 +81,7 @@ const AppService = async (app) => { const defaultLocals = { ocr, paths, + webSearch, fileStrategy, socialLogins, filteredTools, diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index 81a017e41..d1c713e0d 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -141,6 +141,14 @@ describe('AppService', () => { balance: { enabled: true }, filteredTools: undefined, includedTools: undefined, + webSearch: { + cohereApiKey: '${COHERE_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + safeSearch: true, + serperApiKey: '${SERPER_API_KEY}', + }, }); }); @@ -537,7 +545,7 @@ describe('AppService updating app.locals and issuing warnings', () => { const { logger } = require('~/config'); expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining( - 'The \'assistants\' endpoint has both \'supportedIds\' and \'excludedIds\' defined.', + "The 'assistants' endpoint has both 'supportedIds' and 'excludedIds' defined.", ), ); }); @@ -559,7 +567,7 @@ describe('AppService updating app.locals and issuing warnings', () => { const { logger } = require('~/config'); expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining( - 'The \'assistants\' endpoint has both \'privateAssistants\' and \'supportedIds\' or \'excludedIds\' defined.', + "The 'assistants' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined.", ), ); }); diff --git a/api/server/services/Files/MistralOCR/crud.js b/api/server/services/Files/MistralOCR/crud.js index 0c544b9eb..1bd48479a 100644 --- a/api/server/services/Files/MistralOCR/crud.js +++ b/api/server/services/Files/MistralOCR/crud.js @@ -2,7 +2,12 @@ const fs = require('fs'); const path = require('path'); const FormData = require('form-data'); -const { FileSources, envVarRegex, extractEnvVariable } = require('librechat-data-provider'); +const { + FileSources, + envVarRegex, + extractEnvVariable, + extractVariableName, +} = require('librechat-data-provider'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { logger, createAxiosInstance } = require('~/config'); const { logAxiosError } = require('~/utils/axios'); @@ -108,11 +113,6 @@ async function performOCR({ }); } -function extractVariableName(str) { - const match = str.match(envVarRegex); - return match ? match[1] : null; -} - /** * Uploads a file to the Mistral OCR API and processes the OCR result. * diff --git a/api/server/services/PluginService.js b/api/server/services/PluginService.js index e03f7f89e..03e90bce4 100644 --- a/api/server/services/PluginService.js +++ b/api/server/services/PluginService.js @@ -66,16 +66,26 @@ const getUserPluginAuthValue = async (userId, authField, throwError = true) => { // } // }; +/** + * + * @async + * @param {string} userId + * @param {string} authField + * @param {string} pluginKey + * @param {string} value + * @returns {Promise} + * @throws {Error} + */ const updateUserPluginAuth = async (userId, authField, pluginKey, value) => { try { const encryptedValue = await encrypt(value); const pluginAuth = await PluginAuth.findOne({ userId, authField }).lean(); if (pluginAuth) { - const pluginAuth = await PluginAuth.updateOne( + return await PluginAuth.findOneAndUpdate( { userId, authField }, { $set: { value: encryptedValue } }, - ); - return pluginAuth; + { new: true, upsert: true }, + ).lean(); } else { const newPluginAuth = await new PluginAuth({ userId, @@ -84,7 +94,7 @@ const updateUserPluginAuth = async (userId, authField, pluginKey, value) => { pluginKey, }); await newPluginAuth.save(); - return newPluginAuth; + return newPluginAuth.toObject(); } } catch (err) { logger.error('[updateUserPluginAuth]', err); @@ -92,6 +102,14 @@ const updateUserPluginAuth = async (userId, authField, pluginKey, value) => { } }; +/** + * @async + * @param {string} userId + * @param {string} authField + * @param {boolean} [all] + * @returns {Promise} + * @throws {Error} + */ const deleteUserPluginAuth = async (userId, authField, all = false) => { if (all) { try { diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index b71e97f74..60b0128f7 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1,8 +1,8 @@ const fs = require('fs'); const path = require('path'); const { zodToJsonSchema } = require('zod-to-json-schema'); -const { tool: toolFn, Tool, DynamicStructuredTool } = require('@langchain/core/tools'); const { Calculator } = require('@langchain/community/tools/calculator'); +const { tool: toolFn, Tool, DynamicStructuredTool } = require('@langchain/core/tools'); const { Tools, ErrorTypes, @@ -29,6 +29,7 @@ const { toolkits, } = require('~/app/clients/tools'); const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); +const { createOnSearchResults } = require('~/server/services/Tools/search'); const { isActionDomainAllowed } = require('~/server/services/domains'); const { getEndpointsConfig } = require('~/server/services/Config'); const { recordUsage } = require('~/server/services/Threads'); @@ -504,11 +505,15 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) const checkCapability = (capability) => enabledCapabilities.has(capability); const areToolsEnabled = checkCapability(AgentCapabilities.tools); + let includesWebSearch = false; const _agentTools = agent.tools?.filter((tool) => { if (tool === Tools.file_search) { return checkCapability(AgentCapabilities.file_search); } else if (tool === Tools.execute_code) { return checkCapability(AgentCapabilities.execute_code); + } else if (tool === Tools.web_search) { + includesWebSearch = checkCapability(AgentCapabilities.web_search); + return includesWebSearch; } else if (!areToolsEnabled && !tool.includes(actionDelimiter)) { return false; } @@ -518,7 +523,11 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) if (!_agentTools || _agentTools.length === 0) { return {}; } - + /** @type {ReturnType} */ + let webSearchCallbacks; + if (includesWebSearch) { + webSearchCallbacks = createOnSearchResults(res); + } const { loadedTools, toolContextMap } = await loadTools({ agent, functions: true, @@ -532,6 +541,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) uploadImageBuffer, returnMetadata: true, fileStrategy: req.app.locals.fileStrategy, + [Tools.web_search]: webSearchCallbacks, }, }); diff --git a/api/server/services/Tools/search.js b/api/server/services/Tools/search.js new file mode 100644 index 000000000..a5c9947b5 --- /dev/null +++ b/api/server/services/Tools/search.js @@ -0,0 +1,122 @@ +const { nanoid } = require('nanoid'); +const { Tools } = require('librechat-data-provider'); +const { logger } = require('~/config'); + +/** + * Creates a function to handle search results and stream them as attachments + * @param {import('http').ServerResponse} res - The HTTP server response object + * @returns {{ onSearchResults: function(SearchResult, GraphRunnableConfig): void; onGetHighlights: function(string): void}} - Function that takes search results and returns or streams an attachment + */ +function createOnSearchResults(res) { + const context = { + sourceMap: new Map(), + searchResultData: undefined, + toolCallId: undefined, + attachmentName: undefined, + messageId: undefined, + conversationId: undefined, + }; + + /** + * @param {SearchResult} results + * @param {GraphRunnableConfig} runnableConfig + */ + function onSearchResults(results, runnableConfig) { + logger.info( + `[onSearchResults] user: ${runnableConfig.metadata.user_id} | thread_id: ${runnableConfig.metadata.thread_id} | run_id: ${runnableConfig.metadata.run_id}`, + results, + ); + + if (!results.success) { + logger.error( + `[onSearchResults] user: ${runnableConfig.metadata.user_id} | thread_id: ${runnableConfig.metadata.thread_id} | run_id: ${runnableConfig.metadata.run_id} | error: ${results.error}`, + ); + return; + } + + const turn = runnableConfig.toolCall?.turn ?? 0; + const data = { turn, ...structuredClone(results.data ?? {}) }; + context.searchResultData = data; + + // Map sources to links + for (let i = 0; i < data.organic.length; i++) { + const source = data.organic[i]; + if (source.link) { + context.sourceMap.set(source.link, { + type: 'organic', + index: i, + turn, + }); + } + } + for (let i = 0; i < data.topStories.length; i++) { + const source = data.topStories[i]; + if (source.link) { + context.sourceMap.set(source.link, { + type: 'topStories', + index: i, + turn, + }); + } + } + + context.toolCallId = runnableConfig.toolCall.id; + context.messageId = runnableConfig.metadata.run_id; + context.conversationId = runnableConfig.metadata.thread_id; + context.attachmentName = `${runnableConfig.toolCall.name}_${context.toolCallId}_${nanoid()}`; + + const attachment = buildAttachment(context); + + if (!res.headersSent) { + return attachment; + } + res.write(`event: attachment\ndata: ${JSON.stringify(attachment)}\n\n`); + } + + /** + * @param {string} link + * @returns {void} + */ + function onGetHighlights(link) { + const source = context.sourceMap.get(link); + if (!source) { + return; + } + const { type, index } = source; + const data = context.searchResultData; + if (!data) { + return; + } + if (data[type][index] != null) { + data[type][index].processed = true; + } + + const attachment = buildAttachment(context); + res.write(`event: attachment\ndata: ${JSON.stringify(attachment)}\n\n`); + } + + return { + onSearchResults, + onGetHighlights, + }; +} + +/** + * Helper function to build an attachment object + * @param {object} context - The context containing attachment data + * @returns {object} - The attachment object + */ +function buildAttachment(context) { + return { + messageId: context.messageId, + toolCallId: context.toolCallId, + conversationId: context.conversationId, + name: context.attachmentName, + type: Tools.web_search, + [Tools.web_search]: context.searchResultData, + }; +} + +module.exports = { + createOnSearchResults, +}; diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index d9f171ca4..7578c036b 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -38,6 +38,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol agents: interfaceConfig?.agents ?? defaults.agents, temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat, runCode: interfaceConfig?.runCode ?? defaults.runCode, + webSearch: interfaceConfig?.webSearch ?? defaults.webSearch, customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome, }); @@ -48,6 +49,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, }); await updateAccessPermissions(SystemRoles.ADMIN, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, @@ -56,6 +58,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch }, }); let i = 0; @@ -74,7 +77,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol // warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs. if (config?.modelSpecs?.prioritize && loadedInterface.presets) { logger.warn( - 'Note: Prioritizing model specs can conflict with default presets if a default preset is set. It\'s recommended to disable presets from the interface or disable use of a default preset.', + "Note: Prioritizing model specs can conflict with default presets if a default preset is set. It's recommended to disable presets from the interface or disable use of a default preset.", ); i === 0 && i++; } @@ -88,14 +91,14 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol loadedInterface.parameters) ) { logger.warn( - 'Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It\'s recommended to disable these options from the interface or disable enforcing model specs.', + "Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It's recommended to disable these options from the interface or disable enforcing model specs.", ); i === 0 && i++; } // warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior. if (config?.modelSpecs?.enforce && !config?.modelSpecs?.prioritize) { logger.warn( - 'Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It\'s recommended to enable prioritizing model specs if enforcing them.', + "Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It's recommended to enable prioritizing model specs if enforcing them.", ); i === 0 && i++; } diff --git a/api/server/services/start/interface.spec.js b/api/server/services/start/interface.spec.js index 7e248d3df..d0dcfaf55 100644 --- a/api/server/services/start/interface.spec.js +++ b/api/server/services/start/interface.spec.js @@ -16,6 +16,7 @@ describe('loadDefaultInterface', () => { agents: true, temporaryChat: true, runCode: true, + webSearch: true, }, }; const configDefaults = { interface: {} }; @@ -29,6 +30,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, }); }); @@ -41,6 +43,7 @@ describe('loadDefaultInterface', () => { agents: false, temporaryChat: false, runCode: false, + webSearch: false, }, }; const configDefaults = { interface: {} }; @@ -54,6 +57,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: false }, }); }); @@ -70,6 +74,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); @@ -82,6 +87,7 @@ describe('loadDefaultInterface', () => { agents: undefined, temporaryChat: undefined, runCode: undefined, + webSearch: undefined, }, }; const configDefaults = { interface: {} }; @@ -95,6 +101,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); @@ -107,6 +114,7 @@ describe('loadDefaultInterface', () => { agents: true, temporaryChat: undefined, runCode: false, + webSearch: true, }, }; const configDefaults = { interface: {} }; @@ -120,6 +128,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, }); }); @@ -133,6 +142,7 @@ describe('loadDefaultInterface', () => { agents: true, temporaryChat: true, runCode: true, + webSearch: true, }, }; @@ -145,6 +155,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, }); }); @@ -161,6 +172,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); @@ -177,6 +189,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); @@ -193,6 +206,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); @@ -218,6 +232,7 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); @@ -231,6 +246,7 @@ describe('loadDefaultInterface', () => { agents: undefined, temporaryChat: undefined, runCode: undefined, + webSearch: undefined, }, }; @@ -243,6 +259,33 @@ describe('loadDefaultInterface', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: undefined }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, + }); + }); + + it('should call updateAccessPermissions with the correct parameters when WEB_SEARCH is undefined', async () => { + const config = { + interface: { + prompts: true, + bookmarks: false, + multiConvo: true, + agents: false, + temporaryChat: true, + runCode: false, + }, + }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, { + [PermissionTypes.PROMPTS]: { [Permissions.USE]: true }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.AGENTS]: { [Permissions.USE]: false }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined }, }); }); }); diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index f593d6c86..125f27c6b 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -200,11 +200,12 @@ function generateConfig(key, baseURL, endpoint) { config.capabilities = [ AgentCapabilities.execute_code, AgentCapabilities.file_search, + AgentCapabilities.web_search, AgentCapabilities.artifacts, AgentCapabilities.actions, AgentCapabilities.tools, - AgentCapabilities.ocr, AgentCapabilities.chain, + AgentCapabilities.ocr, ]; } diff --git a/api/typedefs.js b/api/typedefs.js index d65d8c919..8da5b3480 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -55,6 +55,12 @@ * @memberof typedefs */ +/** + * @exports MessageContentComplex + * @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex + * @memberof typedefs + */ + /** * @exports EventHandler * @typedef {import('@librechat/agents').EventHandler} EventHandler @@ -186,6 +192,8 @@ * agent_index: number; * last_agent_index: number; * hide_sequential_outputs: boolean; + * version?: 'v1' | 'v2'; + * streamMode?: string * }> & { * toolCall?: LangChainToolCall & { stepId?: string }; * }} GraphRunnableConfig @@ -473,6 +481,25 @@ * @typedef {import('librechat-data-provider').Agents.MessageContentImageUrl} MessageContentImageUrl * @memberof typedefs */ +/** Web Search */ + +/** + * @exports SearchResult + * @typedef {import('@librechat/agents').SearchResult} SearchResult + * @memberof typedefs + */ + +/** + * @exports SearchResultData + * @typedef {import('@librechat/agents').SearchResultData} SearchResultData + * @memberof typedefs + */ + +/** + * @exports ValidSource + * @typedef {import('librechat-data-provider').ValidSource} ValidSource + * @memberof typedefs + */ /** Prompts */ /** @@ -848,6 +875,12 @@ * @memberof typedefs */ +/** + * @exports IPluginAuth + * @typedef {import('@librechat/data-schemas').IPluginAuth} IPluginAuth + * @memberof typedefs + */ + /** * @exports ObjectId * @typedef {import('mongoose').Types.ObjectId} ObjectId @@ -990,6 +1023,18 @@ * @memberof typedefs */ +/** + * @exports TEphemeralAgent + * @typedef {import('librechat-data-provider').TEphemeralAgent} TEphemeralAgent + * @memberof typedefs + */ + +/** + * @exports TWebSearchKeys + * @typedef {import('librechat-data-provider').TWebSearchKeys} TWebSearchKeys + * @memberof typedefs + */ + /** * @exports AgentToolResources * @typedef {import('librechat-data-provider').AgentToolResources} AgentToolResources diff --git a/client/src/Providers/SearchContext.tsx b/client/src/Providers/SearchContext.tsx new file mode 100644 index 000000000..7868cff0e --- /dev/null +++ b/client/src/Providers/SearchContext.tsx @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; +import type { SearchResultData } from 'librechat-data-provider'; + +type SearchContext = { + searchResults?: { [key: string]: SearchResultData }; +}; + +export const SearchContext = createContext({} as SearchContext); +export const useSearchContext = () => useContext(SearchContext); diff --git a/client/src/Providers/index.ts b/client/src/Providers/index.ts index 43da0d346..00191318e 100644 --- a/client/src/Providers/index.ts +++ b/client/src/Providers/index.ts @@ -20,3 +20,4 @@ export * from './ArtifactContext'; export * from './CodeBlockContext'; export * from './ToolCallsMapContext'; export * from './SetConvoContext'; +export * from './SearchContext'; diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index 982cbfdb1..7a6c25d64 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -10,6 +10,7 @@ export type TAgentOption = OptionWithIcon & }; export type TAgentCapabilities = { + [AgentCapabilities.web_search]: boolean; [AgentCapabilities.file_search]: boolean; [AgentCapabilities.execute_code]: boolean; [AgentCapabilities.end_after_tools]?: boolean; diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index 15e99bd42..ed9f4b82c 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -15,6 +15,7 @@ import type { BadgeItem } from '~/common'; import { useChatBadges } from '~/hooks'; import { Badge } from '~/components/ui'; import MCPSelect from './MCPSelect'; +import WebSearch from './WebSearch'; import store from '~/store'; interface BadgeRowProps { @@ -354,6 +355,7 @@ function BadgeRow({ )} {showEphemeralBadges === true && ( <> + diff --git a/client/src/components/Chat/Input/CodeInterpreter.tsx b/client/src/components/Chat/Input/CodeInterpreter.tsx index 3f2c282ff..411f1e27b 100644 --- a/client/src/components/Chat/Input/CodeInterpreter.tsx +++ b/client/src/components/Chat/Input/CodeInterpreter.tsx @@ -1,5 +1,5 @@ import debounce from 'lodash/debounce'; -import React, { memo, useMemo, useCallback } from 'react'; +import React, { memo, useMemo, useCallback, useRef } from 'react'; import { useRecoilState } from 'recoil'; import { TerminalSquareIcon } from 'lucide-react'; import { @@ -32,6 +32,7 @@ const storageCondition = (value: unknown, rawCurrentValue?: string | null) => { }; function CodeInterpreter({ conversationId }: { conversationId?: string | null }) { + const triggerRef = useRef(null); const localize = useLocalize(); const key = conversationId ?? Constants.NEW_CONVO; @@ -73,9 +74,10 @@ function CodeInterpreter({ conversationId }: { conversationId?: string | null }) ); const handleChange = useCallback( - (isChecked: boolean) => { + (e: React.ChangeEvent, isChecked: boolean) => { if (!isAuthenticated) { setIsDialogOpen(true); + e.preventDefault(); return; } setRunCode(isChecked); @@ -95,6 +97,7 @@ function CodeInterpreter({ conversationId }: { conversationId?: string | null }) return ( <> { + if (rawCurrentValue) { + try { + const currentValue = rawCurrentValue?.trim() ?? ''; + if (currentValue === 'true' && value === false) { + return true; + } + } catch (e) { + console.error(e); + } + } + return value !== undefined && value !== null && value !== '' && value !== false; +}; + +function WebSearch({ conversationId }: { conversationId?: string | null }) { + const triggerRef = useRef(null); + const localize = useLocalize(); + const key = conversationId ?? Constants.NEW_CONVO; + + const canUseWebSearch = useHasAccess({ + permissionType: PermissionTypes.WEB_SEARCH, + permission: Permissions.USE, + }); + const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); + const isWebSearchToggleEnabled = useMemo(() => { + return ephemeralAgent?.web_search ?? false; + }, [ephemeralAgent?.web_search]); + + const { data } = useVerifyAgentToolAuth( + { toolId: Tools.web_search }, + { + retry: 1, + }, + ); + const authTypes = useMemo(() => data?.authTypes ?? [], [data?.authTypes]); + const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]); + const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } = + useSearchApiKeyForm({}); + + const setValue = useCallback( + (isChecked: boolean) => { + setEphemeralAgent((prev) => ({ + ...prev, + web_search: isChecked, + })); + }, + [setEphemeralAgent], + ); + + const [webSearch, setWebSearch] = useLocalStorage( + `${LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_}${key}`, + isWebSearchToggleEnabled, + setValue, + storageCondition, + ); + + const handleChange = useCallback( + (e: React.ChangeEvent, isChecked: boolean) => { + if (!isAuthenticated) { + setIsDialogOpen(true); + e.preventDefault(); + return; + } + setWebSearch(isChecked); + }, + [setWebSearch, setIsDialogOpen, isAuthenticated], + ); + + const debouncedChange = useMemo( + () => debounce(handleChange, 50, { leading: true }), + [handleChange], + ); + + if (!canUseWebSearch) { + return null; + } + + return ( + <> + } + /> + + + ); +} + +export default memo(WebSearch); diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index b40014318..055606c8e 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -2,10 +2,12 @@ import { memo, useMemo, useState } from 'react'; import { useRecoilValue, useRecoilState } from 'recoil'; import { ContentTypes } from 'librechat-data-provider'; import type { TMessageContentParts, TAttachment, Agents } from 'librechat-data-provider'; +import { useSearchResultsByTurn } from '~/hooks/Messages/useSearchResultsByTurn'; import { ThinkingButton } from '~/components/Artifacts/Thinking'; import useLocalize from '~/hooks/useLocalize'; import { mapAttachments } from '~/utils/map'; -import { MessageContext } from '~/Providers'; +import { MessageContext, SearchContext } from '~/Providers'; +import Sources from '~/components/Web/Sources'; import { EditTextPart } from './Parts'; import store from '~/store'; import Part from './Part'; @@ -42,13 +44,15 @@ const ContentParts = memo( setSiblingIdx, }: ContentPartsProps) => { const localize = useLocalize(); + const messageAttachmentsMap = useRecoilValue(store.messageAttachmentsMap); const [showThinking, setShowThinking] = useRecoilState(store.showThinking); const [isExpanded, setIsExpanded] = useState(showThinking); - const messageAttachmentsMap = useRecoilValue(store.messageAttachmentsMap); - const attachmentMap = useMemo( - () => mapAttachments(attachments ?? messageAttachmentsMap[messageId] ?? []), + const messageAttachments = useMemo( + () => attachments ?? messageAttachmentsMap[messageId] ?? [], [attachments, messageAttachmentsMap, messageId], ); + const searchResults = useSearchResultsByTurn(messageAttachments); + const attachmentMap = useMemo(() => mapAttachments(messageAttachments), [messageAttachments]); const hasReasoningParts = useMemo(() => { const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false; @@ -98,53 +102,56 @@ const ContentParts = memo( return ( <> - {hasReasoningParts && ( -
- - setIsExpanded((prev) => { - const val = !prev; - setShowThinking(val); - return val; - }) - } - label={ - isSubmitting && isLast ? localize('com_ui_thinking') : localize('com_ui_thoughts') - } - /> -
- )} - {content - .filter((part) => part) - .map((part, idx) => { - const toolCallId = - (part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? ''; - const attachments = attachmentMap[toolCallId]; + + + {hasReasoningParts && ( +
+ + setIsExpanded((prev) => { + const val = !prev; + setShowThinking(val); + return val; + }) + } + label={ + isSubmitting && isLast ? localize('com_ui_thinking') : localize('com_ui_thoughts') + } + /> +
+ )} + {content + .filter((part) => part) + .map((part, idx) => { + const toolCallId = + (part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? ''; + const attachments = attachmentMap[toolCallId]; - return ( - - - - ); - })} + return ( + + + + ); + })} +
); }, diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index d668405d5..741b16fac 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -12,13 +12,16 @@ import type { Pluggable } from 'unified'; import { useToastContext, ArtifactProvider, + useSearchContext, CodeBlockProvider, useCodeBlockContext, } from '~/Providers'; +import { Citation, CompositeCitation, HighlightedText } from '~/components/Web/Citation'; import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact'; import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils'; import CodeBlock from '~/components/Messages/Content/CodeBlock'; import useHasAccess from '~/hooks/Roles/useHasAccess'; +import { unicodeCitation } from '~/components/Web'; import { useFileDownload } from '~/data-provider'; import useLocalize from '~/hooks/useLocalize'; import store from '~/store'; @@ -172,6 +175,7 @@ type TContentProps = { }; const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { + const { searchResults } = useSearchContext(); const LaTeXParsing = useRecoilValue(store.LaTeXParsing); const isInitializing = content === ''; @@ -197,16 +201,22 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { [], ); - const remarkPlugins: Pluggable[] = useMemo( - () => [ + const searchTurns = useMemo(() => Object.keys(searchResults ?? {}).length, [searchResults]); + const remarkPlugins: Pluggable[] = useMemo(() => { + const plugins: Pluggable[] = [ supersub, remarkGfm, remarkDirective, artifactPlugin, [remarkMath, { singleDollarTextMath: true }], - ], - [], - ); + ]; + + if (searchTurns > 0) { + plugins.push(unicodeCitation); + } + + return plugins; + }, [searchTurns]); if (isInitializing) { return ( @@ -232,6 +242,9 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => { a, p, artifact: Artifact, + citation: Citation, + 'highlighted-text': HighlightedText, + 'composite-citation': CompositeCitation, } as { [nodeType: string]: React.ElementType; } diff --git a/client/src/components/Chat/Messages/Content/Part.tsx b/client/src/components/Chat/Messages/Content/Part.tsx index e8d08040f..c63dfe31e 100644 --- a/client/src/components/Chat/Messages/Content/Part.tsx +++ b/client/src/components/Chat/Messages/Content/Part.tsx @@ -12,6 +12,7 @@ import { ErrorMessage } from './MessageContent'; import RetrievalCall from './RetrievalCall'; import CodeAnalyze from './CodeAnalyze'; import Container from './Container'; +import WebSearch from './WebSearch'; import ToolCall from './ToolCall'; import ImageGen from './ImageGen'; import Image from './Image'; @@ -107,6 +108,16 @@ const Part = memo( attachments={attachments} /> ); + } else if (isToolCall && toolCall.name === Tools.web_search) { + return ( + + ); } else if (isToolCall) { return ( }) => { + const [isVisible, setIsVisible] = useState(false); const { handleDownload } = useAttachmentLink({ href: attachment.filepath ?? '', filename: attachment.filename ?? '', }); const extension = attachment.filename?.split('.').pop(); - const [isVisible, setIsVisible] = useState(false); useEffect(() => { const timer = setTimeout(() => setIsVisible(true), 50); @@ -84,6 +84,9 @@ export default function Attachment({ attachment }: { attachment?: TAttachment }) if (!attachment) { return null; } + if (attachment.type === Tools.web_search) { + return null; + } const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata; const isImage = @@ -115,7 +118,7 @@ export function AttachmentGroup({ attachments }: { attachments?: TAttachment[] } if (isImage) { imageAttachments.push(attachment); - } else { + } else if (attachment.type !== Tools.web_search) { fileAttachments.push(attachment); } }); diff --git a/client/src/components/Chat/Messages/Content/WebSearch.tsx b/client/src/components/Chat/Messages/Content/WebSearch.tsx new file mode 100644 index 000000000..afcd3bc2d --- /dev/null +++ b/client/src/components/Chat/Messages/Content/WebSearch.tsx @@ -0,0 +1,91 @@ +import { useMemo } from 'react'; +import type { TAttachment } from 'librechat-data-provider'; +import { StackedFavicons } from '~/components/Web/Sources'; +import { useSearchContext } from '~/Providers'; +import ProgressText from './ProgressText'; +import { useLocalize } from '~/hooks'; + +type ProgressKeys = + | 'com_ui_web_searching' + | 'com_ui_web_searching_again' + | 'com_ui_web_search_processing' + | 'com_ui_web_search_reading'; + +export default function WebSearch({ + initialProgress: progress = 0.1, + isSubmitting, + isLast, + output, +}: { + isLast?: boolean; + isSubmitting: boolean; + output?: string | null; + initialProgress: number; + attachments?: TAttachment[]; +}) { + const localize = useLocalize(); + const { searchResults } = useSearchContext(); + const error = typeof output === 'string' && output.toLowerCase().includes('error processing'); + const cancelled = (!isSubmitting && progress < 1) || error === true; + + const complete = !isLast && progress === 1; + const finalizing = isSubmitting && isLast && progress === 1; + const processedSources = useMemo(() => { + if (complete && !finalizing) { + return []; + } + if (!searchResults) return []; + const values = Object.values(searchResults); + const result = values[values.length - 1]; + if (!result) return []; + if (finalizing) { + return [...(result.organic || []), ...(result.topStories || [])]; + } + return [...(result.organic || []), ...(result.topStories || [])].filter( + (source) => source.processed === true, + ); + }, [searchResults, complete, finalizing]); + const turns = useMemo(() => { + if (!searchResults) return 0; + return Object.values(searchResults).length; + }, [searchResults]); + + const clampedProgress = useMemo(() => { + return Math.min(progress, 0.99); + }, [progress]); + + const showSources = processedSources.length > 0; + const progressText = useMemo(() => { + let text: ProgressKeys = turns > 1 ? 'com_ui_web_searching_again' : 'com_ui_web_searching'; + if (showSources) { + text = 'com_ui_web_search_processing'; + } + if (finalizing) { + text = 'com_ui_web_search_reading'; + } + return localize(text); + }, [turns, localize, showSources, finalizing]); + + if (complete || cancelled) { + return null; + } + return ( + <> +
+ {showSources && ( +
+ +
+ )} + +
+ + ); +} diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx index aebb33fee..c04018e69 100644 --- a/client/src/components/SidePanel/Agents/AgentConfig.tsx +++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx @@ -13,6 +13,7 @@ import { processAgentOption } from '~/utils'; import Instructions from './Instructions'; import AgentAvatar from './AgentAvatar'; import FileContext from './FileContext'; +import SearchForm from './Search/Form'; import { useLocalize } from '~/hooks'; import FileSearch from './FileSearch'; import Artifacts from './Artifacts'; @@ -73,6 +74,10 @@ export default function AgentConfig({ () => agentsConfig?.capabilities?.includes(AgentCapabilities.file_search) ?? false, [agentsConfig], ); + const webSearchEnabled = useMemo( + () => agentsConfig?.capabilities?.includes(AgentCapabilities.web_search) ?? false, + [agentsConfig], + ); const codeEnabled = useMemo( () => agentsConfig?.capabilities?.includes(AgentCapabilities.execute_code) ?? false, [agentsConfig], @@ -257,13 +262,19 @@ export default function AgentConfig({ - {(codeEnabled || fileSearchEnabled || artifactsEnabled || ocrEnabled) && ( + {(codeEnabled || + fileSearchEnabled || + artifactsEnabled || + ocrEnabled || + webSearchEnabled) && (
{/* Code Execution */} {codeEnabled && } + {/* Web Search */} + {webSearchEnabled && } {/* File Context (OCR) */} {ocrEnabled && } {/* Artifacts */} diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx index d89178614..34f83b925 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx @@ -162,6 +162,9 @@ export default function AgentPanel({ if (data.file_search === true) { tools.push(Tools.file_search); } + if (data.web_search === true) { + tools.push(Tools.web_search); + } const { name, diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index 716ad4ce9..d265ba201 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -52,6 +52,7 @@ export default function AgentSelect({ }; const capabilities: TAgentCapabilities = { + [AgentCapabilities.web_search]: false, [AgentCapabilities.file_search]: false, [AgentCapabilities.execute_code]: false, [AgentCapabilities.end_after_tools]: false, diff --git a/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx b/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx index 9e199c3cb..2fa271c73 100644 --- a/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx +++ b/client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx @@ -3,6 +3,7 @@ import type { ApiKeyFormData } from '~/common'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { Input, Button, OGDialog } from '~/components/ui'; import { useLocalize } from '~/hooks'; +import type { RefObject } from 'react'; export default function ApiKeyDialog({ isOpen, @@ -13,6 +14,7 @@ export default function ApiKeyDialog({ isToolAuthenticated, register, handleSubmit, + triggerRef, }: { isOpen: boolean; onOpenChange: (open: boolean) => void; @@ -22,6 +24,7 @@ export default function ApiKeyDialog({ isToolAuthenticated: boolean; register: UseFormRegister; handleSubmit: UseFormHandleSubmit; + triggerRef?: RefObject; }) { const localize = useLocalize(); const languageIcons = [ @@ -38,7 +41,7 @@ export default function ApiKeyDialog({ ]; return ( - + (); + const { control, setValue, getValues } = methods; + const { + onSubmit, + isDialogOpen, + setIsDialogOpen, + handleRevokeApiKey, + methods: keyFormMethods, + } = useSearchApiKeyForm({ + onSubmit: () => { + setValue(AgentCapabilities.web_search, true, { shouldDirty: true }); + }, + onRevoke: () => { + setValue(AgentCapabilities.web_search, false, { shouldDirty: true }); + }, + }); + + const webSearchIsEnabled = useWatch({ control, name: AgentCapabilities.web_search }); + const isUserProvided = authTypes?.some(([, authType]) => authType === AuthType.USER_PROVIDED); + + const handleCheckboxChange = (checked: boolean) => { + if (isToolAuthenticated) { + setValue(AgentCapabilities.web_search, checked, { shouldDirty: true }); + } else if (webSearchIsEnabled) { + setValue(AgentCapabilities.web_search, false, { shouldDirty: true }); + } else { + setIsDialogOpen(true); + } + }; + + return ( + <> + +
+ ( + + )} + /> + +
+ {isUserProvided && (isToolAuthenticated || webSearchIsEnabled) && ( + + )} + + + +
+ + +
+

{localize('com_agents_search_info')}

+
+
+
+
+
+ + + ); +} diff --git a/client/src/components/SidePanel/Agents/Search/ApiKeyDialog.test.tsx b/client/src/components/SidePanel/Agents/Search/ApiKeyDialog.test.tsx new file mode 100644 index 000000000..c8d8f9e33 --- /dev/null +++ b/client/src/components/SidePanel/Agents/Search/ApiKeyDialog.test.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ApiKeyDialog from './ApiKeyDialog'; +import { AuthType, SearchCategories, RerankerTypes } from 'librechat-data-provider'; +import { useGetStartupConfig } from '~/data-provider'; + +// Mock useLocalize to just return the key +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, +})); + +jest.mock('~/data-provider', () => ({ + useGetStartupConfig: jest.fn(), +})); + +const mockRegister = (name: string) => ({ + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + name, +}); + +const defaultProps = { + isOpen: true, + onOpenChange: jest.fn(), + onSubmit: jest.fn(), + onRevoke: jest.fn(), + authTypes: [ + [SearchCategories.PROVIDERS, AuthType.USER_PROVIDED] as [string, AuthType], + [SearchCategories.SCRAPERS, AuthType.USER_PROVIDED] as [string, AuthType], + [SearchCategories.RERANKERS, AuthType.USER_PROVIDED] as [string, AuthType], + ], + isToolAuthenticated: false, + register: mockRegister as any, + handleSubmit: (fn: any) => (e: any) => fn(e), +}; + +describe('ApiKeyDialog', () => { + const mockUseGetStartupConfig = useGetStartupConfig as jest.Mock; + + afterEach(() => jest.clearAllMocks()); + + it('shows all dropdowns and both reranker fields when no config is set', () => { + mockUseGetStartupConfig.mockReturnValue({ data: {} }); + render(); + // Provider dropdown button + expect( + screen.getByRole('button', { name: 'com_ui_web_search_provider_serper' }), + ).toBeInTheDocument(); + // Scraper dropdown button + expect( + screen.getByRole('button', { name: 'com_ui_web_search_scraper_firecrawl' }), + ).toBeInTheDocument(); + // Reranker dropdown button + expect( + screen.getByRole('button', { name: 'com_ui_web_search_reranker_jina' }), + ).toBeInTheDocument(); + // Reranker fields (default is Jina) + expect(screen.getByPlaceholderText('com_ui_web_search_jina_key')).toBeInTheDocument(); + // Switch to Cohere + fireEvent.click(screen.getByText('com_ui_web_search_reranker_cohere')); + expect(screen.getByPlaceholderText('com_ui_web_search_cohere_key')).toBeInTheDocument(); + }); + + it('shows static text for provider and only provider input if provider is set', () => { + mockUseGetStartupConfig.mockReturnValue({ data: { webSearch: { searchProvider: 'serper' } } }); + render(); + expect(screen.getByText('com_ui_web_search_provider_serper')).toBeInTheDocument(); + // Should not find a dropdown button for provider + expect(screen.queryByRole('button', { name: /provider/i })).not.toBeInTheDocument(); + }); + + it('shows only Jina reranker field if rerankerType is set to jina', () => { + mockUseGetStartupConfig.mockReturnValue({ + data: { webSearch: { rerankerType: RerankerTypes.JINA } }, + }); + render(); + expect(screen.getByPlaceholderText('com_ui_web_search_jina_key')).toBeInTheDocument(); + expect(screen.queryByPlaceholderText('com_ui_web_search_cohere_key')).not.toBeInTheDocument(); + }); + + it('shows only Cohere reranker field if rerankerType is set to cohere', () => { + mockUseGetStartupConfig.mockReturnValue({ + data: { webSearch: { rerankerType: RerankerTypes.COHERE } }, + }); + render(); + expect(screen.getByPlaceholderText('com_ui_web_search_cohere_key')).toBeInTheDocument(); + expect(screen.queryByPlaceholderText('com_ui_web_search_jina_key')).not.toBeInTheDocument(); + }); + + it('shows documentation link for the visible reranker', () => { + mockUseGetStartupConfig.mockReturnValue({ data: {} }); + render(); + // Default is Jina + expect(screen.getByText('com_ui_web_search_reranker_jina_key')).toBeInTheDocument(); + // Switch to Cohere + fireEvent.click(screen.getByText('com_ui_web_search_reranker_cohere')); + expect(screen.getByText('com_ui_web_search_reranker_cohere_key')).toBeInTheDocument(); + }); + + it('does not render provider section if SYSTEM_DEFINED', () => { + mockUseGetStartupConfig.mockReturnValue({ data: {} }); + const props = { + ...defaultProps, + authTypes: [ + [SearchCategories.PROVIDERS, AuthType.SYSTEM_DEFINED], + [SearchCategories.SCRAPERS, AuthType.USER_PROVIDED], + [SearchCategories.RERANKERS, AuthType.USER_PROVIDED], + ] as [string, AuthType][], + }; + render(); + expect(screen.queryByText('com_ui_web_search_provider')).not.toBeInTheDocument(); + expect(screen.getByText('com_ui_web_search_scraper')).toBeInTheDocument(); + expect(screen.getByText('com_ui_web_search_reranker')).toBeInTheDocument(); + }); + + it('does not render scraper section if SYSTEM_DEFINED', () => { + mockUseGetStartupConfig.mockReturnValue({ data: {} }); + const props = { + ...defaultProps, + authTypes: [ + [SearchCategories.PROVIDERS, AuthType.USER_PROVIDED], + [SearchCategories.SCRAPERS, AuthType.SYSTEM_DEFINED], + [SearchCategories.RERANKERS, AuthType.USER_PROVIDED], + ] as [string, AuthType][], + }; + render(); + expect(screen.getByText('com_ui_web_search_provider')).toBeInTheDocument(); + expect(screen.queryByText('com_ui_web_search_scraper')).not.toBeInTheDocument(); + expect(screen.getByText('com_ui_web_search_reranker')).toBeInTheDocument(); + }); + + it('does not render reranker section if SYSTEM_DEFINED', () => { + mockUseGetStartupConfig.mockReturnValue({ data: {} }); + const props = { + ...defaultProps, + authTypes: [ + [SearchCategories.PROVIDERS, AuthType.USER_PROVIDED], + [SearchCategories.SCRAPERS, AuthType.USER_PROVIDED], + [SearchCategories.RERANKERS, AuthType.SYSTEM_DEFINED], + ] as [string, AuthType][], + }; + render(); + expect(screen.getByText('com_ui_web_search_provider')).toBeInTheDocument(); + expect(screen.getByText('com_ui_web_search_scraper')).toBeInTheDocument(); + expect(screen.queryByText('com_ui_web_search_reranker')).not.toBeInTheDocument(); + }); +}); diff --git a/client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx b/client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx new file mode 100644 index 000000000..bf82ce670 --- /dev/null +++ b/client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx @@ -0,0 +1,361 @@ +import { useState } from 'react'; +import { ChevronDown } from 'lucide-react'; +import * as Menu from '@ariakit/react/menu'; +import { AuthType, SearchCategories, RerankerTypes } from 'librechat-data-provider'; +import type { UseFormRegister, UseFormHandleSubmit } from 'react-hook-form'; +import type { SearchApiKeyFormData } from '~/hooks/Plugins/useAuthSearchTool'; +import type { MenuItemProps } from '~/common'; +import { Input, Button, OGDialog, Label } from '~/components/ui'; +import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; +import DropdownPopup from '~/components/ui/DropdownPopup'; +import { useGetStartupConfig } from '~/data-provider'; +import { useLocalize } from '~/hooks'; + +export default function ApiKeyDialog({ + isOpen, + onSubmit, + onRevoke, + onOpenChange, + authTypes, + isToolAuthenticated, + register, + handleSubmit, + triggerRef, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: SearchApiKeyFormData) => void; + onRevoke: () => void; + authTypes: [string, AuthType][]; + isToolAuthenticated: boolean; + register: UseFormRegister; + handleSubmit: UseFormHandleSubmit; + triggerRef?: React.RefObject; +}) { + const localize = useLocalize(); + const { data: config } = useGetStartupConfig(); + const [selectedReranker, setSelectedReranker] = useState< + RerankerTypes.JINA | RerankerTypes.COHERE + >( + config?.webSearch?.rerankerType === RerankerTypes.COHERE + ? RerankerTypes.COHERE + : RerankerTypes.JINA, + ); + + const [providerDropdownOpen, setProviderDropdownOpen] = useState(false); + const [scraperDropdownOpen, setScraperDropdownOpen] = useState(false); + const [rerankerDropdownOpen, setRerankerDropdownOpen] = useState(false); + + const providerItems: MenuItemProps[] = [ + { + label: localize('com_ui_web_search_provider_serper'), + onClick: () => {}, + }, + ]; + + const scraperItems: MenuItemProps[] = [ + { + label: localize('com_ui_web_search_scraper_firecrawl'), + onClick: () => {}, + }, + ]; + + const rerankerItems: MenuItemProps[] = [ + { + label: localize('com_ui_web_search_reranker_jina'), + onClick: () => setSelectedReranker(RerankerTypes.JINA), + }, + { + label: localize('com_ui_web_search_reranker_cohere'), + onClick: () => setSelectedReranker(RerankerTypes.COHERE), + }, + ]; + + const showProviderDropdown = !config?.webSearch?.searchProvider; + const showScraperDropdown = !config?.webSearch?.scraperType; + const showRerankerDropdown = !config?.webSearch?.rerankerType; + + // Determine which categories are SYSTEM_DEFINED + const providerAuthType = authTypes.find(([cat]) => cat === SearchCategories.PROVIDERS)?.[1]; + const scraperAuthType = authTypes.find(([cat]) => cat === SearchCategories.SCRAPERS)?.[1]; + const rerankerAuthType = authTypes.find(([cat]) => cat === SearchCategories.RERANKERS)?.[1]; + + function renderRerankerInput() { + if (config?.webSearch?.rerankerType === RerankerTypes.JINA) { + return ( + <> + (e.target.readOnly = false)} + {...register('jinaApiKey')} + /> + + + ); + } + if (config?.webSearch?.rerankerType === RerankerTypes.COHERE) { + return ( + <> + (e.target.readOnly = false)} + {...register('cohereApiKey')} + /> + + + ); + } + if (!config?.webSearch?.rerankerType && selectedReranker === RerankerTypes.JINA) { + return ( + <> + (e.target.readOnly = false)} + {...register('jinaApiKey')} + /> + + + ); + } + if (!config?.webSearch?.rerankerType && selectedReranker === RerankerTypes.COHERE) { + return ( + <> + (e.target.readOnly = false)} + {...register('cohereApiKey')} + /> + + + ); + } + return null; + } + + return ( + + +
{localize('com_ui_web_search')}
+
+ {localize('com_ui_web_search_api_subtitle')} +
+
+ {/* Search Provider Section */} + {providerAuthType !== AuthType.SYSTEM_DEFINED && ( +
+
+ + {showProviderDropdown ? ( + setProviderDropdownOpen(!providerDropdownOpen)} + className="flex items-center rounded-md border border-border-light px-3 py-1 text-sm text-text-secondary" + > + {localize('com_ui_web_search_provider_serper')} + + + } + /> + ) : ( +
+ {localize('com_ui_web_search_provider_serper')} +
+ )} +
+ (e.target.readOnly = false)} + {...register('serperApiKey', { required: true })} + /> + +
+ )} + + {/* Scraper Section */} + {scraperAuthType !== AuthType.SYSTEM_DEFINED && ( +
+
+ + {showScraperDropdown ? ( + setScraperDropdownOpen(!scraperDropdownOpen)} + className="flex items-center rounded-md border border-border-light px-3 py-1 text-sm text-text-secondary" + > + {localize('com_ui_web_search_scraper_firecrawl')} + + + } + /> + ) : ( +
+ {localize('com_ui_web_search_scraper_firecrawl')} +
+ )} +
+ (e.target.readOnly = false)} + className="mb-2" + {...register('firecrawlApiKey')} + /> + + +
+ )} + + {/* Reranker Section */} + {rerankerAuthType !== AuthType.SYSTEM_DEFINED && ( +
+
+ + {showRerankerDropdown && ( + setRerankerDropdownOpen(!rerankerDropdownOpen)} + className="flex items-center rounded-md border border-border-light px-3 py-1 text-sm text-text-secondary" + > + {selectedReranker === RerankerTypes.JINA + ? localize('com_ui_web_search_reranker_jina') + : localize('com_ui_web_search_reranker_cohere')} + + + } + /> + )} + {!showRerankerDropdown && ( +
+ {config?.webSearch?.rerankerType === RerankerTypes.COHERE + ? localize('com_ui_web_search_reranker_cohere') + : localize('com_ui_web_search_reranker_jina')} +
+ )} +
+ {renderRerankerInput()} +
+ )} +
+ + } + selection={{ + selectHandler: handleSubmit(onSubmit), + selectClasses: 'bg-green-500 hover:bg-green-600 text-white', + selectText: localize('com_ui_save'), + }} + buttons={ + isToolAuthenticated && ( + + ) + } + showCancelButton={true} + /> +
+ ); +} diff --git a/client/src/components/SidePanel/Agents/Search/Form.tsx b/client/src/components/SidePanel/Agents/Search/Form.tsx new file mode 100644 index 000000000..cc844ffcc --- /dev/null +++ b/client/src/components/SidePanel/Agents/Search/Form.tsx @@ -0,0 +1,31 @@ +import { Tools } from 'librechat-data-provider'; +import { useVerifyAgentToolAuth } from '~/data-provider'; +import { useLocalize } from '~/hooks'; +import Action from './Action'; + +export default function SearchForm() { + const localize = useLocalize(); + const { data } = useVerifyAgentToolAuth( + { toolId: Tools.web_search }, + { + retry: 1, + }, + ); + + return ( +
+
+
+
+ + {localize('com_ui_web_search')} + +
+
+
+
+ +
+
+ ); +} diff --git a/client/src/components/Web/Citation.tsx b/client/src/components/Web/Citation.tsx new file mode 100644 index 000000000..52dab33a7 --- /dev/null +++ b/client/src/components/Web/Citation.tsx @@ -0,0 +1,168 @@ +import { memo, useState, useContext } from 'react'; +import type { CitationProps } from './types'; +import { SourceHovercard, FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard'; +import { CitationContext, useCitation, useCompositeCitations } from './Context'; +import { useLocalize } from '~/hooks'; + +interface CompositeCitationProps { + citationId?: string; + node?: { + properties?: CitationProps; + }; +} + +export function CompositeCitation(props: CompositeCitationProps) { + const localize = useLocalize(); + const { citations, citationId } = props.node?.properties ?? ({} as CitationProps); + const { setHoveredCitationId } = useContext(CitationContext); + const [currentPage, setCurrentPage] = useState(0); + const sources = useCompositeCitations(citations || []); + + if (!sources || sources.length === 0) return null; + const totalPages = sources.length; + + const getCitationLabel = () => { + if (!sources || sources.length === 0) return localize('com_citation_source'); + + const firstSource = sources[0]; + const remainingCount = sources.length - 1; + const attribution = + firstSource.attribution || + firstSource.title || + getCleanDomain(firstSource.link || '') || + localize('com_citation_source'); + + return remainingCount > 0 ? `${attribution} +${remainingCount}` : attribution; + }; + + const handlePrevPage = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (currentPage > 0) { + setCurrentPage(currentPage - 1); + } + }; + + const handleNextPage = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (currentPage < totalPages - 1) { + setCurrentPage(currentPage + 1); + } + }; + + const currentSource = sources?.[currentPage]; + + return ( + setHoveredCitationId(citationId || null)} + onMouseLeave={() => setHoveredCitationId(null)} + > + {totalPages > 1 && ( + + + + + + + {currentPage + 1}/{totalPages} + + + )} + + + + {currentSource.attribution} + + +

{currentSource.title}

+

+ {currentSource.snippet} +

+
+ ); +} + +interface CitationComponentProps { + citationId: string; + citationType: 'span' | 'standalone' | 'composite' | 'group' | 'navlist'; + node?: { + properties?: CitationProps; + }; +} + +export function Citation(props: CitationComponentProps) { + const localize = useLocalize(); + const { citation, citationId } = props.node?.properties ?? {}; + const { setHoveredCitationId } = useContext(CitationContext); + const refData = useCitation({ + turn: citation?.turn || 0, + refType: citation?.refType, + index: citation?.index || 0, + }); + if (!refData) return null; + + const getCitationLabel = () => { + return ( + refData.attribution || + refData.title || + getCleanDomain(refData.link || '') || + localize('com_citation_source') + ); + }; + + return ( + setHoveredCitationId(citationId || null)} + onMouseLeave={() => setHoveredCitationId(null)} + /> + ); +} + +export interface HighlightedTextProps { + children: React.ReactNode; + citationId?: string; +} + +export function useHighlightState(citationId: string | undefined) { + const { hoveredCitationId } = useContext(CitationContext); + return citationId && hoveredCitationId === citationId; +} + +export const HighlightedText = memo(function HighlightedText({ + children, + citationId, +}: HighlightedTextProps) { + const isHighlighted = useHighlightState(citationId); + + return ( + + {children} + + ); +}); diff --git a/client/src/components/Web/Context.tsx b/client/src/components/Web/Context.tsx new file mode 100644 index 000000000..8c91e8770 --- /dev/null +++ b/client/src/components/Web/Context.tsx @@ -0,0 +1,105 @@ +import { createContext, useContext } from 'react'; +import type { SearchRefType, ValidSource, ResultReference } from 'librechat-data-provider'; +import type * as t from './types'; +import { useSearchContext } from '~/Providers'; + +export interface CitationContextType { + hoveredCitationId: string | null; + setHoveredCitationId: (id: string | null) => void; +} + +export const CitationContext = createContext({ + hoveredCitationId: null, + setHoveredCitationId: () => {}, +}); + +export function useHighlightState(citationId: string | undefined) { + const { hoveredCitationId } = useContext(CitationContext); + return citationId && hoveredCitationId === citationId; +} + +export type CitationSource = (ValidSource | ResultReference) & { + turn: number; + refType: string | SearchRefType; + index: number; +}; + +const refTypeMap: Record = { + search: 'organic', + ref: 'references', + news: 'topStories', +}; + +export function useCitation({ + turn, + index, + refType: _refType, +}: { + turn: number; + index: number; + refType?: SearchRefType | string; +}): (t.Citation & t.Reference) | undefined { + const { searchResults } = useSearchContext(); + if (!_refType) { + return undefined; + } + const refType = refTypeMap[_refType.toLowerCase()] + ? refTypeMap[_refType.toLowerCase()] + : _refType; + + if (!searchResults || !searchResults[turn] || !searchResults[turn][refType]) { + return undefined; + } + + const source: CitationSource = searchResults[turn][refType][index]; + + if (!source) { + return undefined; + } + + return { + ...source, + turn, + refType: _refType.toLowerCase(), + index, + link: source.link ?? '', + title: source.title ?? '', + snippet: source['snippet'] ?? '', + attribution: source.attribution ?? '', + }; +} + +export function useCompositeCitations( + citations: Array<{ turn: number; refType: SearchRefType | string; index: number }>, +): Array { + const { searchResults } = useSearchContext(); + + const result: Array = []; + + for (const { turn, refType: _refType, index } of citations) { + const refType = refTypeMap[_refType.toLowerCase()] + ? refTypeMap[_refType.toLowerCase()] + : _refType; + + if (!searchResults || !searchResults[turn] || !searchResults[turn][refType]) { + continue; + } + const source: CitationSource = searchResults[turn][refType][index]; + if (!source) { + continue; + } + + result.push({ + ...source, + turn, + refType: _refType.toLowerCase(), + index, + link: source.link ?? '', + title: source.title ?? '', + snippet: source['snippet'] ?? '', + attribution: source.attribution ?? '', + }); + } + + return result; +} diff --git a/client/src/components/Web/SourceHovercard.tsx b/client/src/components/Web/SourceHovercard.tsx new file mode 100644 index 000000000..50f7eec6b --- /dev/null +++ b/client/src/components/Web/SourceHovercard.tsx @@ -0,0 +1,113 @@ +import React, { ReactNode } from 'react'; +import * as Ariakit from '@ariakit/react'; +import { ChevronDown } from 'lucide-react'; +import { VisuallyHidden } from '@ariakit/react'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +export interface SourceData { + link: string; + title?: string; + attribution?: string; + snippet?: string; +} + +interface SourceHovercardProps { + source: SourceData; + label: string; + onMouseEnter?: () => void; + onMouseLeave?: () => void; + children?: ReactNode; +} + +/** Helper to get domain favicon */ +function getFaviconUrl(domain: string) { + return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; +} + +/** Helper to get clean domain name */ +export function getCleanDomain(url: string) { + const domain = url.replace(/(^\w+:|^)\/\//, '').split('/')[0]; + return domain.startsWith('www.') ? domain.substring(4) : domain; +} + +export function FaviconImage({ domain, className = '' }: { domain: string; className?: string }) { + return ( +
+
+ {domain} +
+
+ ); +} + +export function SourceHovercard({ + source, + label, + onMouseEnter, + onMouseLeave, + children, +}: SourceHovercardProps) { + const localize = useLocalize(); + const domain = getCleanDomain(source.link || ''); + + return ( + + + + + {label} + + } + /> + + {localize('com_citation_more_details', { label })} + + + + + {children} + {!children && ( + <> + + + + {source.attribution || domain} + + + +

+ {source.title || source.link} +

+ {source.snippet && ( + + {source.snippet} + + )} + + )} +
+
+
+
+ ); +} diff --git a/client/src/components/Web/Sources.tsx b/client/src/components/Web/Sources.tsx new file mode 100644 index 000000000..ea631a1fa --- /dev/null +++ b/client/src/components/Web/Sources.tsx @@ -0,0 +1,409 @@ +import React, { useMemo } from 'react'; +import * as Ariakit from '@ariakit/react'; +import { VisuallyHidden } from '@ariakit/react'; +import { Globe, Newspaper, Image, ChevronDown } from 'lucide-react'; +import type { ValidSource, ImageResult } from 'librechat-data-provider'; +import { FaviconImage, getCleanDomain } from '~/components/Web/SourceHovercard'; +import { useSearchContext } from '~/Providers'; +import { AnimatedTabs } from '~/components/ui'; +import { useLocalize } from '~/hooks'; +import { + OGDialog, + OGDialogTitle, + OGDialogContent, + OGDialogTrigger, +} from '~/components/ui/OriginalDialog'; + +interface SourceItemProps { + source: ValidSource; + isNews?: boolean; + expanded?: boolean; +} + +function SourceItem({ source, isNews, expanded = false }: SourceItemProps) { + const localize = useLocalize(); + const domain = getCleanDomain(source.link); + + if (expanded) { + return ( + +
+ + {domain} +
+
+ + {source.title || source.link} + + {'snippet' in source && source.snippet && ( + + {source.snippet} + + )} +
+
+ ); + } + + return ( + + +
+ +
+ + {domain} +
+
+ + {source.title || source.link} + + {/* {'snippet' in source && source.snippet && ( + + {source.snippet} + + )} */} +
+ + } + /> + + + {localize('com_citation_more_details', { label: domain })} + + + + + +
+
+ +

+ {source.title || source.link} +

+ {'snippet' in source && source.snippet && ( + + {source.snippet} + + )} +
+ {'imageUrl' in source && source.imageUrl && ( +
+ {source.title +
+ )} +
+
+
+
+
+ ); +} + +function ImageItem({ image }: { image: ImageResult }) { + const localize = useLocalize(); + return ( + + {image.imageUrl && ( +
+ {image.title + {image.title && ( +
+ {image.title} +
+ )} +
+ )} +
+ ); +} + +export function StackedFavicons({ + sources, + start = 0, + end = 3, +}: { + sources: ValidSource[]; + start?: number; + end?: number; +}) { + let slice = [start, end]; + if (start < 0) { + slice = [start]; + } + return ( +
+ {sources.slice(...slice).map((source, i) => ( + 0 ? 'ml-[-6px]' : ''} + /> + ))} +
+ ); +} + +function SourcesGroup({ sources, limit = 3 }: { sources: ValidSource[]; limit?: number }) { + const localize = useLocalize(); + const visibleSources = sources.slice(0, limit); + const remainingSources = sources.slice(limit); + const hasMoreSources = remainingSources.length > 0; + + /** Calculate grid columns based on number of items (including the +X sources button if present) */ + const totalItems = hasMoreSources ? visibleSources.length + 1 : visibleSources.length; + const gridCols = `grid-cols-${Math.min(totalItems, 4)}`; + + return ( +
+ + {visibleSources.map((source, i) => ( +
+ +
+ ))} + {hasMoreSources && ( + +
+ + + {localize('com_sources_more_sources', { count: remainingSources.length })} + +
+
+ )} + +
+ + {localize('com_sources_title')} + + +
+ +
+
+
+ ); +} + +function TabWithIcon({ label, icon }: { label: string; icon: React.ReactNode }) { + return ( +
+ {React.cloneElement(icon as React.ReactElement, { size: 14 })} + {label} +
+ ); +} + +export default function Sources() { + const localize = useLocalize(); + const { searchResults } = useSearchContext(); + + const { organicSources, topStories, images, hasAnswerBox } = useMemo(() => { + if (!searchResults) { + return { + organicSources: [], + topStories: [], + images: [], + hasAnswerBox: false, + }; + } + + const organicSourcesMap = new Map(); + const topStoriesMap = new Map(); + const imagesMap = new Map(); + let hasAnswerBox = false; + + Object.values(searchResults).forEach((result) => { + if (!result) return; + + if (result.organic?.length) { + result.organic.forEach((source) => { + if (source.link) { + organicSourcesMap.set(source.link, source); + } + }); + } + if (result.references?.length) { + result.references.forEach((source) => { + if (source.type === 'image') { + imagesMap.set(source.link, { + ...source, + imageUrl: source.link, + }); + return; + } + if (source.link) { + organicSourcesMap.set(source.link, source); + } + }); + } + if (result.topStories?.length) { + result.topStories.forEach((source) => { + if (source.link) { + topStoriesMap.set(source.link, source); + } + }); + } + if (result.images?.length) { + result.images.forEach((image) => { + if (image.imageUrl) { + imagesMap.set(image.imageUrl, image); + } + }); + } + if (result.answerBox) { + hasAnswerBox = true; + } + }); + + return { + organicSources: Array.from(organicSourcesMap.values()), + topStories: Array.from(topStoriesMap.values()), + images: Array.from(imagesMap.values()), + hasAnswerBox, + }; + }, [searchResults]); + + const tabs = useMemo(() => { + const availableTabs: Array<{ label: React.ReactNode; content: React.ReactNode }> = []; + + if (organicSources.length || topStories.length || hasAnswerBox) { + availableTabs.push({ + label: } />, + content: , + }); + } + + if (topStories.length) { + availableTabs.push({ + label: } />, + content: , + }); + } + + if (images.length) { + availableTabs.push({ + label: } />, + content: ( +
+ {images.map((item, i) => ( + + ))} +
+ ), + }); + } + + return availableTabs; + }, [organicSources, topStories, images, hasAnswerBox, localize]); + + if (!tabs.length) return null; + + return ( + + ); +} diff --git a/client/src/components/Web/index.ts b/client/src/components/Web/index.ts new file mode 100644 index 000000000..0f0432f15 --- /dev/null +++ b/client/src/components/Web/index.ts @@ -0,0 +1,2 @@ +export * from './plugin'; +export type * from './types'; diff --git a/client/src/components/Web/plugin.ts b/client/src/components/Web/plugin.ts new file mode 100644 index 000000000..7ca1c5951 --- /dev/null +++ b/client/src/components/Web/plugin.ts @@ -0,0 +1,250 @@ +import { visit } from 'unist-util-visit'; +import type { Node } from 'unist'; +import type { Citation, CitationNode } from './types'; + +const SPAN_REGEX = /(\\ue203.*?\\ue204)/g; +const COMPOSITE_REGEX = /(\\ue200.*?\\ue201)/g; +const STANDALONE_PATTERN = /\\ue202turn(\d+)(search|image|news|video|ref)(\d+)/g; +const CLEANUP_REGEX = /\\ue200|\\ue201|\\ue202|\\ue203|\\ue204|\\ue206/g; + +/** + * Checks if a standalone marker is truly standalone (not inside a composite block) + */ +function isStandaloneMarker(text: string, position: number): boolean { + const beforeText = text.substring(0, position); + const lastUe200 = beforeText.lastIndexOf('\\ue200'); + const lastUe201 = beforeText.lastIndexOf('\\ue201'); + + return lastUe200 === -1 || (lastUe201 !== -1 && lastUe201 > lastUe200); +} + +/** + * Find the next pattern match from the current position + */ +function findNextMatch( + text: string, + position: number, +): { type: string; match: RegExpExecArray | null; index: number } | null { + // Reset regex lastIndex to start from current position + SPAN_REGEX.lastIndex = position; + COMPOSITE_REGEX.lastIndex = position; + STANDALONE_PATTERN.lastIndex = position; + + // Find next occurrence of each pattern + const spanMatch = SPAN_REGEX.exec(text); + const compositeMatch = COMPOSITE_REGEX.exec(text); + + // For standalone, we need to check each match + let standaloneMatch: RegExpExecArray | null = null; + STANDALONE_PATTERN.lastIndex = position; + + // Find the first standalone match that's not inside a composite block + let match: RegExpExecArray | null; + while (!standaloneMatch && (match = STANDALONE_PATTERN.exec(text)) !== null) { + if (isStandaloneMarker(text, match.index)) { + standaloneMatch = match; + } + } + + // Find closest match + let nextMatch: RegExpExecArray | null = null; + let matchType = ''; + let matchIndex = -1; + let typeIndex = -1; + + if (spanMatch && (!nextMatch || spanMatch.index < matchIndex || matchIndex === -1)) { + nextMatch = spanMatch; + matchType = 'span'; + matchIndex = spanMatch.index; + // We can use a counter for typeIndex if needed + typeIndex = 0; + } + + if (compositeMatch && (!nextMatch || compositeMatch.index < matchIndex || matchIndex === -1)) { + nextMatch = compositeMatch; + matchType = 'composite'; + matchIndex = compositeMatch.index; + typeIndex = 0; + } + + if (standaloneMatch && (!nextMatch || standaloneMatch.index < matchIndex || matchIndex === -1)) { + nextMatch = standaloneMatch; + matchType = 'standalone'; + matchIndex = standaloneMatch.index; + typeIndex = 0; + } + + if (!nextMatch) return null; + + return { type: matchType, match: nextMatch, index: typeIndex }; +} + +function processTree(tree: Node) { + visit(tree, 'text', (node, index, parent) => { + const textNode = node as CitationNode; + const parentNode = parent as CitationNode; + + if (typeof textNode.value !== 'string') return; + + const originalValue = textNode.value; + const segments: Array = []; + + // Single-pass processing through the string + let currentPosition = 0; + + // Important change: Create a map to track citation IDs by their position + // This ensures consistent IDs across multiple segments + const citationIds = new Map(); + const typeCounts = { span: 0, composite: 0, standalone: 0 }; + + while (currentPosition < originalValue.length) { + const nextMatchInfo = findNextMatch(originalValue, currentPosition); + + if (!nextMatchInfo) { + // No more matches, add remaining content with cleanup + const remainingText = originalValue.substring(currentPosition).replace(CLEANUP_REGEX, ''); + if (remainingText) { + segments.push({ type: 'text', value: remainingText }); + } + break; + } + + const { type, match } = nextMatchInfo; + const matchIndex = match!.index; + const matchText = match![0]; + + // Add cleaned text before this match + if (matchIndex > currentPosition) { + const textBeforeMatch = originalValue + .substring(currentPosition, matchIndex) + .replace(CLEANUP_REGEX, ''); + + if (textBeforeMatch) { + segments.push({ type: 'text', value: textBeforeMatch }); + } + } + + // Generate a unique ID for this citation based on its position in the text + const citationId = `${type}-${typeCounts[type as keyof typeof typeCounts]}-${matchIndex}`; + citationIds.set(matchIndex, citationId); + + // Process based on match type + switch (type) { + case 'span': { + const spanText = matchText; + const cleanText = spanText.replace(/\\ue203|\\ue204/g, ''); + + // Look ahead for associated citation + let associatedCitationId: string | null = null; + const endOfSpan = matchIndex + matchText.length; + + // Check if there's a citation right after this span + const nextCitation = findNextMatch(originalValue, endOfSpan); + if ( + nextCitation && + (nextCitation.type === 'standalone' || nextCitation.type === 'composite') && + nextCitation.match!.index - endOfSpan < 5 + ) { + // Use the ID that will be generated for the next citation + const nextIndex = nextCitation.match!.index; + const nextType = nextCitation.type; + associatedCitationId = `${nextType}-${typeCounts[nextType as keyof typeof typeCounts]}-${nextIndex}`; + } + + segments.push({ + type: 'highlighted-text', + data: { + hName: 'highlighted-text', + hProperties: { citationId: associatedCitationId }, + }, + children: [{ type: 'text', value: cleanText }], + }); + + typeCounts.span++; + break; + } + + case 'composite': { + const compositeText = matchText; + + // Use a regular expression to extract reference indices + const compositeRefRegex = new RegExp(STANDALONE_PATTERN.source, 'g'); + let refMatch: RegExpExecArray | null; + const citations: Array = []; + + while ((refMatch = compositeRefRegex.exec(compositeText)) !== null) { + const turn = Number(refMatch[1]); + const refType = refMatch[2]; + const refIndex = Number(refMatch[3]); + + citations.push({ + turn, + refType, + index: refIndex, + }); + } + + if (citations.length > 0) { + segments.push({ + type: 'composite-citation', + data: { + hName: 'composite-citation', + hProperties: { + citations, + citationId: citationId, + }, + }, + }); + } + + typeCounts.composite++; + break; + } + + case 'standalone': { + // Extract reference info + const turn = Number(match![1]); + const refType = match![2]; + const refIndex = Number(match![3]); + + segments.push({ + type: 'citation', + data: { + hName: 'citation', + hProperties: { + citation: { + turn, + refType, + index: refIndex, + }, + citationType: 'standalone', + citationId: citationId, + }, + }, + }); + + typeCounts.standalone++; + break; + } + } + + // Move position forward + currentPosition = matchIndex + matchText.length; + } + + // Replace the original node with our segments or clean up the original + if (segments.length > 0 && index !== undefined) { + parentNode.children?.splice(index, 1, ...segments); + return index + segments.length; + } else if (textNode.value !== textNode.value.replace(CLEANUP_REGEX, '')) { + // If we didn't create segments but there are markers to clean up + textNode.value = textNode.value.replace(CLEANUP_REGEX, ''); + } + }); +} + +export function unicodeCitation() { + return (tree: Node) => { + processTree(tree); + }; +} diff --git a/client/src/components/Web/types.ts b/client/src/components/Web/types.ts new file mode 100644 index 000000000..180721ffa --- /dev/null +++ b/client/src/components/Web/types.ts @@ -0,0 +1,32 @@ +import type { SearchRefType } from 'librechat-data-provider'; +export type Citation = { turn: number; refType: SearchRefType | string; index: number }; + +export type CitationProps = { + citationId?: string | null; + citationType?: string; + citations?: Array; + citation?: Citation; +}; + +export type CitationNode = { + type?: string; + value?: string; + data?: { + hName?: string; + hProperties?: CitationProps; + }; + children?: Array; +}; + +export interface Sitelink { + title: string; + link: string; +} + +export interface Reference { + title: string; + link: string; + snippet: string; + sitelinks?: Sitelink[]; + attribution: string; +} diff --git a/client/src/components/ui/AnimatedTabs.css b/client/src/components/ui/AnimatedTabs.css new file mode 100644 index 000000000..808f5c4ac --- /dev/null +++ b/client/src/components/ui/AnimatedTabs.css @@ -0,0 +1,57 @@ +/* AnimatedTabs.css */ +.animated-tab-panel { + transition-property: opacity, translate; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; + animation-duration: 300ms; +} + +/* Sliding underline animation for tabs */ +.animated-tab-list { + position: relative; +} + +.animated-tab-list::after { + content: ''; + position: absolute; + bottom: 0; + height: 2px; + background-color: currentColor; /* Inherit color from active tab */ + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + left: var(--tab-left, 0); + width: var(--tab-width, 0); +} + +.animated-tab { + position: relative; +} + +.animated-tab[data-state="active"] { + border-bottom-color: transparent !important; +} + +.animated-tab-panel[data-enter] { + opacity: 1 !important; + translate: 0 !important; +} + +@media (prefers-reduced-motion: reduce) { + .animated-tab-panel { + transition: none; + } +} + +.animated-tab-panel:not([data-open]) { + position: absolute; + top: 0px; +} + +.animated-panels:has(> [data-was-open]) > .animated-tab-panel { + opacity: 0; + translate: -100%; +} + +.animated-panels [data-was-open] ~ .animated-tab-panel, +.animated-panels [data-open] ~ .animated-tab-panel { + translate: 100%; +} diff --git a/client/src/components/ui/AnimatedTabs.tsx b/client/src/components/ui/AnimatedTabs.tsx new file mode 100644 index 000000000..809699f30 --- /dev/null +++ b/client/src/components/ui/AnimatedTabs.tsx @@ -0,0 +1,160 @@ +import * as Ariakit from '@ariakit/react'; +import { ReactNode, forwardRef, useEffect, useRef } from 'react'; +import type { ElementRef } from 'react'; +import { cn } from '~/utils'; +import './AnimatedTabs.css'; + +export interface TabItem { + id?: string; + label: ReactNode; + content: ReactNode; + disabled?: boolean; +} + +export interface AnimatedTabsProps { + tabs: TabItem[]; + className?: string; + tabListClassName?: string; + tabClassName?: string; + tabPanelClassName?: string; + tabListProps?: Ariakit.TabListProps; + containerClassName?: string; + defaultSelectedId?: string; +} + +function usePrevious(value: T) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} + +const Tab = forwardRef, Ariakit.TabProps>(function Tab(props, ref) { + const tabRef = useRef(null); + useEffect(() => { + const tabElement = tabRef.current; + if (!tabElement) return; + + const updateState = () => { + const isSelected = tabElement.getAttribute('aria-selected') === 'true'; + tabElement.setAttribute('data-state', isSelected ? 'active' : 'inactive'); + }; + + updateState(); + + const observer = new MutationObserver(updateState); + observer.observe(tabElement, { attributes: true, attributeFilter: ['aria-selected'] }); + + return () => observer.disconnect(); + }, []); + + return ( + { + // Forward the ref to both our local ref and the provided ref + tabRef.current = node; + if (typeof ref === 'function') ref(node); + else if (ref) ref.current = node; + }} + {...props} + className={`animated-tab aria-selected:text-token-text-primary flex select-none items-center justify-center gap-2 whitespace-nowrap border-none text-sm font-medium outline-none transition-colors aria-disabled:opacity-50 ${props.className || ''}`} + /> + ); +}); + +const TabPanel = forwardRef, Ariakit.TabPanelProps>( + function TabPanel(props, ref) { + const tab = Ariakit.useTabContext(); + const previousTabId = usePrevious(Ariakit.useStoreState(tab, 'selectedId')); + const wasOpen = props.tabId && previousTabId === props.tabId; + + return ( + + ); + }, +); + +export function AnimatedTabs({ + tabs, + className = '', + tabListClassName = '', + tabClassName = '', + tabPanelClassName = '', + containerClassName = '', + tabListProps = {}, + defaultSelectedId, +}: AnimatedTabsProps) { + const tabIds = tabs.map((tab, index) => tab.id || `tab-${index}`); + const firstTabId = defaultSelectedId || tabIds[0]; + const tabListRef = useRef(null); + + useEffect(() => { + const tabList = tabListRef.current; + if (!tabList) return; + + // Function to update the underline position + const updateUnderline = () => { + const activeTab = tabList.querySelector('[data-state="active"]') as HTMLElement; + if (!activeTab) return; + + tabList.style.setProperty('--tab-left', `${activeTab.offsetLeft}px`); + tabList.style.setProperty('--tab-width', `${activeTab.offsetWidth}px`); + }; + + updateUnderline(); + + const observer = new MutationObserver(updateUnderline); + observer.observe(tabList, { attributes: true, subtree: true, attributeFilter: ['data-state'] }); + + return () => observer.disconnect(); + }, [tabs]); + + return ( +
+ + + {tabs.map((tab, index) => ( + + {tab.label} + + ))} + + +
+ {tabs.map((tab, index) => ( + + {tab.content} + + ))} +
+
+
+ ); +} diff --git a/client/src/components/ui/CheckboxButton.tsx b/client/src/components/ui/CheckboxButton.tsx index 8b6cb1f7b..7985b7374 100644 --- a/client/src/components/ui/CheckboxButton.tsx +++ b/client/src/components/ui/CheckboxButton.tsx @@ -1,22 +1,19 @@ import { useEffect } from 'react'; import { Checkbox, useStoreState, useCheckboxStore } from '@ariakit/react'; import { cn } from '~/utils'; +import * as React from 'react'; -export default function CheckboxButton({ - label, - icon, - setValue, - className, - defaultChecked, - isCheckedClassName, -}: { - label: string; - className?: string; - icon?: React.ReactNode; - defaultChecked?: boolean; - isCheckedClassName?: string; - setValue?: (isChecked: boolean) => void; -}) { +const CheckboxButton = React.forwardRef< + HTMLInputElement, + { + icon?: React.ReactNode; + label: string; + className?: string; + defaultChecked?: boolean; + isCheckedClassName?: string; + setValue?: (e: React.ChangeEvent, isChecked: boolean) => void; + } +>(({ icon, label, setValue, className, defaultChecked, isCheckedClassName }, ref) => { const checkbox = useCheckboxStore(); const isChecked = useStoreState(checkbox, (state) => state?.value); const onChange = (e: React.ChangeEvent) => { @@ -24,7 +21,7 @@ export default function CheckboxButton({ if (typeof isChecked !== 'boolean') { return; } - setValue?.(!isChecked); + setValue?.(e, !isChecked); }; useEffect(() => { if (defaultChecked) { @@ -34,6 +31,7 @@ export default function CheckboxButton({ return ( {label} ); -} +}); + +CheckboxButton.displayName = 'CheckboxButton'; + +export default CheckboxButton; diff --git a/client/src/components/ui/OriginalDialog.tsx b/client/src/components/ui/OriginalDialog.tsx index 83405b1a6..28a817781 100644 --- a/client/src/components/ui/OriginalDialog.tsx +++ b/client/src/components/ui/OriginalDialog.tsx @@ -4,7 +4,7 @@ import { X } from 'lucide-react'; import { cn } from '~/utils'; interface OGDialogProps extends DialogPrimitive.DialogProps { - triggerRef?: React.RefObject; + triggerRef?: React.RefObject; } const Dialog = React.forwardRef( diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts index 2708379b6..5edc18bd1 100644 --- a/client/src/components/ui/index.ts +++ b/client/src/components/ui/index.ts @@ -1,3 +1,4 @@ +export * from './AnimatedTabs'; export * from './AlertDialog'; export * from './Breadcrumb'; export * from './Button'; diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index dd9021c9b..c4d76d6a1 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -245,7 +245,7 @@ export default function useChatFunctions({ const generation = editedText ?? latestMessage?.text ?? ''; const responseText = isEditOrContinue ? generation : ''; - const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null; + const responseMessageId = editedMessageId ?? latestMessage?.messageId + '_' ?? null; const initialResponse: TMessage = { sender: responseSender, text: responseText, diff --git a/client/src/hooks/Messages/useSearchResultsByTurn.ts b/client/src/hooks/Messages/useSearchResultsByTurn.ts new file mode 100644 index 000000000..02d5241ce --- /dev/null +++ b/client/src/hooks/Messages/useSearchResultsByTurn.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react'; +import { TAttachment, Tools, SearchResultData } from 'librechat-data-provider'; + +/** + * Hook that creates a map of turn numbers to SearchResultData from web search attachments + * @param attachments Array of attachment metadata + * @returns A map of turn numbers to their corresponding search result data + */ +export function useSearchResultsByTurn(attachments?: TAttachment[]) { + const searchResultsByTurn = useMemo(() => { + const turnMap: { [key: string]: SearchResultData } = {}; + + attachments?.forEach((attachment) => { + if (attachment.type === Tools.web_search && attachment[Tools.web_search]) { + const searchData = attachment[Tools.web_search]; + if (searchData && typeof searchData.turn === 'number') { + turnMap[searchData.turn.toString()] = searchData; + } + } + }); + + return turnMap; + }, [attachments]); + + return searchResultsByTurn; +} diff --git a/client/src/hooks/Plugins/index.ts b/client/src/hooks/Plugins/index.ts index 6fe68c456..c2a0ffe97 100644 --- a/client/src/hooks/Plugins/index.ts +++ b/client/src/hooks/Plugins/index.ts @@ -1,4 +1,5 @@ export { default as useAuthCodeTool } from './useAuthCodeTool'; export { default as usePluginInstall } from './usePluginInstall'; export { default as useCodeApiKeyForm } from './useCodeApiKeyForm'; +export { default as useSearchApiKeyForm } from './useSearchApiKeyForm'; export { default as usePluginDialogHelpers } from './usePluginDialogHelpers'; diff --git a/client/src/hooks/Plugins/useAuthSearchTool.ts b/client/src/hooks/Plugins/useAuthSearchTool.ts new file mode 100644 index 000000000..cca56485d --- /dev/null +++ b/client/src/hooks/Plugins/useAuthSearchTool.ts @@ -0,0 +1,84 @@ +import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { AuthType, Tools, QueryKeys } from 'librechat-data-provider'; +import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; + +export type SearchApiKeyFormData = { + serperApiKey: string; + firecrawlApiKey: string; + firecrawlApiUrl: string; + jinaApiKey: string; + cohereApiKey: string; +}; + +const useAuthSearchTool = (options?: { isEntityTool: boolean }) => { + const queryClient = useQueryClient(); + const isEntityTool = options?.isEntityTool ?? true; + const updateUserPlugins = useUpdateUserPluginsMutation({ + onMutate: (vars) => { + queryClient.setQueryData([QueryKeys.toolAuth, Tools.web_search], () => { + return { + authenticated: vars.action === 'install', + authTypes: + vars.action === 'install' + ? [ + ['providers', AuthType.USER_PROVIDED], + ['scrapers', AuthType.USER_PROVIDED], + ['rerankers', AuthType.USER_PROVIDED], + ] + : [], + }; + }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.toolAuth, Tools.web_search]); + }, + onError: () => { + queryClient.invalidateQueries([QueryKeys.toolAuth, Tools.web_search]); + }, + }); + + const installTool = useCallback( + (data: SearchApiKeyFormData) => { + const auth = Object.entries({ + serperApiKey: data.serperApiKey, + firecrawlApiKey: data.firecrawlApiKey, + firecrawlApiUrl: data.firecrawlApiUrl, + jinaApiKey: data.jinaApiKey, + cohereApiKey: data.cohereApiKey, + }).reduce( + (acc, [key, value]) => { + if (value) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + updateUserPlugins.mutate({ + pluginKey: Tools.web_search, + action: 'install', + auth, + isEntityTool, + }); + }, + [updateUserPlugins, isEntityTool], + ); + + const removeTool = useCallback(() => { + updateUserPlugins.mutate({ + pluginKey: Tools.web_search, + action: 'uninstall', + auth: {}, + isEntityTool, + }); + }, [updateUserPlugins, isEntityTool]); + + return { + removeTool, + installTool, + }; +}; + +export default useAuthSearchTool; diff --git a/client/src/hooks/Plugins/useSearchApiKeyForm.ts b/client/src/hooks/Plugins/useSearchApiKeyForm.ts new file mode 100644 index 000000000..0044f1c0d --- /dev/null +++ b/client/src/hooks/Plugins/useSearchApiKeyForm.ts @@ -0,0 +1,42 @@ +import { useState, useCallback } from 'react'; +import { useForm } from 'react-hook-form'; +import useAuthSearchTool from '~/hooks/Plugins/useAuthSearchTool'; +import type { SearchApiKeyFormData } from '~/hooks/Plugins/useAuthSearchTool'; + +export default function useSearchApiKeyForm({ + onSubmit, + onRevoke, +}: { + onSubmit?: () => void; + onRevoke?: () => void; +}) { + const methods = useForm(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const { installTool, removeTool } = useAuthSearchTool({ isEntityTool: true }); + const { reset } = methods; + + const onSubmitHandler = useCallback( + (data: SearchApiKeyFormData) => { + reset(); + installTool(data); + setIsDialogOpen(false); + onSubmit?.(); + }, + [onSubmit, reset, installTool], + ); + + const handleRevokeApiKey = useCallback(() => { + reset(); + removeTool(); + setIsDialogOpen(false); + onRevoke?.(); + }, [reset, onRevoke, removeTool]); + + return { + methods, + isDialogOpen, + setIsDialogOpen, + handleRevokeApiKey, + onSubmit: onSubmitHandler, + }; +} diff --git a/client/src/hooks/SSE/useAttachmentHandler.ts b/client/src/hooks/SSE/useAttachmentHandler.ts index 2eb748ab1..cd511defd 100644 --- a/client/src/hooks/SSE/useAttachmentHandler.ts +++ b/client/src/hooks/SSE/useAttachmentHandler.ts @@ -10,7 +10,7 @@ export default function useAttachmentHandler(queryClient?: QueryClient) { return ({ data }: { data: TAttachment; submission: EventSubmission }) => { const { messageId } = data; - if (queryClient && !data?.filepath?.startsWith('/api/files')) { + if (queryClient && data?.filepath && !data.filepath.startsWith('/api/files')) { queryClient.setQueryData([QueryKeys.files], (oldData: TAttachment[] | undefined) => { return [data, ...(oldData || [])]; }); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index d010b2d33..28ef49146 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -21,6 +21,7 @@ "com_agents_name_placeholder": "Optional: The name of the agent", "com_agents_no_access": "You don't have access to edit this agent.", "com_agents_not_available": "Agent Not Available", + "com_agents_search_info": "When enabled, allows your agent to search the web for up-to-date information. Requires a valid API key.", "com_agents_search_name": "Search agents by name", "com_agents_update_error": "There was an error updating your agent.", "com_assistants_action_attempt": "Assistant wants to talk to {{0}}", @@ -724,6 +725,20 @@ "com_ui_librechat_code_api_key": "Get your LibreChat Code Interpreter API key", "com_ui_librechat_code_api_subtitle": "Secure. Multi-language. Input/Output Files.", "com_ui_librechat_code_api_title": "Run AI Code", + "com_ui_web_search_api_subtitle": "Search the web for up-to-date information", + "com_ui_web_search_provider": "Search Provider", + "com_ui_web_search_provider_serper": "Serper API", + "com_ui_web_search_provider_serper_key": "Get your Serper API key", + "com_ui_web_search_scraper": "Scraper", + "com_ui_web_search_scraper_firecrawl": "Firecrawl API", + "com_ui_web_search_scraper_firecrawl_key": "Get your Firecrawl API key", + "com_ui_web_search_reranker": "Reranker", + "com_ui_web_search_reranker_jina": "Jina AI", + "com_ui_web_search_reranker_cohere": "Cohere", + "com_ui_web_search_reranker_cohere_key": "Get your Cohere API key", + "com_ui_web_search_firecrawl_url": "Firecrawl API URL (optional)", + "com_ui_web_search_jina_key": "Enter Jina API Key", + "com_ui_web_search_cohere_key": "Enter Cohere API Key", "com_ui_loading": "Loading...", "com_ui_locked": "Locked", "com_ui_logo": "{{0}} Logo", @@ -800,6 +815,11 @@ "com_ui_roleplay": "Roleplay", "com_ui_run_code": "Run Code", "com_ui_run_code_error": "There was an error running the code", + "com_ui_web_search": "Web Search", + "com_ui_web_search_processing": "Processing results", + "com_ui_web_search_reading": "Reading results", + "com_ui_web_searching": "Searching the web", + "com_ui_web_searching_again": "Searching the web again", "com_ui_save": "Save", "com_ui_save_badge_changes": "Save badge changes?", "com_ui_save_submit": "Save & Submit", @@ -895,5 +915,14 @@ "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", "com_user_message": "You", - "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." + "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint.", + "com_citation_source": "Source", + "com_citation_more_details": "More details about {{label}}", + "com_sources_tab_all": "All", + "com_sources_tab_news": "News", + "com_sources_tab_images": "Images", + "com_sources_more_sources": "+{{count}} sources", + "com_sources_title": "Sources", + "com_sources_image_alt": "Search result image", + "com_ui_web_search_reranker_jina_key": "Get your Jina API key" } diff --git a/client/src/style.css b/client/src/style.css index 309a27f01..17eb38816 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1039,7 +1039,7 @@ pre { --tw-prose-body: #374151; --tw-prose-headings: #111827; --tw-prose-lead: #4b5563; - --tw-prose-links: #111827; + --tw-prose-links: #0066cc; --tw-prose-bold: #111827; --tw-prose-counters: #6b7280; --tw-prose-bullets: #d1d5db; diff --git a/package-lock.json b/package-lock.json index 25a341bd2..5878883b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,7 @@ "@langchain/google-genai": "^0.2.8", "@langchain/google-vertexai": "^0.2.8", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.317", + "@librechat/agents": "^2.4.35", "@librechat/data-schemas": "*", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", @@ -150,6 +150,572 @@ "node-fetch": "^2.6.7" } }, + "api/node_modules/@aws-sdk/client-sso": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.810.0.tgz", + "integrity": "sha512-Txp/3jHqkfA4BTklQEOGiZ1yTUxg+hITislfaWEzJ904vlDt4DvAljTlhfaz7pceCLA2+LhRlYZYSv7t5b0Ltw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/middleware-host-header": "3.804.0", + "@aws-sdk/middleware-logger": "3.804.0", + "@aws-sdk/middleware-recursion-detection": "3.804.0", + "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/region-config-resolver": "3.808.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@aws-sdk/util-user-agent-browser": "3.804.0", + "@aws-sdk/util-user-agent-node": "3.810.0", + "@smithy/config-resolver": "^4.1.2", + "@smithy/core": "^3.3.3", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", + "@smithy/util-endpoints": "^3.0.4", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/client-sso/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/core": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.810.0.tgz", + "integrity": "sha512-s2IJk+qa/15YZcv3pbdQNATDR+YdYnHf94MrAeVAWubtRLnzD8JciC+gh4LSPp7JzrWSvVOg2Ut1S+0y89xqCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/core": "^3.3.3", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/signature-v4": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/core/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/core/node_modules/@smithy/signature-v4": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.0.tgz", + "integrity": "sha512-4t5WX60sL3zGJF/CtZsUQTs3UrZEDO2P7pEaElrekbLqkWPYkgqNW1oeiNYC6xXifBnT9dVBOnNQRvOE9riU9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/core/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.810.0.tgz", + "integrity": "sha512-iwHqF+KryKONfbdFk3iKhhPk4fHxh5QP5fXXR//jhYwmszaLOwc7CLCE9AxhgiMzAs+kV8nBFQZvdjFpPzVGOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.810.0.tgz", + "integrity": "sha512-SKzjLd+8ugif7yy9sOAAdnPE1vCBHQe6jKgs2AadMpCmWm34DiHz/KuulHdvURUGMIi7CvmaC8aH77twDPYbtg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.810.0.tgz", + "integrity": "sha512-H2QCSnxWJ/mj8HTcyHmCmyQ5bO/+imRi4mlBIpUyKjiYKro52WD3gXlGgPIDo2q3UFIHq37kmYvS00i+qIY9tw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.810.0", + "@aws-sdk/credential-provider-env": "3.810.0", + "@aws-sdk/credential-provider-http": "3.810.0", + "@aws-sdk/credential-provider-process": "3.810.0", + "@aws-sdk/credential-provider-sso": "3.810.0", + "@aws-sdk/credential-provider-web-identity": "3.810.0", + "@aws-sdk/nested-clients": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/credential-provider-imds": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.810.0.tgz", + "integrity": "sha512-9E3Chv3x+RBM3N1bwLCyvXxoiPAckCI74wG7ePN4F3b/7ieIkbEl/3Hd67j1fnt62Xa1cjUHRu2tz5pdEv5G1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.810.0", + "@aws-sdk/credential-provider-http": "3.810.0", + "@aws-sdk/credential-provider-ini": "3.810.0", + "@aws-sdk/credential-provider-process": "3.810.0", + "@aws-sdk/credential-provider-sso": "3.810.0", + "@aws-sdk/credential-provider-web-identity": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/credential-provider-imds": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.810.0.tgz", + "integrity": "sha512-42kE6MLdsmMGp1id3Gisal4MbMiF7PIc0tAznTeIuE8r7cIF8yeQWw/PBOIvjyI57DxbyKzLUAMEJuigUpApCw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.810.0.tgz", + "integrity": "sha512-8WjX6tz+FCvM93Y33gsr13p/HiiTJmVn5AK1O8PTkvHBclQDzmtAW5FdPqTpAJGswLW2FB0xRqdsSMN2dQEjNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.810.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/token-providers": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.810.0.tgz", + "integrity": "sha512-uKQJY0AcPyrvMmfGLo36semgjqJ4vmLTqOSW9u40qQDspRnG73/P09lAO2ntqKlhwvMBt3XfcNnOpyyhKRcOfA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.810.0", + "@aws-sdk/nested-clients": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.804.0.tgz", + "integrity": "sha512-bum1hLVBrn2lJCi423Z2fMUYtsbkGI2s4N+2RI2WSjvbaVyMSv/WcejIrjkqiiMR+2Y7m5exgoKeg4/TODLDPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-logger": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.804.0.tgz", + "integrity": "sha512-w/qLwL3iq0KOPQNat0Kb7sKndl9BtceigINwBU7SpkYWX9L/Lem6f8NPEKrC9Tl4wDBht3Yztub4oRTy/horJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.804.0.tgz", + "integrity": "sha512-zqHOrvLRdsUdN/ehYfZ9Tf8svhbiLLz5VaWUz22YndFv6m9qaAcijkpAOlKexsv3nLBMJdSdJ6GUTAeIy3BZzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.810.0.tgz", + "integrity": "sha512-gLMJcqgIq7k9skX8u0Yyi+jil4elbsmLf3TuDuqNdlqiZ44/AKdDFfU3mU5tRUtMfP42a3gvb2U3elP0BIeybQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@smithy/core": "^3.3.3", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/middleware-user-agent/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/nested-clients": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.810.0.tgz", + "integrity": "sha512-w+tGXFSQjzvJ3j2sQ4GJRdD+YXLTgwLd9eG/A+7pjrv2yLLV70M4HqRrFqH06JBjqT5rsOxonc/QSjROyxk+IA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.810.0", + "@aws-sdk/middleware-host-header": "3.804.0", + "@aws-sdk/middleware-logger": "3.804.0", + "@aws-sdk/middleware-recursion-detection": "3.804.0", + "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/region-config-resolver": "3.808.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@aws-sdk/util-user-agent-browser": "3.804.0", + "@aws-sdk/util-user-agent-node": "3.810.0", + "@smithy/config-resolver": "^4.1.2", + "@smithy/core": "^3.3.3", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", + "@smithy/util-endpoints": "^3.0.4", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.808.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.808.0.tgz", + "integrity": "sha512-9x2QWfphkARZY5OGkl9dJxZlSlYM2l5inFeo2bKntGuwg4A4YUe5h7d5yJ6sZbam9h43eBrkOdumx03DAkQF9A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/token-providers": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.810.0.tgz", + "integrity": "sha512-fdgHRCDpnzsD+0km7zuRbHRysJECfS8o9T9/pZ6XAr1z2FNV/UveHtnUYq0j6XpDMrIm0/suvXbshIjQU+a+sw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/nested-clients": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/types": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.804.0.tgz", + "integrity": "sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/util-endpoints": { + "version": "3.808.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.808.0.tgz", + "integrity": "sha512-N6Lic98uc4ADB7fLWlzx+1uVnq04VgVjngZvwHoujcRg9YDhIg9dUDiTzD5VZv13g1BrPYmvYP1HhsildpGV6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/types": "^4.2.0", + "@smithy/util-endpoints": "^3.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.804.0.tgz", + "integrity": "sha512-KfW6T6nQHHM/vZBBdGn6fMyG/MgX5lq82TDdX4HRQRRuHKLgBWGpKXqqvBwqIaCdXwWHgDrg2VQups6GqOWW2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "api/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.810.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.810.0.tgz", + "integrity": "sha512-T56/ANEGNuvhqVoWZdr+0ZY2hjV93cH2OfGHIlVTVSAMACWG54XehDPESEso1CJNhJGYZPsE+FE42HGCk/XDMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.810.0", + "@aws-sdk/types": "3.804.0", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, "api/node_modules/@google/generative-ai": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.23.0.tgz", @@ -171,6 +737,26 @@ "node": ">= 18" } }, + "api/node_modules/@langchain/aws": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@langchain/aws/-/aws-0.1.8.tgz", + "integrity": "sha512-juyCQG8XQ1ikxUNK8zB9VDNOJtlS5zlf83aWZ1OqNJO2LqnMK1TQMTc8hPLvv63HnCk1iXJ7nI0SR7/jflvLPw==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-bedrock-agent-runtime": "^3.755.0", + "@aws-sdk/client-bedrock-runtime": "^3.755.0", + "@aws-sdk/client-kendra": "^3.750.0", + "@aws-sdk/credential-provider-node": "^3.750.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.22.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.41 <0.4.0" + } + }, "api/node_modules/@langchain/community": { "version": "0.3.42", "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.42.tgz", @@ -714,6 +1300,685 @@ "@langchain/core": ">=0.3.48 <0.4.0" } }, + "api/node_modules/@librechat/agents": { + "version": "2.4.35", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.35.tgz", + "integrity": "sha512-K6ct25w7DtjVDvx6gc8xpvQZPqMV8TsK4KjRA8HMFsHN2tGg50EqwbmccuWT+3VbLIKUbbieYCvLcQAeXCcwYA==", + "license": "MIT", + "dependencies": { + "@langchain/anthropic": "^0.3.20", + "@langchain/aws": "0.1.8", + "@langchain/community": "^0.3.42", + "@langchain/core": "^0.3.55", + "@langchain/deepseek": "^0.0.1", + "@langchain/google-genai": "^0.2.8", + "@langchain/google-vertexai": "^0.2.8", + "@langchain/langgraph": "^0.2.72", + "@langchain/mistralai": "^0.2.0", + "@langchain/ollama": "^0.2.0", + "@langchain/openai": "^0.5.10", + "@langchain/xai": "^0.0.2", + "cheerio": "^1.0.0", + "dotenv": "^16.4.7", + "https-proxy-agent": "^7.0.6", + "nanoid": "^3.3.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "api/node_modules/@smithy/abort-controller": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", + "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/config-resolver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.2.tgz", + "integrity": "sha512-7r6mZGwb5LmLJ+zPtkLoznf2EtwEuSWdtid10pjGl/7HefCE4mueOkrfki8JCUm99W6UfP47/r3tbxx9CfBN5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.1", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/core": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.3.tgz", + "integrity": "sha512-CiJNc0b/WdnttAfQ6uMkxPQ3Z8hG/ba8wF89x9KtBBLDdZk6CX52K4F8hbe94uNbc8LDUuZFtbqfdhM3T21naw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.5", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-stream": "^4.2.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/core/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/core/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.4.tgz", + "integrity": "sha512-jN6M6zaGVyB8FmNGG+xOPQB4N89M1x97MMdMnm1ESjljLS3Qju/IegQizKujaNcy2vXAvrz0en8bobe6E55FEA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.1", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", + "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/fetch-http-handler/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/hash-node": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.2.tgz", + "integrity": "sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/hash-node/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/invalid-dependency": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.2.tgz", + "integrity": "sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-content-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.2.tgz", + "integrity": "sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-content-length/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.6.tgz", + "integrity": "sha512-Zdieg07c3ua3ap5ungdcyNnY1OsxmsXXtKDTk28+/YbwIPju0Z1ZX9X5AnkjmDE3+AbqgvhtC/ZuCMSr6VSfPw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.3.3", + "@smithy/middleware-serde": "^4.0.5", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-retry": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.7.tgz", + "integrity": "sha512-lFIFUJ0E/4I0UaIDY5usNUzNKAghhxO0lDH4TZktXMmE+e4ActD9F154Si0Unc01aCPzcwd+NcOwQw6AfXXRRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.1", + "@smithy/protocol-http": "^5.1.0", + "@smithy/service-error-classification": "^4.0.3", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-retry/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "api/node_modules/@smithy/middleware-serde": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.5.tgz", + "integrity": "sha512-yREC3q/HXqQigq29xX3hiy6tFi+kjPKXoYUQmwQdgPORLbQ0n6V2Z/Iw9Nnlu66da9fM/WhDtGvYvqwecrCljQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-serde/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/middleware-stack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.2.tgz", + "integrity": "sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/node-config-provider": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.1.tgz", + "integrity": "sha512-1slS5jf5icHETwl5hxEVBj+mh6B+LbVW4yRINsGtUKH+nxM5Pw2H59+qf+JqYFCHp9jssG4vX81f5WKnjMN3Vw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/node-http-handler": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", + "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/querystring-builder": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/node-http-handler/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/property-provider": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.2.tgz", + "integrity": "sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/querystring-builder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", + "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/querystring-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.2.tgz", + "integrity": "sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/service-error-classification": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.3.tgz", + "integrity": "sha512-FTbcajmltovWMjj3tksDQdD23b2w6gH+A0DYA1Yz3iSpjDj8fmkwy62UnXcWMy4d5YoMoSyLFHMfkEVEzbiN8Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.2.tgz", + "integrity": "sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/smithy-client": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.6.tgz", + "integrity": "sha512-WEqP0wQ1N/lVS4pwNK1Vk+0i6QIr66cq/xbu1dVy1tM0A0qYwAYyz0JhbquzM5pMa8s89lyDBtoGKxo7iG74GA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.3.3", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/smithy-client/node_modules/@smithy/protocol-http": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", + "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/url-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.2.tgz", + "integrity": "sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-base64/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.14.tgz", + "integrity": "sha512-l7QnMX8VcDOH6n/fBRu4zqguSlOBZxFzWqp58dXFSARFBjNlmEDk5G/z4T7BMGr+rI0Pg8MkhmMUfEtHFgpy2g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.2", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.14.tgz", + "integrity": "sha512-Ujs1gsWDo3m/T63VWBTBmHLTD2UlU6J6FEokLCEp7OZQv45jcjLHoxTwgWsi8ULpsYozvH4MTWkRP+bhwr0vDg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.2", + "@smithy/credential-provider-imds": "^4.0.4", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/property-provider": "^4.0.2", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-endpoints": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.4.tgz", + "integrity": "sha512-VfFATC1bmZLV2858B/O1NpMcL32wYo8DPPhHxYxDCodDl3f3mSZ5oJheW1IF91A0EeAADz2WsakM/hGGPGNKLg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.1", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-retry": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.3.tgz", + "integrity": "sha512-DPuYjZQDXmKr/sNvy9Spu8R/ESa2e22wXZzSAY6NkjOLj6spbIje/Aq8rT97iUMdDj0qHMRIe+bTxvlU74d9Ng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.3", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.0.tgz", + "integrity": "sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/types": "^4.2.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-stream/node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "api/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "api/node_modules/@types/node": { "version": "18.19.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", @@ -17698,9 +18963,9 @@ } }, "node_modules/@langchain/langgraph": { - "version": "0.2.72", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.72.tgz", - "integrity": "sha512-2Rs79mLSx0Yxr/omiWOXBlaS+eywZ9KACe06pI6XkA3hT2hwqjMlXYMvbeD7mxZlKrPtLsQaHWvL9IO2VAa+lQ==", + "version": "0.2.73", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.73.tgz", + "integrity": "sha512-vw+IXV2Q7x/QaykNj3VE/Ak3aPlst3spkpM6zYtqwGkQlhLZU4Lb8PHHPjqNNYHSdOTDj9x4jIRUPZArGHx9Aw==", "license": "MIT", "dependencies": { "@langchain/langgraph-checkpoint": "~0.0.17", @@ -17750,9 +19015,9 @@ } }, "node_modules/@langchain/langgraph-sdk": { - "version": "0.0.74", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.74.tgz", - "integrity": "sha512-IUN0m4BYkGWdviFd4EaWDcQgxNq8z+1LIwXajCSt9B+Cb/pz0ZNpIPdu5hAIsf6a0RWu5yRUhzL1L40t7vu3Zg==", + "version": "0.0.77", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.77.tgz", + "integrity": "sha512-DMCONENhhaMS+Buw8s2UkKjAa9I6cT1aVJEDOmTO2lpon4Dqz/jiYUVJK7pTlNVSNvSx0E8aOmtT7NgGBcWflg==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.15", @@ -17942,1835 +19207,6 @@ "@lezer/common": "^1.0.0" } }, - "node_modules/@librechat/agents": { - "version": "2.4.317", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.317.tgz", - "integrity": "sha512-YUjcx2JGftLkaHOdpllCzArE69KKKuXVvb+EhZszOKoxZgoYtcXE/ZwdumvACSwrFvza+mJ7tdD/5TF04EV8Sg==", - "license": "MIT", - "dependencies": { - "@langchain/anthropic": "^0.3.20", - "@langchain/aws": "0.1.8", - "@langchain/community": "^0.3.42", - "@langchain/core": "^0.3.55", - "@langchain/deepseek": "^0.0.1", - "@langchain/google-genai": "^0.2.8", - "@langchain/google-vertexai": "^0.2.8", - "@langchain/langgraph": "^0.2.72", - "@langchain/mistralai": "^0.2.0", - "@langchain/ollama": "^0.2.0", - "@langchain/openai": "^0.5.10", - "@langchain/xai": "^0.0.2", - "dotenv": "^16.4.7", - "https-proxy-agent": "^7.0.6", - "nanoid": "^3.3.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/client-sso": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.808.0.tgz", - "integrity": "sha512-NxGomD0x9q30LPOXf4x7haOm6l2BJdLEzpiC/bPEXUkf2+4XudMQumMA/hDfErY5hCE19mFAouoO465m3Gl3JQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/middleware-host-header": "3.804.0", - "@aws-sdk/middleware-logger": "3.804.0", - "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", - "@aws-sdk/region-config-resolver": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@aws-sdk/util-endpoints": "3.808.0", - "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", - "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/hash-node": "^4.0.2", - "@smithy/invalid-dependency": "^4.0.2", - "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", - "@smithy/middleware-stack": "^4.0.2", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", - "@smithy/util-endpoints": "^3.0.4", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.3", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/client-sso/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/core": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.808.0.tgz", - "integrity": "sha512-+nTmxJVIPtAarGq9Fd/uU2qU/Ngfb9EntT0/kwXdKKMI0wU9fQNWi10xSTVeqOtzWERbQpOJgBAdta+v3W7cng==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/core": "^3.3.1", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/property-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/signature-v4": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", - "@smithy/types": "^4.2.0", - "@smithy/util-middleware": "^4.0.2", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/core/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/core/node_modules/@smithy/signature-v4": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.0.tgz", - "integrity": "sha512-4t5WX60sL3zGJF/CtZsUQTs3UrZEDO2P7pEaElrekbLqkWPYkgqNW1oeiNYC6xXifBnT9dVBOnNQRvOE9riU9w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/core/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.808.0.tgz", - "integrity": "sha512-snPRQnwG9PV4kYHQimo1tenf7P974RcdxkHUThzWSxPEV7HpjxTFYNWGlKbOKBhL4AcgeCVeiZ/j+zveF2lEPA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.808.0.tgz", - "integrity": "sha512-gNXjlx3BIUeX7QpVqxbjBxG6zm45lC39QvUIo92WzEJd2OTPcR8TU0OTTsgq/lpn2FrKcISj5qXvhWykd41+CA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/property-provider": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", - "@smithy/types": "^4.2.0", - "@smithy/util-stream": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.808.0.tgz", - "integrity": "sha512-Y53CW0pCvFQQEvtVFwExCCMbTg+6NOl8b3YOuZVzPmVmDoW7M1JIn9IScesqoGERXL3VoXny6nYTsZj+vfpp7Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/credential-provider-env": "3.808.0", - "@aws-sdk/credential-provider-http": "3.808.0", - "@aws-sdk/credential-provider-process": "3.808.0", - "@aws-sdk/credential-provider-sso": "3.808.0", - "@aws-sdk/credential-provider-web-identity": "3.808.0", - "@aws-sdk/nested-clients": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/credential-provider-imds": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.808.0.tgz", - "integrity": "sha512-lASHlXJ6U5Cpnt9Gs+mWaaSmWcEibr1AFGhp+5UNvfyd+UU2Oiwgbo7rYXygmaVDGkbfXEiTkgYtoNOBSddnWQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.808.0", - "@aws-sdk/credential-provider-http": "3.808.0", - "@aws-sdk/credential-provider-ini": "3.808.0", - "@aws-sdk/credential-provider-process": "3.808.0", - "@aws-sdk/credential-provider-sso": "3.808.0", - "@aws-sdk/credential-provider-web-identity": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/credential-provider-imds": "^4.0.2", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.808.0.tgz", - "integrity": "sha512-ZLqp+xsQUatoo8pMozcfLwf/pwfXeIk0w3n0Lo/rWBgT3RcdECmmPCRcnkYBqxHQyE66aS9HiJezZUwMYPqh6w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.808.0.tgz", - "integrity": "sha512-gWZByAokHX+aps1+syIW/hbKUBrjE2RpPRd/RGQvrBbVVgwsJzsHKsW0zy1B6mgARPG6IahmSUMjNkBCVsiAgw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.808.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/token-providers": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.808.0.tgz", - "integrity": "sha512-SsGa1Gfa05aJM/qYOtHmfg0OKKW6Fl6kyMCcai63jWDVDYy0QSHcesnqRayJolISkdsVK6bqoWoFcPxiopcFcg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/nested-clients": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.804.0.tgz", - "integrity": "sha512-bum1hLVBrn2lJCi423Z2fMUYtsbkGI2s4N+2RI2WSjvbaVyMSv/WcejIrjkqiiMR+2Y7m5exgoKeg4/TODLDPQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-logger": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.804.0.tgz", - "integrity": "sha512-w/qLwL3iq0KOPQNat0Kb7sKndl9BtceigINwBU7SpkYWX9L/Lem6f8NPEKrC9Tl4wDBht3Yztub4oRTy/horJA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.804.0.tgz", - "integrity": "sha512-zqHOrvLRdsUdN/ehYfZ9Tf8svhbiLLz5VaWUz22YndFv6m9qaAcijkpAOlKexsv3nLBMJdSdJ6GUTAeIy3BZzw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.808.0.tgz", - "integrity": "sha512-VckV6l5cf/rL3EtgzSHVTTD4mI0gd8UxDDWbKJsxbQ2bpNPDQG2L1wWGLaolTSzjEJ5f3ijDwQrNDbY9l85Mmg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@aws-sdk/util-endpoints": "3.808.0", - "@smithy/core": "^3.3.1", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/middleware-user-agent/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/nested-clients": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.808.0.tgz", - "integrity": "sha512-NparPojwoBul7XPCasy4psFMJbw7Ys4bz8lVB93ljEUD4VV7mM7zwK27Uhz20B8mBFGmFEoAprPsVymJcK9Vcw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.808.0", - "@aws-sdk/middleware-host-header": "3.804.0", - "@aws-sdk/middleware-logger": "3.804.0", - "@aws-sdk/middleware-recursion-detection": "3.804.0", - "@aws-sdk/middleware-user-agent": "3.808.0", - "@aws-sdk/region-config-resolver": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@aws-sdk/util-endpoints": "3.808.0", - "@aws-sdk/util-user-agent-browser": "3.804.0", - "@aws-sdk/util-user-agent-node": "3.808.0", - "@smithy/config-resolver": "^4.1.2", - "@smithy/core": "^3.3.1", - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/hash-node": "^4.0.2", - "@smithy/invalid-dependency": "^4.0.2", - "@smithy/middleware-content-length": "^4.0.2", - "@smithy/middleware-endpoint": "^4.1.4", - "@smithy/middleware-retry": "^4.1.5", - "@smithy/middleware-serde": "^4.0.3", - "@smithy/middleware-stack": "^4.0.2", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/protocol-http": "^5.1.0", - "@smithy/smithy-client": "^4.2.4", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.12", - "@smithy/util-defaults-mode-node": "^4.0.12", - "@smithy/util-endpoints": "^3.0.4", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.3", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.808.0.tgz", - "integrity": "sha512-9x2QWfphkARZY5OGkl9dJxZlSlYM2l5inFeo2bKntGuwg4A4YUe5h7d5yJ6sZbam9h43eBrkOdumx03DAkQF9A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/types": "^4.2.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/token-providers": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.808.0.tgz", - "integrity": "sha512-PsfKanHmnyO7FxowXqxbLQ+QjURCdSGxyhUiSdZbfvlvme/wqaMyIoMV/i4jppndksoSdPbW2kZXjzOqhQF+ew==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/nested-clients": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/types": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.804.0.tgz", - "integrity": "sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/util-endpoints": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.808.0.tgz", - "integrity": "sha512-N6Lic98uc4ADB7fLWlzx+1uVnq04VgVjngZvwHoujcRg9YDhIg9dUDiTzD5VZv13g1BrPYmvYP1HhsildpGV6w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/types": "^4.2.0", - "@smithy/util-endpoints": "^3.0.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.804.0.tgz", - "integrity": "sha512-KfW6T6nQHHM/vZBBdGn6fMyG/MgX5lq82TDdX4HRQRRuHKLgBWGpKXqqvBwqIaCdXwWHgDrg2VQups6GqOWW2A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.804.0", - "@smithy/types": "^4.2.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@librechat/agents/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.808.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.808.0.tgz", - "integrity": "sha512-5UmB6u7RBSinXZAVP2iDgqyeVA/odO2SLEcrXaeTCw8ICXEoqF0K+GL36T4iDbzCBOAIugOZ6OcQX5vH3ck5UA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.808.0", - "@aws-sdk/types": "3.804.0", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@librechat/agents/node_modules/@langchain/aws": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@langchain/aws/-/aws-0.1.8.tgz", - "integrity": "sha512-juyCQG8XQ1ikxUNK8zB9VDNOJtlS5zlf83aWZ1OqNJO2LqnMK1TQMTc8hPLvv63HnCk1iXJ7nI0SR7/jflvLPw==", - "license": "MIT", - "dependencies": { - "@aws-sdk/client-bedrock-agent-runtime": "^3.755.0", - "@aws-sdk/client-bedrock-runtime": "^3.755.0", - "@aws-sdk/client-kendra": "^3.750.0", - "@aws-sdk/credential-provider-node": "^3.750.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.22.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.41 <0.4.0" - } - }, - "node_modules/@librechat/agents/node_modules/@langchain/community": { - "version": "0.3.42", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.42.tgz", - "integrity": "sha512-dTRoAy4XPalCB4Of5N4uQ8/KSGCddv48OGek8CULtdbBSkQ9s7iWtcb8hQEajkCwMfILVVzw1pU8IzE17oNHPA==", - "license": "MIT", - "dependencies": { - "@langchain/openai": ">=0.2.0 <0.6.0", - "binary-extensions": "^2.2.0", - "expr-eval": "^2.0.2", - "flat": "^5.0.2", - "js-yaml": "^4.1.0", - "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", - "langsmith": "^0.3.16", - "uuid": "^10.0.0", - "zod": "^3.22.3", - "zod-to-json-schema": "^3.22.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@arcjet/redact": "^v1.0.0-alpha.23", - "@aws-crypto/sha256-js": "^5.0.0", - "@aws-sdk/client-bedrock-agent-runtime": "^3.749.0", - "@aws-sdk/client-bedrock-runtime": "^3.749.0", - "@aws-sdk/client-dynamodb": "^3.749.0", - "@aws-sdk/client-kendra": "^3.749.0", - "@aws-sdk/client-lambda": "^3.749.0", - "@aws-sdk/client-s3": "^3.749.0", - "@aws-sdk/client-sagemaker-runtime": "^3.749.0", - "@aws-sdk/client-sfn": "^3.749.0", - "@aws-sdk/credential-provider-node": "^3.388.0", - "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.15.0", - "@browserbasehq/sdk": "*", - "@browserbasehq/stagehand": "^1.0.0", - "@clickhouse/client": "^0.2.5", - "@cloudflare/ai": "*", - "@datastax/astra-db-ts": "^1.0.0", - "@elastic/elasticsearch": "^8.4.0", - "@getmetal/metal-sdk": "*", - "@getzep/zep-cloud": "^1.0.6", - "@getzep/zep-js": "^0.9.0", - "@gomomento/sdk": "^1.51.1", - "@gomomento/sdk-core": "^1.51.1", - "@google-ai/generativelanguage": "*", - "@google-cloud/storage": "^6.10.1 || ^7.7.0", - "@gradientai/nodejs-sdk": "^1.2.0", - "@huggingface/inference": "^2.6.4", - "@huggingface/transformers": "^3.2.3", - "@ibm-cloud/watsonx-ai": "*", - "@lancedb/lancedb": "^0.12.0", - "@langchain/core": ">=0.2.21 <0.4.0", - "@layerup/layerup-security": "^1.5.12", - "@libsql/client": "^0.14.0", - "@mendable/firecrawl-js": "^1.4.3", - "@mlc-ai/web-llm": "*", - "@mozilla/readability": "*", - "@neondatabase/serverless": "*", - "@notionhq/client": "^2.2.10", - "@opensearch-project/opensearch": "*", - "@pinecone-database/pinecone": "*", - "@planetscale/database": "^1.8.0", - "@premai/prem-sdk": "^0.3.25", - "@qdrant/js-client-rest": "^1.8.2", - "@raycast/api": "^1.55.2", - "@rockset/client": "^0.9.1", - "@smithy/eventstream-codec": "^2.0.5", - "@smithy/protocol-http": "^3.0.6", - "@smithy/signature-v4": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "@spider-cloud/spider-client": "^0.0.21", - "@supabase/supabase-js": "^2.45.0", - "@tensorflow-models/universal-sentence-encoder": "*", - "@tensorflow/tfjs-converter": "*", - "@tensorflow/tfjs-core": "*", - "@upstash/ratelimit": "^1.1.3 || ^2.0.3", - "@upstash/redis": "^1.20.6", - "@upstash/vector": "^1.1.1", - "@vercel/kv": "*", - "@vercel/postgres": "*", - "@writerai/writer-sdk": "^0.40.2", - "@xata.io/client": "^0.28.0", - "@zilliz/milvus2-sdk-node": ">=2.3.5", - "apify-client": "^2.7.1", - "assemblyai": "^4.6.0", - "azion": "^1.11.1", - "better-sqlite3": ">=9.4.0 <12.0.0", - "cassandra-driver": "^4.7.2", - "cborg": "^4.1.1", - "cheerio": "^1.0.0-rc.12", - "chromadb": "*", - "closevector-common": "0.1.3", - "closevector-node": "0.1.6", - "closevector-web": "0.1.6", - "cohere-ai": "*", - "convex": "^1.3.1", - "crypto-js": "^4.2.0", - "d3-dsv": "^2.0.0", - "discord.js": "^14.14.1", - "dria": "^0.0.3", - "duck-duck-scrape": "^2.2.5", - "epub2": "^3.0.1", - "fast-xml-parser": "*", - "firebase-admin": "^11.9.0 || ^12.0.0", - "google-auth-library": "*", - "googleapis": "*", - "hnswlib-node": "^3.0.0", - "html-to-text": "^9.0.5", - "ibm-cloud-sdk-core": "*", - "ignore": "^5.2.0", - "interface-datastore": "^8.2.11", - "ioredis": "^5.3.2", - "it-all": "^3.0.4", - "jsdom": "*", - "jsonwebtoken": "^9.0.2", - "llmonitor": "^0.5.9", - "lodash": "^4.17.21", - "lunary": "^0.7.10", - "mammoth": "^1.6.0", - "mariadb": "^3.4.0", - "mem0ai": "^2.1.8", - "mongodb": ">=5.2.0", - "mysql2": "^3.9.8", - "neo4j-driver": "*", - "notion-to-md": "^3.1.0", - "officeparser": "^4.0.4", - "openai": "*", - "pdf-parse": "1.1.1", - "pg": "^8.11.0", - "pg-copy-streams": "^6.0.5", - "pickleparser": "^0.2.1", - "playwright": "^1.32.1", - "portkey-ai": "^0.1.11", - "puppeteer": "*", - "pyodide": ">=0.24.1 <0.27.0", - "redis": "*", - "replicate": "*", - "sonix-speech-recognition": "^2.1.1", - "srt-parser-2": "^1.2.3", - "typeorm": "^0.3.20", - "typesense": "^1.5.3", - "usearch": "^1.1.1", - "voy-search": "0.6.2", - "weaviate-ts-client": "*", - "web-auth-library": "^1.0.3", - "word-extractor": "*", - "ws": "^8.14.2", - "youtubei.js": "*" - }, - "peerDependenciesMeta": { - "@arcjet/redact": { - "optional": true - }, - "@aws-crypto/sha256-js": { - "optional": true - }, - "@aws-sdk/client-bedrock-agent-runtime": { - "optional": true - }, - "@aws-sdk/client-bedrock-runtime": { - "optional": true - }, - "@aws-sdk/client-dynamodb": { - "optional": true - }, - "@aws-sdk/client-kendra": { - "optional": true - }, - "@aws-sdk/client-lambda": { - "optional": true - }, - "@aws-sdk/client-s3": { - "optional": true - }, - "@aws-sdk/client-sagemaker-runtime": { - "optional": true - }, - "@aws-sdk/client-sfn": { - "optional": true - }, - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@aws-sdk/dsql-signer": { - "optional": true - }, - "@azure/search-documents": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@browserbasehq/sdk": { - "optional": true - }, - "@clickhouse/client": { - "optional": true - }, - "@cloudflare/ai": { - "optional": true - }, - "@datastax/astra-db-ts": { - "optional": true - }, - "@elastic/elasticsearch": { - "optional": true - }, - "@getmetal/metal-sdk": { - "optional": true - }, - "@getzep/zep-cloud": { - "optional": true - }, - "@getzep/zep-js": { - "optional": true - }, - "@gomomento/sdk": { - "optional": true - }, - "@gomomento/sdk-core": { - "optional": true - }, - "@google-ai/generativelanguage": { - "optional": true - }, - "@google-cloud/storage": { - "optional": true - }, - "@gradientai/nodejs-sdk": { - "optional": true - }, - "@huggingface/inference": { - "optional": true - }, - "@huggingface/transformers": { - "optional": true - }, - "@lancedb/lancedb": { - "optional": true - }, - "@layerup/layerup-security": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@mendable/firecrawl-js": { - "optional": true - }, - "@mlc-ai/web-llm": { - "optional": true - }, - "@mozilla/readability": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@notionhq/client": { - "optional": true - }, - "@opensearch-project/opensearch": { - "optional": true - }, - "@pinecone-database/pinecone": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@premai/prem-sdk": { - "optional": true - }, - "@qdrant/js-client-rest": { - "optional": true - }, - "@raycast/api": { - "optional": true - }, - "@rockset/client": { - "optional": true - }, - "@smithy/eventstream-codec": { - "optional": true - }, - "@smithy/protocol-http": { - "optional": true - }, - "@smithy/signature-v4": { - "optional": true - }, - "@smithy/util-utf8": { - "optional": true - }, - "@spider-cloud/spider-client": { - "optional": true - }, - "@supabase/supabase-js": { - "optional": true - }, - "@tensorflow-models/universal-sentence-encoder": { - "optional": true - }, - "@tensorflow/tfjs-converter": { - "optional": true - }, - "@tensorflow/tfjs-core": { - "optional": true - }, - "@upstash/ratelimit": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@upstash/vector": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@writerai/writer-sdk": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "@zilliz/milvus2-sdk-node": { - "optional": true - }, - "apify-client": { - "optional": true - }, - "assemblyai": { - "optional": true - }, - "azion": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "cassandra-driver": { - "optional": true - }, - "cborg": { - "optional": true - }, - "cheerio": { - "optional": true - }, - "chromadb": { - "optional": true - }, - "closevector-common": { - "optional": true - }, - "closevector-node": { - "optional": true - }, - "closevector-web": { - "optional": true - }, - "cohere-ai": { - "optional": true - }, - "convex": { - "optional": true - }, - "crypto-js": { - "optional": true - }, - "d3-dsv": { - "optional": true - }, - "discord.js": { - "optional": true - }, - "dria": { - "optional": true - }, - "duck-duck-scrape": { - "optional": true - }, - "epub2": { - "optional": true - }, - "fast-xml-parser": { - "optional": true - }, - "firebase-admin": { - "optional": true - }, - "google-auth-library": { - "optional": true - }, - "googleapis": { - "optional": true - }, - "hnswlib-node": { - "optional": true - }, - "html-to-text": { - "optional": true - }, - "ignore": { - "optional": true - }, - "interface-datastore": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "it-all": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "jsonwebtoken": { - "optional": true - }, - "llmonitor": { - "optional": true - }, - "lodash": { - "optional": true - }, - "lunary": { - "optional": true - }, - "mammoth": { - "optional": true - }, - "mariadb": { - "optional": true - }, - "mem0ai": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "neo4j-driver": { - "optional": true - }, - "notion-to-md": { - "optional": true - }, - "officeparser": { - "optional": true - }, - "pdf-parse": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-copy-streams": { - "optional": true - }, - "pickleparser": { - "optional": true - }, - "playwright": { - "optional": true - }, - "portkey-ai": { - "optional": true - }, - "puppeteer": { - "optional": true - }, - "pyodide": { - "optional": true - }, - "redis": { - "optional": true - }, - "replicate": { - "optional": true - }, - "sonix-speech-recognition": { - "optional": true - }, - "srt-parser-2": { - "optional": true - }, - "typeorm": { - "optional": true - }, - "typesense": { - "optional": true - }, - "usearch": { - "optional": true - }, - "voy-search": { - "optional": true - }, - "weaviate-ts-client": { - "optional": true - }, - "web-auth-library": { - "optional": true - }, - "word-extractor": { - "optional": true - }, - "ws": { - "optional": true - }, - "youtubei.js": { - "optional": true - } - } - }, - "node_modules/@librechat/agents/node_modules/@langchain/community/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@librechat/agents/node_modules/@langchain/openai": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.5.10.tgz", - "integrity": "sha512-hBQIWjcVxGS7tgVvgBBmrZ5jSaJ8nu9g6V64/Tx6KGjkW7VdGmUvqCO+koiQCOZVL7PBJkHWAvDsbghPYXiZEA==", - "license": "MIT", - "dependencies": { - "js-tiktoken": "^1.0.12", - "openai": "^4.96.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.48 <0.4.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/abort-controller": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", - "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/config-resolver": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.2.tgz", - "integrity": "sha512-7r6mZGwb5LmLJ+zPtkLoznf2EtwEuSWdtid10pjGl/7HefCE4mueOkrfki8JCUm99W6UfP47/r3tbxx9CfBN5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.1", - "@smithy/types": "^4.2.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/core": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.3.2.tgz", - "integrity": "sha512-GlLv+syoWolhtjX12XplL9BXBu10cjjD8iQC69fiKTrVNOB3Fjt8CVI9ccm6G3bLbMNe1gzrLD7yyMkYo4hchw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.0.4", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-stream": "^4.2.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/core/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/core/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/credential-provider-imds": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.4.tgz", - "integrity": "sha512-jN6M6zaGVyB8FmNGG+xOPQB4N89M1x97MMdMnm1ESjljLS3Qju/IegQizKujaNcy2vXAvrz0en8bobe6E55FEA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.1", - "@smithy/property-provider": "^4.0.2", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz", - "integrity": "sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.0", - "@smithy/querystring-builder": "^4.0.2", - "@smithy/types": "^4.2.0", - "@smithy/util-base64": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/fetch-http-handler/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/hash-node": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.2.tgz", - "integrity": "sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/hash-node/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/invalid-dependency": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.2.tgz", - "integrity": "sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-content-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.2.tgz", - "integrity": "sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-content-length/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-endpoint": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.5.tgz", - "integrity": "sha512-WlpC9KVkajQf7RaGwi3n6lhHZzYTgm2PyX/2JjcwSHG417gFloNmYqN8rzDRXjT527/ZxZuvCsqq1gWZPW8lag==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.3.2", - "@smithy/middleware-serde": "^4.0.4", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "@smithy/url-parser": "^4.0.2", - "@smithy/util-middleware": "^4.0.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-retry": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.6.tgz", - "integrity": "sha512-bl8q95nvCf7d22spxsBfs2giUDFf7prWLAxF5tmfgGBYHbUNW+OfnwMnabC15GMLA2AoE4HOtQR18a59lx6Blw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.1", - "@smithy/protocol-http": "^5.1.0", - "@smithy/service-error-classification": "^4.0.3", - "@smithy/smithy-client": "^4.2.5", - "@smithy/types": "^4.2.0", - "@smithy/util-middleware": "^4.0.2", - "@smithy/util-retry": "^4.0.3", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-retry/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-serde": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.4.tgz", - "integrity": "sha512-CaLvBtz+Xgs7eOwoinTXhZ02/9u8b28RT8lQAaDh7Q59nygeYYp1UiJjwl6zsay+lp0qVT/S7qLVI5RgcxjyfQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-serde/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/middleware-stack": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.2.tgz", - "integrity": "sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/node-config-provider": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.1.tgz", - "integrity": "sha512-1slS5jf5icHETwl5hxEVBj+mh6B+LbVW4yRINsGtUKH+nxM5Pw2H59+qf+JqYFCHp9jssG4vX81f5WKnjMN3Vw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.2", - "@smithy/shared-ini-file-loader": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/node-http-handler": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", - "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/querystring-builder": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/node-http-handler/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/property-provider": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.2.tgz", - "integrity": "sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/querystring-builder": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", - "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "@smithy/util-uri-escape": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/querystring-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.2.tgz", - "integrity": "sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/service-error-classification": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.3.tgz", - "integrity": "sha512-FTbcajmltovWMjj3tksDQdD23b2w6gH+A0DYA1Yz3iSpjDj8fmkwy62UnXcWMy4d5YoMoSyLFHMfkEVEzbiN8Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.2.tgz", - "integrity": "sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/smithy-client": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.2.5.tgz", - "integrity": "sha512-T3gA/TShe52Ln0ywWGVoDiqRvaxqvrU0CKRRmzT71/I1rRBD8mY85rvMMME6vw5RpBLJC9ADmXSLmpohF7RRhA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.3.2", - "@smithy/middleware-endpoint": "^4.1.5", - "@smithy/middleware-stack": "^4.0.2", - "@smithy/protocol-http": "^5.1.0", - "@smithy/types": "^4.2.0", - "@smithy/util-stream": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/smithy-client/node_modules/@smithy/protocol-http": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", - "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/url-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.2.tgz", - "integrity": "sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.0.2", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-base64/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", - "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.13.tgz", - "integrity": "sha512-HCLfXAyTEpVWLuyxDABg8UQukeRwChL1UErpSQ4KJK2ZoadmXuQY68pTL9KcuEtasTkIjnzyLUL9vhLdJ3VFHQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.5", - "@smithy/types": "^4.2.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.13.tgz", - "integrity": "sha512-lu8E2RyzKzzFbNu4ICmY/2HltMZlJxMNg3saJ+r8I9vWbWbwdX7GOWUJdP4fbjEOm6aa52mnnd+uIRrT3dNEyA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.1.2", - "@smithy/credential-provider-imds": "^4.0.4", - "@smithy/node-config-provider": "^4.1.1", - "@smithy/property-provider": "^4.0.2", - "@smithy/smithy-client": "^4.2.5", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-endpoints": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.4.tgz", - "integrity": "sha512-VfFATC1bmZLV2858B/O1NpMcL32wYo8DPPhHxYxDCodDl3f3mSZ5oJheW1IF91A0EeAADz2WsakM/hGGPGNKLg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.1", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-retry": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.3.tgz", - "integrity": "sha512-DPuYjZQDXmKr/sNvy9Spu8R/ESa2e22wXZzSAY6NkjOLj6spbIje/Aq8rT97iUMdDj0qHMRIe+bTxvlU74d9Ng==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.0.3", - "@smithy/types": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.0.tgz", - "integrity": "sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.0.2", - "@smithy/node-http-handler": "^4.0.4", - "@smithy/types": "^4.2.0", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-stream/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@librechat/agents/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@librechat/agents/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -24232,9 +23668,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", - "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.0.tgz", + "integrity": "sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -24476,12 +23912,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.2.tgz", - "integrity": "sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.3.tgz", + "integrity": "sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.2.0", + "@smithy/types": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -26743,6 +26179,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -27192,6 +26634,57 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -28056,6 +27549,34 @@ "postcss": "^8.4" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -28607,6 +28128,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, "node_modules/domain-browser": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", @@ -28619,6 +28154,18 @@ "url": "https://bevry.me/fund" } }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -28632,6 +28179,35 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -28763,6 +28339,43 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -31645,6 +31258,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -34066,9 +33698,9 @@ } }, "node_modules/langsmith": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.20.tgz", - "integrity": "sha512-zwVQos6tjcksCTfdM67QKq7yyED4GmQiZw/sJ6UCMYZxlvTMMg3PeQ9tOePXAWNWoJygOnH+EwGXr7gYOOETDg==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.29.tgz", + "integrity": "sha512-JPF2B339qpYy9FyuY4Yz1aWYtgPlFc/a+VTj3L/JcFLHCiMP7+Ig8I9jO+o1QwVa+JU3iugL1RS0wwc+Glw0zA==", "license": "MIT", "dependencies": { "@types/uuid": "^10.0.0", @@ -34891,9 +34523,10 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/longest-streak": { "version": "3.1.0", @@ -36873,6 +36506,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -37399,6 +37044,31 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -43362,6 +43032,15 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -45374,7 +45053,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.83", + "version": "0.7.84", "license": "ISC", "dependencies": { "axios": "^1.8.2", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index ca28fc2d4..8cf848dcf 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.83", + "version": "0.7.84", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/specs/web.spec.ts b/packages/data-provider/specs/web.spec.ts new file mode 100644 index 000000000..dc6129ba3 --- /dev/null +++ b/packages/data-provider/specs/web.spec.ts @@ -0,0 +1,899 @@ +import type { + ScraperTypes, + TCustomConfig, + RerankerTypes, + SearchProviders, + TWebSearchConfig, +} from '../src/config'; +import { webSearchAuth, loadWebSearchAuth, extractWebSearchEnvVars } from '../src/web'; +import { AuthType } from '../src/schemas'; + +// Mock the extractVariableName function +jest.mock('../src/utils', () => ({ + extractVariableName: (value: string) => { + if (!value || typeof value !== 'string') return null; + const match = value.match(/^\${(.+)}$/); + return match ? match[1] : null; + }, +})); + +describe('web.ts', () => { + describe('extractWebSearchEnvVars', () => { + it('should return empty array if config is undefined', () => { + const result = extractWebSearchEnvVars({ + keys: ['serperApiKey', 'jinaApiKey'], + config: undefined, + }); + + expect(result).toEqual([]); + }); + + it('should extract environment variable names from config values', () => { + const config: Partial = { + serperApiKey: '${SERPER_API_KEY}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: 'actual-api-key', // Not in env var format + safeSearch: true, + }; + + const result = extractWebSearchEnvVars({ + keys: ['serperApiKey', 'jinaApiKey', 'cohereApiKey'], + config: config as TWebSearchConfig, + }); + + expect(result).toEqual(['SERPER_API_KEY', 'JINA_API_KEY']); + }); + + it('should only extract variables for keys that exist in the config', () => { + const config: Partial = { + serperApiKey: '${SERPER_API_KEY}', + // firecrawlApiKey is missing + safeSearch: true, + }; + + const result = extractWebSearchEnvVars({ + keys: ['serperApiKey', 'firecrawlApiKey'], + config: config as TWebSearchConfig, + }); + + expect(result).toEqual(['SERPER_API_KEY']); + }); + }); + + describe('loadWebSearchAuth', () => { + // Common test variables + const userId = 'test-user-id'; + let mockLoadAuthValues: jest.Mock; + let webSearchConfig: TCustomConfig['webSearch']; + + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + + // Initialize the mock function + mockLoadAuthValues = jest.fn(); + + // Initialize a basic webSearchConfig + webSearchConfig = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + }; + }); + + it('should return authenticated=true when all required categories are authenticated', async () => { + // Mock successful authentication for all services + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + expect(result.authTypes).toHaveLength(3); // providers, scrapers, rerankers + expect(result.authResult).toHaveProperty('serperApiKey', 'test-api-key'); + expect(result.authResult).toHaveProperty('firecrawlApiKey', 'test-api-key'); + + // The implementation only includes one reranker in the result + // It will be either jina or cohere, but not both + if (result.authResult.rerankerType === 'jina') { + expect(result.authResult).toHaveProperty('jinaApiKey', 'test-api-key'); + } else { + expect(result.authResult).toHaveProperty('cohereApiKey', 'test-api-key'); + } + + expect(result.authResult).toHaveProperty('searchProvider', 'serper'); + expect(result.authResult).toHaveProperty('scraperType', 'firecrawl'); + expect(['jina', 'cohere']).toContain(result.authResult.rerankerType as string); + }); + + it('should return authenticated=false when a required category is not authenticated', async () => { + // Mock authentication failure for the providers category + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + // Only provide values for scrapers and rerankers, not for providers + if (field !== 'SERPER_API_KEY') { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(false); + // We should still have authTypes for the categories we checked + expect(result.authTypes.some(([category]) => category === 'providers')).toBe(true); + }); + + it('should handle exceptions from loadAuthValues', async () => { + // Mock loadAuthValues to throw an error + mockLoadAuthValues.mockImplementation(() => { + throw new Error('Authentication failed'); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + throwError: false, // Don't throw errors + }); + + expect(result.authenticated).toBe(false); + }); + + it('should correctly identify user-provided vs system-defined auth', async () => { + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + SERPER_API_KEY: 'system-api-key', + FIRECRAWL_API_KEY: 'system-api-key', + JINA_API_KEY: 'system-api-key', + }; + + // Mock loadAuthValues to return different values for some keys + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + if (field === 'SERPER_API_KEY') { + // This matches the system env var + result[field] = 'system-api-key'; + } else if (field === 'FIRECRAWL_API_KEY') { + // This is different from the system env var (user provided) + result[field] = 'user-api-key'; + } else if (field === 'FIRECRAWL_API_URL') { + result[field] = 'https://api.firecrawl.dev'; + } else if (field === 'JINA_API_KEY') { + // This matches the system env var + result[field] = 'system-api-key'; + } else { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + // Check for providers (system-defined) and scrapers (user-provided) + const providersAuthType = result.authTypes.find( + ([category]) => category === 'providers', + )?.[1]; + const scrapersAuthType = result.authTypes.find(([category]) => category === 'scrapers')?.[1]; + + expect(providersAuthType).toBe(AuthType.SYSTEM_DEFINED); + expect(scrapersAuthType).toBe(AuthType.USER_PROVIDED); + + // Restore original env + process.env = originalEnv; + }); + + it('should handle optional fields correctly', async () => { + // Create a config without the optional firecrawlApiUrl + const configWithoutOptional = { ...webSearchConfig } as Partial; + delete configWithoutOptional.firecrawlApiUrl; + + mockLoadAuthValues.mockImplementation(({ authFields, optional }) => { + const result: Record = {}; + authFields.forEach((field) => { + // Don't provide values for optional fields + if (!optional?.has(field)) { + result[field] = 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig: configWithoutOptional as TWebSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + expect(result.authResult).toHaveProperty('firecrawlApiKey', 'test-api-key'); + // Optional URL should not be in the result + expect(result.authResult.firecrawlApiUrl).toBeUndefined(); + }); + + it('should preserve safeSearch setting from webSearchConfig', async () => { + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = 'test-api-key'; + }); + return Promise.resolve(result); + }); + + // Test with safeSearch: false + const configWithSafeSearchOff = { ...webSearchConfig, safeSearch: false } as TWebSearchConfig; + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig: configWithSafeSearchOff, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authResult).toHaveProperty('safeSearch', false); + }); + + it('should set the correct service types in authResult', async () => { + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + // Check that the correct service types are set + expect(result.authResult.searchProvider).toBe('serper' as SearchProviders); + expect(result.authResult.scraperType).toBe('firecrawl' as ScraperTypes); + // One of the rerankers should be set + expect(['jina', 'cohere']).toContain(result.authResult.rerankerType as string); + }); + + it('should check all services if none are specified', async () => { + // Initialize a webSearchConfig without specific services + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + }; + + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + + // Should have checked all categories + expect(result.authTypes).toHaveLength(3); + + // Should have set values for all categories + expect(result.authResult.searchProvider).toBeDefined(); + expect(result.authResult.scraperType).toBeDefined(); + expect(result.authResult.rerankerType).toBeDefined(); + }); + + it('should correctly identify authTypes based on specific configurations', async () => { + // Set up environment variables for system-defined auth + const originalEnv = process.env; + process.env = { + ...originalEnv, + SERPER_API_KEY: 'system-serper-key', + FIRECRAWL_API_KEY: 'system-firecrawl-key', + FIRECRAWL_API_URL: 'https://api.firecrawl.dev', + JINA_API_KEY: 'system-jina-key', + COHERE_API_KEY: 'system-cohere-key', + }; + + // Initialize webSearchConfig with environment variable references + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + // Specify which services to use + searchProvider: 'serper' as SearchProviders, + scraperType: 'firecrawl' as ScraperTypes, + rerankerType: 'jina' as RerankerTypes, + }; + + // Mock loadAuthValues to return the actual values + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + if (field === 'SERPER_API_KEY') { + result[field] = 'system-serper-key'; + } else if (field === 'FIRECRAWL_API_KEY') { + result[field] = 'system-firecrawl-key'; + } else if (field === 'FIRECRAWL_API_URL') { + result[field] = 'https://api.firecrawl.dev'; + } else if (field === 'JINA_API_KEY') { + result[field] = 'system-jina-key'; + } else if (field === 'COHERE_API_KEY') { + result[field] = 'system-cohere-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + // Verify that all required fields are present in the authResult + expect(result.authResult).toHaveProperty('serperApiKey'); + expect(result.authResult).toHaveProperty('firecrawlApiKey'); + expect(result.authResult).toHaveProperty('firecrawlApiUrl'); + expect(result.authResult).toHaveProperty('jinaApiKey'); + expect(result.authResult).toHaveProperty('searchProvider'); + expect(result.authResult).toHaveProperty('scraperType'); + expect(result.authResult).toHaveProperty('rerankerType'); + + expect(result.authenticated).toBe(true); + + // Verify authTypes for each category + const providersAuthType = result.authTypes.find( + ([category]) => category === 'providers', + )?.[1]; + const scrapersAuthType = result.authTypes.find(([category]) => category === 'scrapers')?.[1]; + const rerankersAuthType = result.authTypes.find( + ([category]) => category === 'rerankers', + )?.[1]; + + // All should be system-defined since we're using environment variables + expect(providersAuthType).toBe(AuthType.SYSTEM_DEFINED); + expect(scrapersAuthType).toBe(AuthType.SYSTEM_DEFINED); + expect(rerankersAuthType).toBe(AuthType.SYSTEM_DEFINED); + + // Verify the authResult contains the correct values + expect(result.authResult).toHaveProperty('serperApiKey', 'system-serper-key'); + expect(result.authResult).toHaveProperty('firecrawlApiKey', 'system-firecrawl-key'); + expect(result.authResult).toHaveProperty('firecrawlApiUrl', 'https://api.firecrawl.dev'); + expect(result.authResult).toHaveProperty('jinaApiKey', 'system-jina-key'); + expect(result.authResult).toHaveProperty('searchProvider', 'serper'); + expect(result.authResult).toHaveProperty('scraperType', 'firecrawl'); + expect(result.authResult).toHaveProperty('rerankerType', 'jina'); + + // Restore original env + process.env = originalEnv; + }); + + it('should handle custom variable names in environment variables', async () => { + // Set up environment variables with custom names + const originalEnv = process.env; + process.env = { + ...originalEnv, + CUSTOM_SERPER_KEY: 'custom-serper-key', + CUSTOM_FIRECRAWL_KEY: 'custom-firecrawl-key', + CUSTOM_FIRECRAWL_URL: 'https://custom.firecrawl.dev', + CUSTOM_JINA_KEY: 'custom-jina-key', + CUSTOM_COHERE_KEY: 'custom-cohere-key', + }; + + // Initialize webSearchConfig with custom variable names + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${CUSTOM_SERPER_KEY}', + firecrawlApiKey: '${CUSTOM_FIRECRAWL_KEY}', + firecrawlApiUrl: '${CUSTOM_FIRECRAWL_URL}', + jinaApiKey: '${CUSTOM_JINA_KEY}', + cohereApiKey: '${CUSTOM_COHERE_KEY}', + safeSearch: true, + // Specify which services to use + searchProvider: 'serper' as SearchProviders, + scraperType: 'firecrawl' as ScraperTypes, + rerankerType: 'jina' as RerankerTypes, // Only Jina will be checked + }; + + // Mock loadAuthValues to return the actual values + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + if (field === 'CUSTOM_SERPER_KEY') { + result[field] = 'custom-serper-key'; + } else if (field === 'CUSTOM_FIRECRAWL_KEY') { + result[field] = 'custom-firecrawl-key'; + } else if (field === 'CUSTOM_FIRECRAWL_URL') { + result[field] = 'https://custom.firecrawl.dev'; + } else if (field === 'CUSTOM_JINA_KEY') { + result[field] = 'custom-jina-key'; + } + // Note: CUSTOM_COHERE_KEY is not checked because we specified jina as rerankerType + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + + // Verify the authResult contains the correct values from custom variables + expect(result.authResult).toHaveProperty('serperApiKey', 'custom-serper-key'); + expect(result.authResult).toHaveProperty('firecrawlApiKey', 'custom-firecrawl-key'); + expect(result.authResult).toHaveProperty('firecrawlApiUrl', 'https://custom.firecrawl.dev'); + expect(result.authResult).toHaveProperty('jinaApiKey', 'custom-jina-key'); + // cohereApiKey should not be in the result since we specified jina as rerankerType + expect(result.authResult).not.toHaveProperty('cohereApiKey'); + + // Verify the service types are set correctly + expect(result.authResult).toHaveProperty('searchProvider', 'serper'); + expect(result.authResult).toHaveProperty('scraperType', 'firecrawl'); + expect(result.authResult).toHaveProperty('rerankerType', 'jina'); + + // Restore original env + process.env = originalEnv; + }); + + it('should always return authTypes array with exactly 3 categories', async () => { + // Set up environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + SERPER_API_KEY: 'test-key', + FIRECRAWL_API_KEY: 'test-key', + FIRECRAWL_API_URL: 'https://api.firecrawl.dev', + JINA_API_KEY: 'test-key', + }; + + // Initialize webSearchConfig with environment variable references + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + }; + + // Mock loadAuthValues to return values + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + // Get the number of categories from webSearchAuth + const expectedCategoryCount = Object.keys(webSearchAuth).length; + + // Verify authTypes array structure + expect(result.authTypes).toHaveLength(expectedCategoryCount); + + // Verify each category exists exactly once + const categories = result.authTypes.map(([category]) => category); + Object.keys(webSearchAuth).forEach((category) => { + expect(categories).toContain(category); + }); + + // Verify no duplicate categories + expect(new Set(categories).size).toBe(expectedCategoryCount); + + // Verify each entry has the correct format [category, AuthType] + result.authTypes.forEach(([category, authType]) => { + expect(typeof category).toBe('string'); + expect([AuthType.SYSTEM_DEFINED, AuthType.USER_PROVIDED]).toContain(authType); + }); + + // Restore original env + process.env = originalEnv; + }); + + it('should maintain authTypes array structure even when authentication fails', async () => { + // Set up environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + SERPER_API_KEY: 'test-key', + // Missing other keys to force authentication failure + }; + + // Initialize webSearchConfig with environment variable references + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + }; + + // Mock loadAuthValues to return partial values + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + if (field === 'SERPER_API_KEY') { + result[field] = 'test-key'; + } + // Other fields are intentionally missing + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + // Get the number of categories from webSearchAuth + const expectedCategoryCount = Object.keys(webSearchAuth).length; + + // Verify authentication failed + expect(result.authenticated).toBe(false); + + // Verify authTypes array structure is maintained + expect(result.authTypes).toHaveLength(expectedCategoryCount); + + // Verify each category exists exactly once + const categories = result.authTypes.map(([category]) => category); + Object.keys(webSearchAuth).forEach((category) => { + expect(categories).toContain(category); + }); + + // Verify no duplicate categories + expect(new Set(categories).size).toBe(expectedCategoryCount); + + // Verify each entry has the correct format [category, AuthType] + result.authTypes.forEach(([category, authType]) => { + expect(typeof category).toBe('string'); + expect([AuthType.SYSTEM_DEFINED, AuthType.USER_PROVIDED]).toContain(authType); + }); + + // Restore original env + process.env = originalEnv; + }); + }); + + describe('webSearchAuth', () => { + it('should have the expected structure', () => { + // Check that all expected categories exist + expect(webSearchAuth).toHaveProperty('providers'); + expect(webSearchAuth).toHaveProperty('scrapers'); + expect(webSearchAuth).toHaveProperty('rerankers'); + + // Check providers + expect(webSearchAuth.providers).toHaveProperty('serper'); + expect(webSearchAuth.providers.serper).toHaveProperty('serperApiKey', 1); + + // Check scrapers + expect(webSearchAuth.scrapers).toHaveProperty('firecrawl'); + expect(webSearchAuth.scrapers.firecrawl).toHaveProperty('firecrawlApiKey', 1); + expect(webSearchAuth.scrapers.firecrawl).toHaveProperty('firecrawlApiUrl', 0); + + // Check rerankers + expect(webSearchAuth.rerankers).toHaveProperty('jina'); + expect(webSearchAuth.rerankers.jina).toHaveProperty('jinaApiKey', 1); + expect(webSearchAuth.rerankers).toHaveProperty('cohere'); + expect(webSearchAuth.rerankers.cohere).toHaveProperty('cohereApiKey', 1); + }); + + it('should mark required keys with value 1', () => { + // All keys with value 1 are required + expect(webSearchAuth.providers.serper.serperApiKey).toBe(1); + expect(webSearchAuth.scrapers.firecrawl.firecrawlApiKey).toBe(1); + expect(webSearchAuth.rerankers.jina.jinaApiKey).toBe(1); + expect(webSearchAuth.rerankers.cohere.cohereApiKey).toBe(1); + }); + + it('should mark optional keys with value 0', () => { + // Keys with value 0 are optional + expect(webSearchAuth.scrapers.firecrawl.firecrawlApiUrl).toBe(0); + }); + }); + describe('loadWebSearchAuth with specific services', () => { + // Common test variables + const userId = 'test-user-id'; + let mockLoadAuthValues: jest.Mock; + + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + + // Initialize the mock function + mockLoadAuthValues = jest.fn(); + }); + + it('should only check the specified searchProvider', async () => { + // Initialize a webSearchConfig with a specific searchProvider + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + searchProvider: 'serper' as SearchProviders, + }; + + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + expect(result.authResult.searchProvider).toBe('serper'); + + // Verify that only SERPER_API_KEY was requested for the providers category + const providerCalls = mockLoadAuthValues.mock.calls.filter((call) => + call[0].authFields.includes('SERPER_API_KEY'), + ); + expect(providerCalls.length).toBe(1); + }); + + it('should only check the specified scraperType', async () => { + // Initialize a webSearchConfig with a specific scraperType + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + scraperType: 'firecrawl' as ScraperTypes, + }; + + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + expect(result.authResult.scraperType).toBe('firecrawl'); + + // Verify that only FIRECRAWL_API_KEY and FIRECRAWL_API_URL were requested for the scrapers category + const scraperCalls = mockLoadAuthValues.mock.calls.filter((call) => + call[0].authFields.includes('FIRECRAWL_API_KEY'), + ); + expect(scraperCalls.length).toBe(1); + }); + + it('should only check the specified rerankerType', async () => { + // Initialize a webSearchConfig with a specific rerankerType + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + rerankerType: 'jina' as RerankerTypes, + }; + + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + expect(result.authResult.rerankerType).toBe('jina'); + + // Verify that only JINA_API_KEY was requested for the rerankers category + const rerankerCalls = mockLoadAuthValues.mock.calls.filter((call) => + call[0].authFields.includes('JINA_API_KEY'), + ); + expect(rerankerCalls.length).toBe(1); + + // Verify that COHERE_API_KEY was not requested + const cohereCalls = mockLoadAuthValues.mock.calls.filter((call) => + call[0].authFields.includes('COHERE_API_KEY'), + ); + expect(cohereCalls.length).toBe(0); + }); + + it('should handle invalid specified service gracefully', async () => { + // Initialize a webSearchConfig with an invalid searchProvider + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + searchProvider: 'invalid-provider' as SearchProviders, + }; + + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + // Should fail because the specified provider doesn't exist + expect(result.authenticated).toBe(false); + }); + + it('should fail authentication when specified service is not authenticated but others are', async () => { + // Initialize a webSearchConfig with a specific rerankerType (jina) + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + rerankerType: 'jina' as RerankerTypes, + }; + + // Mock authentication where cohere is authenticated but jina is not + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + // Authenticate all fields except JINA_API_KEY + if (field !== 'JINA_API_KEY') { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + } + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + // Should fail because the specified reranker (jina) is not authenticated + // even though another reranker (cohere) might be authenticated + expect(result.authenticated).toBe(false); + + // Verify that JINA_API_KEY was requested + const jinaApiKeyCalls = mockLoadAuthValues.mock.calls.filter((call) => + call[0].authFields.includes('JINA_API_KEY'), + ); + expect(jinaApiKeyCalls.length).toBe(1); + + // Verify that COHERE_API_KEY was not requested since we specified jina + const cohereApiKeyCalls = mockLoadAuthValues.mock.calls.filter((call) => + call[0].authFields.includes('COHERE_API_KEY'), + ); + expect(cohereApiKeyCalls.length).toBe(0); + }); + + it('should check all services if none are specified', async () => { + // Initialize a webSearchConfig without specific services + const webSearchConfig: TCustomConfig['webSearch'] = { + serperApiKey: '${SERPER_API_KEY}', + firecrawlApiKey: '${FIRECRAWL_API_KEY}', + firecrawlApiUrl: '${FIRECRAWL_API_URL}', + jinaApiKey: '${JINA_API_KEY}', + cohereApiKey: '${COHERE_API_KEY}', + safeSearch: true, + }; + + // Mock successful authentication + mockLoadAuthValues.mockImplementation(({ authFields }) => { + const result: Record = {}; + authFields.forEach((field) => { + result[field] = + field === 'FIRECRAWL_API_URL' ? 'https://api.firecrawl.dev' : 'test-api-key'; + }); + return Promise.resolve(result); + }); + + const result = await loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result.authenticated).toBe(true); + + // Should have checked all categories + expect(result.authTypes).toHaveLength(3); + + // Should have set values for all categories + expect(result.authResult.searchProvider).toBeDefined(); + expect(result.authResult.scraperType).toBeDefined(); + expect(result.authResult.rerankerType).toBeDefined(); + }); + }); +}); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index fa953decc..55562f4f8 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -167,6 +167,7 @@ export enum AgentCapabilities { end_after_tools = 'end_after_tools', execute_code = 'execute_code', file_search = 'file_search', + web_search = 'web_search', artifacts = 'artifacts', actions = 'actions', tools = 'tools', @@ -245,11 +246,12 @@ export const agentsEndpointSChema = baseEndpointSchema.merge( .default([ AgentCapabilities.execute_code, AgentCapabilities.file_search, + AgentCapabilities.web_search, AgentCapabilities.artifacts, AgentCapabilities.actions, AgentCapabilities.tools, - AgentCapabilities.ocr, AgentCapabilities.chain, + AgentCapabilities.ocr, ]), }), ); @@ -494,6 +496,7 @@ export const intefaceSchema = z agents: z.boolean().optional(), temporaryChat: z.boolean().optional(), runCode: z.boolean().optional(), + webSearch: z.boolean().optional(), }) .default({ endpointsMenu: true, @@ -507,6 +510,7 @@ export const intefaceSchema = z agents: true, temporaryChat: true, runCode: true, + webSearch: true, }); export type TInterfaceConfig = z.infer; @@ -567,6 +571,11 @@ export type TStartupConfig = { instanceProjectId: string; bundlerURL?: string; staticBundlerURL?: string; + webSearch?: { + searchProvider?: SearchProviders; + scraperType?: ScraperTypes; + rerankerType?: RerankerTypes; + }; }; export enum OCRStrategy { @@ -574,10 +583,45 @@ export enum OCRStrategy { CUSTOM_OCR = 'custom_ocr', } +export enum SearchCategories { + PROVIDERS = 'providers', + SCRAPERS = 'scrapers', + RERANKERS = 'rerankers', +} + +export enum SearchProviders { + SERPER = 'serper', + SEARXNG = 'searxng', +} + +export enum ScraperTypes { + FIRECRAWL = 'firecrawl', + SERPER = 'serper', +} + +export enum RerankerTypes { + JINA = 'jina', + COHERE = 'cohere', +} + +export const webSearchSchema = z.object({ + serperApiKey: z.string().optional().default('${SERPER_API_KEY}'), + firecrawlApiKey: z.string().optional().default('${FIRECRAWL_API_KEY}'), + firecrawlApiUrl: z.string().optional().default('${FIRECRAWL_API_URL}'), + jinaApiKey: z.string().optional().default('${JINA_API_KEY}'), + cohereApiKey: z.string().optional().default('${COHERE_API_KEY}'), + searchProvider: z.nativeEnum(SearchProviders).optional(), + scraperType: z.nativeEnum(ScraperTypes).optional(), + rerankerType: z.nativeEnum(RerankerTypes).optional(), + safeSearch: z.boolean().default(true), +}); + +export type TWebSearchConfig = z.infer; + export const ocrSchema = z.object({ mistralModel: z.string().optional(), - apiKey: z.string().optional().default('OCR_API_KEY'), - baseURL: z.string().optional().default('OCR_BASEURL'), + apiKey: z.string().optional().default('${OCR_API_KEY}'), + baseURL: z.string().optional().default('${OCR_BASEURL}'), strategy: z.nativeEnum(OCRStrategy).default(OCRStrategy.MISTRAL_OCR), }); @@ -597,6 +641,7 @@ export const configSchema = z.object({ version: z.string(), cache: z.boolean().default(true), ocr: ocrSchema.optional(), + webSearch: webSearchSchema.optional(), secureImageLinks: z.boolean().optional(), imageOutputType: z.nativeEnum(EImageOutputType).default(EImageOutputType.PNG), includedTools: z.array(z.string()).optional(), @@ -1336,6 +1381,8 @@ export enum LocalStorageKeys { LAST_MCP_ = 'LAST_MCP_', /** Last checked toggle for Code Interpreter API per conversation ID */ LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_', + /** Last checked toggle for Web Search per conversation ID */ + LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_', } export enum ForkOptions { diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index b7289454f..65b2f77a7 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -14,6 +14,8 @@ export * from './generate'; export * from './models'; /* mcp */ export * from './mcp'; +/* web search */ +export * from './web'; /* RBAC */ export * from './permissions'; export * from './roles'; @@ -25,6 +27,7 @@ export * from './types/files'; export * from './types/mutations'; export * from './types/queries'; export * from './types/runs'; +export * from './types/web'; /* query/mutation keys */ export * from './keys'; /* api call helpers */ diff --git a/packages/data-provider/src/permissions.ts b/packages/data-provider/src/permissions.ts index ea78d449b..c75f21053 100644 --- a/packages/data-provider/src/permissions.ts +++ b/packages/data-provider/src/permissions.ts @@ -28,6 +28,10 @@ export enum PermissionTypes { * Type for using the "Run Code" LC Code Interpreter API feature */ RUN_CODE = 'RUN_CODE', + /** + * Type for using the "Web Search" feature + */ + WEB_SEARCH = 'WEB_SEARCH', } /** @@ -79,6 +83,11 @@ export const runCodePermissionsSchema = z.object({ }); export type TRunCodePermissions = z.infer; +export const webSearchPermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(true), +}); +export type TWebSearchPermissions = z.infer; + // Define a single permissions schema that holds all permission types. export const permissionsSchema = z.object({ [PermissionTypes.PROMPTS]: promptPermissionsSchema, @@ -87,4 +96,5 @@ export const permissionsSchema = z.object({ [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema, [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, + [PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema, }); diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index ca7d64b20..bb7b1b5ff 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -6,6 +6,7 @@ import { agentPermissionsSchema, promptPermissionsSchema, runCodePermissionsSchema, + webSearchPermissionsSchema, bookmarkPermissionsSchema, multiConvoPermissionsSchema, temporaryChatPermissionsSchema, @@ -62,6 +63,9 @@ const defaultRolesSchema = z.object({ [PermissionTypes.RUN_CODE]: runCodePermissionsSchema.extend({ [Permissions.USE]: z.boolean().default(true), }), + [PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema.extend({ + [Permissions.USE]: z.boolean().default(true), + }), }), }), [SystemRoles.USER]: roleSchema.extend({ @@ -96,6 +100,9 @@ export const roleDefaults = defaultRolesSchema.parse({ [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true, }, + [PermissionTypes.WEB_SEARCH]: { + [Permissions.USE]: true, + }, }, }, [SystemRoles.USER]: { @@ -107,6 +114,7 @@ export const roleDefaults = defaultRolesSchema.parse({ [PermissionTypes.MULTI_CONVO]: {}, [PermissionTypes.TEMPORARY_CHAT]: {}, [PermissionTypes.RUN_CODE]: {}, + [PermissionTypes.WEB_SEARCH]: {}, }, }, }); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index d7fc2d23d..fca5becdf 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { Tools } from './types/assistants'; import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants'; +import type { SearchResultData } from './types/web'; import type { TEphemeralAgent } from './types'; import type { TFile } from './types/files'; @@ -101,7 +102,8 @@ export const isEphemeralAgent = ( } const hasMCPSelected = (ephemeralAgent?.mcp?.length ?? 0) > 0; const hasCodeSelected = (ephemeralAgent?.execute_code ?? false) === true; - return hasMCPSelected || hasCodeSelected; + const hasSearchSelected = (ephemeralAgent?.web_search ?? false) === true; + return hasMCPSelected || hasCodeSelected || hasSearchSelected; }; export const isParamEndpoint = ( @@ -177,6 +179,7 @@ export const defaultAgentFormValues = { recursion_limit: undefined, [Tools.execute_code]: false, [Tools.file_search]: false, + [Tools.web_search]: false, }; export const ImageVisionTool: FunctionTool = { @@ -517,7 +520,13 @@ export const tMessageSchema = z.object({ iconURL: z.string().nullable().optional(), }); -export type TAttachmentMetadata = { messageId: string; toolCallId: string }; +export type TAttachmentMetadata = { + type?: Tools; + messageId: string; + toolCallId: string; + [Tools.web_search]?: SearchResultData; +}; + export type TAttachment = | (TFile & TAttachmentMetadata) | (Pick & { diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 8e70bdd0a..1967b0a1f 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -44,6 +44,7 @@ export type TEndpointOption = { export type TEphemeralAgent = { mcp?: string[]; + web_search?: boolean; execute_code?: boolean; }; @@ -79,7 +80,7 @@ export type EventSubmission = Omit & { initialRe export type TPluginAction = { pluginKey: string; action: 'install' | 'uninstall'; - auth?: unknown; + auth?: Partial>; isEntityTool?: boolean; }; @@ -89,7 +90,7 @@ export type TUpdateUserPlugins = { isEntityTool?: boolean; pluginKey: string; action: string; - auth?: unknown; + auth?: Partial>; }; // TODO `label` needs to be changed to the proper `TranslationKeys` diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 16f6b7d76..963d3396e 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -19,6 +19,7 @@ export enum Tools { execute_code = 'execute_code', code_interpreter = 'code_interpreter', file_search = 'file_search', + web_search = 'web_search', retrieval = 'retrieval', function = 'function', } diff --git a/packages/data-provider/src/types/mutations.ts b/packages/data-provider/src/types/mutations.ts index 7e76ba8fd..7193e1d35 100644 --- a/packages/data-provider/src/types/mutations.ts +++ b/packages/data-provider/src/types/mutations.ts @@ -129,18 +129,18 @@ export type UpdateAgentVariables = { data: AgentUpdateParams; }; -export type DuplicateVersionError = Error & { - statusCode?: number; - details?: { - duplicateVersion?: any; - versionIndex?: number - } +export type DuplicateVersionError = Error & { + statusCode?: number; + details?: { + duplicateVersion?: unknown; + versionIndex?: number; + }; }; export type UpdateAgentMutationOptions = MutationOptions< - Agent, - UpdateAgentVariables, - unknown, + Agent, + UpdateAgentVariables, + unknown, DuplicateVersionError >; diff --git a/packages/data-provider/src/types/queries.ts b/packages/data-provider/src/types/queries.ts index 5d605ed02..d396c1fff 100644 --- a/packages/data-provider/src/types/queries.ts +++ b/packages/data-provider/src/types/queries.ts @@ -101,7 +101,11 @@ export type AllPromptGroupsResponse = t.TPromptGroup[]; export type ConversationTagsResponse = s.TConversationTag[]; export type VerifyToolAuthParams = { toolId: string }; -export type VerifyToolAuthResponse = { authenticated: boolean; message?: string | s.AuthType }; +export type VerifyToolAuthResponse = { + authenticated: boolean; + message?: string | s.AuthType; + authTypes?: [string, s.AuthType][]; +}; export type GetToolCallParams = { conversationId: string }; export type ToolCallResults = a.ToolCallResult[]; diff --git a/packages/data-provider/src/types/web.ts b/packages/data-provider/src/types/web.ts new file mode 100644 index 000000000..8f3c81220 --- /dev/null +++ b/packages/data-provider/src/types/web.ts @@ -0,0 +1,593 @@ +import type { Logger as WinstonLogger } from 'winston'; +import type { RunnableConfig } from '@langchain/core/runnables'; + +export type SearchRefType = 'search' | 'image' | 'news' | 'video' | 'ref'; + +export enum DATE_RANGE { + PAST_HOUR = 'h', + PAST_24_HOURS = 'd', + PAST_WEEK = 'w', + PAST_MONTH = 'm', + PAST_YEAR = 'y', +} + +export type SearchProvider = 'serper' | 'searxng'; +export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none'; + +export interface Highlight { + score: number; + text: string; + references?: UsedReferences; +} + +export type ProcessedSource = { + content?: string; + attribution?: string; + references?: References; + highlights?: Highlight[]; + processed?: boolean; +}; + +export type ProcessedOrganic = OrganicResult & ProcessedSource; +export type ProcessedTopStory = TopStoryResult & ProcessedSource; +export type ValidSource = ProcessedOrganic | ProcessedTopStory; + +export type ResultReference = { + link: string; + type: 'link' | 'image' | 'video'; + title?: string; + attribution?: string; +}; +export interface SearchResultData { + turn?: number; + organic?: ProcessedOrganic[]; + topStories?: ProcessedTopStory[]; + images?: ImageResult[]; + videos?: VideoResult[]; + places?: PlaceResult[]; + news?: NewsResult[]; + shopping?: ShoppingResult[]; + knowledgeGraph?: KnowledgeGraphResult; + answerBox?: AnswerBoxResult; + peopleAlsoAsk?: PeopleAlsoAskResult[]; + relatedSearches?: Array<{ query: string }>; + references?: ResultReference[]; + error?: string; +} + +export interface SearchResult { + data?: SearchResultData; + error?: string; + success: boolean; +} + +export interface Source { + link: string; + html?: string; + title?: string; + snippet?: string; + date?: string; +} + +export interface SearchConfig { + searchProvider?: SearchProvider; + serperApiKey?: string; + searxngInstanceUrl?: string; + searxngApiKey?: string; +} + +export type References = { + links: MediaReference[]; + images: MediaReference[]; + videos: MediaReference[]; +}; +export interface ScrapeResult { + url: string; + error?: boolean; + content: string; + attribution?: string; + references?: References; + highlights?: Highlight[]; +} + +export interface ProcessSourcesConfig { + topResults?: number; + strategies?: string[]; + filterContent?: boolean; + reranker?: unknown; + logger?: Logger; +} + +export interface FirecrawlConfig { + firecrawlApiKey?: string; + firecrawlApiUrl?: string; + firecrawlFormats?: string[]; +} + +export interface ScraperContentResult { + content: string; +} + +export interface ScraperExtractionResult { + no_extraction: ScraperContentResult; +} + +export interface JinaRerankerResult { + index: number; + relevance_score: number; + document?: string | { text: string }; +} + +export interface JinaRerankerResponse { + model: string; + usage: { + total_tokens: number; + }; + results: JinaRerankerResult[]; +} + +export interface CohereRerankerResult { + index: number; + relevance_score: number; +} + +export interface CohereRerankerResponse { + results: CohereRerankerResult[]; + id: string; + meta: { + api_version: { + version: string; + is_experimental: boolean; + }; + billed_units: { + search_units: number; + }; + }; +} + +export type SafeSearchLevel = 0 | 1 | 2; + +export type Logger = WinstonLogger; +export interface SearchToolConfig extends SearchConfig, ProcessSourcesConfig, FirecrawlConfig { + logger?: Logger; + safeSearch?: SafeSearchLevel; + jinaApiKey?: string; + cohereApiKey?: string; + rerankerType?: RerankerType; + onSearchResults?: (results: SearchResult, runnableConfig?: RunnableConfig) => void; + onGetHighlights?: (link: string) => void; +} +export interface MediaReference { + originalUrl: string; + title?: string; + text?: string; +} + +export type UsedReferences = { + type: 'link' | 'image' | 'video'; + originalIndex: number; + reference: MediaReference; +}[]; + +/** Firecrawl */ + +export interface FirecrawlScrapeOptions { + formats?: string[]; + includeTags?: string[]; + excludeTags?: string[]; + headers?: Record; + waitFor?: number; + timeout?: number; +} + +export interface ScrapeMetadata { + // Core source information + sourceURL?: string; + url?: string; + scrapeId?: string; + statusCode?: number; + // Basic metadata + title?: string; + description?: string; + language?: string; + favicon?: string; + viewport?: string; + robots?: string; + 'theme-color'?: string; + // Open Graph metadata + 'og:url'?: string; + 'og:title'?: string; + 'og:description'?: string; + 'og:type'?: string; + 'og:image'?: string; + 'og:image:width'?: string; + 'og:image:height'?: string; + 'og:site_name'?: string; + ogUrl?: string; + ogTitle?: string; + ogDescription?: string; + ogImage?: string; + ogSiteName?: string; + // Article metadata + 'article:author'?: string; + 'article:published_time'?: string; + 'article:modified_time'?: string; + 'article:section'?: string; + 'article:tag'?: string; + 'article:publisher'?: string; + publishedTime?: string; + modifiedTime?: string; + // Twitter metadata + 'twitter:site'?: string | boolean | number | null; + 'twitter:creator'?: string; + 'twitter:card'?: string; + 'twitter:image'?: string; + 'twitter:dnt'?: string; + 'twitter:app:name:iphone'?: string; + 'twitter:app:id:iphone'?: string; + 'twitter:app:url:iphone'?: string; + 'twitter:app:name:ipad'?: string; + 'twitter:app:id:ipad'?: string; + 'twitter:app:url:ipad'?: string; + 'twitter:app:name:googleplay'?: string; + 'twitter:app:id:googleplay'?: string; + 'twitter:app:url:googleplay'?: string; + // Facebook metadata + 'fb:app_id'?: string; + // App links + 'al:ios:url'?: string; + 'al:ios:app_name'?: string; + 'al:ios:app_store_id'?: string; + // Allow for additional properties that might be present + [key: string]: string | number | boolean | null | undefined; +} + +export interface FirecrawlScrapeResponse { + success: boolean; + data?: { + markdown?: string; + html?: string; + rawHtml?: string; + screenshot?: string; + links?: string[]; + metadata?: ScrapeMetadata; + }; + error?: string; +} + +export interface FirecrawlScraperConfig { + apiKey?: string; + apiUrl?: string; + formats?: string[]; + timeout?: number; + logger?: Logger; +} + +export type GetSourcesParams = { + query: string; + date?: DATE_RANGE; + country?: string; + numResults?: number; + safeSearch?: SearchToolConfig['safeSearch']; + images?: boolean; + videos?: boolean; + news?: boolean; + type?: 'search' | 'images' | 'videos' | 'news'; +}; + +/** Serper API */ +export interface VideoResult { + title?: string; + link?: string; + snippet?: string; + imageUrl?: string; + duration?: string; + source?: string; + channel?: string; + date?: string; + position?: number; +} + +export interface PlaceResult { + position?: number; + name?: string; + address?: string; + latitude?: number; + longitude?: number; + rating?: number; + ratingCount?: number; + category?: string; + identifier?: string; +} + +export interface NewsResult { + title?: string; + link?: string; + snippet?: string; + date?: string; + source?: string; + imageUrl?: string; + position?: number; +} + +export interface ShoppingResult { + title?: string; + source?: string; + link?: string; + price?: string; + delivery?: string; + imageUrl?: string; + rating?: number; + ratingCount?: number; + offers?: string; + productId?: string; + position?: number; +} + +export interface ScholarResult { + title?: string; + link?: string; + publicationInfo?: string; + snippet?: string; + year?: number; + citedBy?: number; +} + +export interface ImageResult { + title?: string; + imageUrl?: string; + imageWidth?: number; + imageHeight?: number; + thumbnailUrl?: string; + thumbnailWidth?: number; + thumbnailHeight?: number; + source?: string; + domain?: string; + link?: string; + googleUrl?: string; + position?: number; +} + +export interface SerperSearchPayload extends SerperSearchInput { + /** + * Search type/vertical + * Options: "search" (web), "images", "news", "places", "videos" + */ + type?: 'search' | 'images' | 'news' | 'places' | 'videos'; + + /** + * Starting index for search results pagination (used instead of page) + */ + start?: number; + + /** + * Filtering for safe search + * Options: "off", "moderate", "active" + */ + safe?: 'off' | 'moderate' | 'active'; +} + +export type SerperSearchParameters = Pick & { + engine: 'google'; +}; + +export interface OrganicResult { + position?: number; + title?: string; + link: string; + snippet?: string; + date?: string; + sitelinks?: Array<{ + title: string; + link: string; + }>; +} + +export interface TopStoryResult { + title?: string; + link: string; + source?: string; + date?: string; + imageUrl?: string; +} +export interface KnowledgeGraphResult { + title?: string; + type?: string; + imageUrl?: string; + description?: string; + descriptionSource?: string; + descriptionLink?: string; + attributes?: Record; + website?: string; +} + +export interface AnswerBoxResult { + title?: string; + snippet?: string; + snippetHighlighted?: string[]; + link?: string; + date?: string; +} + +export interface PeopleAlsoAskResult { + question?: string; + snippet?: string; + title?: string; + link?: string; +} + +export type RelatedSearches = Array<{ query: string }>; + +export interface SerperSearchInput { + /** + * The search query string + */ + q: string; + + /** + * Country code for localized results + * Examples: "us", "uk", "ca", "de", etc. + */ + gl?: string; + + /** + * Interface language + * Examples: "en", "fr", "de", etc. + */ + hl?: string; + + /** + * Number of results to return (up to 100) + */ + num?: number; + /** + * Specific location for contextual results + * Example: "New York, NY" + */ + location?: string; + + /** + * Search autocorrection setting + */ + autocorrect?: boolean; + page?: number; + /** + * Date range for search results + * Options: "h" (past hour), "d" (past 24 hours), "w" (past week), + * "m" (past month), "y" (past year) + * `qdr:${DATE_RANGE}` + */ + tbs?: string; +} + +export type SerperResultData = { + searchParameters: SerperSearchPayload; + organic?: OrganicResult[]; + topStories?: TopStoryResult[]; + images?: ImageResult[]; + videos?: VideoResult[]; + places?: PlaceResult[]; + news?: NewsResult[]; + shopping?: ShoppingResult[]; + peopleAlsoAsk?: PeopleAlsoAskResult[]; + relatedSearches?: RelatedSearches; + knowledgeGraph?: KnowledgeGraphResult; + answerBox?: AnswerBoxResult; + credits?: number; +}; + +/** SearXNG */ + +export interface SearxNGSearchPayload { + /** + * The search query string + * Supports syntax specific to different search engines + * Example: "site:github.com SearXNG" + */ + q: string; + + /** + * Comma-separated list of search categories + * Example: "general,images,news" + */ + categories?: string; + + /** + * Comma-separated list of search engines to use + * Example: "google,bing,duckduckgo" + */ + engines?: string; + + /** + * Code of the language for search results + * Example: "en", "fr", "de", "es" + */ + language?: string; + + /** + * Search page number + * Default: 1 + */ + pageno?: number; + + /** + * Time range filter for search results + * Options: "day", "month", "year" + */ + time_range?: 'day' | 'month' | 'year'; + + /** + * Output format of results + * Options: "json", "csv", "rss" + */ + format?: 'json' | 'csv' | 'rss'; + + /** + * Open search results on new tab + * Options: `0` (off), `1` (on) + */ + results_on_new_tab?: 0 | 1; + + /** + * Proxy image results through SearxNG + * Options: true, false + */ + image_proxy?: boolean; + + /** + * Service for autocomplete suggestions + * Options: "google", "dbpedia", "duckduckgo", "mwmbl", + * "startpage", "wikipedia", "stract", "swisscows", "qwant" + */ + autocomplete?: string; + + /** + * Safe search filtering level + * Options: "0" (off), "1" (moderate), "2" (strict) + */ + safesearch?: 0 | 1 | 2; + + /** + * Theme to use for results page + * Default: "simple" (other themes may be available per instance) + */ + theme?: string; + + /** + * List of enabled plugins + * Default: "Hash_plugin,Self_Information,Tracker_URL_remover,Ahmia_blacklist" + */ + enabled_plugins?: string; + + /** + * List of disabled plugins + */ + disabled_plugins?: string; + + /** + * List of enabled engines + */ + enabled_engines?: string; + + /** + * List of disabled engines + */ + disabled_engines?: string; +} + +export interface SearXNGResult { + title?: string; + url?: string; + content?: string; + publishedDate?: string; + img_src?: string; +} + +export type ProcessSourcesFields = { + result: SearchResult; + numElements: number; + query: string; + news: boolean; + proMode: boolean; + onGetHighlights: SearchToolConfig['onGetHighlights']; +}; diff --git a/packages/data-provider/src/utils.ts b/packages/data-provider/src/utils.ts index de41a93dc..a2798aac9 100644 --- a/packages/data-provider/src/utils.ts +++ b/packages/data-provider/src/utils.ts @@ -1,5 +1,15 @@ export const envVarRegex = /^\${(.+)}$/; +/** Extracts the environment variable name from a template literal string */ +export function extractVariableName(value: string): string | null { + if (!value) { + return null; + } + + const match = value.trim().match(envVarRegex); + return match ? match[1] : null; +} + /** Extracts the value of an environment variable from a string. */ export function extractEnvVariable(value: string) { if (!value) { diff --git a/packages/data-provider/src/web.ts b/packages/data-provider/src/web.ts new file mode 100644 index 000000000..9fad9c1cc --- /dev/null +++ b/packages/data-provider/src/web.ts @@ -0,0 +1,270 @@ +import type { + ScraperTypes, + RerankerTypes, + TCustomConfig, + SearchProviders, + TWebSearchConfig, +} from './config'; +import { extractVariableName } from './utils'; +import { SearchCategories } from './config'; +import { AuthType } from './schemas'; + +export function loadWebSearchConfig( + config: TCustomConfig['webSearch'], +): TCustomConfig['webSearch'] { + const serperApiKey = config?.serperApiKey ?? '${SERPER_API_KEY}'; + const firecrawlApiKey = config?.firecrawlApiKey ?? '${FIRECRAWL_API_KEY}'; + const firecrawlApiUrl = config?.firecrawlApiUrl ?? '${FIRECRAWL_API_URL}'; + const jinaApiKey = config?.jinaApiKey ?? '${JINA_API_KEY}'; + const cohereApiKey = config?.cohereApiKey ?? '${COHERE_API_KEY}'; + const safeSearch = config?.safeSearch ?? true; + + return { + ...config, + safeSearch, + jinaApiKey, + cohereApiKey, + serperApiKey, + firecrawlApiKey, + firecrawlApiUrl, + }; +} + +export type TWebSearchKeys = + | 'serperApiKey' + | 'firecrawlApiKey' + | 'firecrawlApiUrl' + | 'jinaApiKey' + | 'cohereApiKey'; + +export type TWebSearchCategories = + | SearchCategories.PROVIDERS + | SearchCategories.SCRAPERS + | SearchCategories.RERANKERS; + +export const webSearchAuth = { + providers: { + serper: { + serperApiKey: 1 as const, + }, + }, + scrapers: { + firecrawl: { + firecrawlApiKey: 1 as const, + /** Optional (0) */ + firecrawlApiUrl: 0 as const, + }, + }, + rerankers: { + jina: { jinaApiKey: 1 as const }, + cohere: { cohereApiKey: 1 as const }, + }, +}; + +/** + * Extracts all API keys from the webSearchAuth configuration object + */ +export const webSearchKeys: TWebSearchKeys[] = []; + +// Iterate through each category (providers, scrapers, rerankers) +for (const category of Object.keys(webSearchAuth)) { + const categoryObj = webSearchAuth[category as TWebSearchCategories]; + + // Iterate through each service within the category + for (const service of Object.keys(categoryObj)) { + const serviceObj = categoryObj[service as keyof typeof categoryObj]; + + // Extract the API keys from the service + for (const key of Object.keys(serviceObj)) { + webSearchKeys.push(key as TWebSearchKeys); + } + } +} + +export function extractWebSearchEnvVars({ + keys, + config, +}: { + keys: TWebSearchKeys[]; + config: TCustomConfig['webSearch'] | undefined; +}): string[] { + if (!config) { + return []; + } + + const authFields: string[] = []; + const relevantKeys = keys.filter((k) => k in config); + + for (const key of relevantKeys) { + const value = config[key]; + if (typeof value === 'string') { + const varName = extractVariableName(value); + if (varName) { + authFields.push(varName); + } + } + } + + return authFields; +} + +/** + * Type for web search authentication result + */ +export interface WebSearchAuthResult { + /** Whether all required categories have at least one authenticated service */ + authenticated: boolean; + /** Authentication type (user_provided or system_defined) by category */ + authTypes: [TWebSearchCategories, AuthType][]; + /** Original authentication values mapped to their respective keys */ + authResult: Partial; +} + +/** + * Loads and verifies web search authentication values + * @param params - Authentication parameters + * @returns Authentication result + */ +export async function loadWebSearchAuth({ + userId, + webSearchConfig, + loadAuthValues, + throwError = true, +}: { + userId: string; + webSearchConfig: TCustomConfig['webSearch']; + loadAuthValues: (params: { + userId: string; + authFields: string[]; + optional?: Set; + throwError?: boolean; + }) => Promise>; + throwError?: boolean; +}): Promise { + let authenticated = true; + const authResult: Partial = {}; + + /** Type-safe iterator for the category-service combinations */ + async function checkAuth( + category: C, + ): Promise<[boolean, boolean]> { + type ServiceType = keyof (typeof webSearchAuth)[C]; + let isUserProvided = false; + + // Check if a specific service is specified in the config + let specificService: ServiceType | undefined; + if (category === SearchCategories.PROVIDERS && webSearchConfig?.searchProvider) { + specificService = webSearchConfig.searchProvider as unknown as ServiceType; + } else if (category === SearchCategories.SCRAPERS && webSearchConfig?.scraperType) { + specificService = webSearchConfig.scraperType as unknown as ServiceType; + } else if (category === SearchCategories.RERANKERS && webSearchConfig?.rerankerType) { + specificService = webSearchConfig.rerankerType as unknown as ServiceType; + } + + // If a specific service is specified, only check that one + const services = specificService + ? [specificService] + : (Object.keys(webSearchAuth[category]) as ServiceType[]); + + for (const service of services) { + // Skip if the service doesn't exist in the webSearchAuth config + if (!webSearchAuth[category][service]) { + continue; + } + + const serviceConfig = webSearchAuth[category][service]; + + // Split keys into required and optional + const requiredKeys: TWebSearchKeys[] = []; + const optionalKeys: TWebSearchKeys[] = []; + + for (const key in serviceConfig) { + const typedKey = key as TWebSearchKeys; + if (serviceConfig[typedKey as keyof typeof serviceConfig] === 1) { + requiredKeys.push(typedKey); + } else if (serviceConfig[typedKey as keyof typeof serviceConfig] === 0) { + optionalKeys.push(typedKey); + } + } + + if (requiredKeys.length === 0) continue; + + const requiredAuthFields = extractWebSearchEnvVars({ + keys: requiredKeys, + config: webSearchConfig, + }); + const optionalAuthFields = extractWebSearchEnvVars({ + keys: optionalKeys, + config: webSearchConfig, + }); + if (requiredAuthFields.length !== requiredKeys.length) continue; + + const allKeys = [...requiredKeys, ...optionalKeys]; + const allAuthFields = [...requiredAuthFields, ...optionalAuthFields]; + const optionalSet = new Set(optionalAuthFields); + + try { + const authValues = await loadAuthValues({ + userId, + authFields: allAuthFields, + optional: optionalSet, + throwError, + }); + + let allFieldsAuthenticated = true; + for (let j = 0; j < allAuthFields.length; j++) { + const field = allAuthFields[j]; + const value = authValues[field]; + const originalKey = allKeys[j]; + if (originalKey) authResult[originalKey] = value; + if (!optionalSet.has(field) && !value) { + allFieldsAuthenticated = false; + break; + } + if (!isUserProvided && process.env[field] !== value) { + isUserProvided = true; + } + } + + if (!allFieldsAuthenticated) { + continue; + } + if (category === SearchCategories.PROVIDERS) { + authResult.searchProvider = service as SearchProviders; + } else if (category === SearchCategories.SCRAPERS) { + authResult.scraperType = service as ScraperTypes; + } else if (category === SearchCategories.RERANKERS) { + authResult.rerankerType = service as RerankerTypes; + } + return [true, isUserProvided]; + } catch { + continue; + } + } + return [false, isUserProvided]; + } + + const categories = [ + SearchCategories.PROVIDERS, + SearchCategories.SCRAPERS, + SearchCategories.RERANKERS, + ] as const; + const authTypes: [TWebSearchCategories, AuthType][] = []; + for (const category of categories) { + const [isCategoryAuthenticated, isUserProvided] = await checkAuth(category); + if (!isCategoryAuthenticated) { + authenticated = false; + authTypes.push([category, AuthType.USER_PROVIDED]); + continue; + } + authTypes.push([category, isUserProvided ? AuthType.USER_PROVIDED : AuthType.SYSTEM_DEFINED]); + } + + authResult.safeSearch = webSearchConfig?.safeSearch ?? true; + + return { + authTypes, + authResult, + authenticated, + }; +} diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index dd19634d2..99a171a78 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -26,6 +26,9 @@ export interface IRole extends Document { [PermissionTypes.RUN_CODE]?: { [Permissions.USE]?: boolean; }; + [PermissionTypes.WEB_SEARCH]?: { + [Permissions.USE]?: boolean; + }; }; } @@ -54,6 +57,9 @@ const rolePermissionsSchema = new Schema( [PermissionTypes.RUN_CODE]: { [Permissions.USE]: { type: Boolean, default: true }, }, + [PermissionTypes.WEB_SEARCH]: { + [Permissions.USE]: { type: Boolean, default: true }, + }, }, { _id: false }, ); @@ -77,6 +83,7 @@ const roleSchema: Schema = new Schema({ [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true }, }), }, });