From a3d953172ca0a667869c5124e0dd014d082b0eb2 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 10 Dec 2025 22:55:34 +0000 Subject: [PATCH] feat(ai): Add LLM memory system for patrol findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a comprehensive feedback system that allows the LLM to 'remember' user decisions about findings, preventing repetitive/annoying alerts. Backend changes: - Extended Finding struct with dismissed_reason, user_note, times_raised, suppressed - Added Dismiss(), Suppress(), SetUserNote(), IsSuppressed() methods to FindingsStore - Added GetDismissedForContext() to format dismissed findings for LLM context - Enhanced buildPatrolPrompt() to inject user feedback context - Added POST /api/ai/patrol/dismiss and /api/ai/patrol/suppress endpoints - Updated IsActive() to exclude suppressed findings Frontend changes: - Added Dismiss dropdown with options: Not an Issue, Expected Behavior, Will Fix Later - Added Never Alert Again option for permanent suppression - Expected Behavior prompts for optional note to help LLM understand context - Added visual badges: recurrence count (×N), dismissed status, suppressed indicator - Display user notes in expanded finding view Also fixes: - Fixed 403 error on Run Patrol (compilation errors from partial refactoring) - Removed non-LLM patrol checks - patrol now uses LLM analysis only - Fixed function signature mismatches in alert_triggered.go The LLM now receives context about previously dismissed findings and is instructed not to re-raise them unless severity has significantly worsened. --- frontend-modern/src/api/patrol.ts | 35 ++++ frontend-modern/src/pages/Alerts.tsx | 122 ++++++++++- internal/ai/alert_triggered.go | 12 +- internal/ai/findings.go | 177 +++++++++++++++- internal/ai/patrol.go | 301 ++++++--------------------- internal/api/ai_handlers.go | 123 ++++++++++- internal/api/router.go | 3 +- internal/api/security.go | 17 ++ 8 files changed, 543 insertions(+), 247 deletions(-) diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index e79d3d3..17b5dc3 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -27,6 +27,11 @@ export interface Finding { acknowledged_at?: string; snoozed_until?: string; // Finding hidden until this time alert_id?: string; + // User feedback fields (LLM memory system) + dismissed_reason?: 'not_an_issue' | 'expected_behavior' | 'will_fix_later'; + user_note?: string; + times_raised: number; + suppressed: boolean; } export interface FindingsSummary { @@ -188,6 +193,36 @@ export async function resolveFinding(findingId: string): Promise<{ success: bool }); } +/** + * Dismiss a finding with a reason (LLM memory feature) + * The LLM will be told not to re-raise this issue in future patrols. + * @param findingId The ID of the finding to dismiss + * @param reason One of: "not_an_issue", "expected_behavior", "will_fix_later" + * @param note Optional freeform explanation + */ +export async function dismissFinding( + findingId: string, + reason: 'not_an_issue' | 'expected_behavior' | 'will_fix_later', + note?: string +): Promise<{ success: boolean; message: string }> { + return apiFetchJSON('/api/ai/patrol/dismiss', { + method: 'POST', + body: JSON.stringify({ finding_id: findingId, reason, note }), + }); +} + +/** + * Permanently suppress a finding type (LLM memory feature) + * The LLM will never re-raise this type of finding for this resource. + * @param findingId The ID of the finding to suppress + */ +export async function suppressFinding(findingId: string): Promise<{ success: boolean; message: string }> { + return apiFetchJSON('/api/ai/patrol/suppress', { + method: 'POST', + body: JSON.stringify({ finding_id: findingId }), + }); +} + /** * Severity color mapping for UI */ diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index be100ba..19d47f0 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -28,7 +28,7 @@ import History from 'lucide-solid/icons/history'; import Gauge from 'lucide-solid/icons/gauge'; import Send from 'lucide-solid/icons/send'; import Calendar from 'lucide-solid/icons/calendar'; -import { getPatrolStatus, getFindings, getFindingsHistory, getPatrolRunHistory, forcePatrol, subscribeToPatrolStream, type Finding, type PatrolStatus, type PatrolRunRecord, severityColors, formatTimestamp } from '@/api/patrol'; +import { getPatrolStatus, getFindings, getFindingsHistory, getPatrolRunHistory, forcePatrol, subscribeToPatrolStream, dismissFinding, suppressFinding, type Finding, type PatrolStatus, type PatrolRunRecord, severityColors, formatTimestamp } from '@/api/patrol'; import { aiChatStore } from '@/stores/aiChat'; type AlertTab = 'overview' | 'thresholds' | 'destinations' | 'schedule' | 'history'; @@ -2528,6 +2528,29 @@ function OverviewTab(props: { {finding.title} + {/* Recurrence badge - show if raised multiple times */} + 1}> + + ×{finding.times_raised} + + + {/* Dismissed status badge */} + + + {finding.dismissed_reason === 'not_an_issue' && '✓ Dismissed'} + {finding.dismissed_reason === 'expected_behavior' && '✓ Expected'} + {finding.dismissed_reason === 'will_fix_later' && '⏱ Noted'} + + + {/* Suppressed badge */} + + + 🔇 Suppressed + + {/* Resource name pill */}