mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
📥 fix: ChatGPT Conversation Import Failures (#13637)
This commit is contained in:
parent
7eafe317cc
commit
ba5778d0df
3 changed files with 107 additions and 5 deletions
|
|
@ -7,6 +7,7 @@ const { getImporter } = require('./importers');
|
|||
jest.mock('~/models', () => ({
|
||||
bulkSaveConvos: jest.fn(),
|
||||
bulkSaveMessages: jest.fn(),
|
||||
bulkIncrementTagCounts: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetEndpointsConfig = jest.fn().mockResolvedValue(null);
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ function getImporter(jsonData) {
|
|||
return importClaudeConvo;
|
||||
}
|
||||
// ChatGPT format has mapping object in each conversation
|
||||
logger.info('Importing ChatGPT conversation');
|
||||
return importChatGptConvo;
|
||||
if (jsonData.length === 0 || jsonData[0]?.mapping) {
|
||||
logger.info('Importing ChatGPT conversation');
|
||||
return importChatGptConvo;
|
||||
}
|
||||
throw new Error('Unsupported import type');
|
||||
}
|
||||
|
||||
// For ChatbotUI
|
||||
|
|
@ -81,6 +84,7 @@ async function importChatBotUiConvo(
|
|||
logger.info(`user: ${requestUserId} | ChatbotUI conversation imported`);
|
||||
} catch (error) {
|
||||
logger.error(`user: ${requestUserId} | Error creating conversation from ChatbotUI file`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +201,7 @@ async function importClaudeConvo(
|
|||
logger.info(`user: ${requestUserId} | Claude conversation imported`);
|
||||
} catch (error) {
|
||||
logger.error(`user: ${requestUserId} | Error creating conversation from Claude file`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,6 +310,7 @@ async function importLibreChatConvo(
|
|||
logger.debug(`user: ${requestUserId} | Conversation "${jsonData.title}" imported`);
|
||||
} catch (error) {
|
||||
logger.error(`user: ${requestUserId} | Error creating conversation from LibreChat file`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -336,6 +342,7 @@ async function importChatGptConvo(
|
|||
await importBatchBuilder.saveBatch();
|
||||
} catch (error) {
|
||||
logger.error(`user: ${requestUserId} | Error creating conversation from imported file`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +362,7 @@ function processConversation(conv, importBatchBuilder, requestUserId, defaultMod
|
|||
// Map all message IDs to new UUIDs
|
||||
const messageMap = new Map();
|
||||
for (const [id, mapping] of Object.entries(conv.mapping)) {
|
||||
if (mapping.message && mapping.message.content.content_type) {
|
||||
if (mapping.message?.content?.content_type) {
|
||||
const newMessageId = uuidv4();
|
||||
messageMap.set(id, newMessageId);
|
||||
}
|
||||
|
|
@ -467,6 +474,9 @@ function processConversation(conv, importBatchBuilder, requestUserId, defaultMod
|
|||
}
|
||||
|
||||
const newMessageId = messageMap.get(id);
|
||||
if (!newMessageId) {
|
||||
continue;
|
||||
}
|
||||
const parentMessageId = findValidParent(mapping.parent);
|
||||
|
||||
const messageText = formatMessageText(mapping.message);
|
||||
|
|
@ -474,7 +484,7 @@ function processConversation(conv, importBatchBuilder, requestUserId, defaultMod
|
|||
const isCreatedByUser = role === 'user';
|
||||
let sender = isCreatedByUser ? 'user' : 'assistant';
|
||||
const model =
|
||||
mapping.message.metadata.model_slug || defaultModel || openAISettings.model.default;
|
||||
mapping.message.metadata?.model_slug || defaultModel || openAISettings.model.default;
|
||||
|
||||
if (!isCreatedByUser) {
|
||||
/** Extracted model name from model slug */
|
||||
|
|
@ -598,7 +608,7 @@ function formatMessageText(messageData) {
|
|||
messageText = `\`\`\`json\n${JSON.stringify(messageData.content, null, 2)}\n\`\`\``;
|
||||
}
|
||||
|
||||
if (isText && messageData.author.role !== 'user') {
|
||||
if (isText && messageData.author?.role !== 'user') {
|
||||
messageText = processAssistantMessage(messageData, messageText);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -764,6 +764,86 @@ describe('importChatGptConvo', () => {
|
|||
expect(userMsg.createdAt).toEqual(new Date(1000 * 1000));
|
||||
expect(assistantMsg.createdAt).toEqual(new Date(2000 * 1000));
|
||||
});
|
||||
|
||||
it('should import messages missing metadata without failing (newer ChatGPT exports)', async () => {
|
||||
const testData = [
|
||||
{
|
||||
title: 'Missing Metadata Test',
|
||||
create_time: 1714585031.148505,
|
||||
update_time: 1714585060.879308,
|
||||
mapping: {
|
||||
'root-node': {
|
||||
id: 'root-node',
|
||||
message: null,
|
||||
parent: null,
|
||||
children: ['user-msg-1'],
|
||||
},
|
||||
'user-msg-1': {
|
||||
id: 'user-msg-1',
|
||||
message: {
|
||||
id: 'user-msg-1',
|
||||
author: { role: 'user' },
|
||||
create_time: 1714585031.150442,
|
||||
content: { content_type: 'text', parts: ['User message without metadata'] },
|
||||
},
|
||||
parent: 'root-node',
|
||||
children: ['assistant-msg-1'],
|
||||
},
|
||||
'assistant-msg-1': {
|
||||
id: 'assistant-msg-1',
|
||||
message: {
|
||||
id: 'assistant-msg-1',
|
||||
author: { role: 'assistant' },
|
||||
create_time: 1714585032.150442,
|
||||
content: { content_type: 'text', parts: ['Assistant response without metadata'] },
|
||||
},
|
||||
parent: 'user-msg-1',
|
||||
children: ['no-content-msg'],
|
||||
},
|
||||
'no-content-msg': {
|
||||
id: 'no-content-msg',
|
||||
message: {
|
||||
id: 'no-content-msg',
|
||||
author: { role: 'tool' },
|
||||
create_time: 1714585033.150442,
|
||||
},
|
||||
parent: 'assistant-msg-1',
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const requestUserId = 'user-123';
|
||||
const importBatchBuilder = new ImportBatchBuilder(requestUserId);
|
||||
jest.spyOn(importBatchBuilder, 'saveMessage');
|
||||
|
||||
const importer = getImporter(testData);
|
||||
await importer(testData, requestUserId, () => importBatchBuilder);
|
||||
|
||||
const savedMessages = importBatchBuilder.saveMessage.mock.calls.map((call) => call[0]);
|
||||
expect(savedMessages).toHaveLength(2);
|
||||
|
||||
const userMessage = savedMessages.find((msg) => msg.isCreatedByUser);
|
||||
const assistantMessage = savedMessages.find((msg) => !msg.isCreatedByUser);
|
||||
expect(userMessage.model).toBe(openAISettings.model.default);
|
||||
expect(assistantMessage.model).toBe(openAISettings.model.default);
|
||||
expect(assistantMessage.parentMessageId).toBe(userMessage.messageId);
|
||||
});
|
||||
|
||||
it('should rethrow errors so failed imports are not reported as successful', async () => {
|
||||
const jsonData = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '__data__', 'chatgpt-export.json'), 'utf8'),
|
||||
);
|
||||
const requestUserId = 'user-123';
|
||||
const importBatchBuilder = new ImportBatchBuilder(requestUserId);
|
||||
jest.spyOn(importBatchBuilder, 'saveBatch').mockRejectedValue(new Error('db unavailable'));
|
||||
|
||||
const importer = getImporter(jsonData);
|
||||
await expect(importer(jsonData, requestUserId, () => importBatchBuilder)).rejects.toThrow(
|
||||
'db unavailable',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importLibreChatConvo', () => {
|
||||
|
|
@ -1135,6 +1215,17 @@ describe('getImporter', () => {
|
|||
const jsonData = { unsupported: 'data' };
|
||||
expect(() => getImporter(jsonData)).toThrow('Unsupported import type');
|
||||
});
|
||||
|
||||
it('should throw for array-based files that are not ChatGPT or Claude exports', () => {
|
||||
const openWebUiExport = [
|
||||
{ id: 'abc', title: 'Open WebUI Chat', chat: { history: { messages: {} } } },
|
||||
];
|
||||
expect(() => getImporter(openWebUiExport)).toThrow('Unsupported import type');
|
||||
});
|
||||
|
||||
it('should route empty arrays to the ChatGPT importer without throwing', () => {
|
||||
expect(() => getImporter([])).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('processAssistantMessage', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue