const { logger } = require('@librechat/data-schemas'); const { ConversationTag, Conversation } = require('~/db/models'); /** * Retrieves all conversation tags for a user. * @param {string} user - The user ID. * @returns {Promise} An array of conversation tags. */ const getConversationTags = async (user) => { try { return await ConversationTag.find({ user }).sort({ position: 1 }).lean(); } catch (error) { logger.error('[getConversationTags] Error getting conversation tags', error); throw new Error('Error getting conversation tags'); } }; /** * Creates a new conversation tag. * @param {string} user - The user ID. * @param {Object} data - The tag data. * @param {string} data.tag - The tag name. * @param {string} [data.description] - The tag description. * @param {boolean} [data.addToConversation] - Whether to add the tag to a conversation. * @param {string} [data.conversationId] - The conversation ID to add the tag to. * @returns {Promise} The created tag. */ const createConversationTag = async (user, data) => { try { const { tag, description, addToConversation, conversationId } = data; const existingTag = await ConversationTag.findOne({ user, tag }).lean(); if (existingTag) { return existingTag; } const maxPosition = await ConversationTag.findOne({ user }).sort('-position').lean(); const position = (maxPosition?.position || 0) + 1; const newTag = await ConversationTag.findOneAndUpdate( { tag, user }, { tag, user, count: addToConversation ? 1 : 0, position, description, $setOnInsert: { createdAt: new Date() }, }, { new: true, upsert: true, lean: true, }, ); if (addToConversation && conversationId) { await Conversation.findOneAndUpdate( { user, conversationId }, { $addToSet: { tags: tag } }, { new: true }, ); } return newTag; } catch (error) { logger.error('[createConversationTag] Error creating conversation tag', error); throw new Error('Error creating conversation tag'); } }; /** * Updates an existing conversation tag. * @param {string} user - The user ID. * @param {string} oldTag - The current tag name. * @param {Object} data - The updated tag data. * @param {string} [data.tag] - The new tag name. * @param {string} [data.description] - The updated description. * @param {number} [data.position] - The new position. * @returns {Promise} The updated tag. */ const updateConversationTag = async (user, oldTag, data) => { try { const { tag: newTag, description, position } = data; const existingTag = await ConversationTag.findOne({ user, tag: oldTag }).lean(); if (!existingTag) { return null; } if (newTag && newTag !== oldTag) { const tagAlreadyExists = await ConversationTag.findOne({ user, tag: newTag }).lean(); if (tagAlreadyExists) { throw new Error('Tag already exists'); } await Conversation.updateMany({ user, tags: oldTag }, { $set: { 'tags.$': newTag } }); } const updateData = {}; if (newTag) { updateData.tag = newTag; } if (description !== undefined) { updateData.description = description; } if (position !== undefined) { await adjustPositions(user, existingTag.position, position); updateData.position = position; } return await ConversationTag.findOneAndUpdate({ user, tag: oldTag }, updateData, { new: true, lean: true, }); } catch (error) { logger.error('[updateConversationTag] Error updating conversation tag', error); throw new Error('Error updating conversation tag'); } }; /** * Adjusts positions of tags when a tag's position is changed. * @param {string} user - The user ID. * @param {number} oldPosition - The old position of the tag. * @param {number} newPosition - The new position of the tag. * @returns {Promise} */ const adjustPositions = async (user, oldPosition, newPosition) => { if (oldPosition === newPosition) { return; } const update = oldPosition < newPosition ? { $inc: { position: -1 } } : { $inc: { position: 1 } }; const position = oldPosition < newPosition ? { $gt: Math.min(oldPosition, newPosition), $lte: Math.max(oldPosition, newPosition), } : { $gte: Math.min(oldPosition, newPosition), $lt: Math.max(oldPosition, newPosition), }; await ConversationTag.updateMany( { user, position, }, update, ); }; /** * Deletes a conversation tag. * @param {string} user - The user ID. * @param {string} tag - The tag to delete. * @returns {Promise} The deleted tag. */ const deleteConversationTag = async (user, tag) => { try { const deletedTag = await ConversationTag.findOneAndDelete({ user, tag }).lean(); if (!deletedTag) { return null; } await Conversation.updateMany({ user, tags: tag }, { $pull: { tags: tag } }); await ConversationTag.updateMany( { user, position: { $gt: deletedTag.position } }, { $inc: { position: -1 } }, ); return deletedTag; } catch (error) { logger.error('[deleteConversationTag] Error deleting conversation tag', error); throw new Error('Error deleting conversation tag'); } }; /** * Updates tags for a specific conversation. * @param {string} user - The user ID. * @param {string} conversationId - The conversation ID. * @param {string[]} tags - The new set of tags for the conversation. * @returns {Promise} The updated list of tags for the conversation. */ const updateTagsForConversation = async (user, conversationId, tags) => { try { const conversation = await Conversation.findOne({ user, conversationId }).lean(); if (!conversation) { throw new Error('Conversation not found'); } const oldTags = new Set(conversation.tags); const newTags = new Set(tags); const addedTags = [...newTags].filter((tag) => !oldTags.has(tag)); const removedTags = [...oldTags].filter((tag) => !newTags.has(tag)); const bulkOps = []; for (const tag of addedTags) { bulkOps.push({ updateOne: { filter: { user, tag }, update: { $inc: { count: 1 } }, upsert: true, }, }); } for (const tag of removedTags) { bulkOps.push({ updateOne: { filter: { user, tag }, update: { $inc: { count: -1 } }, }, }); } if (bulkOps.length > 0) { await ConversationTag.bulkWrite(bulkOps); } const updatedConversation = ( await Conversation.findOneAndUpdate( { user, conversationId }, { $set: { tags: [...newTags] } }, { new: true }, ) ).toObject(); return updatedConversation.tags; } catch (error) { logger.error('[updateTagsForConversation] Error updating tags', error); throw new Error('Error updating tags for conversation'); } }; /** * Increments tag counts for existing tags only. * @param {string} user - The user ID. * @param {string[]} tags - Array of tag names to increment * @returns {Promise} */ const bulkIncrementTagCounts = async (user, tags) => { if (!tags || tags.length === 0) { return; } try { const uniqueTags = [...new Set(tags.filter(Boolean))]; if (uniqueTags.length === 0) { return; } const bulkOps = uniqueTags.map((tag) => ({ updateOne: { filter: { user, tag }, update: { $inc: { count: 1 } }, }, })); const result = await ConversationTag.bulkWrite(bulkOps); if (result && result.modifiedCount > 0) { logger.debug( `user: ${user} | Incremented tag counts - modified ${result.modifiedCount} tags`, ); } } catch (error) { logger.error('[bulkIncrementTagCounts] Error incrementing tag counts', error); } }; module.exports = { getConversationTags, createConversationTag, updateConversationTag, deleteConversationTag, bulkIncrementTagCounts, updateTagsForConversation, };