Add a minimal HTTP API
This commit is contained in:
@@ -6,12 +6,14 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/artifact"
|
||||
httpadapter "gitea.maximumdirect.net/eric/scriptorium/internal/adapter/http"
|
||||
artifactadapter "gitea.maximumdirect.net/eric/scriptorium/internal/artifact"
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/llm"
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/profile"
|
||||
@@ -40,6 +42,16 @@ type runConfig struct {
|
||||
schemaDir string
|
||||
}
|
||||
|
||||
type serveConfig struct {
|
||||
addr string
|
||||
profileDir string
|
||||
schemaDir string
|
||||
llmBaseURL string
|
||||
llmAPIKey string
|
||||
model string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type listFlag []string
|
||||
|
||||
func (l *listFlag) String() string {
|
||||
@@ -60,6 +72,8 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
||||
switch args[0] {
|
||||
case "run":
|
||||
return runCommand(args[1:], stdout, stderr)
|
||||
case "serve":
|
||||
return serveCommand(args[1:], stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "unknown command %q\n", args[0])
|
||||
printUsage(stderr)
|
||||
@@ -103,7 +117,7 @@ func runCommand(args []string, stdout, stderr io.Writer) int {
|
||||
|
||||
runner := usecase.NewRunner(
|
||||
profile.NewFilesystemRepository(cfg.profileDir),
|
||||
artifact.NewCompositeReader(),
|
||||
artifactadapter.NewCompositeReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
llmClient,
|
||||
validate.NewStandardValidator(cfg.schemaDir),
|
||||
@@ -134,6 +148,47 @@ func runCommand(args []string, stdout, stderr io.Writer) int {
|
||||
return determineExitCode(nil, res)
|
||||
}
|
||||
|
||||
func serveCommand(args []string, stderr io.Writer) int {
|
||||
cfg, err := parseServeArgs(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "serve parse error: %v\n", err)
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
llmClient, err := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
||||
BaseURL: cfg.llmBaseURL,
|
||||
APIKey: cfg.llmAPIKey,
|
||||
Model: cfg.model,
|
||||
Timeout: cfg.timeout,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "llm client error: %v\n", err)
|
||||
return ExitRuntimeError
|
||||
}
|
||||
|
||||
runner := usecase.NewRunner(
|
||||
profile.NewFilesystemRepository(cfg.profileDir),
|
||||
artifactadapter.NewCompositeReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
llmClient,
|
||||
validate.NewStandardValidator(cfg.schemaDir),
|
||||
)
|
||||
|
||||
h := httpadapter.NewHandler(runner)
|
||||
srv := &http.Server{
|
||||
Addr: cfg.addr,
|
||||
Handler: h,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
fmt.Fprintf(stderr, "serving on %s\n", cfg.addr)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
fmt.Fprintf(stderr, "server error: %v\n", err)
|
||||
return ExitRuntimeError
|
||||
}
|
||||
return ExitOK
|
||||
}
|
||||
|
||||
func parseRunArgs(args []string) (*runConfig, error) {
|
||||
cfg := &runConfig{}
|
||||
fs := flag.NewFlagSet("run", flag.ContinueOnError)
|
||||
@@ -183,6 +238,38 @@ func parseRunArgs(args []string) (*runConfig, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseServeArgs(args []string) (*serveConfig, error) {
|
||||
cfg := &serveConfig{}
|
||||
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
fs.StringVar(&cfg.addr, "addr", ":8080", "HTTP listen address")
|
||||
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing prompt profile YAML files")
|
||||
fs.StringVar(&cfg.schemaDir, "schema-dir", ".", "base directory for validation schemas")
|
||||
fs.StringVar(&cfg.llmBaseURL, "llm-base-url", "", "OpenAI-compatible base URL including /v1")
|
||||
fs.StringVar(&cfg.llmAPIKey, "llm-api-key", "", "optional API key")
|
||||
fs.StringVar(&cfg.model, "model", "", "optional default model")
|
||||
fs.DurationVar(&cfg.timeout, "timeout", 60*time.Second, "LLM request timeout")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cfg.profileDir) == "" {
|
||||
return nil, errors.New("--profile-dir is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.llmBaseURL) == "" {
|
||||
return nil, errors.New("--llm-base-url is required")
|
||||
}
|
||||
|
||||
cfg.profileDir = filepath.Clean(cfg.profileDir)
|
||||
cfg.schemaDir = filepath.Clean(cfg.schemaDir)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseMappings(raw []string, allowEmptyValue bool) (map[string]string, error) {
|
||||
out := make(map[string]string)
|
||||
for _, entry := range raw {
|
||||
@@ -258,5 +345,7 @@ func printSummary(stderr io.Writer, res *domain.RunResult) {
|
||||
}
|
||||
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage: scriptorium run --profile-dir DIR --profile-id ID --input name=path [--input ...] --llm-base-url URL --model NAME [--var k=v] [--out path]")
|
||||
fmt.Fprintln(w, "usage: scriptorium <run|serve> ...")
|
||||
fmt.Fprintln(w, " run: scriptorium run --profile-dir DIR --profile-id ID --input name=path [--input ...] --llm-base-url URL --model NAME [--var k=v] [--out path]")
|
||||
fmt.Fprintln(w, " serve: scriptorium serve --addr :8080 --profile-dir DIR --llm-base-url URL [--schema-dir DIR] [--llm-api-key KEY] [--model NAME]")
|
||||
}
|
||||
|
||||
@@ -74,6 +74,26 @@ func TestParseRunArgsRequiredFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServeArgsRequiredFlags(t *testing.T) {
|
||||
_, err := parseServeArgs([]string{"--llm-base-url", "http://x/v1"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing --profile-dir error")
|
||||
}
|
||||
|
||||
_, err = parseServeArgs([]string{"--profile-dir", "./profiles"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing --llm-base-url error")
|
||||
}
|
||||
|
||||
cfg, err := parseServeArgs([]string{"--profile-dir", "./profiles", "--llm-base-url", "http://x/v1"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected valid serve args, got %v", err)
|
||||
}
|
||||
if cfg.addr != ":8080" {
|
||||
t.Fatalf("expected default addr :8080, got %q", cfg.addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineExitCode(t *testing.T) {
|
||||
if got := determineExitCode(errors.New("boom"), nil); got != ExitRuntimeError {
|
||||
t.Fatalf("expected runtime exit code, got %d", got)
|
||||
|
||||
Reference in New Issue
Block a user