aeeb475afb
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).
196 lines
6.9 KiB
Go
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}
|
|
},
|
|
}
|
|
}
|