package agents
import (
"fmt"
"html"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/mudler/xlog"
)
// SkillInfo represents a skill available to the agent.
type SkillInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Content string `json:"content,omitempty"` // full skill content for prompt-mode injection
}
// SkillContentProvider loads full skill info (including content) for a user.
// Used by the scheduler to enrich NATS events without needing a direct DB dependency.
type SkillContentProvider func(userID string) ([]SkillInfo, error)
// SkillProvider loads available skills.
type SkillProvider interface {
ListSkills() ([]SkillInfo, error)
}
// SkillsToolsHint is injected into the system prompt when skills_mode is "tools"
// to guide the agent on using the request_skill tool.
const SkillsToolsHint = `You have access to skills via the ` + "`request_skill`" + ` tool. ` +
`Call it with a skill name to retrieve the full skill instructions, then follow them to complete the task.`
const defaultSkillsTemplate = `You can use the following skills to help with the task.
To request the skill, you need to use the ` + "`request_skill`" + ` tool. The skill name is the name of the skill you want to use.
{{range .Skills}}
{{escapeXML .Name}}
{{if .Content}}{{escapeXML .Content}}{{else}}{{escapeXML .Description}}{{end}}
{{end}}
`
// RenderSkillsPrompt generates the skills prompt text for injection into the system prompt.
// Uses the agent's custom template if set, otherwise the default XML format.
func RenderSkillsPrompt(skills []SkillInfo, customTemplate string) string {
if len(skills) == 0 {
return ""
}
tmplText := customTemplate
if tmplText == "" {
tmplText = defaultSkillsTemplate
}
funcMap := sprig.FuncMap()
funcMap["escapeXML"] = html.EscapeString
tmpl, err := template.New("skills").Funcs(funcMap).Parse(tmplText)
if err != nil {
xlog.Error("Failed to parse skills template", "error", err)
// Fallback: simple listing
var sb strings.Builder
sb.WriteString("Available skills:\n")
for _, s := range skills {
sb.WriteString(fmt.Sprintf("- %s: %s\n", s.Name, s.Description))
}
return sb.String()
}
data := map[string]any{
"Skills": skills,
}
var sb strings.Builder
if err := tmpl.Execute(&sb, data); err != nil {
xlog.Error("Failed to execute skills template", "error", err)
return ""
}
return sb.String()
}
// RequestSkillArgs defines the arguments for the request_skill tool.
type RequestSkillArgs struct {
SkillName string `json:"skill_name" jsonschema:"description=The name of the skill to request"`
}
// RequestSkillTool implements the request_skill cogito tool.
type RequestSkillTool struct {
Skills []SkillInfo
}
func (t RequestSkillTool) Run(args RequestSkillArgs) (string, any, error) {
for _, s := range t.Skills {
if s.Name == args.SkillName {
body := s.Content
if body == "" {
body = s.Description
}
return fmt.Sprintf("Skill '%s':\n%s", s.Name, body), nil, nil
}
}
available := skillNames(t.Skills)
return fmt.Sprintf("Skill '%s' not found. Available skills: %s", args.SkillName, available), nil, nil
}
// skillNames returns a comma-separated list of skill names.
func skillNames(skills []SkillInfo) string {
names := make([]string, len(skills))
for i, s := range skills {
names[i] = s.Name
}
return strings.Join(names, ", ")
}
// FilterSkills filters skills by the agent's selected_skills list.
// If selectedSkills is empty/nil, all skills are returned.
func FilterSkills(all []SkillInfo, selectedSkills []string) []SkillInfo {
if len(selectedSkills) == 0 {
return all
}
selected := make(map[string]bool, len(selectedSkills))
for _, s := range selectedSkills {
selected[s] = true
}
var filtered []SkillInfo
for _, s := range all {
if selected[s.Name] {
filtered = append(filtered, s)
}
}
return filtered
}