diff --git a/engine.go b/engine.go index ffdac71..83dd6ea 100644 --- a/engine.go +++ b/engine.go @@ -52,7 +52,15 @@ type Config struct { } // Option customizes engine construction. -type Option func(*engineOptions) error +type Option interface { + apply(*engineOptions) error +} + +type optionFunc func(*engineOptions) error + +func (f optionFunc) apply(options *engineOptions) error { + return f(options) +} type engineOptions struct { llmClient llm.Client @@ -68,17 +76,17 @@ type engineOptions struct { // WithLLMClient injects a custom LLM client for execution. func WithLLMClient(client LLMClient) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { if client == nil { return ErrInvalidConfig } options.llmClient = publicLLMClientAdapter{client: client} return nil - } + }) } func WithPromptFS(fsys fs.FS, root string) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { if fsys == nil { return ErrInvalidConfig } @@ -88,11 +96,11 @@ func WithPromptFS(fsys fs.FS, root string) Option { options.promptDefs = promptdef.NewFSRepository(fsys, root) options.promptSource = true return nil - } + }) } func WithPromptFile(path string) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { fsys, root, err := fileSource(path) if err != nil { return err @@ -100,11 +108,11 @@ func WithPromptFile(path string) Option { options.promptDefs = promptdef.NewFSRepository(fsys, root) options.promptSource = true return nil - } + }) } func WithProfileFS(fsys fs.FS, root string) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { if fsys == nil { return ErrInvalidConfig } @@ -114,11 +122,11 @@ func WithProfileFS(fsys fs.FS, root string) Option { options.profiles = profile.NewFSRepository(fsys, root) options.profileSource = true return nil - } + }) } func WithProfileFile(path string) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { fsys, root, err := fileSource(path) if err != nil { return err @@ -126,13 +134,13 @@ func WithProfileFile(path string) Option { options.profiles = profile.NewFSRepository(fsys, root) options.profileSource = true return nil - } + }) } // WithProfiles configures in-memory profiles that take precedence over // configured profile files and built-in profiles. func WithProfiles(profiles ...Profile) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { repo, err := newMemoryProfileRepository(profiles) if err != nil { return err @@ -140,11 +148,11 @@ func WithProfiles(profiles ...Profile) Option { options.memoryProfiles = repo options.memorySource = true return nil - } + }) } func WithSchemaFS(fsys fs.FS, root string) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { if fsys == nil { return ErrInvalidConfig } @@ -154,11 +162,11 @@ func WithSchemaFS(fsys fs.FS, root string) Option { options.validator = validate.NewFSValidator(fsys, root) options.validatorSource = true return nil - } + }) } func WithSchemaFile(path string) Option { - return func(options *engineOptions) error { + return optionFunc(func(options *engineOptions) error { fsys, root, err := fileSource(path) if err != nil { return err @@ -166,7 +174,7 @@ func WithSchemaFile(path string) Option { options.validator = validate.NewFSValidator(fsys, root) options.validatorSource = true return nil - } + }) } // NewEngine constructs an Engine using the same default internal components as @@ -177,7 +185,7 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) { if opt == nil { continue } - if err := opt(&options); err != nil { + if err := opt.apply(&options); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) } } diff --git a/engine_test.go b/engine_test.go index fdc2d64..a775678 100644 --- a/engine_test.go +++ b/engine_test.go @@ -1378,6 +1378,45 @@ func TestSourceOptionsRejectInvalidInputs(t *testing.T) { } } +func TestPackageOptionsComposeFromSlice(t *testing.T) { + fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}} + options := []scriptorium.Option{ + nil, + scriptorium.WithProfiles(scriptorium.Profile{ + ID: "slice-profile", + Endpoint: "http://slice/v1", + Model: "slice-model", + }), + scriptorium.WithLLMClient(fake), + } + + engine, err := scriptorium.NewEngine(scriptorium.Config{ + PromptDir: "./examples/prompts", + SchemaDir: "./examples/schemas", + }, options...) + if err != nil { + t.Fatalf("expected package-provided options to compose, got %v", err) + } + + _, err = engine.Run(context.Background(), scriptorium.RunRequest{ + PromptID: "generic.markdown_summary", + ProfileID: "slice-profile", + Inputs: map[string]scriptorium.ArtifactRef{ + "transcript": scriptorium.Inline("Rin opens the gate."), + "glossary": scriptorium.Inline("gate: A guarded passage."), + }, + }) + if err != nil { + t.Fatalf("expected run with composed options to succeed, got %v", err) + } + if len(fake.requests) != 1 { + t.Fatalf("expected one generate request, got %d", len(fake.requests)) + } + if fake.requests[0].Target.Model != "slice-model" { + t.Fatalf("expected profile from composed options, got %q", fake.requests[0].Target.Model) + } +} + func TestExtraParamsTypedNestedValuesAreCopiedAcrossPublicBoundary(t *testing.T) { fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}} engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake))