Refactor: load execution profiles from YAML and split prompt definitions into promptdef repository
This commit is contained in:
151
internal/promptdef/filesystem_repository.go
Normal file
151
internal/promptdef/filesystem_repository.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package promptdef
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPromptDefinitionNotFound = errors.New("prompt definition not found")
|
||||
ErrInvalidYAML = errors.New("invalid YAML format")
|
||||
ErrInvalidPromptDefinition = errors.New("invalid prompt definition configuration")
|
||||
)
|
||||
|
||||
type filesystemRepository struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func NewFilesystemRepository(dir string) Repository {
|
||||
return &filesystemRepository{dir: dir}
|
||||
}
|
||||
|
||||
func (r *filesystemRepository) GetPromptDefinition(ctx context.Context, id string, version string) (*domain.PromptDefinition, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidPromptDefinition)
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(r.dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition directory: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(r.dir, file.Name())
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition file %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if def.ID != id {
|
||||
continue
|
||||
}
|
||||
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 nil, ErrPromptDefinitionNotFound
|
||||
}
|
||||
|
||||
func validatePromptDefinition(d *domain.PromptDefinition) error {
|
||||
if d.ID == "" {
|
||||
return errors.New("prompt id is required")
|
||||
}
|
||||
if d.Version == "" {
|
||||
return errors.New("prompt version is required")
|
||||
}
|
||||
if len(d.Templates) == 0 {
|
||||
return errors.New("at least one prompt template message is required")
|
||||
}
|
||||
if len(d.Inputs) == 0 {
|
||||
return errors.New("at least one prompt input is required")
|
||||
}
|
||||
for i, input := range d.Inputs {
|
||||
if strings.TrimSpace(input.Name) == "" {
|
||||
return fmt.Errorf("input %d has empty name", i)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func isValidOutputFormat(f domain.OutputFormat) bool {
|
||||
switch f {
|
||||
case domain.FormatText, domain.FormatMarkdown, domain.FormatJSON:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidValidationMode(m domain.ValidationMode) bool {
|
||||
switch m {
|
||||
case domain.ValidationNone, domain.ValidationBasic, domain.ValidationJSON, domain.ValidationJSONSchema:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidMessageRole(role string) bool {
|
||||
switch role {
|
||||
case "system", "user", "assistant", "developer":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user