import { executeInE2B } from '@/lib/execution/e2b' import { CodeLanguage } from '@/lib/execution/languages' const RENDER_TIMEOUT_MS = 150_000 // Bound the visual-QA cost: cap pages and rasterization DPI so the JPEGs the // file agent inspects stay small enough for vision input. const MAX_RENDER_PAGES = 20 const RENDER_DPI = 110 /** Extensions LibreOffice can render to page images for the visual QA loop. */ const RENDERABLE_EXTS = new Set(['pptx', 'docx', 'pdf']) export function isRenderableDocExt(ext: string): boolean { return RENDERABLE_EXTS.has(ext.toLowerCase()) } export interface DocRender { /** A single contact-sheet grid JPEG of all pages, for the agent's visual QA. */ grid: Buffer pageCount: number } /** * Renders a compiled document binary to a single contact-sheet grid image inside * the E2B doc sandbox (LibreOffice → PDF → poppler `pdftoppm` → Pillow tile). * Works for any compiled binary regardless of which engine produced it * (isolated-vm JS or E2B Python), so the visual QA loop covers pptx/docx/pdf * uniformly. One grid image fits the VFS single-attachment read and gives the * agent the whole deck/doc at a glance (mirrors Anthropic's thumbnail grid). * * Throws on a sandbox/infra failure or when the doc renders to zero pages. */ export async function renderDocToGrid(args: { binary: Buffer ext: string workspaceId: string }): Promise { const ext = args.ext.toLowerCase() if (!isRenderableDocExt(ext)) { throw new Error(`Cannot render .${ext} to images (supported: pptx, docx, pdf)`) } const script = ` import subprocess, glob, base64, json from PIL import Image ext = ${JSON.stringify(ext)} inp = f"/home/user/input.{ext}" pdf = inp if ext == "pdf" else "/home/user/input.pdf" if ext != "pdf": subprocess.run( ["soffice", "--headless", "--convert-to", "pdf", "--outdir", "/home/user", inp], check=True, timeout=120, capture_output=True, ) subprocess.run( ["pdftoppm", "-jpeg", "-r", "${RENDER_DPI}", "-l", "${MAX_RENDER_PAGES}", pdf, "/home/user/page"], check=True, timeout=120, capture_output=True, ) paths = sorted(glob.glob("/home/user/page*.jpg"))[:${MAX_RENDER_PAGES}] imgs = [Image.open(p).convert("RGB") for p in paths] n = len(imgs) if n == 0: print("__SIM_RESULT__=" + json.dumps({"grid": None, "pageCount": 0})) else: cols = 1 if n == 1 else (2 if n <= 6 else 3) rows = (n + cols - 1) // cols cell_w = max(i.width for i in imgs) cell_h = max(i.height for i in imgs) pad = 12 grid = Image.new("RGB", (cols * cell_w + (cols + 1) * pad, rows * cell_h + (rows + 1) * pad), (240, 240, 240)) for idx, im in enumerate(imgs): r, c = divmod(idx, cols) grid.paste(im, (pad + c * (cell_w + pad), pad + r * (cell_h + pad))) # Cap the grid's longest edge so the JPEG stays a reasonable vision input. max_edge = 2200 if max(grid.size) > max_edge: scale = max_edge / max(grid.size) grid = grid.resize((int(grid.width * scale), int(grid.height * scale))) grid.save("/home/user/grid.jpg", "JPEG", quality=80) with open("/home/user/grid.jpg", "rb") as f: print("__SIM_RESULT__=" + json.dumps({"grid": base64.b64encode(f.read()).decode(), "pageCount": n})) `.trim() const result = await executeInE2B({ code: script, language: CodeLanguage.Python, timeoutMs: RENDER_TIMEOUT_MS, sandboxKind: 'doc', sandboxFiles: [ { path: `/home/user/input.${ext}`, content: args.binary.toString('base64'), encoding: 'base64', }, ], }) if (result.error) { throw new Error(`Document render failed: ${result.error}`) } const payload = result.result as { grid?: string | null; pageCount?: number } | null if (!payload?.grid) { throw new Error('Document render produced no pages') } return { grid: Buffer.from(payload.grid, 'base64'), pageCount: payload.pageCount ?? 0 } }