mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* 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<budget made it unreachable) → real threshold gate - multimodal: only compress when exactly one text block (was stamping joined text into every block) - force-preserve sentences are now 'free' (don't consume budget) so they can't starve top-relevance - preserve inter-sentence whitespace (\n\n survives) instead of flattening to single spaces - ROOT-CAUSE: stop gating preserve on ultraHeuristic FORCE_PRESERVE_RE — it matches the period ending every sentence, so it force-preserved everything (no-op). New SENTENCE_PRESERVE_RE anchors on real signals (digits/URL/Error:/code/at-frame/path/key=value), mirroring #17's UNIT_PRESERVE_RE - core-review suggestion to skip the query message: rejected (self-overlap already protects it; skipping would no-op the single-message RAG case) — documented + test updated * docs(changelog): restore relevance engine bullet (#7, eaten by rebase)
This commit is contained in:
committed by
GitHub
parent
fa94f38de5
commit
21c1f540b3
@@ -14,6 +14,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(compression): relevance extractive engine** — a new opt-in compression engine that scores each sentence by term-overlap (Jaccard) with the user's last query minus a length/boilerplate penalty, greedily keeps the most relevant within a budget, and reconstructs the original order. Pure-string, deterministic, ReDoS-safe (char-code tokenization, no `RegExp` over user input), fail-open, default off. Ideal for trimming long pasted RAG context / tool output to what's relevant. Sentences carrying real signal (digits/URLs/errors/code/paths) are never dropped; `overlapThreshold`/`budgetPercent`/`boilerplateWeight` are configurable. Tier-2 item of the compression feature-extraction roadmap (#7).
|
||||
- **feat(compression): hard-budget mode — compress to ≤ N tokens** — a deterministic post-pass (`targetTokens` / `targetRatio`, default unset → no-op) that trims a body to a token budget. It ranks sentences/lines by average `scoreToken` ascending and drops the lowest-saliency ones until the body fits (measured by the exact cl100k `countTextTokens`), preserving original order. Lines carrying real signal (digits, URLs, `Error:`-family, code fences, stack `at`-frames, multi-segment paths, `key=value`) are never dropped; the budget is distributed proportionally across messages so the total stays ≤ target; an unreachable target (all-preserved) surfaces a `validationWarnings` note instead of failing silently. Does NOT touch the `estimateCompressionTokens` budget-gate estimator. Tier-3 item of the compression feature-extraction roadmap (#17).
|
||||
- **feat(compression): result memoization for deterministic engines (opt-in)** — caches `(input, config) → result` for provably pure, stateless modes (`lite`/`standard`/`rtk` and stacked pipelines of `{lite,caveman,rtk}`) to skip recompute on the hot path. Opt-in via `memoizeCompressionResults` (default off → zero behavior change). Conservative opt-in whitelist (stateful `ccr`/`session-dedup` — which write the cross-request CCR store — and model-backed `ultra`/`aggressive`/`llmlingua` are never cached), principal-scoped (skipped without a principal, so no cross-principal body leak), and clone-on-store + clone-on-read. Tier-3 item of the compression feature-extraction roadmap (#21).
|
||||
- **feat(compression): inline transparency annotation** — surfaces `tokens=847→312; rules: filler×8, dedup×2` derived from existing compression stats. The `X-OmniRoute-Compression` response header is extended **append-only** (the `mode; source=X` prefix stays byte-identical, so existing header parsers don't break) and the compression studio cockpit shows a matching badge. Zero new computation — it aggregates the `rulesApplied`/`techniquesUsed` already on the stats. Tier-3 item of the compression feature-extraction roadmap (#18).
|
||||
|
||||
@@ -44,6 +44,13 @@ export const ENGINE_CATALOG: Record<string, EngineMeta> = {
|
||||
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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string, unknown>): 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<string, unknown>): 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,
|
||||
};
|
||||
}
|
||||
185
open-sse/services/compression/engines/relevance/index.ts
Normal file
185
open-sse/services/compression/engines/relevance/index.ts
Normal file
@@ -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<typeof resolveRelevanceConfig>
|
||||
): { 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<number>();
|
||||
|
||||
// 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<string, unknown>) ?? {});
|
||||
|
||||
let query = "";
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i] as Record<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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);
|
||||
},
|
||||
};
|
||||
85
open-sse/services/compression/engines/relevance/scorer.ts
Normal file
85
open-sse/services/compression/engines/relevance/scorer.ts
Normal file
@@ -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<string>, setB: Set<string>): 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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
361
tests/unit/compression/relevance-engine.test.ts
Normal file
361
tests/unit/compression/relevance-engine.test.ts
Normal file
@@ -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<string, unknown> {
|
||||
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");
|
||||
});
|
||||
70
tests/unit/compression/relevance-scorer.test.ts
Normal file
70
tests/unit/compression/relevance-scorer.test.ts
Normal file
@@ -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]);
|
||||
});
|
||||
Reference in New Issue
Block a user