package handler import ( "errors" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/warmbly/warmbly/internal/api/middleware" "github.com/warmbly/warmbly/internal/app/warmupcontent" "github.com/warmbly/warmbly/internal/errx" "github.com/warmbly/warmbly/internal/models" ) // submitBatchRequest is the admin payload for an async Batch API generation run. // Admins control every parameter; sensible service-side defaults apply when a // field is omitted (pool_type=premium, count=100, completion_window=24h, model // and max_messages from the generation settings). type submitBatchRequest struct { PoolType string `json:"pool_type"` Segment string `json:"segment"` Theme string `json:"theme"` Themes []string `json:"themes"` Model string `json:"model"` Count int `json:"count"` MaxMessages int `json:"max_messages"` CompletionWindow string `json:"completion_window"` } // AdminSubmitWarmupBatch submits an async OpenAI Batch API generation run. // // The Batch API is ~50% cheaper than synchronous generation and processes // asynchronously (up to the completion window, typically 24h). Results are // ingested into the content bank by the batch poller once OpenAI finishes. // // When a non-empty `themes` array is supplied, one batch job is submitted per // theme (each gets `count` threads of that theme); otherwise a single batch // runs with the optional pinned `theme` (empty = rotate the default theme set). func (h *Handler) AdminSubmitWarmupBatch(c *gin.Context) { if h.WarmupContentService == nil { errx.JSON(c, errx.New(errx.BadRequest, "warmup generation is not configured")) return } var req submitBatchRequest if err := c.ShouldBindJSON(&req); err != nil { errx.JSON(c, errx.New(errx.BadRequest, "invalid request body")) return } adminID := middleware.GetAdminUserID(c) // Normalise the theme set: explicit `themes` wins; otherwise fall back to the // single pinned `theme` (which may be empty → service rotates defaults). themes := make([]string, 0, len(req.Themes)) for _, t := range req.Themes { if t != "" { themes = append(themes, t) } } if len(themes) == 0 { themes = []string{req.Theme} } jobIDs := make([]uuid.UUID, 0, len(themes)) for _, theme := range themes { jobID, err := h.WarmupContentService.GenerateBatch(c.Request.Context(), warmupcontent.GenerateRequest{ RequestedBy: adminID, Trigger: "manual", PoolType: req.PoolType, Segment: req.Segment, Theme: theme, Model: req.Model, Count: req.Count, MaxMessages: req.MaxMessages, CompletionWindow: req.CompletionWindow, }) if err != nil { if errors.Is(err, warmupcontent.ErrNotConfigured) { errx.JSON(c, errx.New(errx.BadRequest, "warmup AI generation is not configured (set OPENAI_API_KEY)")) return } errx.JSON(c, errx.New(errx.BadRequest, err.Error())) return } jobIDs = append(jobIDs, jobID) } for _, id := range jobIDs { jid := id h.audit(c, models.AuditActionCreate, warmupContentEntity, &jid, map[string]string{ "mode": "batch", "pool_type": req.PoolType, "segment": req.Segment, "count": strconv.Itoa(req.Count), }) } c.JSON(http.StatusOK, gin.H{"job_ids": jobIDs}) } // AdminCancelWarmupBatch cancels an in-flight batch generation job (OpenAI + the // local job row). func (h *Handler) AdminCancelWarmupBatch(c *gin.Context) { if h.WarmupContentService == nil { errx.JSON(c, errx.New(errx.BadRequest, "warmup generation is not configured")) return } id, err := uuid.Parse(c.Param("id")) if err != nil { errx.JSON(c, errx.New(errx.BadRequest, "invalid id")) return } if err := h.WarmupContentService.CancelBatch(c.Request.Context(), id); err != nil { if errors.Is(err, warmupcontent.ErrNotConfigured) { errx.JSON(c, errx.New(errx.BadRequest, "warmup AI generation is not configured (set OPENAI_API_KEY)")) return } errx.JSON(c, errx.New(errx.BadRequest, err.Error())) return } h.audit(c, models.AuditActionUpdate, warmupContentEntity, &id, map[string]string{"action": "cancel_batch"}) c.JSON(http.StatusOK, gin.H{"ok": true}) }