128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"doormile/db"
|
|
"doormile/internal/ai/playground"
|
|
"doormile/utils"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// POST /admin/ai/playground/run — Agent Studio's Test tab. Runs one prompt
|
|
// through the configured model with a registry skill's tools; see internal/ai/playground for
|
|
// what executes and what only becomes a proposal. Staff only, roleid 1 only,
|
|
// and rate-limited per user because every run is a paid API call.
|
|
|
|
// PlaygroundModel is the model client the playground uses (an OpenAI-compatible
|
|
// provider such as Groq, see main.go). Nil until PLAYGROUND_LLM_API_KEY is set; the endpoint then answers 503 and the console keeps the
|
|
// Test tab labelled as unavailable.
|
|
var PlaygroundModel playground.Model
|
|
|
|
const (
|
|
playgroundRunTimeout = 120 * time.Second
|
|
playgroundRunsPerWin = 10
|
|
playgroundWindow = 10 * time.Minute
|
|
)
|
|
|
|
type playgroundLimiter struct {
|
|
mu sync.Mutex
|
|
runs map[string][]time.Time
|
|
}
|
|
|
|
var playgroundRuns = &playgroundLimiter{runs: map[string][]time.Time{}}
|
|
|
|
// allow records a run for key and reports whether it is within the limit.
|
|
func (l *playgroundLimiter) allow(key string, now time.Time) bool {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
kept := l.runs[key][:0]
|
|
for _, t := range l.runs[key] {
|
|
if now.Sub(t) < playgroundWindow {
|
|
kept = append(kept, t)
|
|
}
|
|
}
|
|
if len(kept) >= playgroundRunsPerWin {
|
|
l.runs[key] = kept
|
|
return false
|
|
}
|
|
l.runs[key] = append(kept, now)
|
|
return true
|
|
}
|
|
|
|
// RunAIPlayground — POST /admin/ai/playground/run {agentid, skillid?, prompt}
|
|
func RunAIPlayground(c *fiber.Ctx) error {
|
|
if PlaygroundModel == nil {
|
|
return utils.Fail(c, fiber.StatusServiceUnavailable, "PLAYGROUND_NOT_CONFIGURED",
|
|
"The Test playground has no model configured on this server.")
|
|
}
|
|
|
|
var body struct {
|
|
Agentid string `json:"agentid"`
|
|
Skillid string `json:"skillid"`
|
|
Prompt string `json:"prompt"`
|
|
}
|
|
if err := c.BodyParser(&body); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
body.Prompt = strings.TrimSpace(body.Prompt)
|
|
if body.Agentid == "" || body.Prompt == "" {
|
|
return utils.BadRequest(c, "agentid and prompt are required")
|
|
}
|
|
if utf8.RuneCountInString(body.Prompt) > playground.MaxPromptChars {
|
|
return utils.BadRequest(c, "prompt is too long (at most 2000 characters)")
|
|
}
|
|
|
|
actor := actorOf(c)
|
|
key := actor.Email
|
|
if key == "" {
|
|
key = "user:" + strconv.Itoa(actor.UserID)
|
|
}
|
|
if !playgroundRuns.allow(key, time.Now()) {
|
|
return utils.Fail(c, fiber.StatusTooManyRequests, "PLAYGROUND_RATE_LIMITED",
|
|
"Playground limit reached: 10 runs per 10 minutes. Try again shortly.")
|
|
}
|
|
|
|
snap, ok := loadRegistry(c)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
plan, err := playground.Prepare(snap, body.Agentid, body.Skillid)
|
|
if errors.Is(err, playground.ErrNotFound) {
|
|
return utils.NotFound(c, err.Error())
|
|
}
|
|
if err != nil {
|
|
return utils.BadRequest(c, err.Error())
|
|
}
|
|
|
|
// An OpenAI-compatible provider serves its own configured model, not the
|
|
// agent's registry pin (a Claude id AI_engine uses); report the real one.
|
|
if named, ok := PlaygroundModel.(interface{ ModelName() string }); ok {
|
|
plan.Model = named.ModelName()
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), playgroundRunTimeout)
|
|
defer cancel()
|
|
trace, err := playground.Run(ctx, PlaygroundModel, plan, body.Prompt, playground.Executors(db.DB, db.Rdb))
|
|
utils.Info("ai playground run", "email", actor.Email, "agent", plan.AgentID, "skill", plan.SkillID,
|
|
"model", plan.Model, "turns", trace.Turns, "ms", trace.Ms, "failed", err != nil)
|
|
if err != nil {
|
|
utils.Error("ai playground: model call failed", "error", err.Error())
|
|
var pe *playground.ProviderError
|
|
if errors.As(err, &pe) && pe.Status == fiber.StatusTooManyRequests {
|
|
return utils.Fail(c, fiber.StatusTooManyRequests, "PLAYGROUND_PROVIDER_RATE_LIMITED",
|
|
"The model provider's rate limit was reached (common on free plans). Wait a minute and try again.")
|
|
}
|
|
return utils.Fail(c, fiber.StatusBadGateway, "PLAYGROUND_MODEL_FAILED",
|
|
"The model request failed; nothing was changed. Try again.")
|
|
}
|
|
return utils.OK(c, trace)
|
|
}
|