|
| 1 | +/** |
| 2 | + * Generate a PowerPoint deck via the public ppt-agent GAP repo. |
| 3 | + * |
| 4 | + * ANTHROPIC_API_KEY=sk-ant-... bun run examples/ppt-agent.ts "<topic + audience>" |
| 5 | + * |
| 6 | + * # With a default topic if no argv: |
| 7 | + * ANTHROPIC_API_KEY=sk-ant-... bun run examples/ppt-agent.ts |
| 8 | + * |
| 9 | + * The agent writes the .pptx into its workdir; we pull it locally via |
| 10 | + * agent.fetchArtifact() and save it under examples/decks/. |
| 11 | + */ |
| 12 | +import { mkdir, writeFile } from "node:fs/promises"; |
| 13 | +import { join } from "node:path"; |
| 14 | +import { ComputerAgent, LocalSubstrate } from "computeragent"; |
| 15 | + |
| 16 | +const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; |
| 17 | +if (!ANTHROPIC_API_KEY) throw new Error("Set ANTHROPIC_API_KEY first."); |
| 18 | + |
| 19 | +const TOPIC = |
| 20 | + process.argv[2] ?? |
| 21 | + "10-slide investor deck for Lyzr AI, an enterprise platform for building and governing AI agents. Audience: Series A VCs. Tone: confident, data-driven."; |
| 22 | + |
| 23 | +const OUT = join(import.meta.dir ?? __dirname, "decks"); |
| 24 | +await mkdir(OUT, { recursive: true }); |
| 25 | + |
| 26 | +console.log(`\nTopic: ${TOPIC}\nOutput dir: ${OUT}\n`); |
| 27 | + |
| 28 | +await using agent = new ComputerAgent({ |
| 29 | + source: { type: "git", url: "github.com/shreyas-lyzr/ppt-agent" }, |
| 30 | + harness: "claude-agent-sdk", |
| 31 | + runtime: new LocalSubstrate(), |
| 32 | + envs: { ANTHROPIC_API_KEY }, |
| 33 | + options: { permissionMode: "bypassPermissions", settingSources: ["project"] }, |
| 34 | +}); |
| 35 | + |
| 36 | +const startedAt = Date.now(); |
| 37 | +let toolCalls = 0; |
| 38 | + |
| 39 | +const handle = agent.chat(TOPIC); |
| 40 | + |
| 41 | +for await (const ev of handle) { |
| 42 | + if (ev.kind === "ca_session_started") { |
| 43 | + console.log(`[session ${ev.sessionId}]\n`); |
| 44 | + } else if (ev.kind === "sdk_message") { |
| 45 | + const p = ev.payload as Record<string, unknown>; |
| 46 | + if (p.type === "assistant") { |
| 47 | + const msg = p.message as { content?: { type: string; text?: string; name?: string; input?: unknown }[] }; |
| 48 | + for (const block of msg.content ?? []) { |
| 49 | + if (block.type === "text" && block.text) { |
| 50 | + process.stdout.write(block.text); |
| 51 | + } else if (block.type === "tool_use") { |
| 52 | + toolCalls++; |
| 53 | + const inp = JSON.stringify(block.input ?? {}).slice(0, 140); |
| 54 | + console.log(`\n → ${block.name}(${inp}${inp.length >= 140 ? "…" : ""})`); |
| 55 | + } |
| 56 | + } |
| 57 | + } else if (p.type === "user") { |
| 58 | + const msg = p.message as { content?: { type: string; content?: unknown }[] }; |
| 59 | + for (const block of msg.content ?? []) { |
| 60 | + if (block.type === "tool_result") { |
| 61 | + const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content); |
| 62 | + console.log(` ← ${content.slice(0, 180)}${content.length > 180 ? "…" : ""}`); |
| 63 | + } |
| 64 | + } |
| 65 | + } |
| 66 | + } else if (ev.kind === "ca_session_ended") { |
| 67 | + console.log(`\n[ended: ${ev.reason}${ev.errorMessage ? ` — ${ev.errorMessage}` : ""}]`); |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1); |
| 72 | +const usage = handle.getUsage(); |
| 73 | +console.log(`\n${"─".repeat(70)}`); |
| 74 | +console.log(`Agent done in ${elapsed}s • ${toolCalls} tool calls • ${usage.inputTokens + usage.outputTokens} tokens • $${usage.costUsd?.toFixed(4) ?? "?"}\n`); |
| 75 | + |
| 76 | +// Find the .pptx the agent produced. |
| 77 | +const tree = await agent.listWorkdir({ depth: 2 }); |
| 78 | +const pptxFiles = tree.filter((e) => e.type === "file" && e.path.toLowerCase().endsWith(".pptx")); |
| 79 | + |
| 80 | +if (pptxFiles.length === 0) { |
| 81 | + console.error("✗ No .pptx file found in workdir."); |
| 82 | + console.error(" Workdir contents:"); |
| 83 | + for (const e of tree.filter((x) => x.type === "file" && !x.path.startsWith("."))) { |
| 84 | + console.error(` ${e.path.padEnd(40)} ${e.size}b`); |
| 85 | + } |
| 86 | + process.exit(1); |
| 87 | +} |
| 88 | + |
| 89 | +// Save the largest .pptx (in case the agent wrote multiple, take the real one) |
| 90 | +pptxFiles.sort((a, b) => b.size - a.size); |
| 91 | +const winner = pptxFiles[0]!; |
| 92 | +const bytes = await agent.fetchArtifact(winner.path); |
| 93 | + |
| 94 | +if (!bytes) { |
| 95 | + console.error(`✗ fetchArtifact returned null for ${winner.path}`); |
| 96 | + process.exit(1); |
| 97 | +} |
| 98 | + |
| 99 | +// PowerPoint files are ZIP archives — verify magic bytes (PK\x03\x04) |
| 100 | +const looksLikePptx = |
| 101 | + bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04; |
| 102 | +if (!looksLikePptx) { |
| 103 | + console.error( |
| 104 | + `✗ ${winner.path} doesn't look like a valid .pptx (ZIP) file. First bytes: ${Array.from(bytes.slice(0, 4)) |
| 105 | + .map((b) => b.toString(16).padStart(2, "0")) |
| 106 | + .join(" ")}`, |
| 107 | + ); |
| 108 | + process.exit(1); |
| 109 | +} |
| 110 | + |
| 111 | +const localName = winner.path.replace(/^\//, ""); |
| 112 | +const localPath = join(OUT, localName); |
| 113 | +await writeFile(localPath, bytes); |
| 114 | + |
| 115 | +console.log(`✓ Saved → ${localPath}`); |
| 116 | +console.log(` ${bytes.length.toLocaleString()} bytes • ZIP magic verified`); |
| 117 | +console.log(`\nOpen with:\n open "${localPath}"\n`); |
0 commit comments