feat(ai): make anomaly detection FREE and add learning status endpoint

Free Features (no license required):
- Anomaly detection - removed license gating, purely statistical analysis
- Learning status endpoint - GET /api/ai/intelligence/learning

Learning Status Response:
- resources_baselined: count of resources with learned baselines
- total_metrics: total metric baselines (cpu + memory + disk)
- metric_breakdown: {cpu: X, memory: Y, disk: Z}
- status: 'waiting' | 'learning' | 'active'
- message: human-readable description

This makes the AI intelligence features visible to all users,
encouraging upgrades for the full LLM-powered patrol experience.
This commit is contained in:
rcourtman 2025-12-21 11:36:54 +00:00
parent d29b3c2324
commit 783537e1d2
4 changed files with 101 additions and 12 deletions

View file

@ -17,10 +17,12 @@ import type {
BaselinesResponse,
RemediationsResponse,
AnomaliesResponse,
LearningStatusResponse,
IntelligenceSummary,
ResourceIntelligence,
} from '@/types/aiIntelligence';
export class AIAPI {
private static baseUrl = '/api';
@ -129,6 +131,12 @@ export class AIAPI {
return apiFetchJSON(`${this.baseUrl}/ai/intelligence/anomalies${params}`) as Promise<AnomaliesResponse>;
}
// Get learning/baseline status (FREE - no license required)
// Shows how many resources have been baselined and the overall learning state
static async getLearningStatus(): Promise<LearningStatusResponse> {
return apiFetchJSON(`${this.baseUrl}/ai/intelligence/learning`) as Promise<LearningStatusResponse>;
}
// ============================================
// Unified Intelligence API
// Aggregates all AI subsystems into a single view

View file

@ -153,6 +153,23 @@ export interface AnomaliesResponse extends LicenseGatedResponse {
};
}
// Learning/baseline status for showing system intelligence state
export type LearningStatus = 'waiting' | 'learning' | 'active';
export interface LearningStatusResponse {
resources_baselined: number;
total_metrics: number;
metric_breakdown: {
cpu?: number;
memory?: number;
disk?: number;
};
status: LearningStatus;
message: string;
license_required: boolean;
}
// ============================================================================
// Unified Intelligence Types (for /api/ai/intelligence endpoint)
// ============================================================================

View file

@ -722,13 +722,6 @@ func (h *AISettingsHandler) HandleGetAnomalies(w http.ResponseWriter, r *http.Re
}
}
// License gating
locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol)
if locked {
w.Header().Set("X-License-Required", "true")
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
}
count := len(result)
// Count by severity for summary
@ -744,18 +737,88 @@ func (h *AISettingsHandler) HandleGetAnomalies(w http.ResponseWriter, r *http.Re
}
}
if locked {
result = []map[string]interface{}{}
}
// NOTE: Anomaly detection is FREE (no license required)
// It's purely deterministic statistical analysis with no LLM costs
// This provides value to all users and encourages Pro upgrades for patrol
if err := utils.WriteJSONResponse(w, map[string]interface{}{
"anomalies": result,
"count": count,
"severity_counts": severityCounts,
"license_required": locked,
"upgrade_url": aiIntelligenceUpgradeURL,
"license_required": false,
}); err != nil {
log.Error().Err(err).Msg("Failed to write anomalies response")
}
}
// HandleGetLearningStatus returns the current state of baseline learning (GET /api/ai/intelligence/learning)
// This is FREE (no license required) and shows users how much the system has learned
func (h *AISettingsHandler) HandleGetLearningStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
patrol := h.aiService.GetPatrolService()
if patrol == nil {
if err := utils.WriteJSONResponse(w, map[string]interface{}{
"resources_baselined": 0,
"total_metrics": 0,
"status": "patrol_not_initialized",
"message": "Baseline learning requires AI Patrol to be initialized",
}); err != nil {
log.Error().Err(err).Msg("Failed to write learning status response")
}
return
}
baselineStore := patrol.GetBaselineStore()
if baselineStore == nil {
if err := utils.WriteJSONResponse(w, map[string]interface{}{
"resources_baselined": 0,
"total_metrics": 0,
"status": "baseline_store_not_initialized",
"message": "Baseline store not yet initialized",
}); err != nil {
log.Error().Err(err).Msg("Failed to write learning status response")
}
return
}
// Get all baselines and count metrics
baselines := baselineStore.GetAllBaselines()
resourceCount := baselineStore.ResourceCount()
// Count unique resources and total metrics
resourceIDs := make(map[string]bool)
totalMetrics := 0
metricCounts := make(map[string]int) // cpu, memory, disk counts
for _, baseline := range baselines {
resourceIDs[baseline.ResourceID] = true
totalMetrics++
metricCounts[baseline.Metric]++
}
// Determine status
status := "learning"
message := "Actively learning baseline patterns"
if resourceCount == 0 {
status = "waiting"
message = "Waiting for metric data to learn from"
} else if resourceCount >= 5 {
status = "active"
message = "Baselines established and anomaly detection is active"
}
if err := utils.WriteJSONResponse(w, map[string]interface{}{
"resources_baselined": resourceCount,
"total_metrics": totalMetrics,
"metric_breakdown": metricCounts,
"status": status,
"message": message,
"license_required": false,
}); err != nil {
log.Error().Err(err).Msg("Failed to write learning status response")
}
}

View file

@ -1217,6 +1217,7 @@ func (r *Router) setupRoutes() {
r.mux.HandleFunc("/api/ai/intelligence/baselines", RequireAuth(r.config, r.aiSettingsHandler.HandleGetBaselines))
r.mux.HandleFunc("/api/ai/intelligence/remediations", RequireAuth(r.config, r.aiSettingsHandler.HandleGetRemediations))
r.mux.HandleFunc("/api/ai/intelligence/anomalies", RequireAuth(r.config, r.aiSettingsHandler.HandleGetAnomalies))
r.mux.HandleFunc("/api/ai/intelligence/learning", RequireAuth(r.config, r.aiSettingsHandler.HandleGetLearningStatus))
// Agent WebSocket for AI command execution
r.mux.HandleFunc("/api/agent/ws", r.handleAgentWebSocket)