Add public asset source options

This commit is contained in:
2026-07-04 17:02:09 +00:00
parent 3ad247039b
commit 6f91603168
8 changed files with 978 additions and 26 deletions

View File

@@ -11,6 +11,7 @@ import (
"reflect"
"strings"
"testing"
"testing/fstest"
"gitea.maximumdirect.net/eric/scriptorium"
)
@@ -709,6 +710,256 @@ unexpected: true
}
}
func TestPrepareWorksWithPromptFSAndRelativeContentFile(t *testing.T) {
promptFS := fstest.MapFS{
"assets/prompts/fs-summary.yaml": &fstest.MapFile{Data: []byte(`
id: fs.summary
version: "1.0.0"
default_profile: local-fast
inputs:
- name: transcript
required: true
messages:
- role: user
content_file: ./messages/summary.tmpl
output:
format: text
validation_mode: none
repair_attempts: 0
`)},
"assets/prompts/messages/summary.tmpl": &fstest.MapFile{Data: []byte(`Summarize {{input "transcript"}} from prompt fs.`)},
}
engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: t.TempDir(),
ProfileDir: "./examples/profiles",
SchemaDir: "./examples/schemas",
}, scriptorium.WithPromptFS(promptFS, "assets/prompts"))
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "fs.summary",
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."),
},
})
if err != nil {
t.Fatalf("expected prepare to succeed, got %v", err)
}
if len(prepared.Messages) != 1 || !strings.Contains(prepared.Messages[0].Content, "prompt fs") {
t.Fatalf("expected content_file body from prompt fs, got %+v", prepared.Messages)
}
}
func TestPrepareWorksWithPromptFile(t *testing.T) {
promptDir := t.TempDir()
promptPath := filepath.Join(promptDir, "single.yaml")
if err := os.WriteFile(promptPath, []byte(`
id: single.file.prompt
version: "1.0.0"
default_profile: local-fast
inputs:
- name: transcript
required: true
messages:
- role: user
content: "Summarize {{input \"transcript\"}} from file."
output:
format: text
validation_mode: none
repair_attempts: 0
`), 0o644); err != nil {
t.Fatal(err)
}
engine, err := scriptorium.NewEngine(scriptorium.Config{
ProfileDir: "./examples/profiles",
SchemaDir: "./examples/schemas",
}, scriptorium.WithPromptFile(promptPath))
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "single.file.prompt",
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."),
},
})
if err != nil {
t.Fatalf("expected prepare to succeed, got %v", err)
}
if prepared.PromptID != "single.file.prompt" {
t.Fatalf("unexpected prompt id: %q", prepared.PromptID)
}
}
func TestPrepareWorksWithProfileFSOverBuiltIns(t *testing.T) {
profileFS := fstest.MapFS{
"profiles/mistral-small-3.yaml": &fstest.MapFile{Data: []byte(`
id: mistral-small-3
endpoint: http://profile-fs/v1
model: profile-fs-model
`)},
}
engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts",
SchemaDir: "./examples/schemas",
}, scriptorium.WithProfileFS(profileFS, "profiles"))
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary",
ProfileID: "mistral-small-3",
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 prepare to succeed, got %v", err)
}
if prepared.EffectiveModelParams.Model != "profile-fs-model" {
t.Fatalf("expected profile fs to override built-in, got %q", prepared.EffectiveModelParams.Model)
}
}
func TestPrepareWorksWithProfileFileOverBuiltIns(t *testing.T) {
profileDir := t.TempDir()
profilePath := filepath.Join(profileDir, "mistral-small-3.yaml")
if err := os.WriteFile(profilePath, []byte(`
id: mistral-small-3
endpoint: http://profile-file/v1
model: profile-file-model
`), 0o644); err != nil {
t.Fatal(err)
}
engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts",
SchemaDir: "./examples/schemas",
}, scriptorium.WithProfileFile(profilePath))
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary",
ProfileID: "mistral-small-3",
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 prepare to succeed, got %v", err)
}
if prepared.EffectiveModelParams.Model != "profile-file-model" {
t.Fatalf("expected profile file to override built-in, got %q", prepared.EffectiveModelParams.Model)
}
}
func TestRunStructuredOutputWorksWithSchemaFS(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}}
engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: t.TempDir(),
ProfileDir: "./examples/profiles",
SchemaDir: t.TempDir(),
},
scriptorium.WithPromptFS(publicStructuredPromptFS("schema.fs.prompt", "events.schema.json"), "prompts"),
scriptorium.WithSchemaFS(publicSchemaFS(), "schemas"),
scriptorium.WithLLMClient(fake),
)
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
result, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "schema.fs.prompt",
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."),
},
})
if err != nil {
t.Fatalf("expected run to succeed, got %v", err)
}
if result.Validation.Status != scriptorium.ValidationPassed || !result.Validation.IsValid {
t.Fatalf("expected schema validation to pass, got %+v", result.Validation)
}
if len(fake.requests) != 1 || fake.requests[0].StructuredOutput == nil {
t.Fatalf("expected structured output request, got %+v", fake.requests)
}
}
func TestRunStructuredOutputWorksWithSchemaFile(t *testing.T) {
schemaDir := t.TempDir()
schemaPath := filepath.Join(schemaDir, "events.schema.json")
if err := os.WriteFile(schemaPath, []byte(publicSchemaJSON()), 0o644); err != nil {
t.Fatal(err)
}
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}}
engine, err := scriptorium.NewEngine(scriptorium.Config{
ProfileDir: "./examples/profiles",
},
scriptorium.WithPromptFS(publicStructuredPromptFS("schema.file.prompt", "events.schema.json"), "prompts"),
scriptorium.WithSchemaFile(schemaPath),
scriptorium.WithLLMClient(fake),
)
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
result, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "schema.file.prompt",
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."),
},
})
if err != nil {
t.Fatalf("expected run to succeed, got %v", err)
}
if result.Validation.Status != scriptorium.ValidationPassed || !result.Validation.IsValid {
t.Fatalf("expected schema validation to pass, got %+v", result.Validation)
}
}
func TestSourceOptionsRejectInvalidInputs(t *testing.T) {
missingFile := filepath.Join(t.TempDir(), "missing.yaml")
directoryPath := t.TempDir()
tests := []struct {
name string
opt scriptorium.Option
}{
{name: "prompt fs nil", opt: scriptorium.WithPromptFS(nil, "prompts")},
{name: "prompt fs empty root", opt: scriptorium.WithPromptFS(fstest.MapFS{}, "")},
{name: "prompt file empty", opt: scriptorium.WithPromptFile("")},
{name: "prompt file missing", opt: scriptorium.WithPromptFile(missingFile)},
{name: "prompt file directory", opt: scriptorium.WithPromptFile(directoryPath)},
{name: "profile fs nil", opt: scriptorium.WithProfileFS(nil, "profiles")},
{name: "profile fs empty root", opt: scriptorium.WithProfileFS(fstest.MapFS{}, "")},
{name: "profile file empty", opt: scriptorium.WithProfileFile("")},
{name: "schema fs nil", opt: scriptorium.WithSchemaFS(nil, "schemas")},
{name: "schema fs empty root", opt: scriptorium.WithSchemaFS(fstest.MapFS{}, "")},
{name: "schema file empty", opt: scriptorium.WithSchemaFile("")},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, tc.opt)
if !errors.Is(err, scriptorium.ErrInvalidConfig) {
t.Fatalf("expected ErrInvalidConfig, got %v", err)
}
})
}
}
func TestExtraParamsTypedNestedValuesAreCopiedAcrossPublicBoundary(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake))
@@ -862,6 +1113,43 @@ api_key_env: ` + apiKeyEnv + `
}
}
func publicStructuredPromptFS(id string, schemaPath string) fstest.MapFS {
return fstest.MapFS{
"prompts/prompt.yaml": &fstest.MapFile{Data: []byte(`id: ` + id + `
version: "1.0.0"
default_profile: local-fast
inputs:
- name: transcript
required: true
messages:
- role: user
content: "Extract events from {{input \"transcript\"}}."
output:
format: json
validation_mode: json_schema
schema_path: ` + schemaPath + `
repair_attempts: 0
`)},
}
}
func publicSchemaFS() fstest.MapFS {
return fstest.MapFS{
"schemas/events.schema.json": &fstest.MapFile{Data: []byte(publicSchemaJSON())},
}
}
func publicSchemaJSON() string {
return `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["events"],
"properties": {
"events": {"type": "array"}
}
}`
}
type fakeLLMClient struct {
response *scriptorium.GenerateResponse
err error