Skip to content

Commit d7998bb

Browse files
chore
1 parent 8bf1d61 commit d7998bb

File tree

5 files changed

+144
-269
lines changed

5 files changed

+144
-269
lines changed

adk/middlewares/skill/local.go

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,40 @@ func NewLocalBackend(config *LocalBackendConfig) (*LocalBackend, error) {
6565
}, nil
6666
}
6767

68-
// skillFrontmatter represents the YAML frontmatter in a SKILL.md file.
69-
type skillFrontmatter struct {
70-
Name string `yaml:"name"`
71-
Description string `yaml:"description"`
72-
License *string `yaml:"license"`
73-
Compatibility *string `yaml:"compatibility"`
74-
Metadata map[string]any `yaml:"metadata"`
75-
AllowedTools []string `yaml:"allowed-tools"`
76-
}
77-
7868
// List returns all skills from the local filesystem.
7969
// It scans subdirectories of baseDir for SKILL.md files and parses them as skills.
80-
func (b *LocalBackend) List(ctx context.Context) ([]Skill, error) {
70+
func (b *LocalBackend) List(ctx context.Context) ([]FrontMatter, error) {
71+
skills, err := b.list(ctx)
72+
if err != nil {
73+
return nil, fmt.Errorf("failed to list skills: %w", err)
74+
}
75+
76+
matters := make([]FrontMatter, 0, len(skills))
77+
for _, skill := range skills {
78+
matters = append(matters, skill.FrontMatter)
79+
}
80+
81+
return matters, nil
82+
}
83+
84+
// Get returns a skill by name from the local filesystem.
85+
// It searches subdirectories for a SKILL.md file with matching name.
86+
func (b *LocalBackend) Get(ctx context.Context, name string) (Skill, error) {
87+
skills, err := b.list(ctx)
88+
if err != nil {
89+
return Skill{}, fmt.Errorf("failed to list skills: %w", err)
90+
}
91+
92+
for _, skill := range skills {
93+
if skill.Name == name {
94+
return skill, nil
95+
}
96+
}
97+
98+
return Skill{}, fmt.Errorf("skill not found: %s", name)
99+
}
100+
101+
func (b *LocalBackend) list(ctx context.Context) ([]Skill, error) {
81102
var skills []Skill
82103

83104
entries, err := os.ReadDir(b.baseDir)
@@ -109,23 +130,6 @@ func (b *LocalBackend) List(ctx context.Context) ([]Skill, error) {
109130
return skills, nil
110131
}
111132

112-
// Get returns a skill by name from the local filesystem.
113-
// It searches subdirectories for a SKILL.md file with matching name.
114-
func (b *LocalBackend) Get(ctx context.Context, name string) (Skill, error) {
115-
skills, err := b.List(ctx)
116-
if err != nil {
117-
return Skill{}, fmt.Errorf("failed to list skills: %w", err)
118-
}
119-
120-
for _, skill := range skills {
121-
if skill.Name == name {
122-
return skill, nil
123-
}
124-
}
125-
126-
return Skill{}, fmt.Errorf("skill not found: %s", name)
127-
}
128-
129133
// loadSkillFromFile loads a skill from a SKILL.md file.
130134
// The file format is:
131135
//
@@ -145,7 +149,7 @@ func (b *LocalBackend) loadSkillFromFile(path string) (Skill, error) {
145149
return Skill{}, fmt.Errorf("failed to parse frontmatter: %w", err)
146150
}
147151

148-
var fm skillFrontmatter
152+
var fm FrontMatter
149153
if err = yaml.Unmarshal([]byte(frontmatter), &fm); err != nil {
150154
return Skill{}, fmt.Errorf("failed to unmarshal frontmatter: %w", err)
151155
}
@@ -157,12 +161,10 @@ func (b *LocalBackend) loadSkillFromFile(path string) (Skill, error) {
157161
}
158162

159163
return Skill{
160-
Name: fm.Name,
161-
Description: fm.Description,
162-
License: fm.License,
163-
Compatibility: fm.Compatibility,
164-
Metadata: fm.Metadata,
165-
AllowedTools: fm.AllowedTools,
164+
FrontMatter: FrontMatter{
165+
Name: fm.Name,
166+
Description: fm.Description,
167+
},
166168
Content: strings.TrimSpace(content),
167169
BaseDirectory: absDir,
168170
}, nil

adk/middlewares/skill/local_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,6 @@ This is the skill content.`), 0644))
162162
require.Len(t, skills, 1)
163163
assert.Equal(t, "pdf-processing", skills[0].Name)
164164
assert.Equal(t, "Extract text and tables from PDF files, fill forms, merge documents.", skills[0].Description)
165-
assert.Equal(t, "Apache-2.0", *skills[0].License)
166-
assert.Equal(t, map[string]any{"author": "example-org", "version": "1.0"}, skills[0].Metadata)
167-
assert.Equal(t, "This is the skill content.", skills[0].Content)
168-
assert.Equal(t, skillDir, skills[0].BaseDirectory)
169165
})
170166

171167
t.Run("multiple skill directories returns all skills", func(t *testing.T) {

adk/middlewares/skill/prompt.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,39 @@
1717
package skill
1818

1919
const (
20-
skillToolDescriptionBase = `Execute a skill within the main conversation
20+
systemPrompt = `
21+
# Skills System
22+
23+
**How to Use Skills (Progressive Disclosure):**
24+
25+
Skills follow a **progressive disclosure** pattern - you see their name and description above, but only read full instructions when needed:
26+
27+
1. **Recognize when a skill applies**: Check if the user's task matches a skill's description
28+
2. **Read the skill's full instructions**: Use the '{tool_name}' tool to load SKILL.md with the skill name
29+
3. **Follow the skill's instructions**: SKILL.md contains step-by-step workflows, best practices, and examples
30+
4. **Access supporting files**: Skills may include helper scripts, configs, or reference docs - use absolute paths
31+
32+
**When to Use Skills:**
33+
- User's request matches a skill's domain (e.g., "research X" -> web-research skill)
34+
- You need specialized knowledge or structured workflows
35+
- A skill provides proven patterns for complex tasks
36+
37+
**Executing Skill Scripts:**
38+
Skills may contain Python scripts or other executable files. Always use absolute paths.
39+
40+
**Example Workflow:**
41+
42+
User: "Can you research the latest developments in quantum computing?"
43+
44+
1. Check available skills -> See "web-research" skill
45+
2. Call '{tool_name}' tool to read the full skill instructions
46+
3. Follow the skill's research workflow (search -> organize -> synthesize)
47+
4. Use any helper scripts with absolute paths
48+
49+
Remember: Skills make you more capable and consistent. When in doubt, check if a skill exists for the task!
50+
`
51+
52+
toolDescriptionBase = `Execute a skill within the main conversation
2153
2254
<skills_instructions>
2355
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
@@ -39,9 +71,9 @@ Important:
3971
</skills_instructions>
4072
4173
`
42-
skillToolDescriptionTemplate = `
74+
toolDescriptionTemplate = `
4375
<available_skills>
44-
{{- range .Skills }}
76+
{{- range .Matters }}
4577
<skill>
4678
<name>
4779
{{ .Name }}
@@ -53,8 +85,9 @@ Important:
5385
{{- end }}
5486
</available_skills>
5587
`
56-
skillToolResult = "Launching skill: %s\n"
57-
skillUserContent = `Base directory for this skill: %s
88+
toolResult = "Launching skill: %s\n"
89+
userContent = `Base directory for this skill: %s
5890
5991
%s`
92+
toolName = "skill"
6093
)

adk/middlewares/skill/skill.go

Lines changed: 35 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,34 @@ import (
2222
"context"
2323
"encoding/json"
2424
"fmt"
25-
"regexp"
2625
"text/template"
2726

27+
"github.com/slongfield/pyfmt"
28+
2829
"github.com/cloudwego/eino/adk"
2930
"github.com/cloudwego/eino/components/tool"
3031
"github.com/cloudwego/eino/schema"
3132
)
3233

33-
type Skill struct {
34-
Name string
35-
Description string
36-
License *string
37-
Compatibility *string
38-
Metadata map[string]any
39-
AllowedTools []string
40-
41-
Content string
34+
type FrontMatter struct {
35+
Name string `yaml:"name"`
36+
Description string `yaml:"description"`
37+
}
4238

39+
type Skill struct {
40+
FrontMatter
41+
Content string
4342
BaseDirectory string
4443
}
4544

4645
type Backend interface {
47-
List(ctx context.Context) ([]Skill, error)
46+
List(ctx context.Context) ([]FrontMatter, error)
4847
Get(ctx context.Context, name string) (Skill, error)
4948
}
5049

5150
type Config struct {
52-
Backend Backend
51+
Backend Backend
52+
SkillToolName *string
5353
}
5454

5555
// New creates a new skill middleware.
@@ -61,17 +61,31 @@ func New(ctx context.Context, config *Config) (adk.AgentMiddleware, error) {
6161
if config.Backend == nil {
6262
return adk.AgentMiddleware{}, fmt.Errorf("backend is required")
6363
}
64+
65+
name := toolName
66+
if config.SkillToolName != nil {
67+
name = *config.SkillToolName
68+
}
69+
6470
return adk.AgentMiddleware{
65-
AdditionalTools: []tool.BaseTool{&skillTool{b: config.Backend}},
71+
AdditionalInstruction: buildSystemPrompt(name),
72+
AdditionalTools: []tool.BaseTool{&skillTool{b: config.Backend, toolName: name}},
6673
}, nil
6774
}
6875

76+
func buildSystemPrompt(skillToolName string) string {
77+
return pyfmt.Must(systemPrompt, map[string]string{
78+
"tool_name": skillToolName,
79+
})
80+
}
81+
6982
type skillTool struct {
70-
b Backend
83+
b Backend
84+
toolName string
7185
}
7286

7387
type descriptionTemplateHelper struct {
74-
Skills []Skill
88+
Matters []FrontMatter
7589
}
7690

7791
func (s *skillTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
@@ -80,23 +94,14 @@ func (s *skillTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
8094
return nil, fmt.Errorf("failed to list skills: %w", err)
8195
}
8296

83-
err = validateSkillMetadata(skills)
84-
if err != nil {
85-
return nil, err
86-
}
87-
err = validateSkillContent(skills)
88-
if err != nil {
89-
return nil, err
90-
}
91-
9297
desc, err := renderToolDescription(skills)
9398
if err != nil {
9499
return nil, fmt.Errorf("failed to render skill tool description: %w", err)
95100
}
96101

97102
return &schema.ToolInfo{
98-
Name: "skill",
99-
Desc: skillToolDescriptionBase + desc,
103+
Name: s.toolName,
104+
Desc: toolDescriptionBase + desc,
100105
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
101106
"skill": {
102107
Type: schema.String,
@@ -122,81 +127,20 @@ func (s *skillTool) InvokableRun(ctx context.Context, argumentsInJSON string, op
122127
return "", fmt.Errorf("failed to get skill: %w", err)
123128
}
124129

125-
err = validateSkillName(skill.Name)
126-
if err != nil {
127-
return "", err
128-
}
129-
err = validateSkillContent([]Skill{skill})
130-
if err != nil {
131-
return "", err
132-
}
133-
134-
return fmt.Sprintf(skillToolResult, skill.Name) + fmt.Sprintf(skillUserContent, skill.BaseDirectory, skill.Content), nil
130+
return fmt.Sprintf(toolResult, skill.Name) + fmt.Sprintf(userContent, skill.BaseDirectory, skill.Content), nil
135131
}
136132

137-
func renderToolDescription(skills []Skill) (string, error) {
138-
tpl, err := template.New("skills").Parse(skillToolDescriptionTemplate)
133+
func renderToolDescription(matters []FrontMatter) (string, error) {
134+
tpl, err := template.New("skills").Parse(toolDescriptionTemplate)
139135
if err != nil {
140136
return "", err
141137
}
142138

143139
var buf bytes.Buffer
144-
err = tpl.Execute(&buf, descriptionTemplateHelper{Skills: skills})
140+
err = tpl.Execute(&buf, descriptionTemplateHelper{Matters: matters})
145141
if err != nil {
146142
return "", err
147143
}
148144

149145
return buf.String(), nil
150146
}
151-
152-
const (
153-
maxSkillNameLength = 64
154-
maxSkillDescriptionLength = 1024
155-
maxSkillContentLength = 10 * 1024 * 1024
156-
)
157-
158-
func validateSkillMetadata(skills []Skill) error {
159-
for _, skill := range skills {
160-
err := validateSkillName(skill.Name)
161-
if err != nil {
162-
return err
163-
}
164-
165-
if len(skill.Description) == 0 {
166-
return fmt.Errorf("skill %s must have a non-empty description", skill.Name)
167-
}
168-
if len(skill.Description) > maxSkillDescriptionLength {
169-
return fmt.Errorf("skill %s description must not exceed %d characters", skill.Name, maxSkillDescriptionLength)
170-
}
171-
}
172-
return nil
173-
}
174-
175-
var (
176-
skillNameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
177-
)
178-
179-
func validateSkillName(name string) error {
180-
if len(name) > maxSkillNameLength {
181-
return fmt.Errorf("skill name must not exceed %d characters", maxSkillNameLength)
182-
}
183-
184-
if len(name) == 0 {
185-
return fmt.Errorf("skill name cannot be empty")
186-
}
187-
188-
if !skillNameRegex.MatchString(name) {
189-
return fmt.Errorf("skill name must contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen: %s", name)
190-
}
191-
192-
return nil
193-
}
194-
195-
func validateSkillContent(skills []Skill) error {
196-
for _, skill := range skills {
197-
if len(skill.Content)+len(skill.BaseDirectory) > maxSkillContentLength {
198-
return fmt.Errorf("skill content must not exceed %d characters", maxSkillContentLength)
199-
}
200-
}
201-
return nil
202-
}

0 commit comments

Comments
 (0)