Route a General Chat question to the profile that fits it

This commit is contained in:
Gmer4Lfe
2026-08-09 00:36:13 -04:00
parent 37784bce6c
commit d9225d06d9
4 changed files with 122 additions and 18 deletions
+27 -2
View File
@@ -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
+28 -15
View File
@@ -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';
if ($profile === 'chat') {
$to = vv_ai_route_from_chat($question);
if ($to !== '' && $to !== $profile) {
$profile = $to;
$escalated = true;
wlog('handoff chat -> varaverk: ' . mb_substr($question, 0, 80));
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 —
@@ -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
+45
View File
@@ -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;