Add AI job management endpoints
This commit is contained in:
@@ -3,6 +3,7 @@ package httpapi
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -37,8 +38,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"service": "ai-service"})
|
||||
case r.Method == http.MethodPost && path == "/api/v1/jobs":
|
||||
s.handleCreateJob(w, r)
|
||||
case r.Method == http.MethodGet && path == "/api/v1/jobs":
|
||||
s.handleListJobs(w, r)
|
||||
case r.Method == http.MethodPost && path == "/api/v1/jobs/batch":
|
||||
s.handleCreateBatch(w, r)
|
||||
case r.Method == http.MethodPost && path == "/api/v1/jobs/retry":
|
||||
s.handleRetryJobs(w, r)
|
||||
case r.Method == http.MethodPost && path == "/api/v1/jobs/cancel":
|
||||
s.handleCancelJobs(w, r)
|
||||
case r.Method == http.MethodPost && path == "/api/v1/jobs/claim":
|
||||
s.handleClaimJobs(w, r)
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(path, "/api/v1/jobs/"):
|
||||
@@ -88,6 +95,22 @@ func (s *Server) handleCreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, job)
|
||||
}
|
||||
|
||||
type listJobsResponse struct {
|
||||
Jobs []*model.Job `json:"jobs"`
|
||||
}
|
||||
|
||||
func (s *Server) handleListJobs(w http.ResponseWriter, r *http.Request) {
|
||||
filter := jobFilterFromQuery(r)
|
||||
ctx, cancel := contextWithTimeout(r, 8*time.Second)
|
||||
defer cancel()
|
||||
jobs, err := s.store.ListJobs(ctx, filter)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, listJobsResponse{Jobs: jobs})
|
||||
}
|
||||
|
||||
type createBatchRequest struct {
|
||||
OwnerService string `json:"owner_service"`
|
||||
TaskType string `json:"task_type"`
|
||||
@@ -144,6 +167,46 @@ func (s *Server) handleCreateBatch(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, out)
|
||||
}
|
||||
|
||||
func (s *Server) handleRetryJobs(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.JobFilter
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad json")
|
||||
return
|
||||
}
|
||||
if !hasAnyFilter(req) {
|
||||
writeError(w, http.StatusBadRequest, "at least one filter is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := contextWithTimeout(r, 20*time.Second)
|
||||
defer cancel()
|
||||
updated, err := s.store.RetryJobs(ctx, req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, model.BatchActionResult{Updated: updated})
|
||||
}
|
||||
|
||||
func (s *Server) handleCancelJobs(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.JobFilter
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad json")
|
||||
return
|
||||
}
|
||||
if !hasAnyFilter(req) {
|
||||
writeError(w, http.StatusBadRequest, "at least one filter is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := contextWithTimeout(r, 20*time.Second)
|
||||
defer cancel()
|
||||
updated, err := s.store.CancelJobs(ctx, req)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, model.BatchActionResult{Updated: updated})
|
||||
}
|
||||
|
||||
type claimJobsResponse struct {
|
||||
Jobs []*model.Job `json:"jobs"`
|
||||
}
|
||||
@@ -291,3 +354,51 @@ func isValidationError(err error) bool {
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, " is required")
|
||||
}
|
||||
|
||||
func jobFilterFromQuery(r *http.Request) model.JobFilter {
|
||||
q := r.URL.Query()
|
||||
return model.JobFilter{
|
||||
OwnerService: q.Get("owner_service"),
|
||||
OwnerRef: q.Get("owner_ref"),
|
||||
TaskType: q.Get("task_type"),
|
||||
ModelProfile: q.Get("model_profile"),
|
||||
Statuses: splitCSV(q.Get("status")),
|
||||
ErrorCodes: splitCSV(q.Get("error_code")),
|
||||
Limit: parseIntDefault(q.Get("limit"), 100),
|
||||
Offset: parseIntDefault(q.Get("offset"), 0),
|
||||
}
|
||||
}
|
||||
|
||||
func splitCSV(raw string) []string {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if v := strings.TrimSpace(part); v != "" {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseIntDefault(raw string, fallback int) int {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return fallback
|
||||
}
|
||||
var out int
|
||||
if _, err := fmt.Sscanf(raw, "%d", &out); err != nil {
|
||||
return fallback
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hasAnyFilter(filter model.JobFilter) bool {
|
||||
return strings.TrimSpace(filter.OwnerService) != "" ||
|
||||
strings.TrimSpace(filter.OwnerRef) != "" ||
|
||||
strings.TrimSpace(filter.TaskType) != "" ||
|
||||
strings.TrimSpace(filter.ModelProfile) != "" ||
|
||||
len(filter.Statuses) > 0 ||
|
||||
len(filter.ErrorCodes) > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user