package scriptorium import ( "context" "errors" "fmt" "net/http" "strings" "time" artifactadapter "gitea.maximumdirect.net/eric/scriptorium/internal/artifact" "gitea.maximumdirect.net/eric/scriptorium/internal/defaults" "gitea.maximumdirect.net/eric/scriptorium/internal/llm" "gitea.maximumdirect.net/eric/scriptorium/internal/profile" "gitea.maximumdirect.net/eric/scriptorium/internal/prompt" "gitea.maximumdirect.net/eric/scriptorium/internal/promptdef" "gitea.maximumdirect.net/eric/scriptorium/internal/usecase" "gitea.maximumdirect.net/eric/scriptorium/internal/validate" ) // ErrInvalidConfig indicates invalid public engine configuration. var ErrInvalidConfig = errors.New("invalid engine configuration") // Engine prepares Scriptorium prompt requests. type Engine struct { runner *usecase.Runner } // Config configures a public Scriptorium engine. type Config struct { PromptDir string ProfileDir string SchemaDir string Timeout time.Duration HTTPClient *http.Client } // Option customizes engine construction. type Option func(*engineOptions) error type engineOptions struct{} // NewEngine constructs an Engine using the same default internal components as // the CLI and HTTP adapters. func NewEngine(cfg Config, opts ...Option) (*Engine, error) { if strings.TrimSpace(cfg.PromptDir) == "" { return nil, fmt.Errorf("%w: prompt directory is required", ErrInvalidConfig) } if strings.TrimSpace(cfg.ProfileDir) == "" { return nil, fmt.Errorf("%w: profile directory is required", ErrInvalidConfig) } var options engineOptions for _, opt := range opts { if opt == nil { continue } if err := opt(&options); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) } } schemaDir := cfg.SchemaDir if strings.TrimSpace(schemaDir) == "" { schemaDir = defaults.SchemaDirDefault } llmClient, err := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{ Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient, }) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) } return &Engine{ runner: usecase.NewRunner( promptdef.NewFilesystemRepository(cfg.PromptDir), profile.NewFilesystemRepository(cfg.ProfileDir), artifactadapter.NewCompositeReader(), prompt.NewGoRenderer(), llmClient, validate.NewStandardValidator(schemaDir), ), }, nil } // Prepare resolves a prompt request without calling an LLM. func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) { if e == nil || e.runner == nil { return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } prepared, err := e.runner.Prepare(ctx, toDomainRunRequest(req)) if err != nil { return nil, err } return fromDomainPreparedRun(prepared), nil }