Add public asset source options
This commit is contained in:
288
engine_test.go
288
engine_test.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user