694 lines
20 KiB
Go
694 lines
20 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/scriptorium"
|
|
httpadapter "gitea.maximumdirect.net/eric/scriptorium/internal/adapter/http"
|
|
appconfig "gitea.maximumdirect.net/eric/scriptorium/internal/config"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
|
|
renderformat "gitea.maximumdirect.net/eric/scriptorium/internal/format"
|
|
)
|
|
|
|
const (
|
|
ExitOK = 0
|
|
ExitRuntimeError = 1
|
|
ExitValidationFailed = 2
|
|
)
|
|
|
|
const (
|
|
errPromptDirRequired = "prompt directory is required; provide --prompt-dir or config.yml prompt_dir"
|
|
)
|
|
|
|
type runConfig struct {
|
|
configPath string
|
|
|
|
promptDir string
|
|
profileDir string
|
|
promptID string
|
|
profileID string
|
|
inputRaw listFlag
|
|
varRaw listFlag
|
|
outputPath string
|
|
llmBaseURL string
|
|
apiKeyEnv string
|
|
model string
|
|
temperature float64
|
|
maxTokens int
|
|
topP float64
|
|
schemaDir string
|
|
timeout time.Duration
|
|
|
|
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
|
|
|
llmBaseURLSet bool
|
|
apiKeyEnvSet bool
|
|
modelSet bool
|
|
temperatureSet bool
|
|
maxTokensSet bool
|
|
topPSet bool
|
|
timeoutSet bool
|
|
}
|
|
|
|
type renderConfig struct {
|
|
runConfig
|
|
outputFormat renderformat.PreparedRunOutputFormat
|
|
}
|
|
|
|
type serveConfig struct {
|
|
configPath string
|
|
|
|
addr string
|
|
promptDir string
|
|
profileDir string
|
|
schemaDir string
|
|
artifactRoot string
|
|
maxRequestBytes int64
|
|
maxArtifactBytes int64
|
|
maxResponseBytes int64
|
|
}
|
|
|
|
type commonCommandSettings struct {
|
|
promptDir string
|
|
profileDir string
|
|
schemaDir string
|
|
serverAddr string
|
|
artifactRoot string
|
|
maxRequestBytes int64
|
|
maxArtifactBytes int64
|
|
maxResponseBytes int64
|
|
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
|
}
|
|
|
|
type listFlag []string
|
|
|
|
func (l *listFlag) String() string {
|
|
return strings.Join(*l, ",")
|
|
}
|
|
|
|
func (l *listFlag) Set(v string) error {
|
|
*l = append(*l, v)
|
|
return nil
|
|
}
|
|
|
|
func Run(args []string, stdout, stderr io.Writer) int {
|
|
if len(args) == 0 {
|
|
printUsage(stderr)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
switch args[0] {
|
|
case "run":
|
|
return runCommand(args[1:], stdout, stderr)
|
|
case "render":
|
|
return renderCommand(args[1:], stdout, stderr)
|
|
case "serve":
|
|
return serveCommand(args[1:], stderr)
|
|
default:
|
|
fmt.Fprintf(stderr, "unknown command %q\n", args[0])
|
|
printUsage(stderr)
|
|
return ExitRuntimeError
|
|
}
|
|
}
|
|
|
|
func runCommand(args []string, stdout, stderr io.Writer) int {
|
|
cfg, err := parseRunArgs(args)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "run parse error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
req, err := buildRunRequestFromConfig(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "run parse error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
engine, err := newEngine(cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "engine error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
res, runErr := engine.Run(context.Background(), req)
|
|
if runErr != nil {
|
|
fmt.Fprintf(stderr, "run error: %v\n", runErr)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
if err := writeOutput(stdout, cfg.outputPath, res.Artifact.Body); err != nil {
|
|
fmt.Fprintf(stderr, "output write error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
printSummary(stderr, res)
|
|
return determineExitCode(nil, res)
|
|
}
|
|
|
|
func renderCommand(args []string, stdout, stderr io.Writer) int {
|
|
cfg, err := parseRenderArgs(args)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "render parse error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
req, err := buildRunRequestFromConfig(&cfg.runConfig)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "render parse error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
engine, err := newEngine(&cfg.runConfig)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "engine error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
prepared, prepErr := engine.Prepare(context.Background(), req)
|
|
if prepErr != nil {
|
|
fmt.Fprintf(stderr, "render error: %v\n", prepErr)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
out, err := renderformat.FormatPreparedRun(prepared, cfg.outputFormat)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "render format error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
if err := writeOutput(stdout, cfg.outputPath, out); err != nil {
|
|
fmt.Fprintf(stderr, "output write error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
return ExitOK
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
artifactReader, err := httpadapter.NewRestrictedArtifactReader(cfg.artifactRoot, cfg.maxArtifactBytes)
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "artifact root error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
engine, err := newEngine(&runConfig{
|
|
promptDir: cfg.promptDir,
|
|
profileDir: cfg.profileDir,
|
|
schemaDir: cfg.schemaDir,
|
|
}, scriptorium.WithArtifactReader(artifactReader))
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "engine error: %v\n", err)
|
|
return ExitRuntimeError
|
|
}
|
|
|
|
h := httpadapter.NewHandlerWithOptions(engine, httpadapter.HandlerOptions{
|
|
MaxRequestBytes: cfg.maxRequestBytes,
|
|
MaxResponseBytes: cfg.maxResponseBytes,
|
|
})
|
|
srv := &http.Server{
|
|
Addr: cfg.addr,
|
|
Handler: h,
|
|
ReadHeaderTimeout: defaults.HTTPReadHeaderTimeoutDefault,
|
|
}
|
|
|
|
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)
|
|
fs.SetOutput(io.Discard)
|
|
|
|
registerConfigPathFlag(fs, &cfg.configPath)
|
|
registerExecutionRequestFlags(fs, cfg)
|
|
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
|
|
|
|
if err := fs.Parse(args); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := finalizeExecutionRequestConfig(fs, cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.schemaDir = filepath.Clean(cfg.schemaDir)
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func parseRenderArgs(args []string) (*renderConfig, error) {
|
|
cfg := &renderConfig{
|
|
outputFormat: renderformat.DefaultPreparedRunOutputFormat,
|
|
}
|
|
fs := flag.NewFlagSet("render", flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
|
|
registerConfigPathFlag(fs, &cfg.configPath)
|
|
registerExecutionRequestFlags(fs, &cfg.runConfig)
|
|
|
|
var rawFormat string
|
|
fs.StringVar(&rawFormat, "format", "", "render output format (text|json)")
|
|
|
|
if err := fs.Parse(args); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := finalizeExecutionRequestConfig(fs, &cfg.runConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if flagWasSet(fs, "format") {
|
|
format, err := renderformat.ParsePreparedRunOutputFormat(rawFormat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.outputFormat = format
|
|
} else {
|
|
cfg.outputFormat = cfg.defaultRenderFormat
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func parseServeArgs(args []string) (*serveConfig, error) {
|
|
cfg := &serveConfig{}
|
|
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
|
|
registerConfigPathFlag(fs, &cfg.configPath)
|
|
fs.StringVar(&cfg.addr, "addr", "", "HTTP listen address")
|
|
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
|
|
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profile YAML files")
|
|
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
|
|
fs.StringVar(&cfg.artifactRoot, "artifact-root", "", "base directory for HTTP file input artifacts")
|
|
fs.Int64Var(&cfg.maxRequestBytes, "max-request-bytes", 0, "maximum HTTP request body bytes; 0 disables the limit")
|
|
fs.Int64Var(&cfg.maxArtifactBytes, "max-artifact-bytes", 0, "maximum HTTP file artifact bytes; 0 disables the limit")
|
|
fs.Int64Var(&cfg.maxResponseBytes, "max-response-bytes", 0, "maximum HTTP response body bytes; 0 disables the limit")
|
|
|
|
if err := fs.Parse(args); err != nil {
|
|
return nil, err
|
|
}
|
|
if fs.NArg() > 0 {
|
|
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
|
}
|
|
|
|
settings, err := resolveCommonSettings(fs, cfg.configPath, appconfig.CLIOverrides{
|
|
PromptDir: cfg.promptDirIfSet(fs),
|
|
ProfileDir: cfg.profileDirIfSet(fs),
|
|
SchemaDir: cfg.schemaDirIfSet(fs),
|
|
ServerAddr: cfg.addrIfSet(fs),
|
|
ArtifactRoot: cfg.artifactRootIfSet(fs),
|
|
MaxRequestBytes: cfg.maxRequestBytesIfSet(fs),
|
|
MaxArtifactBytes: cfg.maxArtifactBytesIfSet(fs),
|
|
MaxResponseBytes: cfg.maxResponseBytesIfSet(fs),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg.promptDir = settings.promptDir
|
|
cfg.profileDir = settings.profileDir
|
|
cfg.schemaDir = settings.schemaDir
|
|
cfg.addr = settings.serverAddr
|
|
cfg.artifactRoot = settings.artifactRoot
|
|
cfg.maxRequestBytes = settings.maxRequestBytes
|
|
cfg.maxArtifactBytes = settings.maxArtifactBytes
|
|
cfg.maxResponseBytes = settings.maxResponseBytes
|
|
|
|
if err := validateRequiredLibraryDirs(cfg.promptDir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
|
if strings.TrimSpace(cfg.profileDir) != "" {
|
|
cfg.profileDir = filepath.Clean(cfg.profileDir)
|
|
}
|
|
cfg.schemaDir = filepath.Clean(cfg.schemaDir)
|
|
if strings.TrimSpace(cfg.artifactRoot) != "" {
|
|
cfg.artifactRoot = filepath.Clean(cfg.artifactRoot)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func registerExecutionRequestFlags(fs *flag.FlagSet, cfg *runConfig) {
|
|
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
|
|
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profile YAML files")
|
|
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to run")
|
|
fs.StringVar(&cfg.profileID, "profile", "", "optional execution profile ID; if omitted, prompt default_profile is used")
|
|
fs.Var(&cfg.inputRaw, "input", "input mapping(s): name=path (repeatable, comma-separated)")
|
|
fs.Var(&cfg.varRaw, "var", "variable mapping(s): name=value (repeatable, comma-separated)")
|
|
fs.StringVar(&cfg.outputPath, "out", "", "optional output file path")
|
|
fs.StringVar(&cfg.llmBaseURL, "llm-base-url", "", "OpenAI-compatible base URL including /v1")
|
|
fs.StringVar(&cfg.apiKeyEnv, "api-key-env", "", "environment variable name containing API key")
|
|
fs.StringVar(&cfg.model, "model", "", "model name")
|
|
fs.Float64Var(&cfg.temperature, "temperature", 0, "optional temperature override")
|
|
fs.IntVar(&cfg.maxTokens, "max-tokens", 0, "optional max tokens override")
|
|
fs.Float64Var(&cfg.topP, "top-p", 0, "optional top_p override")
|
|
fs.DurationVar(&cfg.timeout, "timeout", defaults.LLMRequestTimeoutDefault, "LLM request timeout")
|
|
fs.StringVar(&cfg.promptID, "prompt-id", "", "deprecated alias for --prompt")
|
|
fs.StringVar(&cfg.profileID, "profile-id", "", "deprecated alias for --profile")
|
|
}
|
|
|
|
func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
|
if fs.NArg() > 0 {
|
|
return fmt.Errorf("unexpected positional args: %v", fs.Args())
|
|
}
|
|
|
|
settings, err := resolveCommonSettings(fs, cfg.configPath, appconfig.CLIOverrides{
|
|
PromptDir: cfg.promptDirIfSet(fs),
|
|
ProfileDir: cfg.profileDirIfSet(fs),
|
|
SchemaDir: cfg.schemaDirIfSet(fs),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg.promptDir = settings.promptDir
|
|
cfg.profileDir = settings.profileDir
|
|
cfg.schemaDir = settings.schemaDir
|
|
cfg.defaultRenderFormat = settings.defaultRenderFormat
|
|
|
|
if err := validateRequiredLibraryDirs(cfg.promptDir); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(cfg.promptID) == "" {
|
|
return errors.New("--prompt is required")
|
|
}
|
|
if len(cfg.inputRaw) == 0 {
|
|
return errors.New("at least one --input is required")
|
|
}
|
|
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
|
if strings.TrimSpace(cfg.profileDir) != "" {
|
|
cfg.profileDir = filepath.Clean(cfg.profileDir)
|
|
}
|
|
if cfg.outputPath != "" {
|
|
cfg.outputPath = filepath.Clean(cfg.outputPath)
|
|
}
|
|
cfg.llmBaseURLSet = flagWasSet(fs, "llm-base-url")
|
|
cfg.apiKeyEnvSet = flagWasSet(fs, "api-key-env")
|
|
cfg.modelSet = flagWasSet(fs, "model")
|
|
cfg.temperatureSet = flagWasSet(fs, "temperature")
|
|
cfg.maxTokensSet = flagWasSet(fs, "max-tokens")
|
|
cfg.topPSet = flagWasSet(fs, "top-p")
|
|
cfg.timeoutSet = flagWasSet(fs, "timeout")
|
|
return nil
|
|
}
|
|
|
|
func (c *runConfig) promptDirIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "prompt-dir") {
|
|
return c.promptDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *runConfig) profileDirIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "profile-dir") {
|
|
return c.profileDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *runConfig) schemaDirIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "schema-dir") {
|
|
return c.schemaDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *serveConfig) promptDirIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "prompt-dir") {
|
|
return c.promptDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *serveConfig) profileDirIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "profile-dir") {
|
|
return c.profileDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *serveConfig) schemaDirIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "schema-dir") {
|
|
return c.schemaDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *serveConfig) addrIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "addr") {
|
|
return c.addr
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *serveConfig) artifactRootIfSet(fs *flag.FlagSet) string {
|
|
if flagWasSet(fs, "artifact-root") {
|
|
return c.artifactRoot
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *serveConfig) maxRequestBytesIfSet(fs *flag.FlagSet) *int64 {
|
|
if flagWasSet(fs, "max-request-bytes") {
|
|
return &c.maxRequestBytes
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *serveConfig) maxArtifactBytesIfSet(fs *flag.FlagSet) *int64 {
|
|
if flagWasSet(fs, "max-artifact-bytes") {
|
|
return &c.maxArtifactBytes
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *serveConfig) maxResponseBytesIfSet(fs *flag.FlagSet) *int64 {
|
|
if flagWasSet(fs, "max-response-bytes") {
|
|
return &c.maxResponseBytes
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
|
|
fs.StringVar(
|
|
target,
|
|
"config",
|
|
"",
|
|
fmt.Sprintf(
|
|
"application config path (default search: %s, then %s)",
|
|
appconfig.DefaultConfigPathLocal,
|
|
appconfig.DefaultConfigPath,
|
|
),
|
|
)
|
|
}
|
|
|
|
func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) {
|
|
settings, err := appconfig.LoadConfig(configPath, flagWasSet(fs, "config"))
|
|
if err != nil {
|
|
return appconfig.AppSettings{}, fmt.Errorf("application config error: %w", err)
|
|
}
|
|
|
|
merged, err := appconfig.ApplyCLIOverrides(settings, overrides)
|
|
if err != nil {
|
|
return appconfig.AppSettings{}, fmt.Errorf("application config error: %w", err)
|
|
}
|
|
|
|
return merged, nil
|
|
}
|
|
|
|
func resolveCommonSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (commonCommandSettings, error) {
|
|
settings, err := resolveAppSettings(fs, configPath, overrides)
|
|
if err != nil {
|
|
return commonCommandSettings{}, err
|
|
}
|
|
return commonCommandSettings{
|
|
promptDir: settings.PromptDir,
|
|
profileDir: settings.ProfileDir,
|
|
schemaDir: settings.SchemaDir,
|
|
serverAddr: settings.ServerAddr,
|
|
artifactRoot: settings.ArtifactRoot,
|
|
maxRequestBytes: settings.MaxRequestBytes,
|
|
maxArtifactBytes: settings.MaxArtifactBytes,
|
|
maxResponseBytes: settings.MaxResponseBytes,
|
|
defaultRenderFormat: settings.DefaultRenderFormat,
|
|
}, nil
|
|
}
|
|
|
|
func validateRequiredLibraryDirs(promptDir string) error {
|
|
if strings.TrimSpace(promptDir) == "" {
|
|
return errors.New(errPromptDirRequired)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newEngine(cfg *runConfig, options ...scriptorium.Option) (*scriptorium.Engine, error) {
|
|
return scriptorium.NewEngine(scriptorium.Config{
|
|
PromptDir: cfg.promptDir,
|
|
ProfileDir: cfg.profileDir,
|
|
SchemaDir: cfg.schemaDir,
|
|
}, options...)
|
|
}
|
|
|
|
func buildRunRequestFromConfig(cfg *runConfig) (scriptorium.RunRequest, error) {
|
|
inputMappings, err := parseMappings(cfg.inputRaw, false)
|
|
if err != nil {
|
|
return scriptorium.RunRequest{}, fmt.Errorf("input parse error: %w", err)
|
|
}
|
|
|
|
varMappings := map[string]string{}
|
|
if len(cfg.varRaw) > 0 {
|
|
varMappings, err = parseMappings(cfg.varRaw, false)
|
|
if err != nil {
|
|
return scriptorium.RunRequest{}, fmt.Errorf("var parse error: %w", err)
|
|
}
|
|
}
|
|
|
|
inputs := make(map[string]scriptorium.ArtifactRef, len(inputMappings))
|
|
for name, path := range inputMappings {
|
|
inputs[name] = scriptorium.File(path)
|
|
}
|
|
|
|
var modelOverride *scriptorium.ExecutionTargetOverride
|
|
if cfg.llmBaseURLSet || cfg.modelSet || cfg.temperatureSet || cfg.maxTokensSet || cfg.topPSet || cfg.apiKeyEnvSet || cfg.timeoutSet {
|
|
modelOverride = &scriptorium.ExecutionTargetOverride{
|
|
Endpoint: cfg.llmBaseURL,
|
|
Model: cfg.model,
|
|
APIKeyEnv: cfg.apiKeyEnv,
|
|
}
|
|
if cfg.temperatureSet {
|
|
modelOverride.Temperature = &cfg.temperature
|
|
}
|
|
if cfg.maxTokensSet {
|
|
modelOverride.MaxTokens = &cfg.maxTokens
|
|
}
|
|
if cfg.topPSet {
|
|
modelOverride.TopP = &cfg.topP
|
|
}
|
|
if cfg.timeoutSet {
|
|
timeoutSeconds := int(cfg.timeout.Seconds())
|
|
modelOverride.TimeoutSeconds = &timeoutSeconds
|
|
}
|
|
}
|
|
|
|
return scriptorium.RunRequest{
|
|
PromptID: cfg.promptID,
|
|
ProfileID: cfg.profileID,
|
|
Inputs: inputs,
|
|
Vars: varMappings,
|
|
Execution: modelOverride,
|
|
}, nil
|
|
}
|
|
|
|
func parseMappings(raw []string, allowEmptyValue bool) (map[string]string, error) {
|
|
out := make(map[string]string)
|
|
for _, entry := range raw {
|
|
for _, piece := range strings.Split(entry, ",") {
|
|
piece = strings.TrimSpace(piece)
|
|
if piece == "" {
|
|
continue
|
|
}
|
|
key, value, err := parseMapping(piece)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !allowEmptyValue && strings.TrimSpace(value) == "" {
|
|
return nil, fmt.Errorf("mapping %q has empty value", piece)
|
|
}
|
|
out[key] = value
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, errors.New("no valid mappings provided")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func parseMapping(value string) (string, string, error) {
|
|
idx := strings.IndexRune(value, '=')
|
|
if idx <= 0 {
|
|
return "", "", fmt.Errorf("invalid mapping %q, expected name=value", value)
|
|
}
|
|
key := strings.TrimSpace(value[:idx])
|
|
val := strings.TrimSpace(value[idx+1:])
|
|
if key == "" {
|
|
return "", "", fmt.Errorf("invalid mapping %q, empty name", value)
|
|
}
|
|
return key, val, nil
|
|
}
|
|
|
|
func flagWasSet(fs *flag.FlagSet, name string) bool {
|
|
set := false
|
|
fs.Visit(func(f *flag.Flag) {
|
|
if f.Name == name {
|
|
set = true
|
|
}
|
|
})
|
|
return set
|
|
}
|
|
|
|
func writeOutput(stdout io.Writer, outputPath string, body []byte) error {
|
|
if outputPath == "" {
|
|
_, err := stdout.Write(body)
|
|
return err
|
|
}
|
|
return os.WriteFile(outputPath, body, 0644)
|
|
}
|
|
|
|
func determineExitCode(runErr error, result *scriptorium.RunResult) int {
|
|
if runErr != nil {
|
|
return ExitRuntimeError
|
|
}
|
|
if result != nil && result.Validation.Status == scriptorium.ValidationFailed {
|
|
return ExitValidationFailed
|
|
}
|
|
return ExitOK
|
|
}
|
|
|
|
func printSummary(stderr io.Writer, res *scriptorium.RunResult) {
|
|
if res == nil {
|
|
return
|
|
}
|
|
fmt.Fprintf(stderr, "prompt=%s@%s selected_profile=%s model=%s validation=%s mode=%s validation_errors=%d prompt_hash=%s inputs=%d usage=%d/%d/%d",
|
|
res.PromptID,
|
|
res.PromptVersion,
|
|
res.SelectedProfileID,
|
|
res.ModelName,
|
|
res.Validation.Status,
|
|
res.Validation.Mode,
|
|
len(res.Validation.Errors),
|
|
res.RenderedPromptHash,
|
|
len(res.InputHashes),
|
|
res.Usage.PromptTokens,
|
|
res.Usage.CompletionTokens,
|
|
res.Usage.TotalTokens,
|
|
)
|
|
if res.Usage.CachedTokens != 0 || res.Usage.CacheWriteTokens != 0 {
|
|
fmt.Fprintf(stderr, " cached_tokens=%d cache_write_tokens=%d", res.Usage.CachedTokens, res.Usage.CacheWriteTokens)
|
|
}
|
|
fmt.Fprintln(stderr)
|
|
}
|
|
|
|
func printUsage(w io.Writer) {
|
|
fmt.Fprintln(w, "usage: scriptorium <run|render|serve> ...")
|
|
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--out path] [--timeout 10m]")
|
|
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
|
|
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
|
|
}
|