Add a minimal HTTP API
This commit is contained in:
145
internal/adapter/http/handler.go
Normal file
145
internal/adapter/http/handler.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package httpadapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/profile"
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/usecase"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, req domain.RunRequest) (*domain.RunResult, error)
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
runner Runner
|
||||
}
|
||||
|
||||
func NewHandler(runner Runner) *Handler {
|
||||
return &Handler{runner: runner}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/runs" {
|
||||
writeError(w, http.StatusNotFound, "not_found", "route not found")
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var req runRequestDTO
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_json", fmt.Sprintf("invalid JSON request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.ProfileID) == "" {
|
||||
writeError(w, http.StatusBadRequest, "invalid_request", "profile_id is required")
|
||||
return
|
||||
}
|
||||
if len(req.Inputs) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_request", "inputs is required")
|
||||
return
|
||||
}
|
||||
|
||||
mappedInputs := make(map[string]domain.ArtifactRef, len(req.Inputs))
|
||||
for name, in := range req.Inputs {
|
||||
mappedInputs[name] = domain.ArtifactRef{
|
||||
Type: domain.ArtifactRefType(in.Type),
|
||||
URI: in.URI,
|
||||
Body: in.Body,
|
||||
}
|
||||
}
|
||||
|
||||
var model *domain.ModelTarget
|
||||
if req.Model != nil {
|
||||
model = &domain.ModelTarget{
|
||||
Endpoint: req.Model.Endpoint,
|
||||
Model: req.Model.Model,
|
||||
Temperature: req.Model.Temperature,
|
||||
MaxTokens: req.Model.MaxTokens,
|
||||
TopP: req.Model.TopP,
|
||||
}
|
||||
}
|
||||
|
||||
res, err := h.runner.Run(r.Context(), domain.RunRequest{
|
||||
ProfileID: req.ProfileID,
|
||||
ProfileVersion: req.ProfileVersion,
|
||||
Inputs: mappedInputs,
|
||||
Vars: req.Vars,
|
||||
Model: model,
|
||||
})
|
||||
if err != nil {
|
||||
status, code := mapRunError(err)
|
||||
writeError(w, status, code, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, runResponseDTO{
|
||||
Artifact: artifactDTO{
|
||||
Name: res.Artifact.Name,
|
||||
ContentType: res.Artifact.ContentType,
|
||||
Body: string(res.Artifact.Body),
|
||||
URI: res.Artifact.URI,
|
||||
Size: res.Artifact.Size,
|
||||
Hash: res.Artifact.Hash,
|
||||
},
|
||||
Validation: res.Validation,
|
||||
Metadata: metadataDTO{
|
||||
ProfileID: res.ProfileID,
|
||||
ProfileVersion: res.ProfileVersion,
|
||||
ModelName: res.ModelName,
|
||||
Endpoint: res.Endpoint,
|
||||
InputHashes: res.InputHashes,
|
||||
PromptHash: res.PromptHash,
|
||||
Usage: res.Usage,
|
||||
StartTime: res.StartTime,
|
||||
EndTime: res.EndTime,
|
||||
},
|
||||
RawModelOutput: res.RawOutput,
|
||||
})
|
||||
}
|
||||
|
||||
func mapRunError(err error) (int, string) {
|
||||
switch {
|
||||
case errors.Is(err, profile.ErrProfileNotFound):
|
||||
return http.StatusNotFound, "profile_not_found"
|
||||
case errors.Is(err, usecase.ErrInvalidRequest):
|
||||
return http.StatusBadRequest, "invalid_request"
|
||||
case errors.Is(err, usecase.ErrProfileLoad):
|
||||
return http.StatusBadRequest, "profile_load_failed"
|
||||
case errors.Is(err, usecase.ErrArtifactLoad):
|
||||
return http.StatusBadRequest, "artifact_read_failed"
|
||||
case errors.Is(err, usecase.ErrPromptRender):
|
||||
return http.StatusBadRequest, "prompt_render_failed"
|
||||
case errors.Is(err, usecase.ErrLLMGenerate):
|
||||
return http.StatusBadGateway, "llm_failed"
|
||||
case errors.Is(err, usecase.ErrValidation):
|
||||
return http.StatusInternalServerError, "validation_runtime_failed"
|
||||
default:
|
||||
return http.StatusInternalServerError, "internal_error"
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, code, message string) {
|
||||
writeJSON(w, status, errorResponse{
|
||||
Error: errorBody{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user