Let a finding be answered in words, matched rather than judged
The reply to "shall I fix it" ends in a conf write, so the three actions are matched by pattern with an ambiguity refusal instead of being inferred — and a phrase that supports two readings asks again rather than picking one.
This commit is contained in:
@@ -383,6 +383,106 @@ function vv_ai_findings_for_chat(int $limit = 3): array {
|
||||
return array_slice(vv_ai_findings_list(['needs_operator']), 0, max(1, $limit));
|
||||
}
|
||||
|
||||
// ── Answering a finding in words ─────────────────────────────────────────────────────────────
|
||||
// The buttons are unambiguous by construction. This is for the other path — replying "yeah go
|
||||
// ahead" in the chat that raised the finding — and it is matched here rather than asked of the
|
||||
// model, because the model's answer would be a conf write and a wrong reading of "no, leave it"
|
||||
// is not recoverable by apologising.
|
||||
//
|
||||
// Same shape as vv_ai_route_from_chat(): anchored patterns, most specific first, and anything
|
||||
// unrecognised returns null so the assistant asks again instead of guessing. Two actions both
|
||||
// matching is also null — "leave it, I know" and "leave it for now" differ by one clause and
|
||||
// mean different things, so a phrase that supports both is not an instruction yet.
|
||||
const VV_AI_ACTION_PATTERNS = [
|
||||
// Acknowledge — "this is deliberate, stop telling me".
|
||||
'ack' => [
|
||||
'/\b(i|we) know\b/u',
|
||||
'/\b(that|this|it)(?:\'s| is) (fine|expected|intentional|deliberate|on purpose)\b/u',
|
||||
'/\bon purpose\b/u',
|
||||
'/\b(aware|acknowledge|ack)\b/u',
|
||||
'/\bmeant to be\b/u',
|
||||
],
|
||||
// Apply the proposed value.
|
||||
'fix' => [
|
||||
'/\bfix (it|that|this|them)?\b/u',
|
||||
'/\b(go ahead|do it|apply|make the change|change it|update it|correct it)\b/u',
|
||||
// A bare affirmative, as the whole message — "yes" answering "shall I fix it" is an
|
||||
// instruction, "yes it looks wrong" is agreement about the diagnosis and nothing more.
|
||||
// A trailing please is still bare.
|
||||
'/\b(yes|yeah|yep|yup|sure|ok|okay)\b(\s*,?\s*please)?[\s,.!]*$/u',
|
||||
'/\bplease do\b/u',
|
||||
],
|
||||
// Not now — no state written, it comes back next sweep.
|
||||
'cancel' => [
|
||||
'/\b(not now|later|leave it (alone|for now)|skip( it)?|cancel|ignore for now)\b/u',
|
||||
'/\b(no|nope|nah)\b[\s,.!]*$/u',
|
||||
'/\b(don\'?t|do not) (fix|touch|change|write|apply)\b/u',
|
||||
],
|
||||
];
|
||||
|
||||
// Returns 'fix' | 'ack' | 'cancel', or null when the reply does not clearly mean one of them.
|
||||
//
|
||||
// Only call this when a finding is actually pending. A bare "yes" means fix in answer to "shall
|
||||
// I fix it" and means nothing at all on its own, and the difference is context this function
|
||||
// cannot see.
|
||||
function vv_ai_finding_action_from_text(string $text): ?string {
|
||||
$t = strtolower(trim($text));
|
||||
if ($t === '') return null;
|
||||
$t = preg_replace('/\s+/', ' ', $t);
|
||||
|
||||
$matched = [];
|
||||
foreach (VV_AI_ACTION_PATTERNS as $action => $patterns) {
|
||||
foreach ($patterns as $re) {
|
||||
if (preg_match($re, $t)) { $matched[$action] = true; break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly one reading, or none. "leave it, I know" hits both ack and cancel; that is a
|
||||
// sentence the operator should be asked to restate, not one to pick a winner from.
|
||||
return count($matched) === 1 ? array_key_first($matched) : null;
|
||||
}
|
||||
|
||||
// Carry out an answered action against a stored finding.
|
||||
//
|
||||
// Fix goes through the same guarded write path as everything else, and is the one place a
|
||||
// toggle may be written — because reaching here means the operator asked for it by name. The
|
||||
// unattended sweep never calls this.
|
||||
function vv_ai_finding_apply_action(string $id, string $action, string $note = ''): array {
|
||||
$f = vv_ai_finding_get($id);
|
||||
if ($f === null) return ['ok' => false, 'error' => 'no such finding'];
|
||||
|
||||
switch ($action) {
|
||||
case 'ack':
|
||||
return ['ok' => vv_ai_finding_ack($id, $note), 'action' => 'ack'];
|
||||
|
||||
case 'cancel':
|
||||
// Deliberately writes nothing at all. "Not now" is not a state, it is the absence of
|
||||
// one — recording it would make the finding look decided when it is still open.
|
||||
return ['ok' => true, 'action' => 'cancel'];
|
||||
|
||||
case 'fix':
|
||||
$proposed = $f['proposed'] ?? null;
|
||||
if ($proposed === null) return ['ok' => false, 'error' => 'nothing proposed to write'];
|
||||
|
||||
$key = (string)$f['conf_key'];
|
||||
$ok = vv_conf_write_changes([[
|
||||
'file' => (string)($f['conf_file'] ?? 'master.conf'),
|
||||
'key' => $key,
|
||||
'value' => (string)$proposed,
|
||||
'type' => 'scalar',
|
||||
]]);
|
||||
$wrote = !in_array(false, $ok, true);
|
||||
|
||||
if ($wrote) {
|
||||
vv_ai_finding_set_state($id, 'fixed',
|
||||
$note !== '' ? $note : 'Wrote ' . $key . ' at the operator\'s request.');
|
||||
}
|
||||
return ['ok' => $wrote, 'action' => 'fix',
|
||||
'error' => $wrote ? null : 'conf write refused — see conf_changes.log'];
|
||||
}
|
||||
return ['ok' => false, 'error' => 'unknown action'];
|
||||
}
|
||||
|
||||
// ── Resolving a log line back to a conf key ──────────────────────────────────────────────────
|
||||
// By value wherever possible, by name only as a fallback.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user