const { ObjectId } = require('mongodb'); const { logger } = require('@librechat/data-schemas'); const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider'); const { getProjectByName, addGroupIdsToProject, removeGroupIdsFromProject, removeGroupFromAllProjects, } = require('./Project'); const { removeAllPermissions } = require('~/server/services/PermissionService'); const { PromptGroup, Prompt } = require('~/db/models'); const { escapeRegExp } = require('~/server/utils'); /** * Create a pipeline for the aggregation to get prompt groups * @param {Object} query * @param {number} skip * @param {number} limit * @returns {[Object]} - The pipeline for the aggregation */ const createGroupPipeline = (query, skip, limit) => { return [ { $match: query }, { $sort: { createdAt: -1 } }, { $skip: skip }, { $limit: limit }, { $lookup: { from: 'prompts', localField: 'productionId', foreignField: '_id', as: 'productionPrompt', }, }, { $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } }, { $project: { name: 1, numberOfGenerations: 1, oneliner: 1, category: 1, projectIds: 1, productionId: 1, author: 1, authorName: 1, createdAt: 1, updatedAt: 1, 'productionPrompt.prompt': 1, // 'productionPrompt._id': 1, // 'productionPrompt.type': 1, }, }, ]; }; /** * Create a pipeline for the aggregation to get all prompt groups * @param {Object} query * @param {Partial} $project * @returns {[Object]} - The pipeline for the aggregation */ const createAllGroupsPipeline = ( query, $project = { name: 1, oneliner: 1, category: 1, author: 1, authorName: 1, createdAt: 1, updatedAt: 1, command: 1, 'productionPrompt.prompt': 1, }, ) => { return [ { $match: query }, { $sort: { createdAt: -1 } }, { $lookup: { from: 'prompts', localField: 'productionId', foreignField: '_id', as: 'productionPrompt', }, }, { $unwind: { path: '$productionPrompt', preserveNullAndEmptyArrays: true } }, { $project, }, ]; }; /** * Get all prompt groups with filters * @param {ServerRequest} req * @param {TPromptGroupsWithFilterRequest} filter * @returns {Promise} */ const getAllPromptGroups = async (req, filter) => { try { const { name, ...query } = filter; let searchShared = true; let searchSharedOnly = false; if (name) { query.name = new RegExp(escapeRegExp(name), 'i'); } if (!query.category) { delete query.category; } else if (query.category === SystemCategories.MY_PROMPTS) { searchShared = false; delete query.category; } else if (query.category === SystemCategories.NO_CATEGORY) { query.category = ''; } else if (query.category === SystemCategories.SHARED_PROMPTS) { searchSharedOnly = true; delete query.category; } let combinedQuery = query; if (searchShared) { const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds'); if (project && project.promptGroupIds && project.promptGroupIds.length > 0) { const projectQuery = { _id: { $in: project.promptGroupIds }, ...query }; delete projectQuery.author; combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] }; } } const promptGroupsPipeline = createAllGroupsPipeline(combinedQuery); return await PromptGroup.aggregate(promptGroupsPipeline).exec(); } catch (error) { console.error('Error getting all prompt groups', error); return { message: 'Error getting all prompt groups' }; } }; /** * Get prompt groups with filters * @param {ServerRequest} req * @param {TPromptGroupsWithFilterRequest} filter * @returns {Promise} */ const getPromptGroups = async (req, filter) => { try { const { pageNumber = 1, pageSize = 10, name, ...query } = filter; const validatedPageNumber = Math.max(parseInt(pageNumber, 10), 1); const validatedPageSize = Math.max(parseInt(pageSize, 10), 1); let searchShared = true; let searchSharedOnly = false; if (name) { query.name = new RegExp(escapeRegExp(name), 'i'); } if (!query.category) { delete query.category; } else if (query.category === SystemCategories.MY_PROMPTS) { searchShared = false; delete query.category; } else if (query.category === SystemCategories.NO_CATEGORY) { query.category = ''; } else if (query.category === SystemCategories.SHARED_PROMPTS) { searchSharedOnly = true; delete query.category; } let combinedQuery = query; if (searchShared) { // const projects = req.user.projects || []; // TODO: handle multiple projects const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds'); if (project && project.promptGroupIds && project.promptGroupIds.length > 0) { const projectQuery = { _id: { $in: project.promptGroupIds }, ...query }; delete projectQuery.author; combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] }; } } const skip = (validatedPageNumber - 1) * validatedPageSize; const limit = validatedPageSize; const promptGroupsPipeline = createGroupPipeline(combinedQuery, skip, limit); const totalPromptGroupsPipeline = [{ $match: combinedQuery }, { $count: 'total' }]; const [promptGroupsResults, totalPromptGroupsResults] = await Promise.all([ PromptGroup.aggregate(promptGroupsPipeline).exec(), PromptGroup.aggregate(totalPromptGroupsPipeline).exec(), ]); const promptGroups = promptGroupsResults; const totalPromptGroups = totalPromptGroupsResults.length > 0 ? totalPromptGroupsResults[0].total : 0; return { promptGroups, pageNumber: validatedPageNumber.toString(), pageSize: validatedPageSize.toString(), pages: Math.ceil(totalPromptGroups / validatedPageSize).toString(), }; } catch (error) { console.error('Error getting prompt groups', error); return { message: 'Error getting prompt groups' }; } }; /** * @param {Object} fields * @param {string} fields._id * @param {string} fields.author * @param {string} fields.role * @returns {Promise} */ const deletePromptGroup = async ({ _id, author, role }) => { // Build query - with ACL, author is optional const query = { _id }; const groupQuery = { groupId: new ObjectId(_id) }; // Legacy: Add author filter if provided (backward compatibility) if (author && role !== SystemRoles.ADMIN) { query.author = author; groupQuery.author = author; } const response = await PromptGroup.deleteOne(query); if (!response || response.deletedCount === 0) { throw new Error('Prompt group not found'); } await Prompt.deleteMany(groupQuery); await removeGroupFromAllProjects(_id); try { await removeAllPermissions({ resourceType: 'promptGroup', resourceId: _id }); } catch (error) { logger.error('Error removing promptGroup permissions:', error); } return { message: 'Prompt group deleted successfully' }; }; module.exports = { getPromptGroups, deletePromptGroup, getAllPromptGroups, /** * Create a prompt and its respective group * @param {TCreatePromptRecord} saveData * @returns {Promise} */ createPromptGroup: async (saveData) => { try { const { prompt, group, author, authorName } = saveData; let newPromptGroup = await PromptGroup.findOneAndUpdate( { ...group, author, authorName, productionId: null }, { $setOnInsert: { ...group, author, authorName, productionId: null } }, { new: true, upsert: true }, ) .lean() .select('-__v') .exec(); const newPrompt = await Prompt.findOneAndUpdate( { ...prompt, author, groupId: newPromptGroup._id }, { $setOnInsert: { ...prompt, author, groupId: newPromptGroup._id } }, { new: true, upsert: true }, ) .lean() .select('-__v') .exec(); newPromptGroup = await PromptGroup.findByIdAndUpdate( newPromptGroup._id, { productionId: newPrompt._id }, { new: true }, ) .lean() .select('-__v') .exec(); return { prompt: newPrompt, group: { ...newPromptGroup, productionPrompt: { prompt: newPrompt.prompt }, }, }; } catch (error) { logger.error('Error saving prompt group', error); throw new Error('Error saving prompt group'); } }, /** * Save a prompt * @param {TCreatePromptRecord} saveData * @returns {Promise} */ savePrompt: async (saveData) => { try { const { prompt, author } = saveData; const newPromptData = { ...prompt, author, }; /** @type {TPrompt} */ let newPrompt; try { newPrompt = await Prompt.create(newPromptData); } catch (error) { if (error?.message?.includes('groupId_1_version_1')) { await Prompt.db.collection('prompts').dropIndex('groupId_1_version_1'); } else { throw error; } newPrompt = await Prompt.create(newPromptData); } return { prompt: newPrompt }; } catch (error) { logger.error('Error saving prompt', error); return { message: 'Error saving prompt' }; } }, getPrompts: async (filter) => { try { return await Prompt.find(filter).sort({ createdAt: -1 }).lean(); } catch (error) { logger.error('Error getting prompts', error); return { message: 'Error getting prompts' }; } }, getPrompt: async (filter) => { try { if (filter.groupId) { filter.groupId = new ObjectId(filter.groupId); } return await Prompt.findOne(filter).lean(); } catch (error) { logger.error('Error getting prompt', error); return { message: 'Error getting prompt' }; } }, /** * Get prompt groups with filters * @param {TGetRandomPromptsRequest} filter * @returns {Promise} */ getRandomPromptGroups: async (filter) => { try { const result = await PromptGroup.aggregate([ { $match: { category: { $ne: '' }, }, }, { $group: { _id: '$category', promptGroup: { $first: '$$ROOT' }, }, }, { $replaceRoot: { newRoot: '$promptGroup' }, }, { $sample: { size: +filter.limit + +filter.skip }, }, { $skip: +filter.skip, }, { $limit: +filter.limit, }, ]); return { prompts: result }; } catch (error) { logger.error('Error getting prompt groups', error); return { message: 'Error getting prompt groups' }; } }, getPromptGroupsWithPrompts: async (filter) => { try { return await PromptGroup.findOne(filter) .populate({ path: 'prompts', select: '-_id -__v -user', }) .select('-_id -__v -user') .lean(); } catch (error) { logger.error('Error getting prompt groups', error); return { message: 'Error getting prompt groups' }; } }, getPromptGroup: async (filter) => { try { return await PromptGroup.findOne(filter).lean(); } catch (error) { logger.error('Error getting prompt group', error); return { message: 'Error getting prompt group' }; } }, /** * Deletes a prompt and its corresponding prompt group if it is the last prompt in the group. * * @param {Object} options - The options for deleting the prompt. * @param {ObjectId|string} options.promptId - The ID of the prompt to delete. * @param {ObjectId|string} options.groupId - The ID of the prompt's group. * @param {ObjectId|string} options.author - The ID of the prompt's author. * @param {string} options.role - The role of the prompt's author. * @return {Promise} An object containing the result of the deletion. * If the prompt was deleted successfully, the object will have a property 'prompt' with the value 'Prompt deleted successfully'. * If the prompt group was deleted successfully, the object will have a property 'promptGroup' with the message 'Prompt group deleted successfully' and id of the deleted group. * If there was an error deleting the prompt, the object will have a property 'message' with the value 'Error deleting prompt'. */ deletePrompt: async ({ promptId, groupId, author, role }) => { const query = { _id: promptId, groupId, author }; if (role === SystemRoles.ADMIN) { delete query.author; } const { deletedCount } = await Prompt.deleteOne(query); if (deletedCount === 0) { throw new Error('Failed to delete the prompt'); } // Remove all ACL entries for this prompt try { await removeAllPermissions({ resourceType: 'prompt', resourceId: promptId, }); } catch (error) { logger.error('Error removing prompt permissions:', error); } const remainingPrompts = await Prompt.find({ groupId }) .select('_id') .sort({ createdAt: 1 }) .lean(); if (remainingPrompts.length === 0) { // Remove all ACL entries for the promptGroup when deleting the last prompt try { await removeAllPermissions({ resourceType: 'promptGroup', resourceId: groupId, }); } catch (error) { logger.error('Error removing promptGroup permissions:', error); } await PromptGroup.deleteOne({ _id: groupId }); await removeGroupFromAllProjects(groupId); return { prompt: 'Prompt deleted successfully', promptGroup: { message: 'Prompt group deleted successfully', id: groupId, }, }; } else { const promptGroup = await PromptGroup.findById(groupId).lean(); if (promptGroup.productionId.toString() === promptId.toString()) { await PromptGroup.updateOne( { _id: groupId }, { productionId: remainingPrompts[remainingPrompts.length - 1]._id }, ); } return { prompt: 'Prompt deleted successfully' }; } }, /** * Update prompt group * @param {Partial} filter - Filter to find prompt group * @param {Partial} data - Data to update * @returns {Promise} */ updatePromptGroup: async (filter, data) => { try { const updateOps = {}; if (data.removeProjectIds) { for (const projectId of data.removeProjectIds) { await removeGroupIdsFromProject(projectId, [filter._id]); } updateOps.$pull = { projectIds: { $in: data.removeProjectIds } }; delete data.removeProjectIds; } if (data.projectIds) { for (const projectId of data.projectIds) { await addGroupIdsToProject(projectId, [filter._id]); } updateOps.$addToSet = { projectIds: { $each: data.projectIds } }; delete data.projectIds; } const updateData = { ...data, ...updateOps }; const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, { new: true, upsert: false, }); if (!updatedDoc) { throw new Error('Prompt group not found'); } return updatedDoc; } catch (error) { logger.error('Error updating prompt group', error); return { message: 'Error updating prompt group' }; } }, /** * Function to make a prompt production based on its ID. * @param {String} promptId - The ID of the prompt to make production. * @returns {Object} The result of the production operation. */ makePromptProduction: async (promptId) => { try { const prompt = await Prompt.findById(promptId).lean(); if (!prompt) { throw new Error('Prompt not found'); } await PromptGroup.findByIdAndUpdate( prompt.groupId, { productionId: prompt._id }, { new: true }, ) .lean() .exec(); return { message: 'Prompt production made successfully', }; } catch (error) { logger.error('Error making prompt production', error); return { message: 'Error making prompt production' }; } }, updatePromptLabels: async (_id, labels) => { try { const response = await Prompt.updateOne({ _id }, { $set: { labels } }); if (response.matchedCount === 0) { return { message: 'Prompt not found' }; } return { message: 'Prompt labels updated successfully' }; } catch (error) { logger.error('Error updating prompt labels', error); return { message: 'Error updating prompt labels' }; } }, };