Keep a record of what was asked for and what it was taken to mean

A term that resolved the same way three times stops being an inference and becomes a lookup;
the corrections are the rows worth having, since a term that meant two things is still a
guess and is never promoted.
This commit is contained in:
Gmer4Lfe
2026-08-09 20:23:49 -04:00
parent 6f5c81c9a2
commit 801288c88c
+119
View File
@@ -808,6 +808,125 @@ function vv_ai_triage_log(array $lines, string $sourceLog = ''): array {
return array_values($found);
}
// ── The phrasebook ───────────────────────────────────────────────────────────────────────────
// What the operator said, what it was taken to mean, and whether that was right.
//
// The point is not to fine-tune anything. It is that "daily", said three times and meaning
// daily_sync_maintenance.sh all three, stops being an inference and becomes a lookup. This file
// is how a term earns that promotion, and everything it promotes is exact — the model keeps the
// language, the resolution stays deterministic. Same division as the rest of this subsystem.
//
// The corrections are the rows that matter. A resolution that was right confirms what was
// already believed; a resolution that was wrong, with what it should have been, is the only
// record of a mistake that would otherwise be repeated indefinitely.
//
// JSON Lines rather than the pipe-delimited shape the token ledger uses: that file holds numbers
// and a hostname, this one holds whatever the operator typed, and a delimiter that occurs in the
// data is not a delimiter. Append-only, one object per line, so a truncated write costs the last
// row rather than the corpus.
function vv_ai_phrasebook_path(): string {
return AI_DATA_DIR . '/ai_phrasebook.jsonl';
}
// $said what the operator actually wrote, verbatim but redacted
// $term the fragment that carried the meaning — "daily", "critical rsync", "emby key"
// $target what it resolved to: a conf key, a script id, an array name
// $kind conf_key | script | array | section | unknown
// $outcome accepted | corrected | rejected
// $correctedTo what it should have been, when the resolution was wrong
//
// $target and $correctedTo must be a canonical identifier — CONF_BACKUP_DIR, not
// "CONF_BACKUP_DIR=${DATA_DIR}/Backups/Confs" and not "the backups directory". Promotion works
// by counting how often a term resolved to the same thing, so a target described three different
// ways is three meanings, and the term never promotes. Learned the hard way while seeding this
// with real corrections: the same fix, written up three ways, taught nothing.
function vv_ai_phrase_record(string $said, string $term, string $target, string $kind,
string $outcome = 'accepted', string $correctedTo = ''): bool {
$said = trim($said);
$term = strtolower(trim($term));
if ($said === '' || $term === '') return false;
if (!in_array($outcome, ['accepted', 'corrected', 'rejected'], true)) return false;
if (!is_dir(AI_DATA_DIR)) @mkdir(AI_DATA_DIR, 0755, true);
// An operator asking to set a credential types the credential. This file is long-lived and
// read back for years; it is the last place a key should be preserved verbatim.
$row = [
'ts' => time(),
'said' => mb_substr(vv_ai_redact($said), 0, 500),
'term' => mb_substr($term, 0, 80),
'target' => mb_substr(trim($target), 0, 160),
'kind' => $kind,
'outcome' => $outcome,
];
if ($correctedTo !== '') $row['corrected_to'] = mb_substr(vv_ai_redact($correctedTo), 0, 160);
return @file_put_contents(vv_ai_phrasebook_path(),
json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n",
FILE_APPEND | LOCK_EX) !== false;
}
function vv_ai_phrase_all(): array {
$out = [];
foreach ((array)@file(vv_ai_phrasebook_path(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$r = json_decode($line, true);
if (is_array($r) && isset($r['term'])) $out[] = $r;
}
return $out;
}
// Terms that have earned a deterministic mapping: seen at least $minSeen times, resolving to one
// target every time, and never corrected away from it.
//
// A single contradiction disqualifies the term outright rather than going with the majority. The
// whole value of promoting a term is that it stops being a guess — a term that meant two things
// is still a guess, and a confident wrong alias is worse than no alias, because nothing downstream
// will question it.
function vv_ai_phrase_aliases(int $minSeen = 3): array {
$seen = [];
foreach (vv_ai_phrase_all() as $r) {
$term = (string)$r['term'];
// A correction records both the wrong reading and the right one. The right one is what
// the term means; the wrong one is what it must never be promoted to again.
$target = ($r['outcome'] === 'corrected' && !empty($r['corrected_to']))
? (string)$r['corrected_to'] : (string)$r['target'];
if ($r['outcome'] === 'rejected' || $target === '') continue;
$seen[$term]['targets'][$target] = ($seen[$term]['targets'][$target] ?? 0) + 1;
$seen[$term]['kind'] = (string)($r['kind'] ?? 'unknown');
if ($r['outcome'] === 'corrected') $seen[$term]['wrong'][(string)$r['target']] = true;
}
$aliases = [];
foreach ($seen as $term => $d) {
if (count($d['targets']) !== 1) continue; // meant two things — still a guess
$target = array_key_first($d['targets']);
if (isset($d['wrong'][$target])) continue; // was itself corrected away from
if ($d['targets'][$target] < $minSeen) continue;
$aliases[$term] = ['target' => $target, 'kind' => $d['kind'], 'seen' => $d['targets'][$target]];
}
return $aliases;
}
// Exact alias hit for a term, or null. Deliberately not fuzzy: an alias exists precisely so that
// this lookup is certain, and a near-match would reintroduce the guessing it replaced.
function vv_ai_phrase_lookup(string $term, int $minSeen = 3): ?array {
return vv_ai_phrase_aliases($minSeen)[strtolower(trim($term))] ?? null;
}
// Terms that have been corrected and have not yet earned promotion — what the assistant is still
// getting wrong, and the thing worth reading when asking why it keeps mistaking something.
function vv_ai_phrase_unsettled(): array {
$out = [];
foreach (vv_ai_phrase_all() as $r) {
if (($r['outcome'] ?? '') !== 'corrected') continue;
$out[(string)$r['term']][] = ['said' => $r['said'], 'took_it_as' => $r['target'],
'meant' => $r['corrected_to'] ?? '', 'ts' => $r['ts'] ?? 0];
}
return $out;
}
// ── What the arrs say about themselves ───────────────────────────────────────────────────────
// Sonarr, Radarr and Lidarr each publish a health endpoint listing what they believe is wrong,
// already structured and already graded. No log parsing, no pattern that goes stale when a