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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user