Files
egutierrez f4690a2ba6 feat: tipos puros para sistema de skills
Agregar tipos puros en pkg/skills/ para el sistema de skills:

- SkillMeta: metadata de skills (name, description, category)
- Skill: representacion completa con instrucciones y recursos
- SkillMatch: resultado de matching con confidence score
- Match(): funcion pura de matching por keywords
- FilterByCategory(): filtrado de skills por categorias

Incluye tests unitarios completos para matching y filtrado.

Arquitectura: pure core, cero side effects en pkg/.
2026-03-08 22:13:05 +00:00

104 lines
2.5 KiB
Go

package skills
import (
"sort"
"strings"
)
// Match retorna las skills mas relevantes para una query dada.
// Implementacion inicial: keyword matching simple contra name + description.
// La query y las skills son procesadas en lowercase para matching case-insensitive.
//
// El scoring es basico:
// - Match exacto en name: 1.0
// - Match parcial en name: 0.8
// - Match en description: 0.6 * (palabras coincidentes / palabras totales)
// - Sin match: 0.0
//
// Retorna las skills ordenadas por confidence descendente.
func Match(query string, skills []SkillMeta) []SkillMatch {
query = strings.ToLower(strings.TrimSpace(query))
if query == "" {
return nil
}
queryWords := strings.Fields(query)
var matches []SkillMatch
for _, skill := range skills {
confidence := scoreSkill(queryWords, skill)
if confidence > 0 {
matches = append(matches, SkillMatch{
Skill: skill,
Confidence: confidence,
})
}
}
sort.Sort(ByConfidence(matches))
return matches
}
// scoreSkill calcula el score de relevancia de una skill para las palabras de query.
func scoreSkill(queryWords []string, skill SkillMeta) float64 {
nameLower := strings.ToLower(skill.Name)
descLower := strings.ToLower(skill.Description)
// Match exacto en name
queryStr := strings.Join(queryWords, " ")
if nameLower == queryStr {
return 1.0
}
// Match parcial en name (todas las palabras de query aparecen en name)
nameMatches := 0
for _, word := range queryWords {
if strings.Contains(nameLower, word) {
nameMatches++
}
}
if nameMatches == len(queryWords) {
return 0.8
}
// Match en description (contar palabras coincidentes)
descWords := strings.Fields(descLower)
descMatches := 0
for _, qword := range queryWords {
for _, dword := range descWords {
if strings.Contains(dword, qword) || strings.Contains(qword, dword) {
descMatches++
break
}
}
}
if descMatches > 0 {
ratio := float64(descMatches) / float64(len(queryWords))
return 0.6 * ratio
}
return 0.0
}
// FilterByCategory retorna solo las skills que pertenecen a las categorias especificadas.
// Si categories esta vacio, retorna todas las skills sin filtrar.
func FilterByCategory(skills []SkillMeta, categories []string) []SkillMeta {
if len(categories) == 0 {
return skills
}
catSet := make(map[string]bool)
for _, cat := range categories {
catSet[strings.ToLower(cat)] = true
}
var filtered []SkillMeta
for _, skill := range skills {
if catSet[strings.ToLower(skill.Category)] {
filtered = append(filtered, skill)
}
}
return filtered
}