Files
doormile_backend/controllers/aiEngineProxyController.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)
}