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:
parent
d29b3c2324
commit
783537e1d2
4 changed files with 101 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue