feat(capability/skills): 可复用工作流技能(Skill 模型 + Store + YAML Loader + Runner 展开 + 注册)
This commit is contained in:
1 parent
d947ec41a9
commit
915aa8a100
10 files changed
+1235
No files matched your search
@@ -0,0 +1,135 @@
|
||||
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")
|
||||
}
|
||||
Reference in new issue
Block a user