200 lines
11 KiB
PHP
200 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, the Scheduler dock — 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.
|
|
//
|
|
// Both the shared chat include and the Scheduler dock call this. Before it existed the dock 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";
|
|
}
|