Every bot file was chunked into bot_file_chunks and embedded into bot_file_vectors, and uploads kept their extracted text in media.text_content, which together took most of the database. Bot files are no longer indexed: bots find them by name or path with list_files and read them with read_file, and search_files, ingest_file and the files modal's re-read button are gone. Attachments keep no text; a reply reads a document from the file. Only knowledgebase documents keep text. The schema drops both tables and the ingest columns. At boot, dropStoredFileText removes what older versions stored (file jobs, upload text) and vacuums once much of the database is free. The media:prune-text script it replaces is removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import mediaFilePath from "@/src/utils/media-file-path";
|
|
import { AppData } from "@/src/data/app-data";
|
|
import grabMessagesMedia, {
|
|
type MessageMediaRow,
|
|
} from "@/src/functions/backend/db/bots/grab-messages-media";
|
|
import type { HarnessContentPart } from "@/src/harness/types";
|
|
import type { BotMessageWithAttachments } from "@/src/types";
|
|
import extractBotFileText from "@/src/functions/backend/db/bot-files/extract-bot-file-text";
|
|
import describeAttachment from "./describe-attachment";
|
|
|
|
type Params = {
|
|
/**
|
|
* The conversation a reply reads, oldest first.
|
|
*/
|
|
messages: BotMessageWithAttachments[];
|
|
};
|
|
|
|
/**
|
|
* The files of the latest few messages that have any
|
|
* (`AppData.BotReplyAttachmentMessages`), as model input by message id:
|
|
* images and audio as themselves (a model that can't take them gets them
|
|
* read first), documents as their text, videos as a note. Older files are
|
|
* only named (`botMessagesToHarness`).
|
|
*/
|
|
export default async function loadAttachmentParts({ messages }: Params) {
|
|
const recent = messages
|
|
.filter((message) => message.attachments?.length)
|
|
.slice(-AppData.BotReplyAttachmentMessages)
|
|
.map((message) => Number(message.id));
|
|
|
|
const media = await grabMessagesMedia({ message_ids: recent });
|
|
const parts = new Map<number, HarnessContentPart[]>();
|
|
|
|
for (const [message_id, files] of media) {
|
|
parts.set(
|
|
message_id,
|
|
(await Promise.all(files.map((file) => fileToParts({ file })))).flat(),
|
|
);
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
async function fileToParts({ file }: { file: MessageMediaRow }): Promise<HarnessContentPart[]> {
|
|
const name = file.title || `file ${file.id}`;
|
|
/**
|
|
* The upload's own id, so a bot can name it back to a tool
|
|
* (`set_picture` and `update_workspace` take one) instead of guessing
|
|
* which file the user means.
|
|
*/
|
|
const label = describeAttachment({ name, id: file.id! });
|
|
|
|
if (file.media_type == "document") {
|
|
/**
|
|
* Read from the file each time: no text is kept outside
|
|
* knowledgebases.
|
|
*/
|
|
const text = (
|
|
await extractBotFileText({
|
|
write_path: file.media_write_path,
|
|
name: file.title,
|
|
mime_type: file.mime_type,
|
|
}).catch(() => null)
|
|
)?.trim();
|
|
|
|
if (!text) {
|
|
return [{ type: "text", text: `[Attached file ${label}: its text couldn't be read.]` }];
|
|
}
|
|
|
|
const cut = text.length > AppData.BotAttachmentMaxTextChars;
|
|
|
|
return [
|
|
{
|
|
type: "text",
|
|
text: `Attached file ${label}:\n\n${text.slice(0, AppData.BotAttachmentMaxTextChars)}${cut ? `\n\n[The rest of "${name}" was cut: it's ${text.length} characters long.]` : ""}`,
|
|
},
|
|
];
|
|
}
|
|
|
|
if (file.media_type == "video") {
|
|
return [{ type: "text", text: `[Attached video ${label}: videos can't be read yet.]` }];
|
|
}
|
|
|
|
const absolute = mediaFilePath({ write_path: file.media_write_path });
|
|
const disk = absolute ? Bun.file(absolute) : null;
|
|
|
|
if (!disk || !(await disk.exists())) {
|
|
return [{ type: "text", text: `[Attached file ${label} is missing on the server.]` }];
|
|
}
|
|
|
|
const base64 = Buffer.from(await disk.arrayBuffer()).toString("base64");
|
|
|
|
if (file.media_type == "image") {
|
|
return [
|
|
{ type: "text", text: `[Attached image ${label}]` },
|
|
{ type: "image", url: `data:image/${file.mime_type || "jpeg"};base64,${base64}`, name },
|
|
];
|
|
}
|
|
|
|
return [
|
|
{ type: "text", text: `[Attached audio ${label}]` },
|
|
{ type: "audio", data: base64, format: file.mime_type || "mp3", name },
|
|
];
|
|
}
|