136 lines
3.8 KiB
Go
136 lines
3.8 KiB
Go
package skills
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
)
|
|
|
|
// Loader loads Skill definitions from YAML — a single stream (Load), a file
|
|
// (LoadFile), or a directory of configs/skills/*.yaml files (LoadDir). It does
|
|
// not persist; persist via Store or publish via Register.
|
|
type Loader struct {
|
|
log *slog.Logger
|
|
}
|
|
|
|
// NewLoader builds a Loader. log may be nil.
|
|
func NewLoader(log *slog.Logger) *Loader {
|
|
return &Loader{log: sourcedLog(log)}
|
|
}
|
|
|
|
// skillYAML is the on-disk YAML shape of a skill.
|
|
type skillYAML struct {
|
|
Name string `yaml:"name"`
|
|
Description string `yaml:"description"`
|
|
Prompt string `yaml:"prompt"`
|
|
Steps []yamlStep `yaml:"steps"`
|
|
Refs []yamlRef `yaml:"refs"`
|
|
Guardrails []yamlGuardrail `yaml:"guardrails"`
|
|
}
|
|
|
|
type yamlStep struct {
|
|
Name string `yaml:"name"`
|
|
Prompt string `yaml:"prompt"`
|
|
Depends []string `yaml:"depends"`
|
|
}
|
|
|
|
type yamlRef struct {
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"`
|
|
Value string `yaml:"value"`
|
|
Optional bool `yaml:"optional"`
|
|
}
|
|
|
|
type yamlGuardrail struct {
|
|
ID string `yaml:"id"`
|
|
Message string `yaml:"message"`
|
|
Enforced bool `yaml:"enforced"`
|
|
}
|
|
|
|
// Load parses one skill from r. It returns ErrInvalidSkill when the document has
|
|
// a blank name.
|
|
func (l *Loader) Load(r io.Reader) (*Skill, error) {
|
|
if r == nil {
|
|
return nil, fmt.Errorf("skills: loader: load: %w", ErrInvalidSkill)
|
|
}
|
|
var doc skillYAML
|
|
if err := yaml.NewDecoder(r).Decode(&doc); err != nil {
|
|
return nil, fmt.Errorf("skills: loader: decode: %w", err)
|
|
}
|
|
skill, err := doc.toSkill()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
l.log.Info("skill loaded", "skill", skill.Name())
|
|
return skill, nil
|
|
}
|
|
|
|
// LoadFile loads the single skill YAML file at path.
|
|
func (l *Loader) LoadFile(path string) (*Skill, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("skills: loader: open %q: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
skill, err := l.Load(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("skills: loader: file %q: %w", path, err)
|
|
}
|
|
return skill, nil
|
|
}
|
|
|
|
// LoadDir loads every *.yaml/*.yml skill file in dir, in readdir order. A file
|
|
// that fails to parse stops the load. Non-YAML files are skipped.
|
|
func (l *Loader) LoadDir(dir string) ([]*Skill, error) {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("skills: loader: read dir %q: %w", dir, err)
|
|
}
|
|
out := make([]*Skill, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !isYAML(e.Name()) {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, e.Name())
|
|
skill, err := l.LoadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("skills: loader: dir %q: %w", path, err)
|
|
}
|
|
out = append(out, skill)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// toSkill converts the YAML document into a Skill, validating the required name.
|
|
func (d skillYAML) toSkill() (*Skill, error) {
|
|
steps := make([]sdk.Step, 0, len(d.Steps))
|
|
for _, st := range d.Steps {
|
|
steps = append(steps, sdk.Step{Name: st.Name, Prompt: st.Prompt, Depends: st.Depends})
|
|
}
|
|
refs := make([]sdk.Ref, 0, len(d.Refs))
|
|
for _, rf := range d.Refs {
|
|
refs = append(refs, sdk.Ref{Name: rf.Name, Type: rf.Type, Value: rf.Value, Optional: rf.Optional})
|
|
}
|
|
guardrails := make([]sdk.Guardrail, 0, len(d.Guardrails))
|
|
for _, g := range d.Guardrails {
|
|
guardrails = append(guardrails, sdk.Guardrail{ID: g.ID, Message: g.Message, Enforced: g.Enforced})
|
|
}
|
|
skill, err := New(d.Name, d.Description, d.Prompt, steps, refs, guardrails)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("skills: loader: invalid skill %q: %w", d.Name, err)
|
|
}
|
|
return skill, nil
|
|
}
|
|
|
|
func isYAML(name string) bool {
|
|
lower := strings.ToLower(name)
|
|
return strings.HasSuffix(lower, ".yaml") || strings.HasSuffix(lower, ".yml")
|
|
}
|