Files
Cyrene_v2/internal/persona/loader/load.go
T

90 lines
2.4 KiB
Go

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, ", "))
}
return nil
}
// isYAMLExt reports whether ext is one of the accepted YAML extensions.
func isYAMLExt(ext string) bool {
return ext == ".yaml" || ext == ".yml"
}