| const { v4: uuidv4 } = require('uuid'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { EModelEndpoint, Constants, ForkOptions } = require('librechat-data-provider'); |
| const { createImportBatchBuilder } = require('./importBatchBuilder'); |
| const BaseClient = require('~/app/clients/BaseClient'); |
| const { getConvo } = require('~/models/Conversation'); |
| const { getMessages } = require('~/models/Message'); |
|
|
| |
| |
| |
| |
| |
| |
| function cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder) { |
| const idMapping = new Map(); |
|
|
| |
| const sortedMessages = [...messagesToClone].sort((a, b) => { |
| if (a.parentMessageId === Constants.NO_PARENT) { |
| return -1; |
| } |
| if (b.parentMessageId === Constants.NO_PARENT) { |
| return 1; |
| } |
| return 0; |
| }); |
|
|
| |
| const ensureDate = (dateValue) => { |
| if (!dateValue) { |
| return new Date(); |
| } |
| return dateValue instanceof Date ? dateValue : new Date(dateValue); |
| }; |
|
|
| |
| for (const message of sortedMessages) { |
| const newMessageId = uuidv4(); |
| idMapping.set(message.messageId, newMessageId); |
|
|
| const parentId = |
| message.parentMessageId && message.parentMessageId !== Constants.NO_PARENT |
| ? idMapping.get(message.parentMessageId) |
| : Constants.NO_PARENT; |
|
|
| |
| let createdAt = ensureDate(message.createdAt); |
| if (parentId !== Constants.NO_PARENT) { |
| const parentMessage = importBatchBuilder.messages.find((msg) => msg.messageId === parentId); |
| if (parentMessage) { |
| const parentDate = ensureDate(parentMessage.createdAt); |
| if (createdAt <= parentDate) { |
| createdAt = new Date(parentDate.getTime() + 1); |
| } |
| } |
| } |
|
|
| const clonedMessage = { |
| ...message, |
| messageId: newMessageId, |
| parentMessageId: parentId, |
| createdAt, |
| }; |
|
|
| importBatchBuilder.saveMessage(clonedMessage); |
| } |
|
|
| return idMapping; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function forkConversation({ |
| originalConvoId, |
| targetMessageId: targetId, |
| requestUserId, |
| newTitle, |
| option = ForkOptions.TARGET_LEVEL, |
| records = false, |
| splitAtTarget = false, |
| latestMessageId, |
| builderFactory = createImportBatchBuilder, |
| }) { |
| try { |
| const originalConvo = await getConvo(requestUserId, originalConvoId); |
| let originalMessages = await getMessages({ |
| user: requestUserId, |
| conversationId: originalConvoId, |
| }); |
|
|
| let targetMessageId = targetId; |
| if (splitAtTarget && !latestMessageId) { |
| throw new Error('Latest `messageId` is required for forking from target message.'); |
| } else if (splitAtTarget) { |
| originalMessages = splitAtTargetLevel(originalMessages, targetId); |
| targetMessageId = latestMessageId; |
| } |
|
|
| const importBatchBuilder = builderFactory(requestUserId); |
| importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI); |
|
|
| let messagesToClone = []; |
|
|
| if (option === ForkOptions.DIRECT_PATH) { |
| |
| messagesToClone = BaseClient.getMessagesForConversation({ |
| messages: originalMessages, |
| parentMessageId: targetMessageId, |
| }); |
| } else if (option === ForkOptions.INCLUDE_BRANCHES) { |
| |
| messagesToClone = getAllMessagesUpToParent(originalMessages, targetMessageId); |
| } else if (option === ForkOptions.TARGET_LEVEL || !option) { |
| |
| messagesToClone = getMessagesUpToTargetLevel(originalMessages, targetMessageId); |
| } |
|
|
| cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder); |
|
|
| const result = importBatchBuilder.finishConversation( |
| newTitle || originalConvo.title, |
| new Date(), |
| originalConvo, |
| ); |
| await importBatchBuilder.saveBatch(); |
| logger.debug( |
| `user: ${requestUserId} | New conversation "${ |
| newTitle || originalConvo.title |
| }" forked from conversation ID ${originalConvoId}`, |
| ); |
|
|
| if (!records) { |
| return result; |
| } |
|
|
| const conversation = await getConvo(requestUserId, result.conversation.conversationId); |
| const messages = await getMessages({ |
| user: requestUserId, |
| conversationId: conversation.conversationId, |
| }); |
|
|
| return { |
| conversation, |
| messages, |
| }; |
| } catch (error) { |
| logger.error( |
| `user: ${requestUserId} | Error forking conversation from original ID ${originalConvoId}`, |
| error, |
| ); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function getAllMessagesUpToParent(messages, targetMessageId) { |
| const targetMessage = messages.find((msg) => msg.messageId === targetMessageId); |
| if (!targetMessage) { |
| return []; |
| } |
|
|
| const pathToRoot = new Set(); |
| const visited = new Set(); |
| let current = targetMessage; |
|
|
| while (current) { |
| if (visited.has(current.messageId)) { |
| break; |
| } |
|
|
| visited.add(current.messageId); |
| pathToRoot.add(current.messageId); |
|
|
| const currentParentId = current.parentMessageId ?? Constants.NO_PARENT; |
| if (currentParentId === Constants.NO_PARENT) { |
| break; |
| } |
|
|
| current = messages.find((msg) => msg.messageId === currentParentId); |
| } |
|
|
| |
| |
| return messages.filter( |
| (msg) => |
| (pathToRoot.has(msg.messageId) && msg.messageId !== targetMessageId) || |
| (pathToRoot.has(msg.parentMessageId) && msg.parentMessageId !== targetMessageId) || |
| msg.messageId === targetMessageId, |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function getMessagesUpToTargetLevel(messages, targetMessageId) { |
| if (messages.length === 1 && messages[0] && messages[0].messageId === targetMessageId) { |
| return messages; |
| } |
|
|
| |
| const parentToChildrenMap = new Map(); |
| for (const message of messages) { |
| if (!parentToChildrenMap.has(message.parentMessageId)) { |
| parentToChildrenMap.set(message.parentMessageId, []); |
| } |
| parentToChildrenMap.get(message.parentMessageId).push(message); |
| } |
|
|
| |
| const targetMessage = messages.find((msg) => msg.messageId === targetMessageId); |
| if (!targetMessage) { |
| logger.error('Target message not found.'); |
| return []; |
| } |
|
|
| const visited = new Set(); |
|
|
| const rootMessages = parentToChildrenMap.get(Constants.NO_PARENT) || []; |
| let currentLevel = rootMessages.length > 0 ? [...rootMessages] : [targetMessage]; |
| const results = new Set(currentLevel); |
|
|
| |
| if ( |
| currentLevel.some((msg) => msg.messageId === targetMessageId) && |
| targetMessage.parentMessageId === Constants.NO_PARENT |
| ) { |
| return Array.from(results); |
| } |
|
|
| |
| let targetFound = false; |
| while (!targetFound && currentLevel.length > 0) { |
| const nextLevel = []; |
| for (const node of currentLevel) { |
| if (visited.has(node.messageId)) { |
| logger.warn('Cycle detected in message tree'); |
| continue; |
| } |
| visited.add(node.messageId); |
| const children = parentToChildrenMap.get(node.messageId) || []; |
| for (const child of children) { |
| if (visited.has(child.messageId)) { |
| logger.warn('Cycle detected in message tree'); |
| continue; |
| } |
| nextLevel.push(child); |
| results.add(child); |
| if (child.messageId === targetMessageId) { |
| targetFound = true; |
| } |
| } |
| } |
| currentLevel = nextLevel; |
| } |
|
|
| return Array.from(results); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function splitAtTargetLevel(messages, targetMessageId) { |
| |
| const parentToChildrenMap = new Map(); |
| for (const message of messages) { |
| if (!parentToChildrenMap.has(message.parentMessageId)) { |
| parentToChildrenMap.set(message.parentMessageId, []); |
| } |
| parentToChildrenMap.get(message.parentMessageId).push(message); |
| } |
|
|
| |
| const targetMessage = messages.find((msg) => msg.messageId === targetMessageId); |
| if (!targetMessage) { |
| logger.error('Target message not found.'); |
| return []; |
| } |
|
|
| |
| const rootMessages = parentToChildrenMap.get(Constants.NO_PARENT) || []; |
| let currentLevel = [...rootMessages]; |
| let currentLevelIndex = 0; |
| const levelMap = {}; |
|
|
| |
| rootMessages.forEach((msg) => { |
| levelMap[msg.messageId] = 0; |
| }); |
|
|
| |
| while (currentLevel.length > 0) { |
| const nextLevel = []; |
| for (const node of currentLevel) { |
| const children = parentToChildrenMap.get(node.messageId) || []; |
| for (const child of children) { |
| nextLevel.push(child); |
| levelMap[child.messageId] = currentLevelIndex + 1; |
| } |
| } |
| currentLevel = nextLevel; |
| currentLevelIndex++; |
| } |
|
|
| |
| const targetLevel = levelMap[targetMessageId]; |
| if (targetLevel === undefined) { |
| logger.error('Target level not found.'); |
| return []; |
| } |
|
|
| |
| const filteredMessages = messages |
| .map((msg) => { |
| const messageLevel = levelMap[msg.messageId]; |
| if (messageLevel < targetLevel) { |
| return null; |
| } else if (messageLevel === targetLevel) { |
| return { |
| ...msg, |
| parentMessageId: Constants.NO_PARENT, |
| }; |
| } |
|
|
| return msg; |
| }) |
| .filter((msg) => msg !== null); |
|
|
| return filteredMessages; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function duplicateConversation({ userId, conversationId }) { |
| |
| const originalConvo = await getConvo(userId, conversationId); |
| if (!originalConvo) { |
| throw new Error('Conversation not found'); |
| } |
|
|
| |
| const originalMessages = await getMessages({ |
| user: userId, |
| conversationId, |
| }); |
|
|
| const messagesToClone = getMessagesUpToTargetLevel( |
| originalMessages, |
| originalMessages[originalMessages.length - 1].messageId, |
| ); |
|
|
| const importBatchBuilder = createImportBatchBuilder(userId); |
| importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI); |
|
|
| cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder); |
|
|
| const result = importBatchBuilder.finishConversation( |
| originalConvo.title, |
| new Date(), |
| originalConvo, |
| ); |
| await importBatchBuilder.saveBatch(); |
| logger.debug( |
| `user: ${userId} | New conversation "${originalConvo.title}" duplicated from conversation ID ${conversationId}`, |
| ); |
|
|
| const conversation = await getConvo(userId, result.conversation.conversationId); |
| const messages = await getMessages({ |
| user: userId, |
| conversationId: conversation.conversationId, |
| }); |
|
|
| return { |
| conversation, |
| messages, |
| }; |
| } |
|
|
| module.exports = { |
| forkConversation, |
| splitAtTargetLevel, |
| duplicateConversation, |
| getAllMessagesUpToParent, |
| getMessagesUpToTargetLevel, |
| cloneMessagesWithTimestamps, |
| }; |
|
|