diff --git a/tools/skilltools/skills.go b/tools/skilltools/skills.go new file mode 100644 index 0000000..1697594 --- /dev/null +++ b/tools/skilltools/skills.go @@ -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} + }, + } +}