Pulse/internal/ai/baseline_adapter.go
rcourtman f3e95c24ae feat(ai): Add baseline learning and anomaly detection (Phase 2)
Phase 2 of Pulse AI differentiation:

- Create internal/ai/baseline package for learned baselines
- Implement statistical baseline learning with mean, stddev, percentiles
- Add z-score based anomaly detection with severity classification
  (low, medium, high, critical based on standard deviations)
- Integrate baseline provider into context builder
- Wire baseline store into patrol service with adapters
- Add anomaly enrichment to resource contexts

Key features:
- Learn computes baseline from historical metric data points
- IsAnomaly and CheckAnomaly detect deviations from normal
- Persists baselines to disk as JSON for durability
- Formatted anomaly descriptions for AI consumption
  Example: 'Memory is high above normal (85.2% vs typical 42.1% ± 8.3%)'

The baseline store needs to be initialized and triggered to learn
from metrics history. Next step is adding the learning loop.

All tests passing.
2025-12-12 11:26:31 +00:00

46 lines
1.2 KiB
Go

package ai
import (
"github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline"
)
// BaselineStoreAdapter adapts baseline.Store to the context.BaselineProvider interface
type BaselineStoreAdapter struct {
store *baseline.Store
}
// NewBaselineStoreAdapter creates an adapter for baseline.Store
func NewBaselineStoreAdapter(store *baseline.Store) *BaselineStoreAdapter {
if store == nil {
return nil
}
return &BaselineStoreAdapter{store: store}
}
// CheckAnomaly implements context.BaselineProvider
func (a *BaselineStoreAdapter) CheckAnomaly(resourceID, metric string, value float64) (severity string, zScore float64, mean float64, stddev float64, ok bool) {
if a.store == nil {
return "", 0, 0, 0, false
}
s, z, b := a.store.CheckAnomaly(resourceID, metric, value)
if b == nil {
return "", 0, 0, 0, false
}
return string(s), z, b.Mean, b.StdDev, true
}
// GetBaseline implements context.BaselineProvider
func (a *BaselineStoreAdapter) GetBaseline(resourceID, metric string) (mean float64, stddev float64, sampleCount int, ok bool) {
if a.store == nil {
return 0, 0, 0, false
}
b, exists := a.store.GetBaseline(resourceID, metric)
if !exists || b == nil {
return 0, 0, 0, false
}
return b.Mean, b.StdDev, b.SampleCount, true
}