updates on the ai agents and time series prediction and updates on the api to
This commit is contained in:
@@ -415,6 +415,13 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
adminAuth.Put("/bookings/:id/status", controllers.AdminUpdateBookingStatus)
|
||||
adminAuth.Post("/bookings/:id/cancel", controllers.AdminCancelBooking)
|
||||
adminAuth.Post("/bookings/bulk-cancel", controllers.AdminBulkCancelBookings)
|
||||
// Batch assign, admin side. The same solver /hub/bookings/batch-assign
|
||||
// uses (controllers/batchAssignService.go), with admin auth and scoped to
|
||||
// the login's own tenant. This is what backs the console ops layer's
|
||||
// `assignMiler` proposal, which had no executor because the hub route
|
||||
// 403s for every admin token and /bookings/:id/assign-miler needs a rider
|
||||
// the finding does not pick.
|
||||
adminAuth.Post("/bookings/batch-assign", middlewares.DoormileStaffOnly, controllers.AdminBatchAssign)
|
||||
|
||||
// Consignments
|
||||
adminAuth.Get("/consignments", controllers.GetAdminConsignments)
|
||||
@@ -486,6 +493,23 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
aiRegistry.Patch("/skills/:id", middlewares.RoleCheckMiddleware(1), controllers.PatchAISkill)
|
||||
aiRegistry.Get("/tools", controllers.GetAITools)
|
||||
aiRegistry.Get("/audit", controllers.GetAIRegistryAudit)
|
||||
// What the console's rule skills have been noticing. The findings used to
|
||||
// exist for one render in the operator's browser and then vanish, so
|
||||
// nothing could say whether a skill was useful or whether acting on a
|
||||
// finding cleared it. Writes are open to the same roles as reads here:
|
||||
// the console reports what it evaluated, it is not an operator action.
|
||||
aiRegistry.Get("/findings", controllers.GetAIFindings)
|
||||
aiRegistry.Get("/findings/stats", controllers.GetAIFindingStats)
|
||||
aiRegistry.Post("/findings", controllers.UpsertAIFindings)
|
||||
aiRegistry.Post("/findings/:fingerprint/acted", controllers.RecordAIFindingActed)
|
||||
// Tomorrow's expected pickups per zone, with the staffing gap against
|
||||
// riders actually on duty. A bare forecast number is not actionable — the
|
||||
// gap is (docs/prediction-plan.md §2.4).
|
||||
aiRegistry.Get("/forecast/demand", controllers.GetDemandForecast)
|
||||
// The engine's live agent state, proxied. The console's Agents page cannot
|
||||
// reach :8700 itself (ClusterIP, and it is a browser). A 503 here means
|
||||
// "use the snapshot", not "broken" — see the controller.
|
||||
aiRegistry.Get("/engine/agents", controllers.GetAIEngineAgents)
|
||||
// What the agents did (Phase 4): runs from AI_engine telemetry, decisions
|
||||
// from agent_decisions, live heartbeat from Redis. Read-only.
|
||||
aiRegistry.Get("/insights", controllers.GetAIInsights)
|
||||
@@ -591,10 +615,19 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
internal.Post("/notify", controllers.InternalNotify)
|
||||
internal.Post("/bookings/:id/reassign", controllers.InternalReassign)
|
||||
internal.Post("/agent-decisions", controllers.CreateAgentDecision)
|
||||
internal.Get("/agent-decisions/similar", controllers.FindSimilarDecisions)
|
||||
// POST, not GET: the handler needs a 1536-float embedding in the body, and
|
||||
// a GET with a body is dropped by nginx and most HTTP clients — and this
|
||||
// API is served THROUGH host nginx today (conf/nginx-doormile.conf), so it
|
||||
// would not merely be risky, it would not work. Nothing called it while it
|
||||
// was a GET, so changing the method breaks no caller.
|
||||
internal.Post("/agent-decisions/similar", controllers.FindSimilarDecisions)
|
||||
internal.Patch("/agent-decisions/:id/outcome", controllers.UpdateDecisionOutcome)
|
||||
// The agent registry, for AI_engine to poll (ETag / If-None-Match → 304).
|
||||
internal.Get("/ai/registry", controllers.GetInternalAIRegistry)
|
||||
// The demand forecast, written by AI_engine's forecast job. The model lives
|
||||
// in Python (prophet where it beats a seasonal baseline, the baseline
|
||||
// otherwise); the backend stores and serves it.
|
||||
internal.Post("/demand-forecast", controllers.UpsertDemandForecast)
|
||||
|
||||
// Express-batch dispatch: the ExpressDispatchAgent reads a tenant's riders
|
||||
// and the batch's bookings, then writes back the assignments it decided.
|
||||
|
||||
@@ -36,6 +36,14 @@ var aiReads = []string{
|
||||
"/api/v1/admin/ai/insights?days=30",
|
||||
"/api/v1/admin/ai/decisions",
|
||||
"/api/v1/admin/ai/status",
|
||||
// Added with the agent-platform work. Listed here so the existing
|
||||
// invariants cover them: every /admin/ai read must require a login and
|
||||
// must refuse miler, hub-staff and customer roles. A new route that skips
|
||||
// this table is a new route nobody proved is gated.
|
||||
"/api/v1/admin/ai/findings",
|
||||
"/api/v1/admin/ai/findings/stats",
|
||||
"/api/v1/admin/ai/forecast/demand",
|
||||
"/api/v1/admin/ai/engine/agents",
|
||||
}
|
||||
|
||||
var aiWrites = []struct{ method, path, body string }{
|
||||
@@ -45,6 +53,46 @@ var aiWrites = []struct{ method, path, body string }{
|
||||
{http.MethodPost, "/api/v1/admin/ai/playground/run", `{"agentid":"EXCEPTION_AGENT","prompt":"hi"}`},
|
||||
}
|
||||
|
||||
// Finding reports are NOT registry mutations, and deliberately not role-1-only.
|
||||
//
|
||||
// The aiWrites table above encodes "changing the registry is an operator
|
||||
// decision, so role 1 only". Reporting what the rule skills noticed is a
|
||||
// different thing: it is telemetry from the Exceptions page, which managers (3)
|
||||
// and executives (4) open as part of their job. Gating it to role 1 would mean
|
||||
// a manager's session silently reported nothing, and the "how long has this
|
||||
// finding been open" measurement would depend on who happened to be logged in.
|
||||
//
|
||||
// What it must still refuse is everyone outside the console: miler, hub staff
|
||||
// and customer tokens.
|
||||
var aiFindingWrites = []struct{ method, path, body string }{
|
||||
{http.MethodPost, "/api/v1/admin/ai/findings", `{"findings":[],"cleared":[]}`},
|
||||
{http.MethodPost, "/api/v1/admin/ai/findings/abc/acted", `{"result":"ok"}`},
|
||||
}
|
||||
|
||||
func TestFindingReportsAreOpenToConsoleRolesButNotOutsiders(t *testing.T) {
|
||||
app := newApp()
|
||||
for _, w := range aiFindingWrites {
|
||||
// No token at all is refused.
|
||||
if code, _ := do(t, app, w.method, w.path, "", w.body); code != http.StatusUnauthorized {
|
||||
t.Errorf("%s %s with no token = %d, want 401", w.method, w.path, code)
|
||||
}
|
||||
// Outside the console: refused.
|
||||
for _, role := range []int{5, 6, 9} { // miler, hub staff, customer
|
||||
if code, _ := do(t, app, w.method, w.path, token(t, 1, role), w.body); code != http.StatusForbidden && code != http.StatusUnauthorized {
|
||||
t.Errorf("%s %s as role %d = %d, want 401/403", w.method, w.path, role, code)
|
||||
}
|
||||
}
|
||||
// Console roles: NOT forbidden. The handler may still fail without a
|
||||
// database in this app; what matters here is that authorisation let it
|
||||
// through rather than stopping it.
|
||||
for _, role := range []int{1, 3, 4} {
|
||||
if code, _ := do(t, app, w.method, w.path, token(t, 1, role), w.body); code == http.StatusForbidden {
|
||||
t.Errorf("%s %s as role %d = 403; the Exceptions page must be able to report findings", w.method, w.path, role)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIRegistryRequiresALogin(t *testing.T) {
|
||||
app := newApp()
|
||||
for _, p := range aiReads {
|
||||
|
||||
@@ -54,7 +54,7 @@ func onboardingDB(t *testing.T) *gorm.DB {
|
||||
|
||||
func onboardBody(extra string) string {
|
||||
return `{"companyname":"Peelamedu Provisions","contactname":"Priya Raman","email":"ops@peelamedu.test",
|
||||
"phone":"9876543210","password":"Strong-pass-1","applocationid":1` + extra + `}`
|
||||
"phone":"9876543210","password":"Strong-pass-1","applocationid":1,"deliverycategory":"Clothing"` + extra + `}`
|
||||
}
|
||||
|
||||
const goodAddress = `,"address":"14 DB Road, RS Puram, Coimbatore","city":"Coimbatore","state":"Tamil Nadu",
|
||||
|
||||
Reference in New Issue
Block a user