mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 16:01:16 +00:00
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
// AI contact research endpoints. A run researches one contact on the public web
|
|
// and saves cited findings; it charges 2 credits per run (billable even when it
|
|
// finds nothing). Sync runs execute in the request; the batch endpoint queues
|
|
// and drains in the background. Gated by the AI_RESEARCH scope (JWT callers by
|
|
// the matching contact permission).
|
|
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/app/aitools"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
)
|
|
|
|
// aiActorInvocation builds a minimal invocation for AI feature endpoints. The
|
|
// route middleware already gated access; the research tools (search_web,
|
|
// fetch_url) require no permission, so only identity is needed here.
|
|
func (h *Handler) aiActorInvocation(c *gin.Context) (aitools.Invocation, *errx.Error) {
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
return aitools.Invocation{}, errx.New(errx.BadRequest, "no organization selected")
|
|
}
|
|
userID, _ := middleware.GetUserUUID(c) // uuid.Nil for API-key callers
|
|
return aitools.Invocation{
|
|
OrgID: *orgID,
|
|
UserID: userID,
|
|
IsAPIKey: middleware.GetAPIKeyPermissions(c) != 0,
|
|
APIPerms: middleware.GetAPIKeyPermissions(c),
|
|
IP: c.ClientIP(),
|
|
UserAgent: c.Request.UserAgent(),
|
|
}, nil
|
|
}
|
|
|
|
// ResearchContact — POST /contacts/:id/research (sync)
|
|
func (h *Handler) ResearchContact(c *gin.Context) {
|
|
if h.ResearchService == nil {
|
|
errx.JSON(c, errx.New(errx.ServiceUnavailable, "AI research is not configured"))
|
|
return
|
|
}
|
|
inv, xerr := h.aiActorInvocation(c)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
contactID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid contact id"))
|
|
return
|
|
}
|
|
var req struct {
|
|
Objective string `json:"objective"`
|
|
}
|
|
_ = c.ShouldBindJSON(&req)
|
|
|
|
idemKey := strings.TrimSpace(c.GetHeader("Idempotency-Key"))
|
|
run, rerr := h.ResearchService.RunResearch(c.Request.Context(), inv, contactID, req.Objective, idemKey)
|
|
if rerr != nil {
|
|
errx.JSON(c, rerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, run)
|
|
}
|
|
|
|
// ListContactResearch — GET /contacts/:id/research
|
|
func (h *Handler) ListContactResearch(c *gin.Context) {
|
|
if h.ResearchService == nil {
|
|
errx.JSON(c, errx.New(errx.ServiceUnavailable, "AI research is not configured"))
|
|
return
|
|
}
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
|
|
return
|
|
}
|
|
contactID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid contact id"))
|
|
return
|
|
}
|
|
limit, xerr := parseCreditLimit(c.Query("limit"), 20, 100)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
runs, rerr := h.ResearchService.ListRuns(c.Request.Context(), *orgID, contactID, limit)
|
|
if rerr != nil {
|
|
errx.JSON(c, rerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": runs})
|
|
}
|
|
|
|
// BatchResearch — POST /contacts/research/batch
|
|
func (h *Handler) BatchResearch(c *gin.Context) {
|
|
if h.ResearchService == nil {
|
|
errx.JSON(c, errx.New(errx.ServiceUnavailable, "AI research is not configured"))
|
|
return
|
|
}
|
|
inv, xerr := h.aiActorInvocation(c)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
var req struct {
|
|
ContactIDs []uuid.UUID `json:"contact_ids" binding:"required"`
|
|
Objective string `json:"objective"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
|
|
return
|
|
}
|
|
queued, rerr := h.ResearchService.Batch(c.Request.Context(), inv, req.ContactIDs, req.Objective)
|
|
if rerr != nil {
|
|
errx.JSON(c, rerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"queued": queued})
|
|
}
|