package loader import ( "errors" "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // ErrRequiredField is wrapped by validation errors so callers can detect a // persona that is missing required fields. var ErrRequiredField = errors.New("persona: required field missing") // Load reads and unmarshals a single persona YAML file at path. Missing required // fields produce a %w-wrapped validation error; IO and parse failures are also // wrapped with their source path. func Load(path string) (*Persona, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("persona: read %s: %w", path, err) } p := &Persona{} if err := yaml.Unmarshal(data, p); err != nil { return nil, fmt.Errorf("persona: parse %s: %w", path, err) } if err := p.validate(); err != nil { return nil, err } return p, nil } // LoadDir loads every *.yaml / *.yml file directly inside dir in lexical order. // It fails atomically: the first file that errors aborts the whole load. The // directory itself must exist. func LoadDir(dir string) ([]*Persona, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf("persona: read dir %s: %w", dir, err) } files := make([]string, 0, len(entries)) for _, e := range entries { if e.IsDir() || !isYAMLExt(filepath.Ext(e.Name())) { continue } files = append(files, e.Name()) } personas := make([]*Persona, 0, len(files)) for _, name := range files { p, err := Load(filepath.Join(dir, name)) if err != nil { return nil, fmt.Errorf("persona: dir %s: %w", dir, err) } personas = append(personas, p) } return personas, nil } // validate checks the persona for its required identity fields and reports all // the missing ones at once. func (p *Persona) validate() error { missing := make([]string, 0, 5) if p.ID == "" { missing = append(missing, "id") } if p.Name == "" { missing = append(missing, "name") } if p.CharacterRef == "" { missing = append(missing, "characterRef") } if p.Identity.Role == "" { missing = append(missing, "identity.role") } if p.Identity.SystemPrompt == "" { missing = append(missing, "identity.systemPrompt") } if len(missing) > 0 { return fmt.Errorf("%w %q: %s", ErrRequiredField, p.ID, strings.Join(missing, ", ")) } if err := p.ValidateThinking(); err != nil { return err } if err := p.ValidateAwakening(); err != nil { return err } return nil } // isYAMLExt reports whether ext is one of the accepted YAML extensions. func isYAMLExt(ext string) bool { return ext == ".yaml" || ext == ".yml" }