General Chat now hands a Varaverk question up to the Assistant instead of telling the operator to do it
This commit is contained in:
@@ -73,7 +73,8 @@
|
||||
// JOB FILE STATES
|
||||
// {"status":"retrieving"}
|
||||
// {"status":"generating","sources":[…]}
|
||||
// {"status":"done","answer":…,"thinking":…,"sources":[…],"timing":{…}}
|
||||
// {"status":"done","answer":…,"thinking":…,"sources":[…],"timing":{…},
|
||||
// "profile":the profile that answered,"escalated":true if chat handed it up}
|
||||
// {"status":"error","error":…}
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -92,10 +93,32 @@ if (!preg_match('#/[0-9a-f]{32}\.json$#', $jobFile)) exit(1);
|
||||
|
||||
$profile = in_array($profile, ['varaverk', 'chat', 'code', 'troubleshoot'], true) ? $profile : 'varaverk';
|
||||
|
||||
// Every "is this profile allowed X" question in this file goes through here. Written as a closure
|
||||
// over $profile so no call site can accidentally ask about a different one, which is the shape the
|
||||
// old scattered comparisons kept taking.
|
||||
$can = fn(string $cap): bool => vv_ai_profile_can($profile, $cap);
|
||||
// General Chat cannot answer a question about this installation — that is the whole point of it,
|
||||
// and it is why it holds no capabilities. But refusing is not the same as being unable to help,
|
||||
// and "the Varaverk Assistant profile can answer this" is a sentence the operator then has to act
|
||||
// on: switch profile, retype the question, lose the thread. The question is already here and the
|
||||
// profile that can take it is one line away.
|
||||
//
|
||||
// One direction only, and never the reverse. Escalating adds evidence and tightens the contract,
|
||||
// so a wrong escalation costs tokens and an over-serious answer. Routing the other way — deciding
|
||||
// a Varaverk question is small talk — removes the documentation and produces confident invention
|
||||
// about someone's own server, which is the failure this whole design exists to prevent. There is
|
||||
// no code path that moves a question down the ladder except the retrieval fallback below, which
|
||||
// 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.
|
||||
$escalated = false;
|
||||
if ($profile === 'chat' && vv_ai_chat_needs_varaverk($question)) {
|
||||
$profile = 'varaverk';
|
||||
$escalated = true;
|
||||
wlog('handoff chat -> varaverk: ' . mb_substr($question, 0, 80));
|
||||
}
|
||||
|
||||
// Every "is this profile allowed X" question in this file goes through here. Bound by reference
|
||||
// rather than by value: the profile can still change after this point — the handoff above and the
|
||||
// fallback below both move it — and a capability check that answered for the profile in force at
|
||||
// definition time would be silently wrong for the rest of the run.
|
||||
$can = function (string $cap) use (&$profile): bool { return vv_ai_profile_can($profile, $cap); };
|
||||
|
||||
function jw(string $f, array $d): void {
|
||||
file_put_contents($f, json_encode($d));
|
||||
@@ -134,11 +157,22 @@ if ($can('retrieve')) {
|
||||
exit(1);
|
||||
}
|
||||
if (!$r['results']) {
|
||||
jw($jobFile, ['status' => 'error',
|
||||
'error' => 'No relevant documentation found. Try rephrasing, use the readme filter '
|
||||
. 'for questions about what something is, or switch to General Chat if this '
|
||||
. 'is not a Varaverk question.']);
|
||||
exit(0);
|
||||
// A question that arrived here by handoff must not inherit this error. The operator asked
|
||||
// in General Chat: something in the phrasing looked Varaverk-shaped, the index turned out
|
||||
// to hold nothing on it, and a hard failure would be a worse answer than the polite
|
||||
// deferral they would have got had the handoff never happened. Put it back where it came
|
||||
// from — the only downward move in the file, and it chooses nothing, it just undoes.
|
||||
if ($escalated) {
|
||||
$profile = 'chat';
|
||||
$escalated = false;
|
||||
wlog('handoff reverted -> chat: nothing in the index for it');
|
||||
} else {
|
||||
jw($jobFile, ['status' => 'error',
|
||||
'error' => 'No relevant documentation found. Try rephrasing, use the readme filter '
|
||||
. 'for questions about what something is, or switch to General Chat if this '
|
||||
. 'is not a Varaverk question.']);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
$tRetrieve = microtime(true) - $t0;
|
||||
@@ -406,6 +440,21 @@ if ($profile === 'chat') {
|
||||
. "fill the gap from general knowledge. Prefer the user's own terminology.\n\n");
|
||||
}
|
||||
|
||||
// The operator chose General Chat and is getting the Assistant. That has to be said in the answer
|
||||
// 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.
|
||||
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";
|
||||
}
|
||||
|
||||
// A run-outcome question arrives with the run attached, and the assistant's standing contract —
|
||||
// answer only from the retrieved passages — is wrong for it: the run record and the log are not
|
||||
// in the index and never will be. Without this the honest reading of its own rules is to fall
|
||||
@@ -638,12 +687,17 @@ $tokS = $evalNs > 0 ? round($evalCount / ($evalNs / 1e9), 1) : null;
|
||||
// been closed and nobody ever reads the result. Best-effort by contract — it cannot throw.
|
||||
vv_ai_token_record($profile, 'webgui', (int)($d['prompt_eval_count'] ?? 0), $evalCount, $tokS);
|
||||
|
||||
// profile is the one that actually answered, not the one that was asked for — they differ on a
|
||||
// handoff. Reported so the page can show which contract produced the answer rather than the
|
||||
// button the operator last pressed.
|
||||
jw($jobFile, [
|
||||
'status' => 'done',
|
||||
'answer' => $answer,
|
||||
'thinking' => $thinking,
|
||||
'sources' => $sources,
|
||||
'warnings' => array_values(array_unique($warnings)),
|
||||
'status' => 'done',
|
||||
'answer' => $answer,
|
||||
'thinking' => $thinking,
|
||||
'sources' => $sources,
|
||||
'profile' => $profile,
|
||||
'escalated' => $escalated,
|
||||
'warnings' => array_values(array_unique($warnings)),
|
||||
'timing' => [
|
||||
'retrieve_ms' => (int)round($tRetrieve * 1000),
|
||||
'generate_ms' => (int)round((microtime(true) - $t1) * 1000),
|
||||
|
||||
Reference in New Issue
Block a user