reachy-tales / src /summary-builder.ts
Nicolas Rabault
feat(app): summary-builder for character-swap context handoff
2f2908b
Raw
History Blame Contribute Delete
2.06 kB
import type { TransitionEvent } from "story-engine";
export interface SummaryBuilderOptions {
apiKey: string;
model?: string; // defaults to gpt-4o-mini
baseUrl?: string; // defaults to /openai/v1/chat/completions
}
export class SummaryBuilder {
private apiKey: string;
private model: string;
private baseUrl: string;
constructor(opts: SummaryBuilderOptions) {
this.apiKey = opts.apiKey;
this.model = opts.model ?? "gpt-4o-mini";
this.baseUrl = opts.baseUrl ?? "/openai/v1/chat/completions";
}
async summarize(events: TransitionEvent[]): Promise<string> {
if (events.length === 0) return "(The story is just beginning.)";
const log = events
.map((e) => {
switch (e.type) {
case "storyEntered": return `Story "${e.storyId}" began.`;
case "storyExited": return `User exited the story.`;
case "sceneChanged": return `Scene changed: ${e.from ?? "?"}${e.to}.`;
case "speakerChanged": return `Now speaking: ${e.to} (was ${e.from ?? "narrator"}).`;
case "flagSet": return `Flag set: ${e.name} = ${e.value}.`;
case "blocked": return `Blocked: ${e.reason}.`;
}
})
.join("\n");
const prompt = `Given these recent story events, write a 2-3 sentence summary suitable for handing off to a new character voice. Capture what just happened and what the user was doing. Plain spoken English, no markdown.\n\nEvents:\n${log}`;
const res = await fetch(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: this.model,
messages: [{ role: "user", content: prompt }],
max_tokens: 200,
temperature: 0.4,
}),
});
if (!res.ok) {
throw new Error(`Summary API ${res.status}: ${await res.text()}`);
}
const data = await res.json();
return data.choices[0]?.message?.content?.trim() ?? "(No summary available.)";
}
}