Skip to content

Commit eb9322d

Browse files
feat: prepare embeddable council payload (#2)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3823cd6 commit eb9322d

3 files changed

Lines changed: 97 additions & 0 deletions

File tree

packages/formations/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ const synthesisPrompt = buildSynthesisPrompt(strategyRoom, 'What should we do ne
2929
]);
3030
```
3131

32+
## Embeddable Payload
33+
34+
Use the minimal embed API when you want a website, docs page, or product surface to render a CouncilVerse debate shell without binding to any specific LLM provider.
35+
36+
```bash
37+
node --input-type=module -e "import { buildCouncilEmbedPayload } from '@relaylaunch/councilverse-formations'; console.log(JSON.stringify(buildCouncilEmbedPayload('technical-review', 'Should we ship the embeddable verdict widget?'), null, 2));"
38+
```
39+
40+
The payload includes:
41+
42+
- `schemaVersion: "councilverse.embed.v1"`
43+
- formation metadata and default round count
44+
- one provider-agnostic system prompt per role
45+
- a synthesis prompt ready for your own model gateway
46+
3247
## Formations
3348

3449
| ID | Name | Methodology |
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildCouncilEmbedPayload } from "../src";
3+
4+
describe("CouncilVerse embeddable payload", () => {
5+
it("builds a provider-agnostic embed payload for a formation", () => {
6+
const payload = buildCouncilEmbedPayload(
7+
"technical-review",
8+
"Should we expose CouncilVerse as an embeddable verdict widget?",
9+
);
10+
11+
expect(payload.schemaVersion).toBe("councilverse.embed.v1");
12+
expect(payload.formationId).toBe("technical-review");
13+
expect(payload.agents).toHaveLength(5);
14+
expect(payload.defaultRounds).toBe(2);
15+
expect(payload.synthesisPrompt).toContain("Should we expose CouncilVerse");
16+
});
17+
18+
it("includes one system prompt per role without requiring an LLM provider key", () => {
19+
const payload = buildCouncilEmbedPayload("strategy-room", "Pick the next release wedge.");
20+
21+
expect(payload.agents.map((agent) => agent.title)).toEqual([
22+
"Observer",
23+
"Orienter",
24+
"Strategist",
25+
"Executor",
26+
"Red Cell",
27+
]);
28+
expect(payload.agents[0].systemPrompt).toContain("You are the Observer");
29+
expect(JSON.stringify(payload)).not.toContain("API_KEY");
30+
});
31+
32+
it("trims the public question and rejects empty embed requests", () => {
33+
expect(buildCouncilEmbedPayload("risk-council", " What can break launch? ").question)
34+
.toBe("What can break launch?");
35+
expect(() => buildCouncilEmbedPayload("risk-council", " ")).toThrow("question is required");
36+
});
37+
});

packages/formations/src/index.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,24 @@ export function getFormationsByMethodology(methodology: string): Formation[] {
308308
);
309309
}
310310

311+
export interface CouncilEmbedAgent {
312+
id: string;
313+
title: string;
314+
perspective: string;
315+
systemPrompt: string;
316+
}
317+
318+
export interface CouncilEmbedPayload {
319+
schemaVersion: "councilverse.embed.v1";
320+
formationId: FormationId;
321+
formationName: string;
322+
methodology: string;
323+
question: string;
324+
defaultRounds: number;
325+
agents: CouncilEmbedAgent[];
326+
synthesisPrompt: string;
327+
}
328+
311329
const ANTI_SYCOPHANCY_RULES = [
312330
"6. NEVER use hollow agreement phrases: 'I agree', 'exactly right', 'great point', 'well said', 'as you mentioned'",
313331
"7. If you change your position between rounds, you MUST state what new evidence caused the change",
@@ -333,6 +351,33 @@ export function buildSystemPrompt(formation: Formation, roleIndex: number): stri
333351
].join("\n");
334352
}
335353

354+
export function buildCouncilEmbedPayload(
355+
formationId: FormationId,
356+
question: string,
357+
): CouncilEmbedPayload {
358+
const trimmedQuestion = question.trim();
359+
if (!trimmedQuestion) {
360+
throw new Error("Embed question is required.");
361+
}
362+
363+
const formation = getFormation(formationId);
364+
return {
365+
schemaVersion: "councilverse.embed.v1",
366+
formationId: formation.id,
367+
formationName: formation.name,
368+
methodology: formation.methodology,
369+
question: trimmedQuestion,
370+
defaultRounds: formation.defaultRounds,
371+
agents: formation.roles.map((role, index) => ({
372+
id: `${formation.id}-agent-${index + 1}`,
373+
title: role.title,
374+
perspective: role.perspective,
375+
systemPrompt: buildSystemPrompt(formation, index),
376+
})),
377+
synthesisPrompt: buildSynthesisPrompt(formation, trimmedQuestion, []),
378+
};
379+
}
380+
336381
export interface FlipRateEntry {
337382
agentId: string;
338383
round: number;

0 commit comments

Comments
 (0)