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,
|
BaselinesResponse,
|
||||||
RemediationsResponse,
|
RemediationsResponse,
|
||||||
AnomaliesResponse,
|
AnomaliesResponse,
|
||||||
|
LearningStatusResponse,
|
||||||
IntelligenceSummary,
|
IntelligenceSummary,
|
||||||
ResourceIntelligence,
|
ResourceIntelligence,
|
||||||
} from '@/types/aiIntelligence';
|
} from '@/types/aiIntelligence';
|
||||||
|
|
||||||
|
|
||||||
export class AIAPI {
|
export class AIAPI {
|
||||||
private static baseUrl = '/api';
|
private static baseUrl = '/api';
|
||||||
|
|
||||||
|
|
@ -129,6 +131,12 @@ export class AIAPI {
|
||||||
return apiFetchJSON(`${this.baseUrl}/ai/intelligence/anomalies${params}`) as Promise<AnomaliesResponse>;
|
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
|
// Unified Intelligence API
|
||||||
// Aggregates all AI subsystems into a single view
|
// 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)
|
// 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 := len(result)
|
||||||
|
|
||||||
// Count by severity for summary
|
// Count by severity for summary
|
||||||
|
|
@ -744,18 +737,88 @@ func (h *AISettingsHandler) HandleGetAnomalies(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if locked {
|
// NOTE: Anomaly detection is FREE (no license required)
|
||||||
result = []map[string]interface{}{}
|
// 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{}{
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
"anomalies": result,
|
"anomalies": result,
|
||||||
"count": count,
|
"count": count,
|
||||||
"severity_counts": severityCounts,
|
"severity_counts": severityCounts,
|
||||||
"license_required": locked,
|
"license_required": false,
|
||||||
"upgrade_url": aiIntelligenceUpgradeURL,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to write anomalies response")
|
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/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/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/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
|
// Agent WebSocket for AI command execution
|
||||||
r.mux.HandleFunc("/api/agent/ws", r.handleAgentWebSocket)
|
r.mux.HandleFunc("/api/agent/ws", r.handleAgentWebSocket)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue