LibreChat/scripts/activity-labels/checks.js
Danny Avila a07c0e4ae8
🧪 chore: Add the Activity-Label Prose Eval Harness (#14527)
Grades fast-model activity-label headers against a fixed corpus so
instruction changes are measured rather than eyeballed on one
conversation. This existed untracked while the continuity work was
developed; committing it because it is the only reproducible record of
WHY `ACTIVITY_INSTRUCTION` is ordered and capped the way it is.

- captured.json: 9 real production payloads pulled verbatim from Langfuse
  with the headers that shipped. Irreplaceable — traces age out.
- corpus.js: 17 cases / 28 steps. The captured run replays as one
  sequence, plus synthetic cases for the modes it never exercised
  (all-failed, partial, parallel batches, rapid near-duplicates, entry
  overflow, truncated output, error-shaped success). Multi-step cases
  chain each generated label into the next step's context, which is what
  makes cross-batch redundancy measurable at all.
- prompt.js: faithful port of the SDK's buildActivityLabelPrompt so
  synthetic cases render the bytes production sends, plus a
  previousLabelCap knob for continuity-window experiments.
- variants.js: single-factor instruction variants. The baseline is read
  from the BUILT package (workspace resolution, then dist, then
  LABEL_EVAL_DIST) so a variant can never be graded against a stale copy
  of the shipped instruction.
- checks.js: length/punctuation/markdown/tool-echo/count-echo, plus
  overlap split into `restate` (adds nothing over an earlier header) vs
  `template` (same frame, new payload — often fine).
- run.js / rescore.js: live runner on the production wire shape
  (max_tokens 256) and an offline re-grader, so metric fixes never
  require re-spending on the API.

Results are gitignored — regenerable, and 292K of the 364K. A full sweep
is ~$0.03 per variant and ~45s.

Findings are recorded in the README, two of them counter-intuitive:
enumerating acceptable opening verbs ANCHORED the model rather than
diversifying it (Confirmed 18→23, opener diversity halved), and diverse
examples alone changed nothing. Sentence order is load-bearing, so a
tidying reshuffle of ACTIVITY_INSTRUCTION regresses real output.
2026-07-30 09:22:34 -04:00

156 lines
4.4 KiB
JavaScript

/**
* Mechanical label checks. These catch format violations and the two
* measurable prose failures (register collapse via first-word tallies,
* cross-batch redundancy via content-word overlap); commit-log READABILITY
* still needs the human pass over results/latest.md.
*/
const STOPWORDS = new Set([
'the',
'a',
'an',
'to',
'of',
'and',
'or',
'with',
'for',
'in',
'on',
'at',
'is',
'are',
'was',
'were',
'be',
'been',
'it',
'its',
'as',
'by',
'from',
'that',
'this',
'both',
'all',
'no',
'not',
'via',
]);
function words(label) {
return label.trim().split(/\s+/).filter(Boolean);
}
const SUFFIXES = ['ations', 'ation', 'ence', 'ance', 'ings', 'ing', 'ed', 'es', 's'];
/** Crude suffix stemmer so persists/persistence/persisted collide — enough
* for overlap detection; linguistic correctness is not the goal. */
function stem(word) {
if (word.length < 5) {
return word;
}
for (const suffix of SUFFIXES) {
if (word.endsWith(suffix) && word.length - suffix.length >= 4) {
return word.slice(0, word.length - suffix.length);
}
}
return word;
}
function contentWords(label) {
return words(label.toLowerCase().replace(/[^a-z0-9/._-]+/g, ' '))
.filter((word) => !STOPWORDS.has(word))
.map(stem);
}
/** Payload tokens carry the informative delta between template-shaped
* labels: numbers, versions, paths, filenames. */
function isPayload(word) {
return /\d/.test(word) || word.includes('/') || word.includes('.');
}
function jaccard(a, b) {
const setA = new Set(a);
const setB = new Set(b);
if (setA.size === 0 || setB.size === 0) {
return 0;
}
let intersection = 0;
for (const word of setA) {
if (setB.has(word)) {
intersection += 1;
}
}
return intersection / (setA.size + setB.size - intersection);
}
const GENERIC_OPENER = /^(ran|used|executed|called|invoked|performed)\b/i;
const COUNT_ECHO = /\b\d+\s+(tools?|commands?|calls?)\b/i;
const DUP_THRESHOLD = 0.5;
/**
* @param label generated label text
* @param entries the batch's tool entries (for tool-name echo detection)
* @param previousLabels labels generated EARLIER in the same case chain,
* regardless of whether the variant saw them — redundancy is measured
* uniformly so continuity variants can be compared against blind ones.
*/
function checkLabel(label, { entries = [], previousLabels = [] } = {}) {
const flags = [];
const wordList = words(label);
if (wordList.length < 4 || wordList.length > 9) {
flags.push(`len:${wordList.length}`);
}
if (/[.!?,;:]$/.test(label.trim())) {
flags.push('punct');
}
if (/^["'`]|["'`]$/.test(label.trim())) {
flags.push('quote');
}
if (/[*`]|^#|\[.*\]\(/.test(label)) {
flags.push('md');
}
if (GENERIC_OPENER.test(label.trim())) {
flags.push('opener');
}
const lower = label.toLowerCase();
for (const entry of entries) {
const name = String(entry.toolName ?? '').toLowerCase();
if (name.length > 3 && (lower.includes(name) || lower.includes(name.replace(/_/g, ' ')))) {
flags.push(`tool-echo:${entry.toolName}`);
break;
}
}
if (COUNT_ECHO.test(label)) {
flags.push('count-echo');
}
/** Overlap splits into two flags: `restate` (high overlap, no payload
* delta — the line adds nothing over a previous header; the production
* 2/3 and 7/8 failure) and `template` (high overlap but the differing
* tokens are numbers/paths — same sentence frame, new information, e.g.
* fib(1)→fib(2). Often fine, arguably better than synonym churn). */
const own = contentWords(label);
let maxOverlap = 0;
let worst = null;
for (const previous of previousLabels) {
const other = contentWords(previous);
const overlap = jaccard(own, other);
if (overlap > maxOverlap) {
maxOverlap = overlap;
worst = other;
}
}
if (maxOverlap > DUP_THRESHOLD && worst != null) {
const otherSet = new Set(worst);
const ownSet = new Set(own);
const differing = [
...own.filter((word) => !otherSet.has(word)),
...worst.filter((word) => !ownSet.has(word)),
];
const informativeDelta = differing.some(isPayload);
flags.push(`${informativeDelta ? 'template' : 'restate'}:${maxOverlap.toFixed(2)}`);
}
return { flags, wordCount: wordList.length, firstWord: wordList[0] ?? '', maxOverlap };
}
module.exports = { checkLabel, contentWords, jaccard, words, stem };