Skip to content

Commit 7471174

Browse files
kapaleshreyasclaude
andcommitted
examples: ppt-agent demo — generate a real .pptx via the public GAP repo
Runs github.com/shreyas-lyzr/ppt-agent and pulls the produced .pptx via agent.fetchArtifact(). Closes the loop on issue #1 — binary artifact output is now a one-liner against a real, hosted agent. The agent (separate repo) installs python-pptx, writes a Python script, runs it, produces an 8-slide deck. We pull the bytes locally and verify ZIP magic before saving. Usage: ANTHROPIC_API_KEY=sk-... bun run examples/ppt-agent.ts \ "8-slide investor deck for <product>, audience <X>, tone <Y>" Defaults to a Lyzr AI investor deck if no argv. Output lands in examples/decks/ (gitignored). Live-validated: $ bun run examples/ppt-agent.ts "8-slide product overview deck for Lyzr AI..." → Saved → examples/decks/lyzr-ai-product-overview-2026-05-16.pptx → 38,479 bytes • 8 slides • 104.4s • $0.2001 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2be87b0 commit 7471174

3 files changed

Lines changed: 120 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ coverage/
1313
.cache/
1414
fixtures/.tmp/
1515
examples/binary-outputs/
16+
examples/decks/
1617
examples/marketing-outputs/
1718
examples/marketing-real-outputs/
1819
examples/security-reports/

examples/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"wedge1": "bun run wedge1-server.ts",
99
"marketing-agent": "bun run marketing-agent.ts",
1010
"security-agent": "bun run security-agent.ts",
11-
"binary-artifact-demo": "bun run binary-artifact-demo.ts"
11+
"binary-artifact-demo": "bun run binary-artifact-demo.ts",
12+
"ppt-agent": "bun run ppt-agent.ts"
1213
},
1314
"dependencies": {
1415
"computeragent": "workspace:*",

examples/ppt-agent.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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

Comments
 (0)