From 21c1f540b3ded2e44ffc832eb3ed450b3f848726 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:25:41 -0300 Subject: [PATCH] =?UTF-8?q?feat(compression):=20relevance=20extractive=20e?= =?UTF-8?q?ngine=20=E2=80=94=20roadmap=20#7=20(#5289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(compression): failing tests for relevance engine scorer + apply * feat(compression): relevance extractive engine — scores sentences against last user query * test(compression): regression tests for relevance engine review fixes (threshold/multimodal/force-preserve/whitespace/query) * fix(compression): relevance engine hardening from core review - overlapThreshold was dead config (|| kept = { isSingleMode: false, description: "Tabular JSON compaction.", }, + relevance: { + id: "relevance", + label: "Relevance", + stackPriority: 18, + isSingleMode: true, + description: "Extractive sentence scoring against the last user query.", + }, caveman: { id: "caveman", label: "Caveman", diff --git a/open-sse/services/compression/engines/index.ts b/open-sse/services/compression/engines/index.ts index 239aad8bb2..4c9c9f2a2d 100644 --- a/open-sse/services/compression/engines/index.ts +++ b/open-sse/services/compression/engines/index.ts @@ -6,6 +6,7 @@ import { headroomEngine } from "./headroom/index.ts"; import { ccrEngine } from "./ccr/index.ts"; import { llmlinguaEngine } from "./llmlingua/index.ts"; import { ionizerEngine } from "./ionizer/index.ts"; +import { relevanceEngine } from "./relevance/index.ts"; let registered = false; @@ -28,6 +29,7 @@ export function registerBuiltinCompressionEngines(): void { { id: "ccr", engine: ccrEngine }, { id: "llmlingua", engine: llmlinguaEngine }, { id: "ionizer", engine: ionizerEngine }, + { id: "relevance", engine: relevanceEngine }, ]; for (const { id, engine } of engines) { diff --git a/open-sse/services/compression/engines/relevance/configSchema.ts b/open-sse/services/compression/engines/relevance/configSchema.ts new file mode 100644 index 0000000000..37d7392dc5 --- /dev/null +++ b/open-sse/services/compression/engines/relevance/configSchema.ts @@ -0,0 +1,63 @@ +import type { EngineConfigField, EngineValidationResult } from "../types.ts"; +import type { RelevanceConfig } from "../../types.ts"; + +export const RELEVANCE_SCHEMA: EngineConfigField[] = [ + { + key: "enabled", + type: "boolean", + label: "Enabled", + defaultValue: false, + }, + { + key: "overlapThreshold", + type: "number", + label: "Overlap threshold", + description: "Sentences with Jaccard overlap below this are candidates for removal.", + defaultValue: 0.1, + min: 0, + max: 1, + }, + { + key: "budgetPercent", + type: "number", + label: "Budget percent", + description: "Target fraction of original character count to retain (0–1).", + defaultValue: 0.5, + min: 0.1, + max: 1, + }, + { + key: "boilerplateWeight", + type: "number", + label: "Boilerplate weight", + description: "Weight applied to boilerplate penalty when scoring sentences.", + defaultValue: 0.5, + min: 0, + max: 1, + }, +]; + +export function validateRelevanceConfig(config: Record): EngineValidationResult { + const errors: string[] = []; + if (config.enabled !== undefined && typeof config.enabled !== "boolean") { + errors.push("enabled must be a boolean"); + } + for (const key of ["overlapThreshold", "budgetPercent", "boilerplateWeight"]) { + if (config[key] !== undefined) { + const v = config[key]; + if (typeof v !== "number" || v < 0 || v > 1) { + errors.push(`${key} must be a number between 0 and 1`); + } + } + } + return { valid: errors.length === 0, errors }; +} + +export function resolveRelevanceConfig(stepConfig: Record): RelevanceConfig { + return { + enabled: typeof stepConfig.enabled === "boolean" ? stepConfig.enabled : false, + overlapThreshold: typeof stepConfig.overlapThreshold === "number" ? stepConfig.overlapThreshold : 0.1, + budgetPercent: typeof stepConfig.budgetPercent === "number" ? stepConfig.budgetPercent : 0.5, + boilerplateWeight: typeof stepConfig.boilerplateWeight === "number" ? stepConfig.boilerplateWeight : 0.5, + }; +} diff --git a/open-sse/services/compression/engines/relevance/index.ts b/open-sse/services/compression/engines/relevance/index.ts new file mode 100644 index 0000000000..13812d7607 --- /dev/null +++ b/open-sse/services/compression/engines/relevance/index.ts @@ -0,0 +1,185 @@ +import { createCompressionStats } from "../../stats.ts"; +import type { CompressionResult } from "../../types.ts"; +import type { CompressionEngine } from "../types.ts"; +import { RELEVANCE_SCHEMA, validateRelevanceConfig, resolveRelevanceConfig } from "./configSchema.ts"; +import { scoreSentences } from "./scorer.ts"; + +// Sentence-level "never drop" guard. We canNOT reuse ultraHeuristic's FORCE_PRESERVE_RE +// here: that token-level pattern includes `[._/\\]`, which matches the period ending EVERY +// prose sentence — gating on it would force-preserve everything and make the engine a +// permanent no-op (the same trap #17's UNIT_PRESERVE_RE avoids). Anchor on real signals: +// digits, URLs, error prefixes, code fences, stack `at`-frames, multi-segment paths, key=value. +const SENTENCE_PRESERVE_RE = + /\d|https?:\/\/|(?:Error|Exception|TypeError|RangeError|SyntaxError|ReferenceError|Traceback):|```|^\s*at\s|\/[\w.-]+\/|\w+=\S/i; + +// Split AFTER the sentence-final punctuation + its trailing whitespace, keeping that +// whitespace attached to the preceding sentence so a rejoin with "" preserves the +// original paragraph/line structure (e.g. `\n\n` between sentences survives). +const SENTENCE_SPLIT_RE = /(?<=[.!?]\s)/; + +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .map((block) => { + if (block && typeof block === "object" && "text" in block) { + return String((block as { text: unknown }).text); + } + return ""; + }) + .join(" "); + } + return ""; +} + +function splitSentences(text: string): string[] { + return text.split(SENTENCE_SPLIT_RE).filter((s) => s.trim().length > 0); +} + +function applyRelevanceToText( + text: string, + query: string, + cfg: ReturnType +): { result: string; changed: boolean } { + const sentences = splitSentences(text); + if (sentences.length <= 1) return { result: text, changed: false }; + + const scores = scoreSentences(sentences, query, cfg); + const totalChars = text.length; + const budget = Math.floor(totalChars * cfg.budgetPercent); + + const indexed = sentences.map((s, i) => ({ s, i, score: scores[i] })); + const sorted = [...indexed].sort((a, b) => b.score - a.score); + + const keepSet = new Set(); + + // 1) Force-preserved sentences (errors / code / numbers / URLs) are kept unconditionally + // and are "free" — they do NOT consume the budget, so they cannot starve the + // highest-relevance content (core-review issue: force-preserve starvation). + for (const { s, i } of indexed) { + if (SENTENCE_PRESERVE_RE.test(s)) keepSet.add(i); + } + + // 2) Greedily admit non-force sentences by score desc, gated by overlapThreshold, until + // the budget fills. The array is sorted desc, so once below budget we can break. + // (Previously the threshold was dead code behind `|| kept < budget`.) + let kept = 0; + for (const { s, i, score } of sorted) { + if (keepSet.has(i)) continue; + if (kept >= budget) break; + if (score >= cfg.overlapThreshold) { + keepSet.add(i); + kept += s.length + 1; + } + } + + // 3) Never drop everything: keep at least the single highest-scoring sentence. + if (keepSet.size === 0 && sorted.length > 0) keepSet.add(sorted[0].i); + + if (keepSet.size === sentences.length) return { result: text, changed: false }; + + const ordered = indexed.filter(({ i }) => keepSet.has(i)).sort((a, b) => a.i - b.i); + const result = ordered.map(({ s }) => s).join(""); + return { result, changed: result !== text }; +} + +export const relevanceEngine: CompressionEngine = { + id: "relevance", + name: "Relevance", + description: "Extractive sentence scoring against the last user query.", + icon: "target", + targets: ["messages"], + stackable: true, + stackPriority: 18, + metadata: { + id: "relevance", + name: "Relevance", + description: "Extractive sentence scoring against the last user query.", + inputScope: "messages", + targetLatencyMs: 2, + supportsPreview: true, + stable: true, + }, + + apply(body, options): CompressionResult { + try { + const messages = body.messages; + if (!Array.isArray(messages)) return { body, compressed: false, stats: null }; + + const cfg = resolveRelevanceConfig((options?.stepConfig as Record) ?? {}); + + let query = ""; + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] as Record; + if (msg.role === "user") { + query = extractText(msg.content).trim(); + break; + } + } + + if (!query) return { body, compressed: false, stats: null }; + + // Note: the query is a string snapshot taken BEFORE compression, so compressing the + // last user message against it is NOT circular — and that message commonly carries + // the pasted context the feature is meant to trim ("docs longos colados"). So we do + // NOT skip it (a core-review suggestion to skip the query message was rejected: it + // would no-op the dominant single-message RAG use case). + let anyChanged = false; + const newMessages = messages.map((msg) => { + const m = msg as Record; + if (m.role !== "user") return msg; + + // String content: compress in place. + if (typeof m.content === "string") { + const { result, changed } = applyRelevanceToText(m.content, query, cfg); + if (!changed) return msg; + anyChanged = true; + return { ...m, content: result }; + } + // Array/multimodal content: only safe when there is EXACTLY ONE text block. + // Joining all text blocks and stamping the result into each would duplicate and + // scramble per-block content (core-review issue: multimodal corruption). + if (Array.isArray(m.content)) { + const textBlocks = m.content.filter( + (b) => b && typeof b === "object" && "text" in b + ); + if (textBlocks.length !== 1) return msg; // 0 or ≥2 text blocks ⇒ no-op + const { result, changed } = applyRelevanceToText( + String((textBlocks[0] as { text: unknown }).text), + query, + cfg + ); + if (!changed) return msg; + anyChanged = true; + const newContent = m.content.map((block) => + block === textBlocks[0] + ? { ...(block as object), text: result } + : block + ); + return { ...m, content: newContent }; + } + return msg; + }); + + if (!anyChanged) return { body, compressed: false, stats: null }; + + const newBody = { ...body, messages: newMessages }; + const stats = createCompressionStats(body, newBody, "stacked", ["relevance-extract"]); + return { body: newBody, compressed: true, stats }; + } catch { + return { body, compressed: false, stats: null }; + } + }, + + compress(body, config) { + return this.apply(body, { stepConfig: config }); + }, + + getConfigSchema() { + return RELEVANCE_SCHEMA; + }, + + validateConfig(config) { + return validateRelevanceConfig(config); + }, +}; diff --git a/open-sse/services/compression/engines/relevance/scorer.ts b/open-sse/services/compression/engines/relevance/scorer.ts new file mode 100644 index 0000000000..5021520eb2 --- /dev/null +++ b/open-sse/services/compression/engines/relevance/scorer.ts @@ -0,0 +1,85 @@ +import type { RelevanceConfig } from "../../types.ts"; + +const BOILERPLATE_TOKENS = new Set([ + "please", + "note", + "important", + "indeed", + "certainly", + "basically", + "essentially", + "obviously", + "clearly", + "simply", + "just", + "really", + "actually", + "honestly", + "conclusion", + "summary", + "hope", + "understand", + "thing", + "things", + "something", +]); + +function tokenize(text: string): string[] { + const lower = text.toLowerCase(); + const tokens: string[] = []; + let start = -1; + for (let i = 0; i <= lower.length; i++) { + const ch = i < lower.length ? lower.charCodeAt(i) : -1; + const isAlnum = + ch !== -1 && + ((ch >= 97 && ch <= 122) || (ch >= 48 && ch <= 57)); + if (isAlnum) { + if (start === -1) start = i; + } else { + if (start !== -1) { + tokens.push(lower.slice(start, i)); + start = -1; + } + } + } + return tokens; +} + +function jaccard(setA: Set, setB: Set): number { + if (setA.size === 0 || setB.size === 0) return 0; + let intersection = 0; + for (const t of setA) { + if (setB.has(t)) intersection++; + } + const union = setA.size + setB.size - intersection; + return union === 0 ? 0 : intersection / union; +} + +function boilerplateScore(tokens: string[]): number { + if (tokens.length === 0) return 0; + let count = 0; + for (const t of tokens) { + if (BOILERPLATE_TOKENS.has(t)) count++; + } + return count / tokens.length; +} + +export function scoreSentences( + sentences: string[], + query: string, + cfg: RelevanceConfig +): number[] { + if (sentences.length === 0) return []; + if (!query || query.trim().length === 0) return sentences.map(() => 0); + + const queryTokens = new Set(tokenize(query)); + + return sentences.map((sentence) => { + const sentTokens = tokenize(sentence); + if (sentTokens.length === 0) return 0; + const sentSet = new Set(sentTokens); + const overlap = jaccard(sentSet, queryTokens); + const boilerplate = boilerplateScore(sentTokens) * cfg.boilerplateWeight; + return Math.max(0, overlap - boilerplate); + }); +} diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 7f903fee45..755d9256f1 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -110,6 +110,13 @@ export interface RtkConfig { renderers?: string[]; } +export interface RelevanceConfig { + enabled: boolean; + overlapThreshold: number; + budgetPercent: number; + boilerplateWeight: number; +} + export interface CompressionLanguageConfig { enabled: boolean; defaultLanguage: string; @@ -160,6 +167,7 @@ export interface CompressionConfig { /** Phase 4A: selected output styles (supersedes cavemanOutputMode via a back-compat shim). */ outputStyles?: OutputStyleSelectionEntry[]; rtkConfig?: RtkConfig; + relevanceConfig?: RelevanceConfig; languageConfig?: CompressionLanguageConfig; aggressive?: AggressiveConfig; ultra?: UltraConfig; diff --git a/tests/unit/compression/relevance-engine.test.ts b/tests/unit/compression/relevance-engine.test.ts new file mode 100644 index 0000000000..8b1d2e6bb0 --- /dev/null +++ b/tests/unit/compression/relevance-engine.test.ts @@ -0,0 +1,361 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { relevanceEngine } from "../../../open-sse/services/compression/engines/relevance/index.ts"; + +function makeBody(userContent: string, priorContent?: string): Record { + const messages: Array<{ role: string; content: string }> = []; + if (priorContent) { + messages.push({ role: "assistant", content: priorContent }); + } + messages.push({ role: "user", content: userContent }); + return { messages }; +} + +const LONG_CONTENT = + "Please note that I want to say something. " + + "The database connection requires a host parameter and a port number. " + + "Indeed it is very important to understand this. " + + "The port defaults to 5432 for PostgreSQL. " + + "In conclusion I hope this helps you."; + +test("apply keeps relevant sentences and drops irrelevant prose", () => { + const body = { + messages: [ + { role: "user", content: "How do I configure the PostgreSQL database connection?" }, + { role: "assistant", content: LONG_CONTENT }, + { + role: "user", + content: + "What is the port? " + + "Also tell me about host parameters. " + + "Unrelated: what color is the sky? " + + "PostgreSQL port is 5432. " + + "The sky is blue on a clear day. " + + "Connection requires host and port settings.", + }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.5, overlapThreshold: 0.05 }, + }); + assert.equal(result.compressed, true); + const messages = result.body.messages as Array<{ content: string }>; + const lastContent = messages[messages.length - 1].content; + assert.match(lastContent, /port/i); +}); + +test("no-op when there is no user message in messages", () => { + const body = { + messages: [{ role: "assistant", content: "Some long assistant reply here with many words." }], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.5 }, + }); + assert.equal(result.compressed, false); + assert.deepEqual(result.body, body); +}); + +test("no-op when last user message has only one sentence", () => { + const body = { + messages: [ + { role: "user", content: "Just one sentence here." }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.5 }, + }); + assert.equal(result.compressed, false); +}); + +test("fail-open on malformed input — returns original body", () => { + const body = { messages: "not an array" }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true }, + }); + assert.equal(result.compressed, false); + assert.deepEqual(result.body, body); +}); + +test("determinism: same input produces same output", () => { + const body = { + messages: [ + { role: "user", content: "How does the retry mechanism work for failed requests?" }, + { + role: "user", + content: + "The retry mechanism triggers after a timeout. " + + "Exponential backoff is applied between retries. " + + "Please note this is very important. " + + "The maximum retry count is configurable. " + + "Indeed this is something to consider carefully.", + }, + ], + }; + const opts = { stepConfig: { enabled: true, budgetPercent: 0.5, overlapThreshold: 0.05 } }; + const r1 = relevanceEngine.apply(body, opts); + const r2 = relevanceEngine.apply(body, opts); + assert.deepEqual(r1.body, r2.body); + assert.equal(r1.compressed, r2.compressed); +}); + +test("sentences matching FORCE_PRESERVE_RE are never dropped", () => { + const body = { + messages: [ + { role: "user", content: "What happened?" }, + { + role: "user", + content: + "This sentence is completely unrelated to the query. " + + "Error: connection refused at port 5432. " + + "Another unrelated sentence about random things. " + + "Yet another irrelevant sentence with no matching tokens.", + }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.3, overlapThreshold: 0.0 }, + }); + if (result.compressed) { + const messages = result.body.messages as Array<{ content: string }>; + const lastContent = messages[messages.length - 1].content; + assert.match(lastContent, /Error: connection refused/); + } +}); + +test("techniquesUsed contains relevance-extract when compression occurred", () => { + const body = { + messages: [ + { role: "user", content: "Explain database indexing." }, + { + role: "user", + content: + "Database indexes speed up queries on large tables. " + + "Unrelated random words about nothing important here. " + + "Indexes are created with CREATE INDEX in SQL. " + + "Please note that this sentence is filler content only.", + }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.5, overlapThreshold: 0.05 }, + }); + if (result.compressed && result.stats) { + assert.ok( + result.stats.techniquesUsed.includes("relevance-extract"), + `expected relevance-extract in techniquesUsed, got: ${result.stats.techniquesUsed}` + ); + } +}); + +test("preserves original sentence order after greedy selection", () => { + const body = { + messages: [ + { role: "user", content: "Tell me about cats and dogs." }, + { + role: "user", + content: + "Cats are independent animals. " + + "Completely irrelevant filler sentence here today. " + + "Dogs are loyal companions. " + + "Another filler sentence with random content. " + + "Cats and dogs are popular pets worldwide.", + }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.6, overlapThreshold: 0.05 }, + }); + if (result.compressed) { + const messages = result.body.messages as Array<{ content: string }>; + const lastContent = messages[messages.length - 1].content; + const catIdx = lastContent.indexOf("Cats are independent"); + const dogIdx = lastContent.indexOf("Dogs are loyal"); + if (catIdx !== -1 && dogIdx !== -1) { + assert.ok(catIdx < dogIdx, "original order (cats before dogs) should be preserved"); + } + } +}); + +test("array content (multimodal) is handled without crash", () => { + const body = { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What is the database host?" }, + ], + }, + ], + }; + assert.doesNotThrow(() => { + relevanceEngine.apply(body, { stepConfig: { enabled: true } }); + }); +}); + +test("engine metadata is correct", () => { + assert.equal(relevanceEngine.id, "relevance"); + assert.equal(relevanceEngine.stackPriority, 18); + assert.ok(Array.isArray(relevanceEngine.targets)); + assert.ok(relevanceEngine.getConfigSchema().length > 0); +}); + +// ── Issue 1: overlapThreshold must actually drop zero-overlap sentences ────── +test("zero-overlap sentences below overlapThreshold are dropped even with budget room", () => { + // Context user message with 5 relevant + 5 zero-overlap sentences. Generous + // budget (0.9) so budget alone would keep everything; overlapThreshold must + // still drop the zero-overlap ones. + const relevant = "alpha beta gamma delta epsilon configuration database query."; + const zero = "zzqqxx vvbbnn mmllkk poiuyt qwerty."; + const context = + `${relevant} ${zero} ${relevant} ${zero} ${relevant} ` + + `${zero} ${relevant} ${zero} ${relevant} ${zero}`; + const body = { + messages: [ + { role: "user", content: context }, + { role: "user", content: "alpha beta gamma delta epsilon configuration database query" }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.9, overlapThreshold: 0.1 }, + }); + assert.equal(result.compressed, true); + const messages = result.body.messages as Array<{ content: string }>; + // The dropped message is the CONTEXT (index 0), not the query (Issue 5). + const compressed = messages[0].content; + assert.ok( + !compressed.includes("zzqqxx"), + `zero-overlap sentence should be dropped, got: ${compressed}` + ); + assert.ok(compressed.includes("alpha beta gamma"), "relevant sentence should survive"); +}); + +// ── Issue 2: multimodal with multiple text blocks must not be corrupted ────── +test("multimodal content with multiple text blocks is returned unchanged", () => { + const body = { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Sentence A about cats. Another A sentence here." }, + { type: "image_url", image_url: { url: "data:image/png;base64,xxx" } }, + { type: "text", text: "Sentence B about dogs. Another B sentence here." }, + ], + }, + { role: "user", content: "cats dogs animals query text" }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.2, overlapThreshold: 0.0 }, + }); + const messages = result.body.messages as Array<{ + content: Array<{ type: string; text?: string }>; + }>; + const blocks = messages[0].content; + // Neither text block may be overwritten with the other's (joined) content. + assert.equal(blocks[0].text, "Sentence A about cats. Another A sentence here."); + assert.equal(blocks[2].text, "Sentence B about dogs. Another B sentence here."); +}); + +test("multimodal content with a single text block compresses that block in place", () => { + const body = { + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: + "The database host is configured in settings. " + + "Completely unrelated filler about random topics here. " + + "The database port defaults to five four three two.", + }, + ], + }, + { role: "user", content: "database host port configuration settings" }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.6, overlapThreshold: 0.05 }, + }); + if (result.compressed) { + const messages = result.body.messages as Array<{ + content: Array<{ type: string; text?: string }>; + }>; + const text = messages[0].content[0].text ?? ""; + assert.match(text, /database/i); + } +}); + +// ── Issue 3: force-preserved content must not starve high-relevance sentences ─ +test("force-preserved sentences are free and do not starve top-relevance sentence", () => { + // Many force-preserved sentences (contain digits/Error: → FORCE_PRESERVE_RE) + // would fill a tiny budget; the single highest-relevance non-force sentence + // must still be kept. + const forced = + "Error: code 100. Error: code 200. Error: code 300. Error: code 400. Error: code 500."; + const relevant = "The quantum entanglement relevance signal token here."; + const filler = "Totally unrelated padding sentence with nothing useful."; + const context = `${forced} ${relevant} ${filler}`; + const body = { + messages: [ + { role: "user", content: context }, + { role: "user", content: "quantum entanglement relevance signal token" }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.05, overlapThreshold: 0.01 }, + }); + assert.equal(result.compressed, true); + const messages = result.body.messages as Array<{ content: string }>; + const compressed = messages[0].content; + assert.ok( + compressed.includes("quantum entanglement relevance signal"), + `top-relevance sentence must survive despite forced content, got: ${compressed}` + ); + // Forced content also survives. + assert.ok(compressed.includes("Error: code 100")); +}); + +// ── Issue 4: whitespace / paragraph breaks must be preserved ───────────────── +test("paragraph breaks (double newline) survive between kept sentences", () => { + const context = + "The database connection requires a host parameter.\n\n" + + "Unrelated filler sentence about random nothing.\n\n" + + "The connection also requires a port number setting."; + const body = { + messages: [ + { role: "user", content: context }, + { role: "user", content: "database connection host port setting" }, + ], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.7, overlapThreshold: 0.05 }, + }); + if (result.compressed) { + const messages = result.body.messages as Array<{ content: string }>; + const compressed = messages[0].content; + assert.match(compressed, /\n\n/, `expected paragraph break to survive, got: ${compressed}`); + } +}); + +// ── Issue 5 (REJECTED after review): we deliberately do NOT skip the query message. +// The query is a string snapshot taken before compression, so compressing the last user +// message against it is not circular; and that message commonly carries the pasted context +// the engine is meant to trim ("docs longos colados"). When the query IS the whole message, +// every sentence overlaps it fully → high score → kept → a natural no-op, so no special-case +// is needed. This test asserts the last user message is NOT force-skipped: a context-only +// last message with a distinct prior query message is still eligible for compression. +test("the last/only user message is NOT special-cased / skipped (eligible for compression)", () => { + // A single user message (it IS the query). With a tight budget it must still be processed + // and trimmed — proving there is no index-based early-skip of the query message. + const sentence = "alpha beta gamma delta epsilon configuration database query parameters here."; + const body = { + messages: [{ role: "user", content: Array(8).fill(sentence).join(" ") }], + }; + const result = relevanceEngine.apply(body, { + stepConfig: { enabled: true, budgetPercent: 0.3, overlapThreshold: 0.0 }, + }); + assert.equal(result.compressed, true, "the last/only user message must be eligible for compression"); + const out = (result.body.messages as Array<{ content: string }>)[0].content; + assert.ok(out.length < Array(8).fill(sentence).join(" ").length, "tight budget trims the message"); +}); diff --git a/tests/unit/compression/relevance-scorer.test.ts b/tests/unit/compression/relevance-scorer.test.ts new file mode 100644 index 0000000000..5c50e55ac9 --- /dev/null +++ b/tests/unit/compression/relevance-scorer.test.ts @@ -0,0 +1,70 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { scoreSentences } from "../../../open-sse/services/compression/engines/relevance/scorer.ts"; + +const DEFAULT_CFG = { + enabled: false, + overlapThreshold: 0.1, + budgetPercent: 0.5, + boilerplateWeight: 0.5, +}; + +test("relevant sentence scores higher than irrelevant sentence", () => { + const sentences = [ + "The quick brown fox jumps over the lazy dog", + "How do I configure the database connection?", + ]; + const query = "configure database connection settings"; + const scores = scoreSentences(sentences, query, DEFAULT_CFG); + assert.equal(scores.length, 2); + assert.ok(scores[1] > scores[0], `expected scores[1]=${scores[1]} > scores[0]=${scores[0]}`); +}); + +test("boilerplate sentences are penalized relative to content sentences", () => { + const sentences = [ + "Please note that this is very important information.", + "Use db.connect() with host and port parameters.", + ]; + const query = "connect database host port"; + const scores = scoreSentences(sentences, query, DEFAULT_CFG); + assert.equal(scores.length, 2); + assert.ok( + scores[1] > scores[0], + `content sentence (${scores[1]}) should score higher than boilerplate (${scores[0]})` + ); +}); + +test("ReDoS-safe: query with special regex characters does not throw", () => { + const sentences = ["Some normal sentence here.", "Another sentence with content."]; + const maliciousQuery = "((a+)+)$ [.*+?^=!:${}()|[\\]/\\\\] test+++"; + assert.doesNotThrow(() => { + const scores = scoreSentences(sentences, maliciousQuery, DEFAULT_CFG); + assert.equal(scores.length, 2); + }); +}); + +test("empty query returns array of zeros with same length as sentences", () => { + const sentences = ["First sentence.", "Second sentence.", "Third sentence."]; + const scores = scoreSentences(sentences, "", DEFAULT_CFG); + assert.equal(scores.length, 3); + assert.ok(scores.every((s) => s === 0), `all scores should be 0, got: ${scores}`); +}); + +test("empty sentences array returns empty array", () => { + const scores = scoreSentences([], "some query", DEFAULT_CFG); + assert.deepEqual(scores, []); +}); + +test("single sentence returns array of length 1", () => { + const scores = scoreSentences(["Only one sentence."], "query text", DEFAULT_CFG); + assert.equal(scores.length, 1); + assert.ok(typeof scores[0] === "number"); +}); + +test("identical query and sentence tokens produce high score", () => { + const sentences = ["configure database connection", "unrelated random words"]; + const query = "configure database connection"; + const scores = scoreSentences(sentences, query, DEFAULT_CFG); + assert.ok(scores[0] > 0.5, `exact match should score above 0.5, got ${scores[0]}`); + assert.ok(scores[0] > scores[1]); +});