mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🚑 fix(export): Issue exporting Conversation with Assistants (#2769)
* 🚑 fix(export): use content as text if content is present in the message
If the endpoint is assistants, the text of the message goes into content, not message.text.
* refactor(ExportModel): TypeScript, remove unused code
---------
Co-authored-by: Yuichi Ohneda <ohneda@gmail.com>
This commit is contained in:
parent
31479d6a48
commit
53fe2f6453
7 changed files with 627 additions and 493 deletions
369
client/src/hooks/Conversations/useExportConversation.ts
Normal file
369
client/src/hooks/Conversations/useExportConversation.ts
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import download from 'downloadjs';
|
||||
import exportFromJSON from 'export-from-json';
|
||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
ContentTypes,
|
||||
ToolCallTypes,
|
||||
imageGenTools,
|
||||
isImageVisionTool,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
TMessage,
|
||||
TPreset,
|
||||
TConversation,
|
||||
TMessageContentParts,
|
||||
} from 'librechat-data-provider';
|
||||
import useBuildMessageTree from '~/hooks/Messages/useBuildMessageTree';
|
||||
import { useScreenshot } from '~/hooks/ScreenshotContext';
|
||||
import { cleanupPreset, buildTree } from '~/utils';
|
||||
|
||||
export default function useExportConversation({
|
||||
conversation,
|
||||
filename,
|
||||
type,
|
||||
includeOptions,
|
||||
exportBranches,
|
||||
recursive,
|
||||
}: {
|
||||
conversation: TConversation | null;
|
||||
filename: string;
|
||||
type: string;
|
||||
includeOptions: boolean | 'indeterminate';
|
||||
exportBranches: boolean | 'indeterminate';
|
||||
recursive: boolean | 'indeterminate';
|
||||
}) {
|
||||
const { captureScreenshot } = useScreenshot();
|
||||
const buildMessageTree = useBuildMessageTree();
|
||||
const { data: messagesTree = null } = useGetMessagesByConvoId(
|
||||
conversation?.conversationId ?? '',
|
||||
{
|
||||
select: (data) => {
|
||||
const dataTree = buildTree({ messages: data });
|
||||
return dataTree?.length === 0 ? null : dataTree ?? null;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const getMessageText = (message: TMessage, format = 'text') => {
|
||||
if (!message) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formatText = (sender, text) => {
|
||||
if (format === 'text') {
|
||||
return `>> ${sender}:\n${text}`;
|
||||
}
|
||||
return `**${sender}**\n${text}`;
|
||||
};
|
||||
|
||||
if (!message.content) {
|
||||
return formatText(message.sender, message.text);
|
||||
}
|
||||
|
||||
return message.content
|
||||
.map((content) => getMessageContent(message.sender, content))
|
||||
.map((text) => {
|
||||
return formatText(text[0], text[1]);
|
||||
})
|
||||
.join('\n\n\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Format and return message texts according to the type of content.
|
||||
* Currently, content whose type is `TOOL_CALL` basically returns JSON as is.
|
||||
* In the future, different formatted text may be returned for each type.
|
||||
*/
|
||||
const getMessageContent = (sender: string, content: TMessageContentParts): string[] => {
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (content.type === ContentTypes.ERROR) {
|
||||
// ERROR
|
||||
return [sender, content[ContentTypes.TEXT].value];
|
||||
}
|
||||
|
||||
if (content.type === ContentTypes.TEXT) {
|
||||
// TEXT
|
||||
return [sender, content[ContentTypes.TEXT].value];
|
||||
}
|
||||
|
||||
if (content.type === ContentTypes.TOOL_CALL) {
|
||||
const type = content[ContentTypes.TOOL_CALL].type;
|
||||
|
||||
if (type === ToolCallTypes.CODE_INTERPRETER) {
|
||||
// CODE_INTERPRETER
|
||||
const toolCall = content[ContentTypes.TOOL_CALL];
|
||||
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
|
||||
return ['Code Interpreter', JSON.stringify(code_interpreter)];
|
||||
}
|
||||
|
||||
if (type === ToolCallTypes.RETRIEVAL) {
|
||||
// RETRIEVAL
|
||||
const toolCall = content[ContentTypes.TOOL_CALL];
|
||||
return ['Retrieval', JSON.stringify(toolCall)];
|
||||
}
|
||||
|
||||
if (
|
||||
type === ToolCallTypes.FUNCTION &&
|
||||
imageGenTools.has(content[ContentTypes.TOOL_CALL].function.name)
|
||||
) {
|
||||
// IMAGE_GENERATION
|
||||
const toolCall = content[ContentTypes.TOOL_CALL];
|
||||
return ['Tool', JSON.stringify(toolCall)];
|
||||
}
|
||||
|
||||
if (type === ToolCallTypes.FUNCTION) {
|
||||
// IMAGE_VISION
|
||||
const toolCall = content[ContentTypes.TOOL_CALL];
|
||||
if (isImageVisionTool(toolCall)) {
|
||||
return ['Tool', JSON.stringify(toolCall)];
|
||||
}
|
||||
return ['Tool', JSON.stringify(toolCall)];
|
||||
}
|
||||
}
|
||||
|
||||
if (content.type === ContentTypes.IMAGE_FILE) {
|
||||
// IMAGE
|
||||
const imageFile = content[ContentTypes.IMAGE_FILE];
|
||||
return ['Image', JSON.stringify(imageFile)];
|
||||
}
|
||||
|
||||
return [sender, JSON.stringify(content)];
|
||||
};
|
||||
|
||||
const exportScreenshot = async () => {
|
||||
let data;
|
||||
try {
|
||||
data = await captureScreenshot();
|
||||
} catch (err) {
|
||||
console.error('Failed to capture screenshot');
|
||||
return console.error(err);
|
||||
}
|
||||
download(data, `${filename}.png`, 'image/png');
|
||||
};
|
||||
|
||||
const exportCSV = async () => {
|
||||
const data: TMessage[] = [];
|
||||
|
||||
const messages = await buildMessageTree({
|
||||
messageId: conversation?.conversationId,
|
||||
message: null,
|
||||
messages: messagesTree,
|
||||
branches: !!exportBranches,
|
||||
recursive: false,
|
||||
});
|
||||
|
||||
if (Array.isArray(messages)) {
|
||||
for (const message of messages) {
|
||||
data.push(message);
|
||||
}
|
||||
} else {
|
||||
data.push(messages);
|
||||
}
|
||||
|
||||
exportFromJSON({
|
||||
data: data,
|
||||
fileName: filename,
|
||||
extension: 'csv',
|
||||
exportType: exportFromJSON.types.csv,
|
||||
beforeTableEncode: (entries) => [
|
||||
{
|
||||
fieldName: 'sender',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'sender')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'text',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'text')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'isCreatedByUser',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'isCreatedByUser')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'error',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'error')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'unfinished',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'unfinished')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'messageId',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'messageId')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'parentMessageId',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'parentMessageId')?.fieldValues ?? [],
|
||||
},
|
||||
{
|
||||
fieldName: 'createdAt',
|
||||
fieldValues: entries?.find((e) => e.fieldName == 'createdAt')?.fieldValues ?? [],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const exportMarkdown = async () => {
|
||||
let data =
|
||||
'# Conversation\n' +
|
||||
`- conversationId: ${conversation?.conversationId}\n` +
|
||||
`- endpoint: ${conversation?.endpoint}\n` +
|
||||
`- title: ${conversation?.title}\n` +
|
||||
`- exportAt: ${new Date().toTimeString()}\n`;
|
||||
|
||||
if (includeOptions) {
|
||||
data += '\n## Options\n';
|
||||
const options = cleanupPreset({ preset: conversation as TPreset });
|
||||
|
||||
for (const key of Object.keys(options)) {
|
||||
data += `- ${key}: ${options[key]}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const messages = await buildMessageTree({
|
||||
messageId: conversation?.conversationId,
|
||||
message: null,
|
||||
messages: messagesTree,
|
||||
branches: false,
|
||||
recursive: false,
|
||||
});
|
||||
|
||||
data += '\n## History\n';
|
||||
if (Array.isArray(messages)) {
|
||||
for (const message of messages) {
|
||||
data += `${getMessageText(message, 'md')}\n`;
|
||||
if (message.error) {
|
||||
data += '*(This is an error message)*\n';
|
||||
}
|
||||
if (message.unfinished) {
|
||||
data += '*(This is an unfinished message)*\n';
|
||||
}
|
||||
data += '\n\n';
|
||||
}
|
||||
} else {
|
||||
data += `${getMessageText(messages, 'md')}\n`;
|
||||
if (messages.error) {
|
||||
data += '*(This is an error message)*\n';
|
||||
}
|
||||
if (messages.unfinished) {
|
||||
data += '*(This is an unfinished message)*\n';
|
||||
}
|
||||
}
|
||||
|
||||
exportFromJSON({
|
||||
data: data,
|
||||
fileName: filename,
|
||||
extension: 'md',
|
||||
exportType: exportFromJSON.types.txt,
|
||||
});
|
||||
};
|
||||
|
||||
const exportText = async () => {
|
||||
let data =
|
||||
'Conversation\n' +
|
||||
'########################\n' +
|
||||
`conversationId: ${conversation?.conversationId}\n` +
|
||||
`endpoint: ${conversation?.endpoint}\n` +
|
||||
`title: ${conversation?.title}\n` +
|
||||
`exportAt: ${new Date().toTimeString()}\n`;
|
||||
|
||||
if (includeOptions) {
|
||||
data += '\nOptions\n########################\n';
|
||||
const options = cleanupPreset({ preset: conversation as TPreset });
|
||||
|
||||
for (const key of Object.keys(options)) {
|
||||
data += `${key}: ${options[key]}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const messages = await buildMessageTree({
|
||||
messageId: conversation?.conversationId,
|
||||
message: null,
|
||||
messages: messagesTree,
|
||||
branches: false,
|
||||
recursive: false,
|
||||
});
|
||||
|
||||
data += '\nHistory\n########################\n';
|
||||
if (Array.isArray(messages)) {
|
||||
for (const message of messages) {
|
||||
data += `${getMessageText(message)}\n`;
|
||||
if (message.error) {
|
||||
data += '(This is an error message)\n';
|
||||
}
|
||||
if (message.unfinished) {
|
||||
data += '(This is an unfinished message)\n';
|
||||
}
|
||||
data += '\n\n';
|
||||
}
|
||||
} else {
|
||||
data += `${getMessageText(messages)}\n`;
|
||||
if (messages.error) {
|
||||
data += '(This is an error message)\n';
|
||||
}
|
||||
if (messages.unfinished) {
|
||||
data += '(This is an unfinished message)\n';
|
||||
}
|
||||
}
|
||||
|
||||
exportFromJSON({
|
||||
data: data,
|
||||
fileName: filename,
|
||||
extension: 'txt',
|
||||
exportType: exportFromJSON.types.txt,
|
||||
});
|
||||
};
|
||||
|
||||
const exportJSON = async () => {
|
||||
const data = {
|
||||
conversationId: conversation?.conversationId,
|
||||
endpoint: conversation?.endpoint,
|
||||
title: conversation?.title,
|
||||
exportAt: new Date().toTimeString(),
|
||||
branches: exportBranches,
|
||||
recursive: recursive,
|
||||
};
|
||||
|
||||
if (includeOptions) {
|
||||
data['options'] = cleanupPreset({ preset: conversation as TPreset });
|
||||
}
|
||||
|
||||
const messages = await buildMessageTree({
|
||||
messageId: conversation?.conversationId,
|
||||
message: null,
|
||||
messages: messagesTree,
|
||||
branches: !!exportBranches,
|
||||
recursive: !!recursive,
|
||||
});
|
||||
|
||||
if (recursive && !Array.isArray(messages)) {
|
||||
data['messagesTree'] = messages.children;
|
||||
} else {
|
||||
data['messages'] = messages;
|
||||
}
|
||||
|
||||
exportFromJSON({
|
||||
data: data,
|
||||
fileName: filename,
|
||||
extension: 'json',
|
||||
exportType: exportFromJSON.types.json,
|
||||
});
|
||||
};
|
||||
|
||||
const exportConversation = () => {
|
||||
if (type === 'json') {
|
||||
exportJSON();
|
||||
} else if (type == 'text') {
|
||||
exportText();
|
||||
} else if (type == 'markdown') {
|
||||
exportMarkdown();
|
||||
} else if (type == 'csv') {
|
||||
exportCSV();
|
||||
} else if (type == 'screenshot') {
|
||||
exportScreenshot();
|
||||
}
|
||||
};
|
||||
|
||||
return { exportConversation };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue