mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(antigravity): preserve uploaded file references
Keep original file paths and metadata instead of decoding every small upload as UTF-8. This accepts attachments without losing binary contents or requiring file reads during prompt encoding. Co-authored-by: ax <chinaszlcx@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { ProviderPrompt } from "@getpaseo/plugin/server/provider";
|
||||
@@ -10,8 +10,6 @@ interface EncodedPrompt {
|
||||
nativeText: string;
|
||||
}
|
||||
|
||||
const MAX_INLINE_FILE_BYTES = 2 * 1024 * 1024; // 2 MB
|
||||
|
||||
export class PromptFiles {
|
||||
private directory: string | null = null;
|
||||
|
||||
@@ -22,16 +20,19 @@ export class PromptFiles {
|
||||
prompt.outputSchema !== undefined
|
||||
)
|
||||
throw new AntigravityError("Antigravity supports messages only");
|
||||
|
||||
const parts = prompt.input.content.map((part) => {
|
||||
if (part.type === "text" || part.type === "uploaded_file") return part;
|
||||
if (part.type === "image") return { ...part, extension: imageExtension(part.mimeType) };
|
||||
throw new AntigravityError("Antigravity supports text, image and uploaded file content only");
|
||||
});
|
||||
const text: string[] = [];
|
||||
const native: string[] = [];
|
||||
|
||||
for (const part of prompt.input.content) {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") {
|
||||
text.push(part.text);
|
||||
native.push(part.text);
|
||||
} else if (part.type === "image") {
|
||||
const extension = imageExtension(part.mimeType);
|
||||
const extension = part.extension;
|
||||
if (this.directory === null) {
|
||||
this.directory = await mkdtemp(path.join(os.tmpdir(), "paseo-antigravity-images-"));
|
||||
await chmod(this.directory, 0o700);
|
||||
@@ -41,31 +42,20 @@ export class PromptFiles {
|
||||
await chmod(file, 0o600);
|
||||
native.push(`Attached image: ${file}`);
|
||||
} else if (part.type === "uploaded_file") {
|
||||
if (part.mimeType && part.mimeType.startsWith("image/")) {
|
||||
if (part.mimeType.startsWith("image/")) {
|
||||
native.push(`Attached image: ${part.path}`);
|
||||
} else if (part.size > MAX_INLINE_FILE_BYTES) {
|
||||
const notice = `[Attached large file: ${part.fileName} at ${part.path} (${part.size} bytes)]`;
|
||||
text.push(notice);
|
||||
native.push(notice);
|
||||
} else {
|
||||
try {
|
||||
const content = await readFile(part.path, "utf8");
|
||||
const fileBlock = `[File: ${part.fileName}]\n${content}`;
|
||||
text.push(fileBlock);
|
||||
native.push(fileBlock);
|
||||
} catch {
|
||||
const notice = `[Attached file: ${part.fileName} at ${part.path}]`;
|
||||
text.push(notice);
|
||||
native.push(notice);
|
||||
}
|
||||
const reference = [
|
||||
`Uploaded file: ${part.fileName}`,
|
||||
`Path: ${part.path}`,
|
||||
`MIME: ${part.mimeType}`,
|
||||
`Size: ${part.size} bytes`,
|
||||
].join("\n");
|
||||
text.push(reference);
|
||||
native.push(reference);
|
||||
}
|
||||
} else {
|
||||
throw new AntigravityError(
|
||||
`Antigravity does not support content type: ${(part as { type?: string }).type ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { text: text.join("\n"), nativeText: native.join("\n") };
|
||||
}
|
||||
|
||||
|
||||
@@ -775,7 +775,7 @@ describe("image prompts", () => {
|
||||
});
|
||||
|
||||
describe("uploaded file prompts", () => {
|
||||
it("inlines readable text files and references uploaded images and large files", async () => {
|
||||
it("references uploaded text, images and large files without changing their bytes", async () => {
|
||||
const h = await harness();
|
||||
await h.open();
|
||||
const txtFile = path.join(h.cwd, "notes.txt");
|
||||
@@ -815,8 +815,10 @@ describe("uploaded file prompts", () => {
|
||||
|
||||
const sent = (await h.records()).find((entry) => entry.input).input.message.content;
|
||||
expect(sent).toBe(
|
||||
`SYSTEM PREFIX\n\nPlease review these attachments:\n[File: notes.txt]\nexport const meaning = 42;\nAttached image: ${imgFile}\n[Attached large file: dataset.bin at /tmp/dataset.bin (3145728 bytes)]`,
|
||||
`SYSTEM PREFIX\n\nPlease review these attachments:\nUploaded file: notes.txt\nPath: ${txtFile}\nMIME: text/plain\nSize: 26 bytes\nAttached image: ${imgFile}\nUploaded file: dataset.bin\nPath: /tmp/dataset.bin\nMIME: application/octet-stream\nSize: 3145728 bytes`,
|
||||
);
|
||||
expect(await readFile(txtFile, "utf8")).toBe("export const meaning = 42;");
|
||||
expect(await readFile(imgFile, "utf8")).toBe("png bytes");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -835,3 +837,29 @@ it.runIf(process.platform === "win32")(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves a usable reference for an uploaded binary file", async () => {
|
||||
const h = await harness();
|
||||
await h.open();
|
||||
const file = path.join(h.cwd, "document.pdf");
|
||||
const bytes = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x00, 0xff, 0xfe]);
|
||||
await writeFile(file, bytes);
|
||||
await h.completed(
|
||||
await h.message([
|
||||
{ type: "text", text: "Read the attachment" },
|
||||
{
|
||||
type: "uploaded_file",
|
||||
id: "pdf-upload",
|
||||
fileName: "document.pdf",
|
||||
mimeType: "application/pdf",
|
||||
size: 7,
|
||||
path: file,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const sent = (await h.records()).find((entry) => entry.input).input.message.content;
|
||||
expect(sent).toContain(file);
|
||||
expect(sent).not.toContain("\u0000");
|
||||
expect(sent).not.toContain("\ufffd");
|
||||
expect(await readFile(file)).toEqual(bytes);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user