Files
Varaverk/Plugin/unraid/include/ai_profiles.php
T
Gmer4Lfe 6be765a9bf Say there is one chat component, in the places that claimed otherwise
Several headers argued the Scheduler dock was deliberately separate, and the Scheduler's help
never mentioned the assistant at all — including the fix flow that just changed shape.
2026-08-09 12:22:25 -04:00

201 lines
11 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The one definition of what an AI profile is. Every consumer — the endpoint, the worker, the
// shared chat include and every page that renders it — reads it from here instead of
// restating it.
//
// WHY THIS EXISTS
// A profile used to be defined in five places: history depth in api/ai.php, capabilities in
// include/ai.php, label/hint/depth again in the chat's JavaScript, a prompt branch in the
// worker, and a label map in pages/scheduler.php. They had already drifted — the JavaScript
// knew three profiles where PHP knew four, so the shared chat could not offer troubleshoot at
// all and the Scheduler dock hand-rolled its own labels to compensate. The include carried a
// comment telling the next person not to let the two tables diverge, which is a comment doing
// a data structure's job.
//
// WHAT LIVES HERE, AND WHAT DELIBERATELY DOES NOT
// Here: anything more than one file needs to agree on — the set of profiles, their labels and
// hints, history depth, capabilities, and whether a profile is offered as a button.
//
// Not here: the system prompts. They are long, delicate, and have exactly one consumer, so
// moving them would be churn against the most sensitive text in the subsystem for no reduction
// in duplication. Tools/ai_chat_worker.php still owns them; it just keys off ids validated
// here rather than an if-chain that invents its own vocabulary.
//
// CAPABILITIES ARE PER PROFILE, NOT PER CAPABILITY
// The old table was inverted — capability => [profiles] — which reads well when adding a
// capability and badly when answering the question actually asked at runtime, which is always
// "what can this profile do". Same content, turned the right way round.
//
// A profile is a contract plus a set of inputs, and the inputs are the half that has to be
// enforced rather than requested. The caps list is that half.
//
// It exists because the alternative already failed. The same permissions used to live as a
// dozen `$profile === 'varaverk' || $profile === 'troubleshoot'` conditions spread across the
// worker, and answering "may chat ever be shown a log?" meant reading all of them. It could —
// a gate added for run-outcome questions granted it by omission, and the chat profile, whose
// entire value is that it has NOT been shown this installation, was one phrasing away from
// being handed a health sweep and 120 lines of log. Nothing about that was visible at the
// point of the mistake. Here it would have been one missing word on one line.
//
// A capability is permission, not need. varaverk holds 'health' but only attaches it when the
// question looks diagnostic; troubleshoot attaches it always. The gates decide whether an
// input is warranted, this decides whether it is allowed, and a gate can never widen the grant.
//
// chat holding an empty capability list is a guarantee, not an oversight. Anything added to it
// stops being general chat and becomes an assistant that sometimes lies about this
// installation.
//
// EXPORTS
// vv_ai_profiles() the whole table
// vv_ai_profile($id) one entry, or null
// vv_ai_profile_ok($id) is this a real profile
// vv_ai_profile_turns($id) history depth in turns
// vv_ai_profile_can($id,$cap) capability check
// vv_ai_profile_caps($id) every capability a profile holds
// vv_ai_profile_label($id) display name, falling back to the id
// vv_ai_profiles_ui() those offered as buttons, in order
// vv_ai_profiles_max_turns() deepest window of any profile
// vv_ai_profiles_client() the subset the browser needs, for json_encode
// vv_ai_profiles_script() publishes that subset as window.VvAiProfiles, once per page
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// turns — conversation depth. Varaverk Assistant spends ~2500 of its 16384 on retrieved
// passages so it cannot afford much; troubleshoot carries a whole log tail and can
// afford least of all; the two that retrieve nothing can hold a real conversation.
// caps — see VV_AI_CAP_MEANING below.
// ui — appears in the profile bar. troubleshoot is false because it is entered by opening a
// log on the Scheduler tab, not by choosing it: it is a context you are in, and a
// button offering it from the dashboard would produce a troubleshooting contract with
// nothing to troubleshoot.
const VV_AI_PROFILES_DEF = [
'varaverk' => [
'label' => 'Varaverk Assistant',
'short' => 'Assistant',
'hint' => 'Answers only from Varaverk\'s own docs, with sources. Says so when they don\'t cover it.',
'turns' => 3,
'ui' => true,
'caps' => ['retrieve', 'kind_filter', 'health', 'run_evidence', 'incidents', 'conf_lookup'],
],
'chat' => [
'label' => 'General Chat',
'short' => 'Chat',
'hint' => 'Ordinary conversation. Hands anything about this install to the assistant on its own.',
'turns' => 8,
'ui' => true,
'caps' => [],
],
'code' => [
'label' => 'Code Sketcher',
'short' => 'Code',
'hint' => 'Drafts short scripts for Custom Scripts. First drafts — test before trusting.',
'turns' => 4,
'ui' => true,
'caps' => ['code_scan'],
],
// Offered as a button as well as entered by context. It was context-only on the reasoning
// that choosing it from a dashboard produces a troubleshooting contract with nothing to
// troubleshoot — true of the inputs, but not of the profile: name a script in the question
// and run_evidence resolves the run record and log for it without any scope at all. The
// prompt now states which of those it actually received rather than asserting a log, so
// picking it with nothing open degrades to an honest "I cannot see one" instead of an
// invented reading of a log that was never attached.
'troubleshoot' => [
'label' => 'Troubleshoot',
'short' => 'Troubleshoot',
'hint' => 'Reasons from evidence — the log you have open, or one you name. Says so when there is none.',
'turns' => 2,
'ui' => true,
'caps' => ['retrieve', 'health', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', 'file_bugs'],
],
];
// Documentation only — nothing branches on it. Kept beside the table because a capability name
// with no stated meaning is how one ends up granted to a profile that should not have it.
const VV_AI_CAP_MEANING = [
'retrieve' => 'retrieval passages from the documentation index',
'kind_filter' => 'the retrieval kind filter the page exposes',
'health' => 'live health sweep measured at question time',
'run_evidence' => 'run record and log tail for a script named in the question',
'scoped_log' => 'log tail for whatever the operator currently has open',
'incidents' => 'operator-written history of what previously went wrong with this thing',
'conf_lookup' => 'deterministic "where does this conf key actually live" lookup',
'file_bugs' => 'may file a bug report against Varaverk itself',
'code_scan' => 'destructive-operation scan of generated shell',
];
function vv_ai_profiles(): array { return VV_AI_PROFILES_DEF; }
function vv_ai_profile(string $id): ?array {
return VV_AI_PROFILES_DEF[$id] ?? null;
}
function vv_ai_profile_ok(string $id): bool {
return isset(VV_AI_PROFILES_DEF[$id]);
}
// Falls back rather than throwing. An unknown id reaching here is a caller that failed to
// validate, and the shallowest window is the safe way to be wrong: it spends the least context.
function vv_ai_profile_turns(string $id): int {
return (int)(VV_AI_PROFILES_DEF[$id]['turns'] ?? 3);
}
function vv_ai_profile_caps(string $id): array {
return VV_AI_PROFILES_DEF[$id]['caps'] ?? [];
}
function vv_ai_profile_can(string $id, string $cap): bool {
return in_array($cap, VV_AI_PROFILES_DEF[$id]['caps'] ?? [], true);
}
function vv_ai_profile_label(string $id): string {
return VV_AI_PROFILES_DEF[$id]['label'] ?? $id;
}
// Insertion order is the button order, and varaverk is first because the strict profile is the
// one to land on: a misuse there costs "the docs don't cover that" rather than an invented claim
// about the system.
function vv_ai_profiles_ui(): array {
return array_filter(VV_AI_PROFILES_DEF, fn($p) => !empty($p['ui']));
}
// The cap api/ai.php applies when storing a conversation, which has to hold the deepest window
// any profile could later ask for — a chat saved under one profile can be reopened under another.
function vv_ai_profiles_max_turns(): int {
return max(array_column(VV_AI_PROFILES_DEF, 'turns'));
}
// What the browser needs, and nothing more. The prompts are not here to be sent and the server
// re-derives depth and capability on every request regardless — this is for drawing buttons and
// deciding whether to show the kind filter, not for the client to make policy with.
function vv_ai_profiles_client(): array {
$out = [];
foreach (VV_AI_PROFILES_DEF as $id => $p) {
$out[$id] = [
'label' => $p['label'],
'short' => $p['short'],
'hint' => $p['hint'],
'turns' => $p['turns'],
'ui' => (bool)$p['ui'],
'kind' => in_array('kind_filter', $p['caps'], true),
];
}
return $out;
}
// Publishes the registry to the browser as window.VvAiProfiles, once per page however many
// surfaces ask for it. Emitted as its own tag so any script block that consumes it stays pure
// JavaScript and remains syntax-checkable outside PHP.
//
// The shared chat include calls this, as does any page emitting the registry ahead of it. Before
// it existed the Scheduler had its own literal `{ code: 'Code', troubleshoot: 'Troubleshoot' }`
// map, which is how a fourth copy of the profile list came to exist in the first place.
function vv_ai_profiles_script(): void {
static $done = false;
if ($done) return;
$done = true;
echo '<script>window.VvAiProfiles = '
. json_encode(vv_ai_profiles_client(), JSON_UNESCAPED_SLASHES) . ";</script>\n";
}