78 lines
1.6 KiB
Go
78 lines
1.6 KiB
Go
// skill_match.go — Semantic skill discovery.
|
|
//
|
|
// Matches user requests to skills using embedding similarity. Returns
|
|
// high-scoring skills (above threshold) up to a limit. Uses the shared
|
|
// embedding model for cosine similarity ranking.
|
|
|
|
package agent
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
|
|
"ollie/skills"
|
|
)
|
|
|
|
// Skill matching configuration.
|
|
const (
|
|
skillMatchThreshold = 0.35
|
|
skillMatchLimit = 3
|
|
)
|
|
|
|
var (
|
|
skillIndex *skills.Index
|
|
skillInitErr error
|
|
skillOnce sync.Once
|
|
)
|
|
|
|
// matchSkills finds skills relevant to the user's input and returns
|
|
// their content formatted for injection. Initializes the skill index
|
|
// on first call.
|
|
func matchSkills(input string) string {
|
|
skillOnce.Do(func() {
|
|
idx, err := skills.NewIndex(skills.DefaultModelDir(), skills.DefaultSkillDirs())
|
|
if err != nil {
|
|
skillInitErr = err
|
|
return
|
|
}
|
|
skillIndex = idx
|
|
})
|
|
|
|
if skillIndex == nil {
|
|
return ""
|
|
}
|
|
|
|
results, err := skillIndex.Match(input, skillMatchThreshold, skillMatchLimit)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if len(results) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("<skills>\n")
|
|
for _, r := range results {
|
|
sb.WriteString("<skill name=\"")
|
|
sb.WriteString(r.Skill.Name)
|
|
sb.WriteString("\" relevance=\"")
|
|
sb.WriteString(formatScore(r.Score))
|
|
sb.WriteString("\">\n")
|
|
sb.WriteString(r.Skill.Content)
|
|
if !strings.HasSuffix(r.Skill.Content, "\n") {
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("</skill>\n")
|
|
}
|
|
sb.WriteString("</skills>\n")
|
|
return sb.String()
|
|
}
|
|
|
|
func formatScore(s float32) string {
|
|
pct := int(s * 100)
|
|
if pct > 99 {
|
|
pct = 99
|
|
}
|
|
return string([]byte{'0' + byte(pct/10), '0' + byte(pct%10), '%'})
|
|
}
|