| 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, |
| }; |
|
|