| const { ObjectId } = require('mongodb'); |
| const { escapeRegExp } = require('@librechat/api'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { |
| Constants, |
| SystemRoles, |
| ResourceType, |
| SystemCategories, |
| } = require('librechat-data-provider'); |
| const { |
| removeGroupFromAllProjects, |
| removeGroupIdsFromProject, |
| addGroupIdsToProject, |
| getProjectByName, |
| } = require('./Project'); |
| const { removeAllPermissions } = require('~/server/services/PermissionService'); |
| const { PromptGroup, Prompt, AclEntry } = require('~/db/models'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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, |
| |
| |
| }, |
| }, |
| ]; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| 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, |
| }, |
| ]; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| 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' }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| 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 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' }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const deletePromptGroup = async ({ _id, author, role }) => { |
| |
| const query = { _id }; |
| const groupQuery = { groupId: new ObjectId(_id) }; |
|
|
| |
| 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: ResourceType.PROMPTGROUP, resourceId: _id }); |
| } catch (error) { |
| logger.error('Error removing promptGroup permissions:', error); |
| } |
|
|
| return { message: 'Prompt group deleted successfully' }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function getListPromptGroupsByAccess({ |
| accessibleIds = [], |
| otherParams = {}, |
| limit = null, |
| after = null, |
| }) { |
| const isPaginated = limit !== null && limit !== undefined; |
| const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null; |
|
|
| |
| const baseQuery = { ...otherParams, _id: { $in: accessibleIds } }; |
|
|
| |
| if (after && typeof after === 'string' && after !== 'undefined' && after !== 'null') { |
| try { |
| const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')); |
| const { updatedAt, _id } = cursor; |
|
|
| const cursorCondition = { |
| $or: [ |
| { updatedAt: { $lt: new Date(updatedAt) } }, |
| { updatedAt: new Date(updatedAt), _id: { $gt: new ObjectId(_id) } }, |
| ], |
| }; |
|
|
| |
| if (Object.keys(baseQuery).length > 0) { |
| baseQuery.$and = [{ ...baseQuery }, cursorCondition]; |
| |
| Object.keys(baseQuery).forEach((key) => { |
| if (key !== '$and') delete baseQuery[key]; |
| }); |
| } else { |
| Object.assign(baseQuery, cursorCondition); |
| } |
| } catch (error) { |
| logger.warn('Invalid cursor:', error.message); |
| } |
| } |
|
|
| |
| const pipeline = [{ $match: baseQuery }, { $sort: { updatedAt: -1, _id: 1 } }]; |
|
|
| |
| if (isPaginated) { |
| pipeline.push({ $limit: normalizedLimit + 1 }); |
| } |
|
|
| |
| pipeline.push( |
| { |
| $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, |
| }, |
| }, |
| ); |
|
|
| const promptGroups = await PromptGroup.aggregate(pipeline).exec(); |
|
|
| const hasMore = isPaginated ? promptGroups.length > normalizedLimit : false; |
| const data = (isPaginated ? promptGroups.slice(0, normalizedLimit) : promptGroups).map( |
| (group) => { |
| if (group.author) { |
| group.author = group.author.toString(); |
| } |
| return group; |
| }, |
| ); |
|
|
| |
| let nextCursor = null; |
| if (isPaginated && hasMore && data.length > 0) { |
| const lastGroup = promptGroups[normalizedLimit - 1]; |
| nextCursor = Buffer.from( |
| JSON.stringify({ |
| updatedAt: lastGroup.updatedAt.toISOString(), |
| _id: lastGroup._id.toString(), |
| }), |
| ).toString('base64'); |
| } |
|
|
| return { |
| object: 'list', |
| data, |
| first_id: data.length > 0 ? data[0]._id.toString() : null, |
| last_id: data.length > 0 ? data[data.length - 1]._id.toString() : null, |
| has_more: hasMore, |
| after: nextCursor, |
| }; |
| } |
|
|
| module.exports = { |
| getPromptGroups, |
| deletePromptGroup, |
| getAllPromptGroups, |
| getListPromptGroupsByAccess, |
| |
| |
| |
| |
| |
| 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'); |
| } |
| }, |
| |
| |
| |
| |
| |
| savePrompt: async (saveData) => { |
| try { |
| const { prompt, author } = saveData; |
| const newPromptData = { |
| ...prompt, |
| author, |
| }; |
|
|
| |
| 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' }; |
| } |
| }, |
| |
| |
| |
| |
| |
| 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' }; |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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'); |
| } |
|
|
| const remainingPrompts = await Prompt.find({ groupId }) |
| .select('_id') |
| .sort({ createdAt: 1 }) |
| .lean(); |
|
|
| if (remainingPrompts.length === 0) { |
| |
| try { |
| await removeAllPermissions({ |
| resourceType: 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' }; |
| } |
| }, |
| |
| |
| |
| |
| |
| deleteUserPrompts: async (req, userId) => { |
| try { |
| const promptGroups = await getAllPromptGroups(req, { author: new ObjectId(userId) }); |
|
|
| if (promptGroups.length === 0) { |
| return; |
| } |
|
|
| const groupIds = promptGroups.map((group) => group._id); |
|
|
| for (const groupId of groupIds) { |
| await removeGroupFromAllProjects(groupId); |
| } |
|
|
| await AclEntry.deleteMany({ |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: { $in: groupIds }, |
| }); |
|
|
| await PromptGroup.deleteMany({ author: new ObjectId(userId) }); |
| await Prompt.deleteMany({ author: new ObjectId(userId) }); |
| } catch (error) { |
| logger.error('[deleteUserPrompts] General error:', error); |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| 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' }; |
| } |
| }, |
| |
| |
| |
| |
| |
| 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' }; |
| } |
| }, |
| }; |
|
|