Add bots files handling logic

This commit is contained in:
2026-10-21 17:56:09 +01:00
parent b0af4db1bf
commit d17a4a7bdd
97 changed files with 5843 additions and 41 deletions
@@ -0,0 +1,95 @@
import { z } from "zod";
import { AppData } from "@/src/data/app-data";
import grabBotFile from "@/src/functions/backend/db/bot-files/grab-bot-file";
import grabDirNames from "@/src/utils/grab-dir-names";
import readMedia from "@/src/harness/media/read-media";
import type { HarnessTool } from "../../types";
import { fileRef, type StorageToolContext } from "./storage-tool-context";
type Args = {
file_id?: number;
path?: string;
offset?: number;
scope?: string;
};
const TextExtensions = [
"txt",
"md",
"markdown",
"csv",
"json",
"html",
"htm",
"xml",
"yml",
"yaml",
"log",
"tsv",
];
/**
* Reads one of the bot's files: text as it is (a window at a time when it
* is long), an image or audio through the media harness, and a PDF or DOCX
* through the text extractor.
*/
export default function readFileTool(context: StorageToolContext): HarnessTool<Args> {
return {
name: "read_file",
description: `Read a file you saved, by its ref id or its path. Text comes back in a window of about ${Math.round(AppData.BotFileReadMaxChars / 1000)}k characters; use "offset" for the next part. Images and audio are read for you, and PDFs and Word documents are turned into text.`,
schema: z.object({
file_id: z.number().int().positive().optional(),
path: z.string().max(AppData.BotFilePathMaxChars).optional(),
offset: z.number().int().min(0).optional(),
scope: z.string().optional().describe(`"bot:<id>" or "workspace:<id>", with "path"`),
}),
run: async ({ args }) => {
const [scope_name, owner] = (args.scope || "").split(":");
const file = await grabBotFile({
scopes: context.scopes,
file_id: args.file_id,
scope: scope_name,
owner_id: owner,
path: args.path,
});
if (!file?.id || !file.media?.media_write_path) {
return `No file matches that. Use list_files to see what you have.`;
}
const { DATA_DIR } = grabDirNames();
const absolute = `${DATA_DIR}/${file.media.media_write_path}`;
const ext = (file.name || "").split(".").pop()?.toLowerCase() || "";
if (TextExtensions.includes(ext)) {
const text = await Bun.file(absolute).text();
const offset = args.offset || 0;
const window = text.slice(offset, offset + AppData.BotFileReadMaxChars);
const end = offset + window.length;
return [
`${file.path} ${fileRef({ file_id: file.id })}`,
window,
end < text.length
? `\n[Read ${offset}-${end} of ${text.length} characters. Call read_file with offset ${end} for the rest.]`
: "",
]
.filter(Boolean)
.join("\n\n");
}
const bytes = new Uint8Array(await Bun.file(absolute).arrayBuffer());
const text = await readMedia({
data: bytes,
media_type: file.mime_type || "application/octet-stream",
name: file.name || undefined,
bot_id: context.bot_id,
workspace_id: context.workspace_id,
});
return `${file.path} ${fileRef({ file_id: file.id })}\n\n${text.slice(0, AppData.BotFileReadMaxChars)}`;
},
};
}