diff --git a/internal/profile/testdata/json_schema_missing_path.yaml b/internal/profile/testdata/json_schema_missing_path.yaml deleted file mode 100644 index 2d1a589..0000000 --- a/internal/profile/testdata/json_schema_missing_path.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: json-schema-missing-path -version: "1.0.0" -inputs: - - name: transcript - required: true -templates: - - role: user - content: "Return JSON" -output_format: json -validation: - validation_mode: json_schema diff --git a/internal/profile/testdata/negative_timeout.yaml b/internal/profile/testdata/negative_timeout.yaml deleted file mode 100644 index 8f96b11..0000000 --- a/internal/profile/testdata/negative_timeout.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: negative-timeout -version: "1.0.0" -inputs: - - name: transcript - required: true -templates: - - role: user - content: "Say hi" -output_format: text -validation: - validation_mode: none diff --git a/internal/profile/testdata/no_templates.yaml b/internal/profile/testdata/no_templates.yaml deleted file mode 100644 index c36aa34..0000000 --- a/internal/profile/testdata/no_templates.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: no-templates -version: 1.0.0 -inputs: - - name: transcript - required: true -templates: [] -output_format: text -validation: - validation_mode: none diff --git a/internal/profile/testdata/valid.yaml b/internal/profile/testdata/valid.yaml deleted file mode 100644 index b8d2318..0000000 --- a/internal/profile/testdata/valid.yaml +++ /dev/null @@ -1,19 +0,0 @@ -id: test-profile -version: "1.0.0" -default_profile: test-exec -description: A valid test prompt definition -inputs: - - name: transcript - required: true - content_type: text/markdown - - name: glossary - required: false - content_type: text/yaml -templates: - - role: system - content: "You are a helpful assistant." - - role: user - content: 'Analyze this: {{input "transcript"}}' -output_format: markdown -validation: - validation_mode: basic diff --git a/internal/prompt/go_renderer.go b/internal/prompt/go_renderer.go index 241ff50..fb0bb09 100644 --- a/internal/prompt/go_renderer.go +++ b/internal/prompt/go_renderer.go @@ -64,9 +64,6 @@ func (r *goRenderer) Render(ctx context.Context, definition *domain.PromptDefini } // Parse and execute template - if tmplMsg.ContentFile != "" { - return nil, fmt.Errorf("%w: message %d: content_file is not implemented yet", ErrRenderFailure, i) - } tmpl, err := template.New(fmt.Sprintf("msg_%d", i)).Funcs(funcs).Option("missingkey=error").Parse(tmplMsg.Content) if err != nil { return nil, fmt.Errorf("%w: message %d: %v", ErrInvalidTemplate, i, err) diff --git a/internal/promptdef/filesystem_repository.go b/internal/promptdef/filesystem_repository.go index db730fd..a872262 100644 --- a/internal/promptdef/filesystem_repository.go +++ b/internal/promptdef/filesystem_repository.go @@ -23,6 +23,36 @@ type filesystemRepository struct { dir string } +type promptDefinitionFile struct { + ID string `yaml:"id"` + Version string `yaml:"version"` + DefaultProfile *string `yaml:"default_profile"` + Description string `yaml:"description"` + Inputs []promptInputFile `yaml:"inputs"` + Messages []promptMessageFile `yaml:"messages"` + Output promptOutputContractFile `yaml:"output"` +} + +type promptInputFile struct { + Name string `yaml:"name"` + Required bool `yaml:"required"` + ContentType string `yaml:"content_type"` + Description string `yaml:"description"` +} + +type promptMessageFile struct { + Role string `yaml:"role"` + Content string `yaml:"content"` + ContentFile string `yaml:"content_file"` +} + +type promptOutputContractFile struct { + Format domain.OutputFormat `yaml:"format"` + ValidationMode domain.ValidationMode `yaml:"validation_mode"` + SchemaPath string `yaml:"schema_path"` + RepairAttempts int `yaml:"repair_attempts"` +} + func NewFilesystemRepository(dir string) Repository { return &filesystemRepository{dir: dir} } @@ -44,22 +74,25 @@ func (r *filesystemRepository) GetPromptDefinition(ctx context.Context, id strin default: } - if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) { + if file.IsDir() || !isYAMLFile(file.Name()) { continue } fullPath := filepath.Join(r.dir, file.Name()) - data, err := os.ReadFile(fullPath) + fileMatch := promptIDFromFileName(file.Name()) == id + + raw, err := loadPromptDefinitionFile(fullPath) if err != nil { - return nil, fmt.Errorf("failed to read prompt definition file %s: %w", file.Name(), err) + if fileMatch { + return nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, file.Name(), err) + } + continue } - var def domain.PromptDefinition - decoder := yaml.NewDecoder(bytes.NewReader(data)) - decoder.KnownFields(true) - if err := decoder.Decode(&def); err != nil { - if strings.TrimSuffix(strings.TrimSuffix(file.Name(), ".yaml"), ".yml") == id { - return nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, file.Name(), err) + def, err := normalizePromptDefinition(raw, fullPath) + if err != nil { + if fileMatch || strings.TrimSpace(raw.ID) == id { + return nil, fmt.Errorf("%w: %s: %v", ErrInvalidPromptDefinition, file.Name(), err) } continue } @@ -70,82 +103,166 @@ func (r *filesystemRepository) GetPromptDefinition(ctx context.Context, id strin if version != "" && def.Version != version { continue } - if err := validatePromptDefinition(&def); err != nil { - return nil, fmt.Errorf("%w: %s: %v", ErrInvalidPromptDefinition, file.Name(), err) - } - return &def, nil + return def, nil } return nil, ErrPromptDefinitionNotFound } -func validatePromptDefinition(d *domain.PromptDefinition) error { - if d.ID == "" { - return errors.New("prompt id is required") +func loadPromptDefinitionFile(path string) (*promptDefinitionFile, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read prompt definition file: %w", err) } - if d.Version == "" { - return errors.New("prompt version is required") + + var raw promptDefinitionFile + decoder := yaml.NewDecoder(bytes.NewReader(data)) + decoder.KnownFields(true) + if err := decoder.Decode(&raw); err != nil { + return nil, err } - if len(d.Templates) == 0 { - return errors.New("at least one prompt template message is required") + return &raw, nil +} + +func normalizePromptDefinition(raw *promptDefinitionFile, sourcePath string) (*domain.PromptDefinition, error) { + if raw == nil { + return nil, errors.New("prompt definition is nil") } - if len(d.Inputs) == 0 { - return errors.New("at least one prompt input is required") + + id := strings.TrimSpace(raw.ID) + if id == "" { + return nil, errors.New("id is required") } - for i, input := range d.Inputs { - if strings.TrimSpace(input.Name) == "" { - return fmt.Errorf("input %d has empty name", i) + + version := strings.TrimSpace(raw.Version) + if version == "" { + return nil, errors.New("version is required") + } + + if len(raw.Messages) == 0 { + return nil, errors.New("at least one message is required") + } + + inputs := make([]domain.PromptInput, 0, len(raw.Inputs)) + seenInputNames := make(map[string]struct{}, len(raw.Inputs)) + for i, in := range raw.Inputs { + name := strings.TrimSpace(in.Name) + if name == "" { + return nil, fmt.Errorf("input %d has empty name", i) + } + if _, exists := seenInputNames[name]; exists { + return nil, fmt.Errorf("duplicate input name %q", name) + } + seenInputNames[name] = struct{}{} + + inputs = append(inputs, domain.PromptInput{ + Name: name, + Required: in.Required, + ContentType: strings.TrimSpace(in.ContentType), + Description: strings.TrimSpace(in.Description), + }) + } + + templates := make([]domain.PromptMessageTemplate, 0, len(raw.Messages)) + promptDir := filepath.Dir(sourcePath) + for i, msg := range raw.Messages { + role := strings.TrimSpace(msg.Role) + if role == "" { + return nil, fmt.Errorf("message %d role is required", i) + } + + hasContent := strings.TrimSpace(msg.Content) != "" + hasContentFile := strings.TrimSpace(msg.ContentFile) != "" + if hasContent == hasContentFile { + return nil, fmt.Errorf("message %d (%s) must set exactly one of content or content_file", i, role) + } + + templateContent := msg.Content + resolvedContentFile := "" + if hasContentFile { + resolvedPath := strings.TrimSpace(msg.ContentFile) + if !filepath.IsAbs(resolvedPath) { + resolvedPath = filepath.Join(promptDir, resolvedPath) + } + resolvedPath = filepath.Clean(resolvedPath) + + body, err := os.ReadFile(resolvedPath) + if err != nil { + return nil, fmt.Errorf("prompt %q message %d (%s): failed to read content_file %q: %w", id, i, role, msg.ContentFile, err) + } + templateContent = string(body) + resolvedContentFile = resolvedPath + } + + templates = append(templates, domain.PromptMessageTemplate{ + Role: role, + Content: templateContent, + ContentFile: resolvedContentFile, + }) + } + + if !isValidOutputFormat(raw.Output.Format) { + return nil, fmt.Errorf("invalid output format: %q", raw.Output.Format) + } + if !isValidValidationMode(raw.Output.ValidationMode) { + return nil, fmt.Errorf("invalid validation mode: %q", raw.Output.ValidationMode) + } + if raw.Output.ValidationMode == domain.ValidationJSONSchema && strings.TrimSpace(raw.Output.SchemaPath) == "" { + return nil, errors.New("output.schema_path is required when output.validation_mode is json_schema") + } + if raw.Output.RepairAttempts < 0 { + return nil, errors.New("output.repair_attempts must be greater than or equal to 0") + } + + defaultProfile := "" + if raw.DefaultProfile != nil { + defaultProfile = strings.TrimSpace(*raw.DefaultProfile) + if defaultProfile == "" { + return nil, errors.New("default_profile must be a non-empty string when set") } } - for i, t := range d.Templates { - if !isValidMessageRole(t.Role) { - return fmt.Errorf("template message %d has invalid role %q", i, t.Role) - } - if strings.TrimSpace(t.Content) == "" && strings.TrimSpace(t.ContentFile) == "" { - return fmt.Errorf("template message %d must provide content or content_file", i) - } - if strings.TrimSpace(t.Content) != "" && strings.TrimSpace(t.ContentFile) != "" { - return fmt.Errorf("template message %d cannot set both content and content_file", i) - } - } - if !isValidOutputFormat(d.OutputFormat) { - return fmt.Errorf("invalid output format: %s", d.OutputFormat) - } - if !isValidValidationMode(d.Validation.ValidationMode) { - return fmt.Errorf("invalid validation mode: %s", d.Validation.ValidationMode) - } - if d.Validation.RepairAttempts < 0 { - return errors.New("validation.repair_attempts must be greater than or equal to 0") - } - if d.Validation.ValidationMode == domain.ValidationJSONSchema && strings.TrimSpace(d.Validation.SchemaPath) == "" { - return errors.New("validation.schema_path is required when validation_mode is json_schema") - } - if d.Validation.Format != "" && d.Validation.Format != d.OutputFormat { - return fmt.Errorf("validation format %q does not match output format %q", d.Validation.Format, d.OutputFormat) - } - return nil + + return &domain.PromptDefinition{ + ID: id, + Version: version, + DefaultProfile: defaultProfile, + Description: strings.TrimSpace(raw.Description), + Inputs: inputs, + Templates: templates, + OutputFormat: raw.Output.Format, + Validation: domain.OutputContract{ + Format: raw.Output.Format, + ValidationMode: raw.Output.ValidationMode, + SchemaPath: strings.TrimSpace(raw.Output.SchemaPath), + RepairAttempts: raw.Output.RepairAttempts, + }, + }, nil +} + +func isYAMLFile(name string) bool { + return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") +} + +func promptIDFromFileName(name string) string { + name = strings.TrimSuffix(name, ".yaml") + name = strings.TrimSuffix(name, ".yml") + return name } func isValidOutputFormat(f domain.OutputFormat) bool { switch f { case domain.FormatText, domain.FormatMarkdown, domain.FormatJSON: return true + default: + return false } - return false } func isValidValidationMode(m domain.ValidationMode) bool { switch m { case domain.ValidationNone, domain.ValidationBasic, domain.ValidationJSON, domain.ValidationJSONSchema: return true + default: + return false } - return false -} - -func isValidMessageRole(role string) bool { - switch role { - case "system", "user", "assistant", "developer": - return true - } - return false } diff --git a/internal/promptdef/repository_test.go b/internal/promptdef/repository_test.go index 0a8859a..050bb64 100644 --- a/internal/promptdef/repository_test.go +++ b/internal/promptdef/repository_test.go @@ -3,104 +3,143 @@ package promptdef import ( "context" "errors" + "io/fs" "os" "path/filepath" + "strings" "testing" "gitea.maximumdirect.net/eric/scriptorium/internal/domain" ) func TestFilesystemRepository_GetPromptDefinition(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "promptdef_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - testDataDir := "testdata" - files, err := os.ReadDir(testDataDir) - if err != nil { - t.Fatalf("failed to read testdata: %v", err) - } - - for _, f := range files { - src := filepath.Join(testDataDir, f.Name()) - dst := filepath.Join(tmpDir, f.Name()) - data, err := os.ReadFile(src) - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(dst, data, 0644); err != nil { - t.Fatal(err) - } + tmpDir := t.TempDir() + if err := copyTree("testdata", tmpDir); err != nil { + t.Fatalf("failed to copy testdata: %v", err) } repo := NewFilesystemRepository(tmpDir) ctx := context.Background() - t.Run("valid prompt definition", func(t *testing.T) { - p, err := repo.GetPromptDefinition(ctx, "test-profile", "") + t.Run("valid inline prompt", func(t *testing.T) { + p, err := repo.GetPromptDefinition(ctx, "valid-inline", "") if err != nil { t.Fatalf("expected no error, got %v", err) } - if p == nil || p.ID != "test-profile" { - t.Errorf("expected prompt definition test-profile, got %v", p) + if p.ID != "valid-inline" { + t.Fatalf("unexpected id: %q", p.ID) } if p.Version != "1.0.0" { - t.Fatalf("expected version 1.0.0, got %q", p.Version) - } - if len(p.Inputs) != 2 || p.Inputs[0].Name != "transcript" || p.Inputs[1].Name != "glossary" { - t.Fatalf("unexpected inputs: %#v", p.Inputs) - } - if len(p.Templates) != 2 { - t.Fatalf("expected 2 templates, got %d", len(p.Templates)) - } - if p.Templates[0].Role != "system" || p.Templates[1].Role != "user" { - t.Fatalf("unexpected template roles: %#v", p.Templates) + t.Fatalf("unexpected version: %q", p.Version) } if p.OutputFormat != domain.FormatMarkdown { - t.Fatalf("expected output format markdown, got %q", p.OutputFormat) + t.Fatalf("unexpected output format: %q", p.OutputFormat) } if p.Validation.ValidationMode != domain.ValidationBasic { - t.Fatalf("expected validation mode basic, got %q", p.Validation.ValidationMode) + t.Fatalf("unexpected validation mode: %q", p.Validation.ValidationMode) } - if p.DefaultProfile != "test-exec" { - t.Fatalf("expected default profile test-exec, got %q", p.DefaultProfile) + if len(p.Templates) != 2 { + t.Fatalf("expected 2 messages, got %d", len(p.Templates)) } }) - t.Run("invalid YAML", func(t *testing.T) { - _, err := repo.GetPromptDefinition(ctx, "invalid_yaml", "") - if !errors.Is(err, ErrInvalidYAML) { - t.Errorf("expected ErrInvalidYAML, got %v", err) + t.Run("valid file-backed prompt", func(t *testing.T) { + p, err := repo.GetPromptDefinition(ctx, "valid-file-backed", "") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(p.Templates) != 2 { + t.Fatalf("expected 2 messages, got %d", len(p.Templates)) + } + if !strings.Contains(p.Templates[1].Content, "{{input \"transcript\"}}") { + t.Fatalf("expected content_file template body to be loaded, got %q", p.Templates[1].Content) + } + if p.Templates[1].ContentFile == "" { + t.Fatal("expected ContentFile source metadata to be preserved") + } + if !filepath.IsAbs(p.Templates[1].ContentFile) { + t.Fatalf("expected resolved content_file path to be absolute, got %q", p.Templates[1].ContentFile) } }) - t.Run("missing ID", func(t *testing.T) { - _, err := repo.GetPromptDefinition(ctx, "missing-id", "") + t.Run("prompt with default_profile", func(t *testing.T) { + p, err := repo.GetPromptDefinition(ctx, "with-default-profile", "") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if p.DefaultProfile != "local-default" { + t.Fatalf("unexpected default profile: %q", p.DefaultProfile) + } + }) + + t.Run("version lookup", func(t *testing.T) { + _, err := repo.GetPromptDefinition(ctx, "valid-inline", "9.9.9") if !errors.Is(err, ErrPromptDefinitionNotFound) { - t.Errorf("expected ErrPromptDefinitionNotFound for profile with missing ID, got %v", err) + t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err) } }) - t.Run("no templates", func(t *testing.T) { - _, err := repo.GetPromptDefinition(ctx, "no-templates", "") - if !errors.Is(err, ErrInvalidPromptDefinition) { - t.Errorf("expected ErrInvalidPromptDefinition for profile with no templates, got %v", err) - } - }) + cases := []struct { + name string + id string + targetErr error + errSubstrs []string + }{ + {name: "invalid YAML", id: "invalid_yaml", targetErr: ErrInvalidYAML}, + {name: "missing id", id: "missing_id", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"id is required"}}, + {name: "no messages", id: "no_messages", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"at least one message is required"}}, + {name: "both content and content_file", id: "both_content_and_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"exactly one"}}, + {name: "neither content nor content_file", id: "neither_content_nor_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"exactly one"}}, + {name: "missing content_file", id: "missing_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"failed to read content_file"}}, + {name: "duplicate input names", id: "duplicate_input_names", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"duplicate input name"}}, + {name: "invalid validation mode", id: "invalid_validation_mode", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"invalid validation mode"}}, + {name: "json_schema without schema_path", id: "json_schema_without_schema_path", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"schema_path"}}, + } - t.Run("json schema mode missing schema path", func(t *testing.T) { - _, err := repo.GetPromptDefinition(ctx, "json-schema-missing-path", "") - if !errors.Is(err, ErrInvalidPromptDefinition) { - t.Errorf("expected ErrInvalidPromptDefinition for json_schema profile without schema_path, got %v", err) - } - }) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := repo.GetPromptDefinition(ctx, tc.id, "") + if !errors.Is(err, tc.targetErr) { + t.Fatalf("expected %v, got %v", tc.targetErr, err) + } + for _, sub := range tc.errSubstrs { + if !strings.Contains(err.Error(), sub) { + t.Fatalf("expected error to contain %q, got %v", sub, err) + } + } + }) + } t.Run("prompt definition not found", func(t *testing.T) { - _, err := repo.GetPromptDefinition(ctx, "unknown", "") + _, err := repo.GetPromptDefinition(ctx, "does-not-exist", "") if !errors.Is(err, ErrPromptDefinitionNotFound) { - t.Errorf("expected ErrPromptDefinitionNotFound, got %v", err) + t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err) } }) } + +func copyTree(src, dst string) error { + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + + target := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(target, 0o755) + } + + data, err := os.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(target, data, 0o644) + }) +} diff --git a/internal/promptdef/testdata/both_content_and_content_file.yaml b/internal/promptdef/testdata/both_content_and_content_file.yaml new file mode 100644 index 0000000..de11155 --- /dev/null +++ b/internal/promptdef/testdata/both_content_and_content_file.yaml @@ -0,0 +1,10 @@ +id: both-content-and-content-file +version: "1.0.0" +messages: + - role: user + content: "Hi" + content_file: ./messages/user_prompt.tmpl +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/duplicate_input_names.yaml b/internal/promptdef/testdata/duplicate_input_names.yaml new file mode 100644 index 0000000..9412e4d --- /dev/null +++ b/internal/promptdef/testdata/duplicate_input_names.yaml @@ -0,0 +1,14 @@ +id: duplicate-input-names +version: "1.0.0" +inputs: + - name: transcript + required: true + - name: transcript + required: false +messages: + - role: user + content: "Hi" +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/invalid_validation_mode.yaml b/internal/promptdef/testdata/invalid_validation_mode.yaml new file mode 100644 index 0000000..478abc5 --- /dev/null +++ b/internal/promptdef/testdata/invalid_validation_mode.yaml @@ -0,0 +1,9 @@ +id: invalid-validation-mode +version: "1.0.0" +messages: + - role: user + content: "Hi" +output: + format: text + validation_mode: nope + repair_attempts: 0 diff --git a/internal/promptdef/testdata/invalid_yaml.yaml b/internal/promptdef/testdata/invalid_yaml.yaml index 801ce79..8cc64e8 100644 --- a/internal/promptdef/testdata/invalid_yaml.yaml +++ b/internal/promptdef/testdata/invalid_yaml.yaml @@ -1,5 +1,9 @@ id: invalid-yaml -version: 1.0.0 -templates: - - role: system - content: [unclosed bracket +version: "1.0.0" +messages: + - role: user + content: [broken +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/json_schema_missing_path.yaml b/internal/promptdef/testdata/json_schema_missing_path.yaml deleted file mode 100644 index 2d1a589..0000000 --- a/internal/promptdef/testdata/json_schema_missing_path.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: json-schema-missing-path -version: "1.0.0" -inputs: - - name: transcript - required: true -templates: - - role: user - content: "Return JSON" -output_format: json -validation: - validation_mode: json_schema diff --git a/internal/promptdef/testdata/json_schema_without_schema_path.yaml b/internal/promptdef/testdata/json_schema_without_schema_path.yaml new file mode 100644 index 0000000..6dbea5b --- /dev/null +++ b/internal/promptdef/testdata/json_schema_without_schema_path.yaml @@ -0,0 +1,9 @@ +id: json-schema-without-schema-path +version: "1.0.0" +messages: + - role: user + content: "Return JSON" +output: + format: json + validation_mode: json_schema + repair_attempts: 0 diff --git a/internal/promptdef/testdata/messages/user_prompt.tmpl b/internal/promptdef/testdata/messages/user_prompt.tmpl new file mode 100644 index 0000000..c8fc459 --- /dev/null +++ b/internal/promptdef/testdata/messages/user_prompt.tmpl @@ -0,0 +1,2 @@ +Use transcript: +{{input "transcript"}} diff --git a/internal/promptdef/testdata/missing_content_file.yaml b/internal/promptdef/testdata/missing_content_file.yaml new file mode 100644 index 0000000..42e44fa --- /dev/null +++ b/internal/promptdef/testdata/missing_content_file.yaml @@ -0,0 +1,9 @@ +id: missing-content-file +version: "1.0.0" +messages: + - role: user + content_file: ./messages/does_not_exist.tmpl +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/missing_id.yaml b/internal/promptdef/testdata/missing_id.yaml index 21a928d..6d21ccc 100644 --- a/internal/promptdef/testdata/missing_id.yaml +++ b/internal/promptdef/testdata/missing_id.yaml @@ -1,11 +1,8 @@ -version: 1.0.0 -description: Missing ID -inputs: - - name: transcript - required: true -templates: - - role: system - content: Hello -output_format: text -validation: +version: "1.0.0" +messages: + - role: user + content: "Hi" +output: + format: text validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/negative_timeout.yaml b/internal/promptdef/testdata/negative_timeout.yaml deleted file mode 100644 index 8f96b11..0000000 --- a/internal/promptdef/testdata/negative_timeout.yaml +++ /dev/null @@ -1,11 +0,0 @@ -id: negative-timeout -version: "1.0.0" -inputs: - - name: transcript - required: true -templates: - - role: user - content: "Say hi" -output_format: text -validation: - validation_mode: none diff --git a/internal/promptdef/testdata/neither_content_nor_content_file.yaml b/internal/promptdef/testdata/neither_content_nor_content_file.yaml new file mode 100644 index 0000000..525419b --- /dev/null +++ b/internal/promptdef/testdata/neither_content_nor_content_file.yaml @@ -0,0 +1,8 @@ +id: neither-content-nor-content-file +version: "1.0.0" +messages: + - role: user +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/no_messages.yaml b/internal/promptdef/testdata/no_messages.yaml new file mode 100644 index 0000000..b4b2bbf --- /dev/null +++ b/internal/promptdef/testdata/no_messages.yaml @@ -0,0 +1,6 @@ +id: no-messages +version: "1.0.0" +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/internal/promptdef/testdata/no_templates.yaml b/internal/promptdef/testdata/no_templates.yaml deleted file mode 100644 index c36aa34..0000000 --- a/internal/promptdef/testdata/no_templates.yaml +++ /dev/null @@ -1,9 +0,0 @@ -id: no-templates -version: 1.0.0 -inputs: - - name: transcript - required: true -templates: [] -output_format: text -validation: - validation_mode: none diff --git a/internal/promptdef/testdata/valid.yaml b/internal/promptdef/testdata/valid.yaml deleted file mode 100644 index b8d2318..0000000 --- a/internal/promptdef/testdata/valid.yaml +++ /dev/null @@ -1,19 +0,0 @@ -id: test-profile -version: "1.0.0" -default_profile: test-exec -description: A valid test prompt definition -inputs: - - name: transcript - required: true - content_type: text/markdown - - name: glossary - required: false - content_type: text/yaml -templates: - - role: system - content: "You are a helpful assistant." - - role: user - content: 'Analyze this: {{input "transcript"}}' -output_format: markdown -validation: - validation_mode: basic diff --git a/internal/promptdef/testdata/valid_file_backed.yaml b/internal/promptdef/testdata/valid_file_backed.yaml new file mode 100644 index 0000000..f57f56a --- /dev/null +++ b/internal/promptdef/testdata/valid_file_backed.yaml @@ -0,0 +1,14 @@ +id: valid-file-backed +version: "1.0.0" +inputs: + - name: transcript + required: true +messages: + - role: system + content: "Return markdown." + - role: user + content_file: ./messages/user_prompt.tmpl +output: + format: markdown + validation_mode: basic + repair_attempts: 0 diff --git a/internal/promptdef/testdata/valid_inline.yaml b/internal/promptdef/testdata/valid_inline.yaml new file mode 100644 index 0000000..3f61059 --- /dev/null +++ b/internal/promptdef/testdata/valid_inline.yaml @@ -0,0 +1,18 @@ +id: valid-inline +version: "1.0.0" +inputs: + - name: transcript + required: true + content_type: text/markdown + description: Transcript content +messages: + - role: system + content: "You are concise." + - role: user + content: | + Summarize: + {{input "transcript"}} +output: + format: markdown + validation_mode: basic + repair_attempts: 0 diff --git a/internal/promptdef/testdata/with_default_profile.yaml b/internal/promptdef/testdata/with_default_profile.yaml new file mode 100644 index 0000000..1628b41 --- /dev/null +++ b/internal/promptdef/testdata/with_default_profile.yaml @@ -0,0 +1,13 @@ +id: with-default-profile +version: "1.0.0" +default_profile: local-default +inputs: + - name: transcript + required: true +messages: + - role: user + content: "Write output" +output: + format: text + validation_mode: none + repair_attempts: 0 diff --git a/profiles/dnd.session_recap.yaml b/profiles/dnd.session_recap.yaml index 5e7907e..f0ab2ae 100644 --- a/profiles/dnd.session_recap.yaml +++ b/profiles/dnd.session_recap.yaml @@ -9,7 +9,7 @@ inputs: - name: glossary required: false content_type: text/yaml -templates: +messages: - role: system content: | You create concise tabletop RPG session recaps. @@ -26,6 +26,7 @@ templates: Glossary: {{input "glossary"}} -output_format: markdown -validation: +output: + format: markdown validation_mode: basic + repair_attempts: 0 diff --git a/profiles/generic.markdown_summary.yaml b/profiles/generic.markdown_summary.yaml index caf7c5d..0f33bd6 100644 --- a/profiles/generic.markdown_summary.yaml +++ b/profiles/generic.markdown_summary.yaml @@ -11,7 +11,7 @@ inputs: required: false content_type: text/yaml description: Optional glossary context -templates: +messages: - role: system content: | You are a concise analysis assistant. @@ -25,6 +25,7 @@ templates: Reference glossary: {{input "glossary"}} -output_format: markdown -validation: +output: + format: markdown validation_mode: basic + repair_attempts: 0 diff --git a/profiles/generic.structured_events.yaml b/profiles/generic.structured_events.yaml index d873280..0edb918 100644 --- a/profiles/generic.structured_events.yaml +++ b/profiles/generic.structured_events.yaml @@ -9,7 +9,7 @@ inputs: - name: glossary required: false content_type: text/yaml -templates: +messages: - role: system content: | Return only JSON following the requested schema. @@ -23,8 +23,7 @@ templates: Glossary: {{input "glossary"}} -output_format: json -validation: +output: format: json validation_mode: json_schema schema_path: structured_events.schema.json