Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion supabase/functions/_shared/debate-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
boundOutputDigests,
type DebateOutputCandidate,
digestOutput,
MAX_CASE_DIGEST_CHARS,
MAX_CASE_PARAGRAPH_CHARS,
MAX_CONTEXT_OUTPUTS,
MAX_OUTPUT_DIGEST_CHARS,
MAX_OUTPUTS_TOTAL_CHARS,
Expand Down Expand Up @@ -91,7 +93,9 @@ Deno.test("digests are bounded and whole results are dropped, never cut, when th
contentions: Array.from({ length: 10 }, (_, n) => ({ claim: `Claim ${n}`, reasoning: "R".repeat(2_000), impact: "I".repeat(2_000) })),
};
const digest = digestOutput("case", longCase);
assert(digest.length <= MAX_OUTPUT_DIGEST_CHARS, "one result exceeded its digest bound");
assert(digest.length <= MAX_CASE_DIGEST_CHARS, "one case exceeded its digest bound");
const longAttack = { weaknesses: Array.from({ length: 10 }, () => ({ description: "D".repeat(2_000), howOpponentAttacks: "H".repeat(2_000) })) };
assert(digestOutput("attack", longAttack).length <= MAX_OUTPUT_DIGEST_CHARS, "an attack exceeded its digest bound");
const items = Array.from({ length: 5 }, (_, n) => ({ id: String(n), digest }));
const bounded = boundOutputDigests(items);
const used = bounded.included.reduce((total, item) => total + item.digest.length, 0);
Expand All @@ -102,6 +106,34 @@ Deno.test("digests are bounded and whole results are dropped, never cut, when th
assert(empty.empty.length === 1 && empty.omittedForSize.length === 0, "an empty result was reported as too large");
});

Deno.test("a case's R2 backing-up paragraphs, If they say answers and Conclusor reasons reach the prompt in full", () => {
const paragraph = (label: string) => `${label} opens the paragraph. ${"Evidence and explanation follow here. ".repeat(28)}${label} ENDS HERE.`;
const r2 = {
contentions: [],
reinforce: [
{ argument: "Our first argument: fare-free transport helps those who need it most.", text: paragraph("Backing one") },
{ argument: "Our second argument: free buses are faster.", text: paragraph("Backing two") },
],
counters: Array.from({ length: 6 }, (_, n) => ({ targetArgument: `Their claim ${n + 1}`, counterpoint: paragraph(`Answer ${n + 1}`) })),
whyWeWin: [paragraph("Reason one")],
};
assert(paragraph("x").length > 1_000, "the test paragraphs should be as long as a real R2's");
const digest = digestOutput("case", r2);
for (const label of ["Backing one", "Backing two", "Answer 1", "Answer 6", "Reason one"]) {
assert(digest.includes(`${label} ENDS HERE.`), `${label} was cut`);
}
assert(!digest.includes("…"), "nothing in a normal R2 should be shortened");
assert(boundOutputDigests([{ digest }]).included.length === 1, "a full R2 must fit the total budget");

const huge = digestOutput("case", {
reinforce: [{ argument: "A", text: "R".repeat(50_000) }],
counters: Array.from({ length: 20 }, () => ({ targetArgument: "T".repeat(5_000), counterpoint: "C".repeat(50_000) })),
});
assert(huge.length <= MAX_CASE_DIGEST_CHARS, "a huge case exceeded its digest bound");
assert(!huge.includes("R".repeat(MAX_CASE_PARAGRAPH_CHARS)), "one paragraph exceeded its bound");
assert((huge.match(/If they say/g) ?? []).length <= 8, "more than 8 answers were kept");
});

Deno.test("each feature receives only the debate material its guarantees allow", () => {
const loaded = {
debateId: DEBATE,
Expand Down
21 changes: 16 additions & 5 deletions supabase/functions/_shared/debate-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,18 @@ export const MAX_AUTO_OUTPUTS_PER_KIND = 2;
export const MAX_PINNED_OUTPUTS = 3;
/** Characters one saved result may contribute. */
export const MAX_OUTPUT_DIGEST_CHARS = 2_500;
/**
* Characters one saved case may contribute. A case is read out in the round,
* so its R2 "backing up" paragraphs, "If they say" answers and Conclusor
* reasons reach the prompt in full: an R2 with 6-8 answers runs ~10,000.
*/
export const MAX_CASE_DIGEST_CHARS = 12_000;
/** Characters kept from one of those spoken paragraphs; a safety bound, not a summary length. */
export const MAX_CASE_PARAGRAPH_CHARS = 2_000;
/** "If they say" answers kept from one case; R2 writes at most 8. */
export const MAX_CASE_COUNTERS = 8;
/** Characters all saved results together may contribute. */
export const MAX_OUTPUTS_TOTAL_CHARS = 8_000;
export const MAX_OUTPUTS_TOTAL_CHARS = 16_000;

/**
* Saved results that help each feature, in order of usefulness. Results of
Expand Down Expand Up @@ -186,9 +196,9 @@ function digestCase(data: Record<string, unknown>): string {
text(item.impact, 240) && ` Why it matters: ${text(item.impact, 240)}`,
])),
...list(data.reinforce).map(record).filter(Boolean).slice(0, 3).map((item) =>
text(item!.text, 240) && `Backing up ${text(item!.argument, 80) || "an argument"}: ${text(item!.text, 240)}`),
...list(data.whyWeWin).slice(0, 3).map((item) => text(item, 240) && `Why we win: ${text(item, 240)}`),
...counters.slice(0, 3).map((item) => `If they say "${text(item.targetArgument, 120)}", we answer: ${text(item.counterpoint, 200)}`),
text(item!.text, MAX_CASE_PARAGRAPH_CHARS) && `Backing up ${text(item!.argument, 200) || "an argument"}: ${text(item!.text, MAX_CASE_PARAGRAPH_CHARS)}`),
...list(data.whyWeWin).slice(0, 3).map((item) => text(item, MAX_CASE_PARAGRAPH_CHARS) && `Why we win: ${text(item, MAX_CASE_PARAGRAPH_CHARS)}`),
...counters.slice(0, MAX_CASE_COUNTERS).map((item) => `If they say "${text(item.targetArgument, 200)}", we answer: ${text(item.counterpoint, MAX_CASE_PARAGRAPH_CHARS)}`),
]);
}

Expand Down Expand Up @@ -276,7 +286,8 @@ export function digestOutput(kind: DebateContextOutputKind, data: unknown): stri
: kind === "chat" ? digestChat(data)
: kind === "crossfire" ? digestCrossfire(value)
: digestSparring(value);
return digest.length > MAX_OUTPUT_DIGEST_CHARS ? `${digest.slice(0, MAX_OUTPUT_DIGEST_CHARS - 1)}…` : digest;
const max = kind === "case" ? MAX_CASE_DIGEST_CHARS : MAX_OUTPUT_DIGEST_CHARS;
return digest.length > max ? `${digest.slice(0, max - 1)}…` : digest;
}

export interface DebateOutputDigest extends DebateOutputRef {
Expand Down
Loading