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