updates on the airegistry and controllers and test files are been integrated
This commit is contained in:
@@ -205,6 +205,9 @@ func GetInternalAIRegistry(c *fiber.Ctx) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Proof the engine is following the registry — see GetAIStatus. Counted
|
||||||
|
// for 304s too: an unchanged registry is still a successful read.
|
||||||
|
markRegistryRead()
|
||||||
tag := registry.ETag(snap)
|
tag := registry.ETag(snap)
|
||||||
c.Set(fiber.HeaderETag, tag)
|
c.Set(fiber.HeaderETag, tag)
|
||||||
c.Set(fiber.HeaderCacheControl, "no-cache")
|
c.Set(fiber.HeaderCacheControl, "no-cache")
|
||||||
|
|||||||
100
controllers/aiStatusController.go
Normal file
100
controllers/aiStatusController.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
20
controllers/aiStatus_test.go
Normal file
20
controllers/aiStatus_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Without Redis (as in this test binary) the in-process stamp still works, so
|
||||||
|
// a single replica reports the engine's reads correctly.
|
||||||
|
func TestRegistryReadStampWithoutRedis(t *testing.T) {
|
||||||
|
lastRegistryRead.Store(0)
|
||||||
|
if registryLastRead() != nil {
|
||||||
|
t.Fatal("no read yet must report nil")
|
||||||
|
}
|
||||||
|
markRegistryRead()
|
||||||
|
got := registryLastRead()
|
||||||
|
if got == nil || time.Since(*got) > 5*time.Second {
|
||||||
|
t.Fatalf("stamp not recorded: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -465,6 +465,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
// from agent_decisions, live heartbeat from Redis. Read-only.
|
// from agent_decisions, live heartbeat from Redis. Read-only.
|
||||||
aiRegistry.Get("/insights", controllers.GetAIInsights)
|
aiRegistry.Get("/insights", controllers.GetAIInsights)
|
||||||
aiRegistry.Get("/decisions", controllers.GetAIDecisions)
|
aiRegistry.Get("/decisions", controllers.GetAIDecisions)
|
||||||
|
// What is actually wired (engine reading the registry, Test tab model), for
|
||||||
|
// the Skills & Tools banner.
|
||||||
|
aiRegistry.Get("/status", controllers.GetAIStatus)
|
||||||
// Test tab (Phase 6): one prompt through Claude with a skill's tools. Reads
|
// Test tab (Phase 6): one prompt through Claude with a skill's tools. Reads
|
||||||
// run redacted; writes only become proposals. Admin only — every run is a
|
// run redacted; writes only become proposals. Admin only — every run is a
|
||||||
// paid API call — and rate-limited per user in the handler.
|
// paid API call — and rate-limited per user in the handler.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ var aiReads = []string{
|
|||||||
"/api/v1/admin/ai/insights",
|
"/api/v1/admin/ai/insights",
|
||||||
"/api/v1/admin/ai/insights?days=30",
|
"/api/v1/admin/ai/insights?days=30",
|
||||||
"/api/v1/admin/ai/decisions",
|
"/api/v1/admin/ai/decisions",
|
||||||
|
"/api/v1/admin/ai/status",
|
||||||
}
|
}
|
||||||
|
|
||||||
var aiWrites = []struct{ method, path, body string }{
|
var aiWrites = []struct{ method, path, body string }{
|
||||||
|
|||||||
Reference in New Issue
Block a user