Files
doormile_backend/controllers/aiStatusController.go

101 lines
2.9 KiB
Go

package controllers
import (
"context"
"strconv"
"sync/atomic"
"time"
"doormile/db"
"doormile/internal/ai/telemetry"
"doormile/models"
"doormile/utils"
"github.com/gofiber/fiber/v2"
)
// GET /admin/ai/status — what is actually wired, for the banner on Settings →
// Skills & Tools. Replaces a fixed "Half wired" note that stayed on screen
// after everything was deployed, because nothing on the page checked.
//
// "AI_engine reads these settings" is observed, not assumed: every poll of
// GET /internal/ai/registry (a 200 or a 304) stamps the time in Redis, shared by
// every backend replica. The engine polls about every 30 seconds, so a stamp
// younger than engineReadFreshFor means it is following the registry now.
const (
registryReadKey = "ai:registry:lastread"
engineReadFreshFor = 5 * time.Minute
)
// lastRegistryRead is the in-process copy, used when Redis is unavailable.
var lastRegistryRead atomic.Int64
// markRegistryRead records an AI_engine registry poll. Best effort: a Redis
// failure must never fail the poll itself.
func markRegistryRead() {
now := time.Now().Unix()
lastRegistryRead.Store(now)
if db.Rdb == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
_ = db.Rdb.Set(ctx, registryReadKey, now, 30*time.Minute).Err()
}
// registryLastRead returns the latest poll time seen by any replica, or nil.
func registryLastRead() *time.Time {
secs := lastRegistryRead.Load()
if db.Rdb != nil {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if v, err := db.Rdb.Get(ctx, registryReadKey).Result(); err == nil {
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > secs {
secs = n
}
}
}
if secs == 0 {
return nil
}
t := time.Unix(secs, 0)
return &t
}
type aiStatus struct {
Engine struct {
ReadingSettings bool `json:"readingsettings"`
LastReadAt *time.Time `json:"lastreadat"`
Telemetry bool `json:"telemetry"`
LiveAgents int `json:"liveagents"`
} `json:"engine"`
Playground struct {
Configured bool `json:"configured"`
Model string `json:"model,omitempty"`
} `json:"playground"`
}
// GetAIStatus — GET /admin/ai/status
func GetAIStatus(c *fiber.Ctx) error {
var s aiStatus
s.Engine.LastReadAt = registryLastRead()
s.Engine.ReadingSettings = s.Engine.LastReadAt != nil && time.Since(*s.Engine.LastReadAt) < engineReadFreshFor
s.Engine.Telemetry = telemetry.Receiving.Load()
var engineAgents []string
if db.DB != nil {
db.DB.Model(&models.AIAgent{}).Where("runtime = ?", "engine").Pluck("agentid", &engineAgents)
}
s.Engine.LiveAgents = len(telemetry.LiveStates(db.Rdb, engineAgents))
if PlaygroundModel != nil {
s.Playground.Configured = true
if named, ok := PlaygroundModel.(interface{ ModelName() string }); ok {
s.Playground.Model = named.ModelName()
}
}
return utils.OK(c, s)
}