From d9225d06d991c9df31ff4b1272f8d38df5837021 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sun, 9 Aug 2026 00:36:13 -0400 Subject: [PATCH] Route a General Chat question to the profile that fits it --- AI/README-AI.md | 29 ++++++++++++- Plugin/unraid/Tools/ai_chat_worker.php | 45 +++++++++++++-------- Plugin/unraid/Tools/ai_explain_fixtures.txt | 21 ++++++++++ Plugin/unraid/include/ai.php | 45 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 18 deletions(-) diff --git a/AI/README-AI.md b/AI/README-AI.md index 2b2c6cc..d12c41e 100644 --- a/AI/README-AI.md +++ b/AI/README-AI.md @@ -247,8 +247,33 @@ incidents, conf lookup, bug filing, code scanning. `chat` holding an empty list not an oversight: anything added to it stops being general chat and becomes an assistant that sometimes invents claims about this installation. -`chat` escalates to `varaverk` on its own when a question is genuinely about Varaverk, and -reverts if the index turns out to have nothing — so the loose profile is safe to sit in. +### Routing out of General Chat + +`chat` hands a question to whichever profile fits, decided by `vv_ai_route_from_chat()`. Ordered +most specific first, because these overlap on purpose: + +| Question | Goes to | Why | +|---|---|---| +| "write me a script that prunes logs" | `code` | asked for something written | +| "why did the daily orch fail" | `troubleshoot` | diagnostic phrasing **and** something here to diagnose | +| "how did the daily orch go" | `varaverk` | about this install, but not a fault | +| "what does arr_sync.sh do" | `varaverk` | names a script, wants documentation | +| "why is the sky blue" | stays `chat` | diagnostic phrasing about nothing here | + +`code` is checked first because it is the only intent about a thing that does not exist yet, so +nothing else can claim it — and it is anchored on the verb, which is what keeps "write me a +script" apart from "what does this script do". Escalation adds capability, so a wrong escalation +costs more than a missed one: anything unrecognised stays in `chat`, the profile that cannot +invent claims about this system. The worker reverts to `chat` anyway if retrieval comes back +empty. + +The answer opens with one line naming the profile that took it, because the button still shows +the one you picked and an answer arriving under a different contract otherwise reads as the +assistant ignoring you. + +Routing is asserted by `Plugin/unraid/Tools/ai_explain_check.sh` against +`ai_explain_fixtures.txt` — every case runs through the worker's `--explain` mode, which stops +where deterministic assembly ends and never calls the model. This used to live in five places — history depth in the endpoint, capabilities in `include/ai.php`, label and depth again in JavaScript, a prompt branch in the worker, and a label map on the diff --git a/Plugin/unraid/Tools/ai_chat_worker.php b/Plugin/unraid/Tools/ai_chat_worker.php index ba24b1b..8498350 100644 --- a/Plugin/unraid/Tools/ai_chat_worker.php +++ b/Plugin/unraid/Tools/ai_chat_worker.php @@ -151,11 +151,19 @@ $profileAsked = $profile; // returns it to where it started rather than choosing a profile for it. // // The trigger is the same detector the deterministic backstop uses, so the two cannot disagree. +// The target is chosen by vv_ai_route_from_chat(), not fixed at varaverk. General Chat used to +// have exactly one place to escalate to, which meant "why did the daily orch fail" and "write me +// a script that prunes logs" both arrived at the documentation assistant — the first wanting a +// log it is not given by default, the second wanting code from a profile whose contract is to +// answer only from passages. Both were answered adequately and neither was answered well. $escalated = false; -if ($profile === 'chat' && vv_ai_chat_needs_varaverk($question)) { - $profile = 'varaverk'; - $escalated = true; - wlog('handoff chat -> varaverk: ' . mb_substr($question, 0, 80)); +if ($profile === 'chat') { + $to = vv_ai_route_from_chat($question); + if ($to !== '' && $to !== $profile) { + $profile = $to; + $escalated = true; + wlog('handoff chat -> ' . $to . ': ' . mb_substr($question, 0, 80)); + } } // Every "is this profile allowed X" question in this file goes through here. Bound by reference @@ -279,11 +287,10 @@ $rec = ($runTarget !== '' && vv_ai_scope_ok($runTarget)) // no record at all — is not clean and is treated as worth investigating. $runClean = $rec['ok'] && $rec['status'] === 'ok' && $rec['exit'] === 0 && $rec['end'] !== null; -$kwDiagnostic = (bool)preg_match( - '/\b(why|fail(ed|ing|ure)?|error|broken?|not work|isn.t work|wrong|stuck|hang|' - . 'never runs?|didn.t|won.t|debug|troubleshoot|diagnos)/i', - $question -); +// The same detector the router uses to send a question here in the first place. Two copies of +// this regex would mean a question could be routed to Troubleshoot as diagnostic and then have +// live state withheld from it as not-diagnostic, which is the worst of both. +$kwDiagnostic = vv_ai_is_diagnostic($question); // Permission first, need second: the capability decides whether live state may be attached at // all, and only then does the phrasing decide whether this particular question warrants it. @@ -566,15 +573,21 @@ if ($profile === 'chat') { // itself: they picked a profile, the profile button in the tab still shows the one they picked, // and an answer that quietly arrives under a different contract — with citations and a refusal // rule they did not ask for — reads as the assistant ignoring them. One line, at the top. +// Names the profile that actually took it. The line used to say "the Varaverk Assistant" +// regardless, which was true while that was the only place a question could go and became a lie +// the moment the router could hand one to Troubleshoot or Code Sketcher. if ($escalated) { $system .= "HOW THIS QUESTION REACHED YOU\n" - . "The operator asked in General Chat, which is not shown Varaverk's documentation. " - . "Their message named something specific to this installation, so it was handed to " - . "you, and you do have the material. Open with one short line saying so — something " - . "like \"General Chat can't see your docs, so I've picked this up as the Varaverk " - . "Assistant\" — then answer the question normally. One line: do not apologise for " - . "the switch, do not explain how the profiles work, and do not suggest they switch " - . "profile themselves. It has already happened.\n\n"; + . "The operator asked in General Chat, which is not shown Varaverk's documentation " + . "and holds no tools. Their message was " . match ($profile) { + 'troubleshoot' => 'a question about something going wrong', + 'code' => 'a request for a script to be written', + default => 'about something specific to this installation', + } . ", so it was handed to you, and you do have what it needs. Open with one short " + . "line saying so — something like \"General Chat can't do that, so I've picked this " + . "up as " . vv_ai_profile_label($profile) . "\" — then answer the question normally. " + . "One line: do not apologise for the switch, do not explain how the profiles work, " + . "and do not suggest they switch profile themselves. It has already happened.\n\n"; } // A run-outcome question arrives with the run attached, and the assistant's standing contract — diff --git a/Plugin/unraid/Tools/ai_explain_fixtures.txt b/Plugin/unraid/Tools/ai_explain_fixtures.txt index 40af487..36a83af 100644 --- a/Plugin/unraid/Tools/ai_explain_fixtures.txt +++ b/Plugin/unraid/Tools/ai_explain_fixtures.txt @@ -84,3 +84,24 @@ what is going on here | troubleshoot | Orchestrators/daily_sync_maintenance | | # ── The code profile answers from the model alone: no passages, no live state ────────────────── write me a script that copies a folder | code | | | hasnt=health,log_tail,incidents,conf_keys + +# ── General Chat routes to the profile that fits, not always to the Assistant ────────────────── +# Chat had one escalation target, so a fault report and a request for code both landed on the +# documentation assistant. Both were answered adequately; neither was answered well. Order is the +# policy — code is checked first because it is the only intent about a thing that does not exist +# yet, and troubleshoot requires diagnostic phrasing AND something here to diagnose. +why did the daily orch fail | chat | | | profile=troubleshoot diag=yes target=Orchestrators/daily_sync_maintenance +the daily orch is broken | chat | | | profile=troubleshoot diag=yes +why did arr_sync.sh error out | chat | | | profile=troubleshoot diag=yes +create a script to prune old logs | chat | | | profile=code hasnt=health,log_tail,incidents +make me a one-liner that counts files | chat | | | profile=code +write me a bash script for backups | chat | | | profile=code + +# Diagnostic phrasing about nothing here stays in chat — "why" is not a Varaverk question on its +# own, and escalating adds capability, so a wrong escalation costs more than a missed one. +why is the sky blue | chat | | | profile=chat caps=none hasnt=health,log_tail,incidents,conf_keys +what is wrong with my car | chat | | | profile=chat caps=none + +# Naming a script and asking what it does is documentation, not a request to write one. The code +# router is anchored on the verb for exactly this pair. +what does arr_sync.sh do | chat | | | profile=varaverk diff --git a/Plugin/unraid/include/ai.php b/Plugin/unraid/include/ai.php index 753faaa..236915d 100644 --- a/Plugin/unraid/include/ai.php +++ b/Plugin/unraid/include/ai.php @@ -109,6 +109,51 @@ function vv_ai_chat_needs_varaverk(string $question): bool { || vv_ai_resolve_run_target($question) !== ''; } +// Asking for a script to be written, as opposed to asking about one that exists. The verb is +// required: "what does arr_sync.sh do" names a script and wants documentation, "write me a script +// that does X" names none and wants code. Getting that backwards in either direction is the whole +// risk here, so the pattern is anchored on the request rather than on the word "script". +function vv_ai_wants_code(string $question): bool { + return (bool)preg_match( + '/\b(write|create|make|draft|generate|build|give\s+me|need|want)\b[^.?!]{0,40}' + . '\b(a\s+|an\s+|me\s+a\s+)?(bash\s+|shell\s+|sh\s+)?' + . '(script|one[- ]?liner|cron\s?job|command|snippet)\b/i', + $question + ); +} + +// Diagnostic phrasing — "why did X fail", "X is broken", "it never runs". Shared with the +// worker's own gate so the router and the gate cannot disagree about what counts as a diagnosis. +function vv_ai_is_diagnostic(string $question): bool { + return (bool)preg_match( + '/\b(why|fail(ed|ing|ure)?|error|broken?|not work|isn.t work|wrong|stuck|hang|' + . 'never runs?|didn.t|won.t|debug|troubleshoot|diagnos)/i', + $question + ); +} + +// Which profile should answer a General Chat question, or '' to leave it in chat. +// +// One router, ordered most specific first, because these overlap on purpose: "why did the script +// I asked you to write fail" is diagnostic AND mentions a script, and the operator means the +// first. The order is the policy. +// +// code asked for something to be written. Checked first — it is the only intent that +// is about a thing that does not exist yet, so nothing else can claim it. +// troubleshoot something is wrong. Requires diagnostic phrasing AND something Varaverk-shaped +// to diagnose; "why is the sky blue" is diagnostic phrasing about nothing here. +// varaverk about this installation, but not a fault and not a request for code. +// +// Deliberately conservative: an unrecognised question stays in chat, which is the profile that +// cannot invent claims about this system. Escalation adds capability, so a wrong escalation costs +// more than a missed one — and the worker reverts to chat anyway if retrieval comes back empty. +function vv_ai_route_from_chat(string $question): string { + if (vv_ai_wants_code($question)) return 'code'; + if (vv_ai_is_diagnostic($question) && vv_ai_chat_needs_varaverk($question)) return 'troubleshoot'; + if (vv_ai_chat_needs_varaverk($question)) return 'varaverk'; + return ''; +} + function vv_ai_config(): array { static $cfg = null; if ($cfg !== null) return $cfg;