Files
doormile_backend/controllers/agentDecisionController.go
Suriya 7c55b523af Add AI agent decision memory layer with pgvector similarity search
- models/agentdecision.go: AgentDecision GORM model (context/decision as jsonb, reasoning as text)
- migrations/migrate.go: AutoMigrate AgentDecision then ALTER TABLE to add vector(1536) column and CREATE ivfflat index via raw SQL
- controllers/agentDecisionController.go: CreateAgentDecision, FindSimilarDecisions (cosine distance), UpdateDecisionOutcome
- routes/routes.go: three routes under /api/v1/internal (InternalKeyAuth applied at group level)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:57:53 +05:30

157 lines
4.4 KiB
Go

package controllers
import (
"encoding/json"
"strconv"
"strings"
"time"
"doormile/db"
"doormile/models"
"doormile/utils"
"github.com/gofiber/fiber/v2"
)
// POST /api/v1/internal/agent-decisions
func CreateAgentDecision(c *fiber.Ctx) error {
type req struct {
DecisionType string `json:"decision_type"`
BookingID *uint64 `json:"booking_id"`
Context map[string]interface{} `json:"context"`
Decision map[string]interface{} `json:"decision"`
Reasoning string `json:"reasoning"`
ContextEmbedding []float32 `json:"context_embedding"`
}
body := new(req)
if err := c.BodyParser(body); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if body.DecisionType == "" {
return utils.BadRequest(c, "decision_type is required")
}
contextJSON, err := json.Marshal(body.Context)
if err != nil {
return utils.BadRequest(c, "invalid context JSON")
}
decisionJSON, err := json.Marshal(body.Decision)
if err != nil {
return utils.BadRequest(c, "invalid decision JSON")
}
record := models.AgentDecision{
DecisionType: body.DecisionType,
BookingID: body.BookingID,
Context: string(contextJSON),
Decision: string(decisionJSON),
Reasoning: body.Reasoning,
CreatedAt: time.Now(),
}
if err := db.DB.Create(&record).Error; err != nil {
utils.Error("CreateAgentDecision: insert failed", "error", err)
return utils.Internal(c, "failed to create agent decision")
}
if len(body.ContextEmbedding) > 0 {
parts := make([]string, len(body.ContextEmbedding))
for i, v := range body.ContextEmbedding {
parts[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
}
embeddingStr := "[" + strings.Join(parts, ",") + "]"
if err := db.DB.Exec(
"UPDATE agent_decisions SET context_embedding = ? WHERE id = ?",
embeddingStr, record.ID,
).Error; err != nil {
utils.Warn("CreateAgentDecision: embedding update failed", "id", record.ID, "error", err)
}
}
return utils.Created(c, fiber.Map{"id": record.ID})
}
// GET /api/v1/internal/agent-decisions/similar
func FindSimilarDecisions(c *fiber.Ctx) error {
decisionType := c.Query("decision_type")
limit, err := strconv.Atoi(c.Query("limit", "5"))
if err != nil || limit < 1 {
limit = 5
}
type req struct {
Embedding []float32 `json:"embedding"`
}
body := new(req)
if err := c.BodyParser(body); err != nil || len(body.Embedding) == 0 {
return utils.BadRequest(c, "embedding array is required")
}
parts := make([]string, len(body.Embedding))
for i, v := range body.Embedding {
parts[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
}
embeddingStr := "[" + strings.Join(parts, ",") + "]"
type row struct {
Decision string `json:"decision"`
Reasoning string `json:"reasoning"`
Outcome *string `json:"outcome"`
Distance float64 `json:"distance"`
}
var results []row
if err := db.DB.Raw(
`SELECT decision, reasoning, outcome,
context_embedding <=> ? AS distance
FROM agent_decisions
WHERE decision_type = ? AND outcome IS NOT NULL
ORDER BY context_embedding <=> ?
LIMIT ?`,
embeddingStr, decisionType, embeddingStr, limit,
).Scan(&results).Error; err != nil {
utils.Error("FindSimilarDecisions: query failed", "error", err)
return utils.Internal(c, "failed to query similar decisions")
}
return utils.List(c, results, int64(len(results)))
}
// PATCH /api/v1/internal/agent-decisions/:id/outcome
func UpdateDecisionOutcome(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return utils.BadRequest(c, "invalid decision ID")
}
type req struct {
Outcome string `json:"outcome"`
}
body := new(req)
if err := c.BodyParser(body); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if body.Outcome == "" {
return utils.BadRequest(c, "outcome is required")
}
now := time.Now()
result := db.DB.Model(&models.AgentDecision{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"outcome": body.Outcome,
"outcome_recorded_at": now,
})
if result.Error != nil {
utils.Error("UpdateDecisionOutcome: update failed", "id", id, "error", result.Error)
return utils.Internal(c, "failed to update outcome")
}
if result.RowsAffected == 0 {
return utils.NotFound(c, "agent decision not found")
}
return utils.OK(c, fiber.Map{"id": id, "outcome": body.Outcome})
}