Files
agents_and_robots/tools/skilltools/skills.go
T
egutierrez aeeb475afb feat: tools de function calling para skills
Agregar tools en tools/skilltools/ para interactuar con skills:

- skill_search: busca skills relevantes por query
- skill_load: carga instrucciones completas de una skill
- skill_read_resource: lee recursos especificos (scripts, references, templates)
- skill_run_script: ejecuta scripts de skills con argumentos

Las tools permiten al LLM descubrir, cargar y ejecutar skills
de forma progresiva (metadata → instrucciones → recursos).

Patron standard: Def (puro) + Exec (impuro).
2026-03-08 22:13:26 +00:00

196 lines
6.9 KiB
Go

package skilltools
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/enmanuel/agents/pkg/skills"
shellskills "github.com/enmanuel/agents/shell/skills"
"github.com/enmanuel/agents/tools"
)
// NewSkillSearch creates a skill_search tool that finds relevant skills.
func NewSkillSearch(loader *shellskills.Loader, categories []string) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "skill_search",
Description: "Search for skills relevant to a query. Returns a list of skills with their names, descriptions, and relevance scores. Use this when you need to find a skill to help with a task.",
Parameters: []tools.Param{
{Name: "query", Type: "string", Description: "Search query describing the task or capability needed", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) tools.Result {
query := tools.GetString(args, "query")
if query == "" {
return tools.Result{Err: fmt.Errorf("query is required")}
}
// Load all skill metadata
metas, err := loader.LoadMeta()
if err != nil {
return tools.Result{Err: fmt.Errorf("load skills metadata: %w", err)}
}
// Filter by categories if configured
metas = skills.FilterByCategory(metas, categories)
// Match skills to query
matches := skills.Match(query, metas)
if len(matches) == 0 {
return tools.Result{Output: "No skills found matching the query."}
}
// Format output
var lines []string
lines = append(lines, fmt.Sprintf("Found %d relevant skill(s):\n", len(matches)))
for i, match := range matches {
if i >= 5 {
break // limit to top 5
}
lines = append(lines, fmt.Sprintf("%d. **%s** (category: %s, confidence: %.2f)",
i+1, match.Skill.Name, match.Skill.Category, match.Confidence))
lines = append(lines, fmt.Sprintf(" %s\n", match.Skill.Description))
}
return tools.Result{Output: strings.Join(lines, "\n")}
},
}
}
// NewSkillLoad creates a skill_load tool that loads full instructions for a skill.
func NewSkillLoad(loader *shellskills.Loader) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "skill_load",
Description: "Load the complete instructions for a skill. This returns the full markdown content of the skill, which you should follow to complete the task. Use this after finding a skill with skill_search.",
Parameters: []tools.Param{
{Name: "skill_name", Type: "string", Description: "Name of the skill to load", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) tools.Result {
skillName := tools.GetString(args, "skill_name")
if skillName == "" {
return tools.Result{Err: fmt.Errorf("skill_name is required")}
}
skill, err := loader.LoadSkill(skillName)
if err != nil {
return tools.Result{Err: fmt.Errorf("load skill: %w", err)}
}
// Format output with metadata + instructions
var output strings.Builder
output.WriteString(fmt.Sprintf("# Skill: %s\n\n", skill.Meta.Name))
output.WriteString(fmt.Sprintf("**Category**: %s\n\n", skill.Meta.Category))
output.WriteString(fmt.Sprintf("**Description**: %s\n\n", skill.Meta.Description))
if len(skill.Scripts) > 0 {
output.WriteString(fmt.Sprintf("**Scripts available**: %s\n", strings.Join(skill.Scripts, ", ")))
}
if len(skill.References) > 0 {
output.WriteString(fmt.Sprintf("**References available**: %s\n", strings.Join(skill.References, ", ")))
}
if len(skill.Templates) > 0 {
output.WriteString(fmt.Sprintf("**Templates available**: %s\n", strings.Join(skill.Templates, ", ")))
}
output.WriteString("\n---\n\n")
output.WriteString(skill.Instructions)
return tools.Result{Output: output.String()}
},
}
}
// NewSkillReadResource creates a skill_read_resource tool that reads a specific resource.
func NewSkillReadResource(loader *shellskills.Loader) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "skill_read_resource",
Description: "Read a specific resource file from a skill (script, reference doc, template, or asset). Use this to load additional documentation or code referenced in the skill instructions.",
Parameters: []tools.Param{
{Name: "skill_name", Type: "string", Description: "Name of the skill", Required: true},
{Name: "resource_path", Type: "string", Description: "Path to the resource relative to the skill directory (e.g., 'scripts/deploy.sh', 'references/api.md')", Required: true},
},
},
Exec: func(ctx context.Context, args map[string]any) tools.Result {
skillName := tools.GetString(args, "skill_name")
resourcePath := tools.GetString(args, "resource_path")
if skillName == "" || resourcePath == "" {
return tools.Result{Err: fmt.Errorf("skill_name and resource_path are required")}
}
content, err := loader.ReadResource(skillName, resourcePath)
if err != nil {
return tools.Result{Err: fmt.Errorf("read resource: %w", err)}
}
return tools.Result{Output: content}
},
}
}
// NewSkillRunScript creates a skill_run_script tool that executes a skill script.
func NewSkillRunScript(loader *shellskills.Loader, executor *shellskills.Executor) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: "skill_run_script",
Description: "Execute a script from a skill with the given arguments. The script must be in the skill's scripts/ directory and use an allowed interpreter. Returns the script output.",
Parameters: []tools.Param{
{Name: "skill_name", Type: "string", Description: "Name of the skill", Required: true},
{Name: "script_name", Type: "string", Description: "Name of the script file (e.g., 'deploy.sh')", Required: true},
{Name: "args", Type: "array", Description: "Array of arguments to pass to the script", Required: false},
},
},
Exec: func(ctx context.Context, args map[string]any) tools.Result {
skillName := tools.GetString(args, "skill_name")
scriptName := tools.GetString(args, "script_name")
if skillName == "" || scriptName == "" {
return tools.Result{Err: fmt.Errorf("skill_name and script_name are required")}
}
// Parse args array
var scriptArgs []string
if argsRaw, ok := args["args"]; ok {
argsJSON, _ := json.Marshal(argsRaw)
_ = json.Unmarshal(argsJSON, &scriptArgs)
}
// Load skill to get base path
skill, err := loader.LoadSkill(skillName)
if err != nil {
return tools.Result{Err: fmt.Errorf("load skill: %w", err)}
}
// Verify script exists
scriptFound := false
for _, s := range skill.Scripts {
if s == scriptName {
scriptFound = true
break
}
}
if !scriptFound {
return tools.Result{Err: fmt.Errorf("script not found in skill: %s", scriptName)}
}
// Execute script
scriptPath := fmt.Sprintf("%s/scripts/%s", skill.BasePath, scriptName)
output, err := executor.Run(ctx, scriptPath, scriptArgs)
if err != nil {
return tools.Result{
Output: output,
Err: fmt.Errorf("script execution failed: %w", err),
}
}
return tools.Result{Output: output}
},
}
}