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).
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user