88 lines
3.3 KiB
Go
88 lines
3.3 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"doormile/utils"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// Proxying AI_engine's live agent state to the console.
|
|
//
|
|
// The console's Agents page has run on a hand-maintained snapshot
|
|
// (krow_talent_app/src/lib/agentNetwork.js, dated 16-20 Sep) because the engine
|
|
// exposed no endpoint it could read. The engine now serves GET /agents/status
|
|
// from core/health.py — but the console cannot call it directly: that surface
|
|
// is a ClusterIP Service on port 8700, and the console is a browser.
|
|
//
|
|
// So this proxies it. Staff-only, like the rest of /admin/ai.
|
|
//
|
|
// ─── Fails soft, on purpose ────────────────────────────────────────────────
|
|
//
|
|
// A 503 here means "ask the snapshot", not "something is broken". The engine is
|
|
// not deployed in Kubernetes yet, AI_ENGINE_BASE_URL is empty by default, and
|
|
// the Agents page must keep rendering in all of those cases. The console reads
|
|
// the status code and falls back; it never shows an error for this.
|
|
//
|
|
// Short timeout for the same reason: an unreachable engine must not hold a
|
|
// console request open. Two seconds is longer than an in-cluster call needs and
|
|
// shorter than anyone will wait.
|
|
|
|
// AIEngineBaseURL is set at boot from config. Empty disables the proxy.
|
|
var AIEngineBaseURL string
|
|
|
|
var engineClient = &http.Client{Timeout: 2 * time.Second}
|
|
|
|
// GET /api/v1/admin/ai/engine/agents
|
|
func GetAIEngineAgents(c *fiber.Ctx) error {
|
|
if AIEngineBaseURL == "" {
|
|
// Not an error: the engine has no in-cluster address configured, which
|
|
// is the default and is true today. The console falls back.
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
|
"code": "AI_ENGINE_NOT_CONFIGURED",
|
|
"message": "AI_ENGINE_BASE_URL is not set; the console should use its snapshot.",
|
|
})
|
|
}
|
|
|
|
resp, err := engineClient.Get(AIEngineBaseURL + "/agents/status")
|
|
if err != nil {
|
|
utils.Warn("GetAIEngineAgents: engine unreachable", "error", err)
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
|
"code": "AI_ENGINE_UNREACHABLE",
|
|
"message": "The engine did not answer; the console should use its snapshot.",
|
|
})
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Cap the read. This is a trusted in-cluster service, but a proxy that
|
|
// streams an unbounded body into a console response is a bad shape
|
|
// regardless of who is on the other end.
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
|
"code": "AI_ENGINE_UNREADABLE", "message": "Could not read the engine's response.",
|
|
})
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
// The engine answers 503 on /agents/status when it has no agent state
|
|
// (a non-production run mode). Pass that through rather than
|
|
// reinterpreting it.
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
|
"code": "AI_ENGINE_NO_STATE",
|
|
"message": "The engine is running but reports no agent state.",
|
|
})
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
|
"code": "AI_ENGINE_BAD_JSON", "message": "The engine's response was not JSON.",
|
|
})
|
|
}
|
|
return utils.OK(c, payload)
|
|
}
|