🚑 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:
Danny Avila 2024-05-17 14:10:40 -04:00 committed by GitHub
parent 31479d6a48
commit 53fe2f6453
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 627 additions and 493 deletions

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