Route a General Chat question to the profile that fits it
This commit is contained in:
+27
-2
@@ -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
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user