From 8d393a1e1cef37a7b7cbe6622ebef9eb2220aacb Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Mon, 3 Aug 2026 17:36:24 -0400 Subject: [PATCH] Add three chat profiles: Varaverk Assistant, General Chat, Code Sketcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explicit buttons rather than an automatic router. Misclassifying a Varaverk question as chat produces a confident invention about the user's system, which is exactly what retrieval exists to prevent — with buttons there is no hidden heuristic to be wrong and the strict profile is the default you land on. Only Varaverk Assistant retrieves; the other two would be carrying passages that cannot help write a folder-copy script. Memory goes to all three, since that is what lets chat know the setup without claiming authority over it. History depth is per profile and set server-side: retrieval costs ~2500 of 16384, so the profiles that skip it can hold a real conversation. Code Sketcher is told to flag flags it is unsure of, after it invented rsync --no-overwrite. --- Plugin/unraid/Tools/ai_chat_worker.php | 106 +++++++++++++++++-------- Plugin/unraid/api/ai.php | 27 +++++-- Plugin/unraid/pages/ai.php | 67 ++++++++++++++-- 3 files changed, 157 insertions(+), 43 deletions(-) diff --git a/Plugin/unraid/Tools/ai_chat_worker.php b/Plugin/unraid/Tools/ai_chat_worker.php index 55dc130..179bc0d 100644 --- a/Plugin/unraid/Tools/ai_chat_worker.php +++ b/Plugin/unraid/Tools/ai_chat_worker.php @@ -82,11 +82,14 @@ if (PHP_SAPI !== 'cli') { require_once dirname(__DIR__) . '/include/ai.php'; -[$jobFile, $question, $historyJson, $kind, $think] = array_slice($argv, 1, 5) + array_fill(0, 5, ''); +[$jobFile, $question, $historyJson, $kind, $think, $profile] = + array_slice($argv, 1, 6) + array_fill(0, 6, ''); if ($jobFile === '' || $question === '') exit(1); if (!preg_match('#/[0-9a-f]{32}\.json$#', $jobFile)) exit(1); +$profile = in_array($profile, ['varaverk', 'chat', 'code'], true) ? $profile : 'varaverk'; + function jw(string $f, array $d): void { file_put_contents($f, json_encode($d)); } @@ -94,40 +97,47 @@ function jw(string $f, array $d): void { $cfg = vv_ai_config(); $t0 = microtime(true); -jw($jobFile, ['status' => 'retrieving']); +// Only the Varaverk profile retrieves. The other two answer from memory and the model's own +// knowledge, so passages would be noise competing for context — documentation cannot help write +// a folder-copy script, and it has nothing to say about how someone's day is going. +$sources = []; +$context = ''; +$tRetrieve = 0.0; -$r = vv_ai_retrieve($question, $kind); -if (!$r['ok']) { - jw($jobFile, ['status' => 'error', 'error' => $r['error'] ?? 'retrieval failed']); - exit(1); -} -if (!$r['results']) { - jw($jobFile, ['status' => 'error', - 'error' => 'No relevant documentation found. Try rephrasing, or use the readme filter ' - . 'for questions about what something is.']); - exit(0); -} +if ($profile === 'varaverk') { + jw($jobFile, ['status' => 'retrieving']); -$tRetrieve = microtime(true) - $t0; + $r = vv_ai_retrieve($question, $kind); + if (!$r['ok']) { + jw($jobFile, ['status' => 'error', 'error' => $r['error'] ?? 'retrieval failed']); + 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); + } -$sources = array_map(fn($x) => [ - 'path' => $x['path'] ?? '', 'section' => $x['section'] ?? '', - 'heading' => $x['heading'] ?? '', 'score' => $x['score'] ?? 0, -], $r['results']); + $tRetrieve = microtime(true) - $t0; -jw($jobFile, ['status' => 'generating', 'sources' => $sources]); + $sources = array_map(fn($x) => [ + 'path' => $x['path'] ?? '', 'section' => $x['section'] ?? '', + 'heading' => $x['heading'] ?? '', 'score' => $x['score'] ?? 0, + ], $r['results']); -$context = ''; -foreach ($r['results'] as $i => $x) { - $label = implode(' › ', array_filter([$x['path'] ?? '', $x['section'] ?? '', $x['heading'] ?? ''])); - $context .= '[' . ($i + 1) . '] ' . $label . "\n" . trim($x['content'] ?? '') . "\n\n"; + foreach ($r['results'] as $i => $x) { + $label = implode(' › ', array_filter([$x['path'] ?? '', $x['section'] ?? '', $x['heading'] ?? ''])); + $context .= '[' . ($i + 1) . '] ' . $label . "\n" . trim($x['content'] ?? '') . "\n\n"; + } } // Diagnostic questions get live state as well as documentation. The docs say what a script is // supposed to do; only the logs and the current config say what it actually did. Attached only // when the question is asking why something failed — otherwise it is a few thousand tokens of // noise competing with the retrieved passages for a context budget that is already tight. -$diagnostic = (bool)preg_match( +$diagnostic = $profile === 'varaverk' && (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 @@ -155,12 +165,46 @@ if ($diagnostic) { } } -$system = "You are Varaverk's documentation assistant. Varaverk is this user's private " - . "two-server Unraid media ecosystem; it is not in your training data, so the material " - . "below is the only thing you know about it.\n\n" - . "Answer only from this material and cite the passages inline as [1], [2]. If it does " - . "not contain the answer, say so plainly and name what is missing — do not fill the " - . "gap from general knowledge. Prefer the user's own terminology.\n\n"; +// One prompt per profile. Explicit profiles rather than an automatic router: misclassifying a +// Varaverk question as chat produces a confident invention about the user's system, which is +// precisely what retrieval exists to prevent. The user always knows which contract is in force, +// and the strict profile is the default. +if ($profile === 'chat') { + $system = "You are the Varaverk assistant, talking with the operator of a private two-server " + . "Unraid media ecosystem called Varaverk. This is ordinary conversation — no " + . "documentation has been retrieved for it.\n\n" + . "Talk like a knowledgeable colleague. Be warm and direct, follow a tangent if one " + . "is interesting, and use your general knowledge freely.\n\n" + . "The one hard rule: you have NOT been given Varaverk's documentation in this mode. " + . "Anything you know about their setup comes from the memory section below and from " + . "what they tell you. If they ask something specific about how Varaverk works — a " + . "script, a config variable, a safeguard — say you would need the Varaverk Assistant " + . "profile for that, and do not guess. Being wrong about their own system is worse " + . "than sending them one click away.\n\n"; + +} elseif ($profile === 'code') { + $system = "You are drafting a short shell script for the operator of an Unraid server, to be " + . "saved as a Varaverk Custom Script. Assume bash on Unraid: GNU coreutils, paths " + . "under /mnt/user, no systemd, and no packages beyond what Unraid ships.\n\n" + . "Write the smallest script that does the job. Include a shebang, quote variables, " + . "check that inputs exist, and exit non-zero on failure. Prefer plain coreutils and " + . "rsync over anything exotic.\n\n" + . "This is the important part: you sometimes invent command-line flags that sound " + . "plausible but do not exist. After the script, list every flag or command you are " + . "not completely certain about, and say how to check it — 'verify with rsync --help' " + . "or similar. A stated assumption the operator can check beats a confident mistake " + . "they run at 3am. If you are unsure a flag exists, say so rather than picking the " + . "one that sounds right.\n\n" + . "Treat this as a first draft to be tested, and say so.\n\n"; + +} else { + $system = "You are Varaverk's documentation assistant. Varaverk is this user's private " + . "two-server Unraid media ecosystem; it is not in your training data, so the material " + . "below is the only thing you know about it.\n\n" + . "Answer only from this material and cite the passages inline as [1], [2]. If it does " + . "not contain the answer, say so plainly and name what is missing — do not fill the " + . "gap from general knowledge. Prefer the user's own terminology.\n\n"; +} // Memory first, before the passages. It is the standing context — who the operator is and what // has already been decided — so it should frame everything that follows rather than read as one @@ -190,7 +234,7 @@ if ($diagBlock !== '') { . $diagBlock; } -$system .= "PASSAGES\n" . $context; +if ($context !== '') $system .= "PASSAGES\n" . $context; $messages = [['role' => 'system', 'content' => $system]]; $hist = json_decode($historyJson ?: '[]', true); diff --git a/Plugin/unraid/api/ai.php b/Plugin/unraid/api/ai.php index 72218e8..a6b3cdf 100644 --- a/Plugin/unraid/api/ai.php +++ b/Plugin/unraid/api/ai.php @@ -89,7 +89,12 @@ header('Content-Type: application/json'); header('Cache-Control: no-store, no-cache'); require_once dirname(__DIR__) . '/include/ai.php'; -const VV_AI_MAX_TURNS = 3; // user+assistant pairs retained; see OPERATIONAL MODEL +// History depth is per profile, and decided here rather than by the page. Varaverk Assistant +// spends ~2500 of its 16384 on retrieved passages, so it cannot afford deep history; the other +// two retrieve nothing and can carry a real conversation. Reasoning is not stored in history, +// so it does not compound. +const VV_AI_PROFILES = ['varaverk' => 3, 'chat' => 8, 'code' => 4]; +const VV_AI_MAX_TURNS = 3; // fallback when a profile is not recognised const VV_AI_MAX_QUESTION = 4000; // characters const VV_AI_MAX_HIST_MSG = 4000; // characters per retained message const VV_AI_JOB_TTL = 3600; // seconds before a job file is reaped @@ -179,7 +184,14 @@ if ($action === 'ask') { echo json_encode(['ok' => false, 'error' => 'question exceeds ' . VV_AI_MAX_QUESTION . ' characters']); exit; } - $kind = trim($_POST['kind'] ?? ''); + $profile = trim($_POST['profile'] ?? 'varaverk'); + if (!isset(VV_AI_PROFILES[$profile])) { + echo json_encode(['ok' => false, 'error' => 'Unknown profile: ' . $profile]); exit; + } + $maxTurns = VV_AI_PROFILES[$profile] ?? VV_AI_MAX_TURNS; + + // The retrieval filter only means anything to the profile that retrieves. + $kind = $profile === 'varaverk' ? trim($_POST['kind'] ?? '') : ''; if ($kind !== '' && !in_array($kind, VV_AI_KINDS, true)) { echo json_encode(['ok' => false, 'error' => 'Unknown kind: ' . $kind]); exit; } @@ -196,8 +208,8 @@ if ($action === 'ask') { $clean[] = ['role' => $role, 'content' => mb_substr($text, 0, VV_AI_MAX_HIST_MSG)]; } } - if (count($clean) > VV_AI_MAX_TURNS * 2) { - $clean = array_slice($clean, -(VV_AI_MAX_TURNS * 2)); + if (count($clean) > $maxTurns * 2) { + $clean = array_slice($clean, -($maxTurns * 2)); } $dir = vv_ai_job_dir(); @@ -226,12 +238,13 @@ if ($action === 'ask') { . escapeshellarg($question) . ' ' . escapeshellarg(json_encode($clean)) . ' ' . escapeshellarg($kind) . ' ' - . escapeshellarg(($_POST['think'] ?? '1') === '1' ? '1' : '0') + . escapeshellarg(($_POST['think'] ?? '1') === '1' ? '1' : '0') . ' ' + . escapeshellarg($profile) . ' >/dev/null 2>&1 true, 'token' => $token]); exit; diff --git a/Plugin/unraid/pages/ai.php b/Plugin/unraid/pages/ai.php index c16460f..06b1abc 100644 --- a/Plugin/unraid/pages/ai.php +++ b/Plugin/unraid/pages/ai.php @@ -83,6 +83,16 @@ if (is_dir('/var/log/varaverk')) { .vv-ai-warn { color:#ffb74d !important; } .vv-ai-bad { color:#e57 !important; } +/* ── Profiles ───────────────────────────────────────────────────────────── */ +.vv-ai-profiles { display:flex; gap:6px; align-items:center; flex-wrap:wrap; } +.vv-ai-prof { background:#0e0e0e; border:1px solid #262626; color:#5a5a5a; font-size:11px; + padding:5px 12px; border-radius:4px; cursor:pointer; font-family:inherit; } +.vv-ai-prof:hover { color:#8a8a8a; border-color:#333; } +.vv-ai-prof.active { background:#152238; border-color:#2d4a6a; color:#9bd; } +.vv-ai-prof-hint { font-size:10px; color:#4a4a4a; margin-left:6px; flex:1; min-width:180px; } +.vv-ai-switch { text-align:center; font-size:10px; color:#3a3a3a; margin:10px 0; + border-top:1px dashed #1e1e1e; padding-top:8px; } + /* ── Chat ───────────────────────────────────────────────────────────────── */ .vv-ai-chat { border:1px solid #262626; border-radius:6px; background:#0b0b0b; min-height:340px; max-height:60vh; overflow-y:auto; padding:14px; } @@ -180,6 +190,13 @@ if (is_dir('/var/log/varaverk')) { +
+ + + + +
+
Ask Varaverk about itself.
@@ -238,11 +255,23 @@ if (is_dir('/var/log/varaverk')) {