| | const { z } = require('zod'); |
| | const fs = require('fs').promises; |
| | const { nanoid } = require('nanoid'); |
| | const { logger } = require('@librechat/data-schemas'); |
| | const { |
| | agentCreateSchema, |
| | agentUpdateSchema, |
| | mergeAgentOcrConversion, |
| | convertOcrToContextInPlace, |
| | } = require('@librechat/api'); |
| | const { |
| | Tools, |
| | Constants, |
| | FileSources, |
| | ResourceType, |
| | AccessRoleIds, |
| | PrincipalType, |
| | EToolResources, |
| | PermissionBits, |
| | actionDelimiter, |
| | removeNullishValues, |
| | CacheKeys, |
| | Time, |
| | } = require('librechat-data-provider'); |
| | const { |
| | getListAgentsByAccess, |
| | countPromotedAgents, |
| | revertAgentVersion, |
| | createAgent, |
| | updateAgent, |
| | deleteAgent, |
| | getAgent, |
| | } = require('~/models/Agent'); |
| | const { |
| | findPubliclyAccessibleResources, |
| | findAccessibleResources, |
| | hasPublicPermission, |
| | grantPermission, |
| | } = require('~/server/services/PermissionService'); |
| | const { getStrategyFunctions } = require('~/server/services/Files/strategies'); |
| | const { resizeAvatar } = require('~/server/services/Files/images/avatar'); |
| | const { getFileStrategy } = require('~/server/utils/getFileStrategy'); |
| | const { refreshS3Url } = require('~/server/services/Files/S3/crud'); |
| | const { filterFile } = require('~/server/services/Files/process'); |
| | const { updateAction, getActions } = require('~/models/Action'); |
| | const { getCachedTools } = require('~/server/services/Config'); |
| | const { deleteFileByFilter } = require('~/models/File'); |
| | const { getCategoriesWithCounts } = require('~/models'); |
| | const { getLogStores } = require('~/cache'); |
| |
|
| | const systemTools = { |
| | [Tools.execute_code]: true, |
| | [Tools.file_search]: true, |
| | [Tools.web_search]: true, |
| | }; |
| |
|
| | const MAX_SEARCH_LEN = 100; |
| | const escapeRegex = (str = '') => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const refreshListAvatars = async (agents, userId) => { |
| | if (!agents?.length) { |
| | return; |
| | } |
| |
|
| | const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL); |
| | const refreshKey = `${userId}:agents_list`; |
| | const alreadyChecked = await cache.get(refreshKey); |
| | if (alreadyChecked) { |
| | return; |
| | } |
| |
|
| | await Promise.all( |
| | agents.map(async (agent) => { |
| | if (agent?.avatar?.source !== FileSources.s3 || !agent?.avatar?.filepath) { |
| | return; |
| | } |
| |
|
| | try { |
| | const newPath = await refreshS3Url(agent.avatar); |
| | if (newPath && newPath !== agent.avatar.filepath) { |
| | agent.avatar = { ...agent.avatar, filepath: newPath }; |
| | } |
| | } catch (err) { |
| | logger.debug('[/Agents] Avatar refresh error for list item', err); |
| | } |
| | }), |
| | ); |
| |
|
| | await cache.set(refreshKey, true, Time.THIRTY_MINUTES); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const createAgentHandler = async (req, res) => { |
| | try { |
| | const validatedData = agentCreateSchema.parse(req.body); |
| | const { tools = [], ...agentData } = removeNullishValues(validatedData); |
| |
|
| | const { id: userId } = req.user; |
| |
|
| | agentData.id = `agent_${nanoid()}`; |
| | agentData.author = userId; |
| | agentData.tools = []; |
| |
|
| | const availableTools = await getCachedTools(); |
| | for (const tool of tools) { |
| | if (availableTools[tool]) { |
| | agentData.tools.push(tool); |
| | } else if (systemTools[tool]) { |
| | agentData.tools.push(tool); |
| | } else if (tool.includes(Constants.mcp_delimiter)) { |
| | agentData.tools.push(tool); |
| | } |
| | } |
| |
|
| | const agent = await createAgent(agentData); |
| |
|
| | |
| | try { |
| | await grantPermission({ |
| | principalType: PrincipalType.USER, |
| | principalId: userId, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | accessRoleId: AccessRoleIds.AGENT_OWNER, |
| | grantedBy: userId, |
| | }); |
| | logger.debug( |
| | `[createAgent] Granted owner permissions to user ${userId} for agent ${agent.id}`, |
| | ); |
| | } catch (permissionError) { |
| | logger.error( |
| | `[createAgent] Failed to grant owner permissions for agent ${agent.id}:`, |
| | permissionError, |
| | ); |
| | } |
| |
|
| | res.status(201).json(agent); |
| | } catch (error) { |
| | if (error instanceof z.ZodError) { |
| | logger.error('[/Agents] Validation error', error.errors); |
| | return res.status(400).json({ error: 'Invalid request data', details: error.errors }); |
| | } |
| | logger.error('[/Agents] Error creating agent', error); |
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const getAgentHandler = async (req, res, expandProperties = false) => { |
| | try { |
| | const id = req.params.id; |
| | const author = req.user.id; |
| |
|
| | |
| | |
| | const agent = await getAgent({ id }); |
| |
|
| | if (!agent) { |
| | return res.status(404).json({ error: 'Agent not found' }); |
| | } |
| |
|
| | agent.version = agent.versions ? agent.versions.length : 0; |
| |
|
| | if (agent.avatar && agent.avatar?.source === FileSources.s3) { |
| | try { |
| | agent.avatar = { |
| | ...agent.avatar, |
| | filepath: await refreshS3Url(agent.avatar), |
| | }; |
| | } catch (e) { |
| | logger.warn('[/Agents/:id] Failed to refresh S3 URL', e); |
| | } |
| | } |
| |
|
| | agent.author = agent.author.toString(); |
| |
|
| | |
| | agent.isCollaborative = !!agent.isCollaborative; |
| |
|
| | |
| | const isPublic = await hasPublicPermission({ |
| | resourceType: ResourceType.AGENT, |
| | resourceId: agent._id, |
| | requiredPermissions: PermissionBits.VIEW, |
| | }); |
| | agent.isPublic = isPublic; |
| |
|
| | if (agent.author !== author) { |
| | delete agent.author; |
| | } |
| |
|
| | if (!expandProperties) { |
| | |
| | return res.status(200).json({ |
| | _id: agent._id, |
| | id: agent.id, |
| | name: agent.name, |
| | description: agent.description, |
| | avatar: agent.avatar, |
| | author: agent.author, |
| | provider: agent.provider, |
| | model: agent.model, |
| | projectIds: agent.projectIds, |
| | |
| | isCollaborative: agent.isCollaborative, |
| | isPublic: agent.isPublic, |
| | version: agent.version, |
| | |
| | createdAt: agent.createdAt, |
| | updatedAt: agent.updatedAt, |
| | }); |
| | } |
| |
|
| | |
| | return res.status(200).json(agent); |
| | } catch (error) { |
| | logger.error('[/Agents/:id] Error retrieving agent', error); |
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const updateAgentHandler = async (req, res) => { |
| | try { |
| | const id = req.params.id; |
| | const validatedData = agentUpdateSchema.parse(req.body); |
| | |
| | const { avatar: avatarField, _id, ...rest } = validatedData; |
| | const updateData = removeNullishValues(rest); |
| | if (avatarField === null) { |
| | updateData.avatar = avatarField; |
| | } |
| |
|
| | |
| | convertOcrToContextInPlace(updateData); |
| |
|
| | const existingAgent = await getAgent({ id }); |
| |
|
| | if (!existingAgent) { |
| | return res.status(404).json({ error: 'Agent not found' }); |
| | } |
| |
|
| | |
| | const ocrConversion = mergeAgentOcrConversion(existingAgent, updateData); |
| | if (ocrConversion.tool_resources) { |
| | updateData.tool_resources = ocrConversion.tool_resources; |
| | } |
| | if (ocrConversion.tools) { |
| | updateData.tools = ocrConversion.tools; |
| | } |
| |
|
| | let updatedAgent = |
| | Object.keys(updateData).length > 0 |
| | ? await updateAgent({ id }, updateData, { |
| | updatingUserId: req.user.id, |
| | }) |
| | : existingAgent; |
| |
|
| | |
| | updatedAgent.version = updatedAgent.versions ? updatedAgent.versions.length : 0; |
| |
|
| | if (updatedAgent.author) { |
| | updatedAgent.author = updatedAgent.author.toString(); |
| | } |
| |
|
| | if (updatedAgent.author !== req.user.id) { |
| | delete updatedAgent.author; |
| | } |
| |
|
| | return res.json(updatedAgent); |
| | } catch (error) { |
| | if (error instanceof z.ZodError) { |
| | logger.error('[/Agents/:id] Validation error', error.errors); |
| | return res.status(400).json({ error: 'Invalid request data', details: error.errors }); |
| | } |
| |
|
| | logger.error('[/Agents/:id] Error updating Agent', error); |
| |
|
| | if (error.statusCode === 409) { |
| | return res.status(409).json({ |
| | error: error.message, |
| | details: error.details, |
| | }); |
| | } |
| |
|
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const duplicateAgentHandler = async (req, res) => { |
| | const { id } = req.params; |
| | const { id: userId } = req.user; |
| | const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret']; |
| |
|
| | try { |
| | const agent = await getAgent({ id }); |
| | if (!agent) { |
| | return res.status(404).json({ |
| | error: 'Agent not found', |
| | status: 'error', |
| | }); |
| | } |
| |
|
| | const { |
| | id: _id, |
| | _id: __id, |
| | author: _author, |
| | createdAt: _createdAt, |
| | updatedAt: _updatedAt, |
| | tool_resources: _tool_resources = {}, |
| | versions: _versions, |
| | __v: _v, |
| | ...cloneData |
| | } = agent; |
| | cloneData.name = `${agent.name} (${new Date().toLocaleString('en-US', { |
| | dateStyle: 'short', |
| | timeStyle: 'short', |
| | hour12: false, |
| | })})`; |
| |
|
| | if (_tool_resources?.[EToolResources.context]) { |
| | cloneData.tool_resources = { |
| | [EToolResources.context]: _tool_resources[EToolResources.context], |
| | }; |
| | } |
| |
|
| | if (_tool_resources?.[EToolResources.ocr]) { |
| | cloneData.tool_resources = { |
| | |
| | [EToolResources.context]: { |
| | ...(_tool_resources[EToolResources.context] ?? {}), |
| | ..._tool_resources[EToolResources.ocr], |
| | }, |
| | }; |
| | } |
| |
|
| | const newAgentId = `agent_${nanoid()}`; |
| | const newAgentData = Object.assign(cloneData, { |
| | id: newAgentId, |
| | author: userId, |
| | }); |
| |
|
| | const newActionsList = []; |
| | const originalActions = (await getActions({ agent_id: id }, true)) ?? []; |
| | const promises = []; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const duplicateAction = async (action) => { |
| | const newActionId = nanoid(); |
| | const [domain] = action.action_id.split(actionDelimiter); |
| | const fullActionId = `${domain}${actionDelimiter}${newActionId}`; |
| |
|
| | |
| | const filteredMetadata = { ...(action.metadata || {}) }; |
| | for (const field of sensitiveFields) { |
| | delete filteredMetadata[field]; |
| | } |
| |
|
| | const newAction = await updateAction( |
| | { action_id: newActionId }, |
| | { |
| | metadata: filteredMetadata, |
| | agent_id: newAgentId, |
| | user: userId, |
| | }, |
| | ); |
| |
|
| | newActionsList.push(newAction); |
| | return fullActionId; |
| | }; |
| |
|
| | for (const action of originalActions) { |
| | promises.push( |
| | duplicateAction(action).catch((error) => { |
| | logger.error('[/agents/:id/duplicate] Error duplicating Action:', error); |
| | }), |
| | ); |
| | } |
| |
|
| | const agentActions = await Promise.all(promises); |
| | newAgentData.actions = agentActions; |
| | const newAgent = await createAgent(newAgentData); |
| |
|
| | |
| | try { |
| | await grantPermission({ |
| | principalType: PrincipalType.USER, |
| | principalId: userId, |
| | resourceType: ResourceType.AGENT, |
| | resourceId: newAgent._id, |
| | accessRoleId: AccessRoleIds.AGENT_OWNER, |
| | grantedBy: userId, |
| | }); |
| | logger.debug( |
| | `[duplicateAgent] Granted owner permissions to user ${userId} for duplicated agent ${newAgent.id}`, |
| | ); |
| | } catch (permissionError) { |
| | logger.error( |
| | `[duplicateAgent] Failed to grant owner permissions for duplicated agent ${newAgent.id}:`, |
| | permissionError, |
| | ); |
| | } |
| |
|
| | return res.status(201).json({ |
| | agent: newAgent, |
| | actions: newActionsList, |
| | }); |
| | } catch (error) { |
| | logger.error('[/Agents/:id/duplicate] Error duplicating Agent:', error); |
| |
|
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const deleteAgentHandler = async (req, res) => { |
| | try { |
| | const id = req.params.id; |
| | const agent = await getAgent({ id }); |
| | if (!agent) { |
| | return res.status(404).json({ error: 'Agent not found' }); |
| | } |
| | await deleteAgent({ id }); |
| | return res.json({ message: 'Agent deleted' }); |
| | } catch (error) { |
| | logger.error('[/Agents/:id] Error deleting Agent', error); |
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const getListAgentsHandler = async (req, res) => { |
| | try { |
| | const userId = req.user.id; |
| | const { category, search, limit, cursor, promoted } = req.query; |
| | let requiredPermission = req.query.requiredPermission; |
| | if (typeof requiredPermission === 'string') { |
| | requiredPermission = parseInt(requiredPermission, 10); |
| | if (isNaN(requiredPermission)) { |
| | requiredPermission = PermissionBits.VIEW; |
| | } |
| | } else if (typeof requiredPermission !== 'number') { |
| | requiredPermission = PermissionBits.VIEW; |
| | } |
| | |
| | const filter = {}; |
| |
|
| | |
| | if (category !== undefined && category.trim() !== '') { |
| | filter.category = category; |
| | } |
| |
|
| | |
| | if (promoted === '1') { |
| | filter.is_promoted = true; |
| | } else if (promoted === '0') { |
| | filter.is_promoted = { $ne: true }; |
| | } |
| |
|
| | |
| | if (search && search.trim() !== '') { |
| | const safeSearch = escapeRegex(search.trim().slice(0, MAX_SEARCH_LEN)); |
| | const regex = new RegExp(safeSearch, 'i'); |
| | filter.$or = [{ name: regex }, { description: regex }]; |
| | } |
| |
|
| | |
| | const accessibleIds = await findAccessibleResources({ |
| | userId, |
| | role: req.user.role, |
| | resourceType: ResourceType.AGENT, |
| | requiredPermissions: requiredPermission, |
| | }); |
| |
|
| | const publiclyAccessibleIds = await findPubliclyAccessibleResources({ |
| | resourceType: ResourceType.AGENT, |
| | requiredPermissions: PermissionBits.VIEW, |
| | }); |
| |
|
| | |
| | const data = await getListAgentsByAccess({ |
| | accessibleIds, |
| | otherParams: filter, |
| | limit, |
| | after: cursor, |
| | }); |
| |
|
| | const agents = data?.data ?? []; |
| | if (!agents.length) { |
| | return res.json(data); |
| | } |
| |
|
| | const publicSet = new Set(publiclyAccessibleIds.map((oid) => oid.toString())); |
| |
|
| | data.data = agents.map((agent) => { |
| | try { |
| | if (agent?._id && publicSet.has(agent._id.toString())) { |
| | agent.isPublic = true; |
| | } |
| | } catch (e) { |
| | |
| | void e; |
| | } |
| | return agent; |
| | }); |
| |
|
| | |
| | try { |
| | await refreshListAvatars(data.data, req.user.id); |
| | } catch (err) { |
| | logger.debug('[/Agents] Skipping avatar refresh for list', err); |
| | } |
| | return res.json(data); |
| | } catch (error) { |
| | logger.error('[/Agents] Error listing Agents', error); |
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const uploadAgentAvatarHandler = async (req, res) => { |
| | try { |
| | const appConfig = req.config; |
| | if (!req.file) { |
| | return res.status(400).json({ message: 'No file uploaded' }); |
| | } |
| | filterFile({ req, file: req.file, image: true, isAvatar: true }); |
| | const { agent_id } = req.params; |
| | if (!agent_id) { |
| | return res.status(400).json({ message: 'Agent ID is required' }); |
| | } |
| |
|
| | const existingAgent = await getAgent({ id: agent_id }); |
| |
|
| | if (!existingAgent) { |
| | return res.status(404).json({ error: 'Agent not found' }); |
| | } |
| |
|
| | const buffer = await fs.readFile(req.file.path); |
| | const fileStrategy = getFileStrategy(appConfig, { isAvatar: true }); |
| | const resizedBuffer = await resizeAvatar({ |
| | userId: req.user.id, |
| | input: buffer, |
| | }); |
| |
|
| | const { processAvatar } = getStrategyFunctions(fileStrategy); |
| | const avatarUrl = await processAvatar({ |
| | buffer: resizedBuffer, |
| | userId: req.user.id, |
| | manual: 'false', |
| | agentId: agent_id, |
| | }); |
| |
|
| | const image = { |
| | filepath: avatarUrl, |
| | source: fileStrategy, |
| | }; |
| |
|
| | let _avatar = existingAgent.avatar; |
| |
|
| | if (_avatar && _avatar.source) { |
| | const { deleteFile } = getStrategyFunctions(_avatar.source); |
| | try { |
| | await deleteFile(req, { filepath: _avatar.filepath }); |
| | await deleteFileByFilter({ user: req.user.id, filepath: _avatar.filepath }); |
| | } catch (error) { |
| | logger.error('[/:agent_id/avatar] Error deleting old avatar', error); |
| | } |
| | } |
| |
|
| | const data = { |
| | avatar: { |
| | filepath: image.filepath, |
| | source: image.source, |
| | }, |
| | }; |
| |
|
| | const updatedAgent = await updateAgent({ id: agent_id }, data, { |
| | updatingUserId: req.user.id, |
| | }); |
| | res.status(201).json(updatedAgent); |
| | } catch (error) { |
| | const message = 'An error occurred while updating the Agent Avatar'; |
| | logger.error( |
| | `[/:agent_id/avatar] ${message} (${req.params?.agent_id ?? 'unknown agent'})`, |
| | error, |
| | ); |
| | res.status(500).json({ message }); |
| | } finally { |
| | try { |
| | await fs.unlink(req.file.path); |
| | logger.debug('[/:agent_id/avatar] Temp. image upload file deleted'); |
| | } catch { |
| | logger.debug('[/:agent_id/avatar] Temp. image upload file already deleted'); |
| | } |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const revertAgentVersionHandler = async (req, res) => { |
| | try { |
| | const { id } = req.params; |
| | const { version_index } = req.body; |
| |
|
| | if (version_index === undefined) { |
| | return res.status(400).json({ error: 'version_index is required' }); |
| | } |
| |
|
| | const existingAgent = await getAgent({ id }); |
| |
|
| | if (!existingAgent) { |
| | return res.status(404).json({ error: 'Agent not found' }); |
| | } |
| |
|
| | |
| |
|
| | const updatedAgent = await revertAgentVersion({ id }, version_index); |
| |
|
| | if (updatedAgent.author) { |
| | updatedAgent.author = updatedAgent.author.toString(); |
| | } |
| |
|
| | if (updatedAgent.author !== req.user.id) { |
| | delete updatedAgent.author; |
| | } |
| |
|
| | return res.json(updatedAgent); |
| | } catch (error) { |
| | logger.error('[/agents/:id/revert] Error reverting Agent version', error); |
| | res.status(500).json({ error: error.message }); |
| | } |
| | }; |
| | |
| | |
| | |
| | |
| | |
| | |
| | const getAgentCategories = async (_req, res) => { |
| | try { |
| | const categories = await getCategoriesWithCounts(); |
| | const promotedCount = await countPromotedAgents(); |
| | const formattedCategories = categories.map((category) => ({ |
| | value: category.value, |
| | label: category.label, |
| | count: category.agentCount, |
| | description: category.description, |
| | })); |
| |
|
| | if (promotedCount > 0) { |
| | formattedCategories.unshift({ |
| | value: 'promoted', |
| | label: 'Promoted', |
| | count: promotedCount, |
| | description: 'Our recommended agents', |
| | }); |
| | } |
| |
|
| | formattedCategories.push({ |
| | value: 'all', |
| | label: 'All', |
| | description: 'All available agents', |
| | }); |
| |
|
| | res.status(200).json(formattedCategories); |
| | } catch (error) { |
| | logger.error('[/Agents/Marketplace] Error fetching agent categories:', error); |
| | res.status(500).json({ |
| | error: 'Failed to fetch agent categories', |
| | userMessage: 'Unable to load categories. Please refresh the page.', |
| | suggestion: 'Try refreshing the page or check your network connection', |
| | }); |
| | } |
| | }; |
| | module.exports = { |
| | createAgent: createAgentHandler, |
| | getAgent: getAgentHandler, |
| | updateAgent: updateAgentHandler, |
| | duplicateAgent: duplicateAgentHandler, |
| | deleteAgent: deleteAgentHandler, |
| | getListAgents: getListAgentsHandler, |
| | uploadAgentAvatar: uploadAgentAvatarHandler, |
| | revertAgentVersion: revertAgentVersionHandler, |
| | getAgentCategories, |
| | }; |
| |
|