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
|
not an oversight: anything added to it stops being general chat and becomes an assistant that
|
||||||
sometimes invents claims about this installation.
|
sometimes invents claims about this installation.
|
||||||
|
|
||||||
`chat` escalates to `varaverk` on its own when a question is genuinely about Varaverk, and
|
### Routing out of General Chat
|
||||||
reverts if the index turns out to have nothing — so the loose profile is safe to sit in.
|
|
||||||
|
`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`,
|
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
|
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.
|
// 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 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;
|
$escalated = false;
|
||||||
if ($profile === 'chat' && vv_ai_chat_needs_varaverk($question)) {
|
if ($profile === 'chat') {
|
||||||
$profile = 'varaverk';
|
$to = vv_ai_route_from_chat($question);
|
||||||
|
if ($to !== '' && $to !== $profile) {
|
||||||
|
$profile = $to;
|
||||||
$escalated = true;
|
$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
|
// 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.
|
// 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;
|
$runClean = $rec['ok'] && $rec['status'] === 'ok' && $rec['exit'] === 0 && $rec['end'] !== null;
|
||||||
|
|
||||||
$kwDiagnostic = (bool)preg_match(
|
// The same detector the router uses to send a question here in the first place. Two copies of
|
||||||
'/\b(why|fail(ed|ing|ure)?|error|broken?|not work|isn.t work|wrong|stuck|hang|'
|
// this regex would mean a question could be routed to Troubleshoot as diagnostic and then have
|
||||||
. 'never runs?|didn.t|won.t|debug|troubleshoot|diagnos)/i',
|
// live state withheld from it as not-diagnostic, which is the worst of both.
|
||||||
$question
|
$kwDiagnostic = vv_ai_is_diagnostic($question);
|
||||||
);
|
|
||||||
|
|
||||||
// Permission first, need second: the capability decides whether live state may be attached at
|
// 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.
|
// 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,
|
// 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
|
// 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.
|
// 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) {
|
if ($escalated) {
|
||||||
$system .= "HOW THIS QUESTION REACHED YOU\n"
|
$system .= "HOW THIS QUESTION REACHED YOU\n"
|
||||||
. "The operator asked in General Chat, which is not shown Varaverk's documentation. "
|
. "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 "
|
. "and holds no tools. Their message was " . match ($profile) {
|
||||||
. "you, and you do have the material. Open with one short line saying so — something "
|
'troubleshoot' => 'a question about something going wrong',
|
||||||
. "like \"General Chat can't see your docs, so I've picked this up as the Varaverk "
|
'code' => 'a request for a script to be written',
|
||||||
. "Assistant\" — then answer the question normally. One line: do not apologise for "
|
default => 'about something specific to this installation',
|
||||||
. "the switch, do not explain how the profiles work, and do not suggest they switch "
|
} . ", so it was handed to you, and you do have what it needs. Open with one short "
|
||||||
. "profile themselves. It has already happened.\n\n";
|
. "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 —
|
// 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 ──────────────────
|
# ── 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
|
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) !== '';
|
|| 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 {
|
function vv_ai_config(): array {
|
||||||
static $cfg = null;
|
static $cfg = null;
|
||||||
if ($cfg !== null) return $cfg;
|
if ($cfg !== null) return $cfg;
|
||||||
|
|||||||
Reference in New Issue
Block a user