ollama/agent/tools/skill.go
2026-06-15 15:33:47 -07:00

137 lines
3.2 KiB
Go

package tools
import (
"context"
"fmt"
"strings"
"github.com/ollama/ollama/agent"
"github.com/ollama/ollama/agent/skills"
"github.com/ollama/ollama/api"
)
type Skill struct {
catalog *skills.Catalog
}
func NewSkill(catalog *skills.Catalog) *Skill {
return &Skill{catalog: catalog}
}
func (s *Skill) Name() string {
return "skill"
}
func (s *Skill) Description() string {
return "Load the full SKILL.md instructions for an installed agent skill by name."
}
func (s *Skill) Schema() api.ToolFunction {
props := api.NewToolPropertiesMap()
props.Set("name", api.ToolProperty{
Type: api.PropertyType{"string"},
Description: "Name of the skill to load.",
})
return api.ToolFunction{
Name: s.Name(),
Description: s.Description(),
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"name"},
Properties: props,
},
}
}
func (s *Skill) Execute(_ context.Context, _ agent.ToolContext, args map[string]any) (agent.ToolResult, error) {
name, _ := args["name"].(string)
name = skills.NormalizeName(name)
if name == "" {
return agent.ToolResult{}, fmt.Errorf("name parameter is required")
}
if s.catalog == nil || s.catalog.Empty() {
return agent.ToolResult{}, fmt.Errorf("no skills are installed")
}
skill, ok := s.catalog.Find(name)
if !ok {
return agent.ToolResult{}, fmt.Errorf("unknown skill: %s", name)
}
content, err := SkillResultContent(skill)
if err != nil {
return agent.ToolResult{}, err
}
return agent.ToolResult{Content: content}, nil
}
func ManualSkillMessages(skill skills.Skill, request string, ordinal int) ([]api.Message, error) {
content, err := SkillResultContent(skill)
if err != nil {
return nil, err
}
args := api.NewToolCallFunctionArguments()
args.Set("name", skill.Name)
callID := manualSkillToolCallID(skill.Name, ordinal)
userContent := strings.TrimSpace(request)
if userContent == "" {
userContent = fmt.Sprintf("Use the %s skill.", skill.Name)
}
return []api.Message{
{Role: "user", Content: userContent},
{
Role: "assistant",
ToolCalls: []api.ToolCall{{
ID: callID,
Function: api.ToolCallFunction{
Name: "skill",
Arguments: args,
},
}},
},
{Role: "tool", ToolName: "skill", ToolCallID: callID, Content: content},
}, nil
}
func SkillResultContent(skill skills.Skill) (string, error) {
content, err := skill.Read()
if err != nil {
return "", err
}
var b strings.Builder
b.WriteString("Loaded skill: ")
b.WriteString(skill.Name)
b.WriteByte('\n')
b.WriteString("Skill directory: ")
b.WriteString(skill.Dir)
b.WriteString("\nResolve relative file references from the skill directory.\n\n")
b.WriteString(content)
return b.String(), nil
}
func manualSkillToolCallID(skillName string, ordinal int) string {
name := strings.Trim(strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= 'A' && r <= 'Z':
return r
case r >= '0' && r <= '9':
return r
case r == '-' || r == '_':
return r
default:
return '-'
}
}, skillName), "-")
if name == "" {
name = "skill"
}
if ordinal <= 0 {
return "manual-skill-" + name
}
return fmt.Sprintf("manual-skill-%d-%s", ordinal, name)
}