Merge pull request #7 from TanLuong/feat/add-update-skill-tool-4324387879818579483
feat: add update_skill tool to dynamically update SKILL.md
This commit is contained in:
commit
8e1024e47b
7 changed files with 186 additions and 0 deletions
|
|
@ -489,6 +489,9 @@
|
||||||
"install_skill": {
|
"install_skill": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
"update_skill": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"list_dir": {
|
"list_dir": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,7 @@ func registerSharedTools(
|
||||||
skills_enabled := cfg.Tools.IsToolEnabled("skills")
|
skills_enabled := cfg.Tools.IsToolEnabled("skills")
|
||||||
find_skills_enable := cfg.Tools.IsToolEnabled("find_skills")
|
find_skills_enable := cfg.Tools.IsToolEnabled("find_skills")
|
||||||
install_skills_enable := cfg.Tools.IsToolEnabled("install_skill")
|
install_skills_enable := cfg.Tools.IsToolEnabled("install_skill")
|
||||||
|
update_skills_enable := cfg.Tools.IsToolEnabled("update_skill")
|
||||||
if skills_enabled && (find_skills_enable || install_skills_enable) {
|
if skills_enabled && (find_skills_enable || install_skills_enable) {
|
||||||
clawHubConfig := cfg.Tools.Skills.Registries.ClawHub
|
clawHubConfig := cfg.Tools.Skills.Registries.ClawHub
|
||||||
registryMgr := skills.NewRegistryManagerFromConfig(skills.RegistryConfig{
|
registryMgr := skills.NewRegistryManagerFromConfig(skills.RegistryConfig{
|
||||||
|
|
@ -285,6 +286,10 @@ func registerSharedTools(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if update_skills_enable {
|
||||||
|
agent.Tools.Register(tools.NewUpdateSkillTool(agent.Workspace))
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn and spawn_status tools share a SubagentManager.
|
// Spawn and spawn_status tools share a SubagentManager.
|
||||||
// Construct it when either tool is enabled (both require subagent).
|
// Construct it when either tool is enabled (both require subagent).
|
||||||
spawnEnabled := cfg.Tools.IsToolEnabled("spawn")
|
spawnEnabled := cfg.Tools.IsToolEnabled("spawn")
|
||||||
|
|
|
||||||
|
|
@ -1152,6 +1152,7 @@ type ToolsConfig struct {
|
||||||
FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
|
FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
|
||||||
I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"`
|
I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"`
|
||||||
InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
|
InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
|
||||||
|
UpdateSkill ToolConfig `json:"update_skill" envPrefix:"PICOCLAW_TOOLS_UPDATE_SKILL_"`
|
||||||
ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
|
ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
|
||||||
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
|
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
|
||||||
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
|
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
|
||||||
|
|
@ -2113,6 +2114,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
|
||||||
return t.I2C.Enabled
|
return t.I2C.Enabled
|
||||||
case "install_skill":
|
case "install_skill":
|
||||||
return t.InstallSkill.Enabled
|
return t.InstallSkill.Enabled
|
||||||
|
case "update_skill":
|
||||||
|
return t.UpdateSkill.Enabled
|
||||||
case "list_dir":
|
case "list_dir":
|
||||||
return t.ListDir.Enabled
|
return t.ListDir.Enabled
|
||||||
case "message":
|
case "message":
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ type toolsConfigV0 struct {
|
||||||
FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
|
FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
|
||||||
I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"`
|
I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"`
|
||||||
InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
|
InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
|
||||||
|
UpdateSkill ToolConfig `json:"update_skill" envPrefix:"PICOCLAW_TOOLS_UPDATE_SKILL_"`
|
||||||
ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
|
ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
|
||||||
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
|
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
|
||||||
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
|
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
|
||||||
|
|
|
||||||
|
|
@ -471,6 +471,9 @@ func DefaultConfig() *Config {
|
||||||
InstallSkill: ToolConfig{
|
InstallSkill: ToolConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
|
UpdateSkill: ToolConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
ListDir: ToolConfig{
|
ListDir: ToolConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
102
pkg/tools/skills_update.go
Normal file
102
pkg/tools/skills_update.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateSkillTool allows the LLM agent to analyze its own performance and
|
||||||
|
// write new skills or improvements into SKILL.md.
|
||||||
|
type UpdateSkillTool struct {
|
||||||
|
workspace string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUpdateSkillTool creates a new UpdateSkillTool.
|
||||||
|
func NewUpdateSkillTool(workspace string) *UpdateSkillTool {
|
||||||
|
return &UpdateSkillTool{
|
||||||
|
workspace: workspace,
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *UpdateSkillTool) Name() string {
|
||||||
|
return "update_skill"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *UpdateSkillTool) Description() string {
|
||||||
|
return "Analyze a completed conversation to extract strengths, weaknesses, and new knowledge, then update the local SKILL.md file to improve future performance. Call this tool after solving a complex problem to learn from it."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *UpdateSkillTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"analysis": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Analysis of the conversation, noting strengths and weaknesses.",
|
||||||
|
},
|
||||||
|
"skills_to_improve": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Specific skills, commands, or knowledge identified that need improvement to solve similar problems faster next time.",
|
||||||
|
},
|
||||||
|
"markdown_content": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The exact markdown content to append to SKILL.md containing the new learned skill or instruction.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"analysis", "skills_to_improve", "markdown_content"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *UpdateSkillTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
analysis, _ := args["analysis"].(string)
|
||||||
|
skillsToImprove, _ := args["skills_to_improve"].(string)
|
||||||
|
markdownContent, _ := args["markdown_content"].(string)
|
||||||
|
|
||||||
|
if analysis == "" || skillsToImprove == "" || markdownContent == "" {
|
||||||
|
return ErrorResult("analysis, skills_to_improve, and markdown_content are all required")
|
||||||
|
}
|
||||||
|
|
||||||
|
skillFilePath := filepath.Join(t.workspace, "SKILL.md")
|
||||||
|
|
||||||
|
// Prepare the content to append
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
contentToAppend := fmt.Sprintf("\n\n## Learned Skill: %s\n\n**Analysis**: %s\n\n**Skills Improved**: %s\n\n%s\n",
|
||||||
|
timestamp,
|
||||||
|
analysis,
|
||||||
|
skillsToImprove,
|
||||||
|
markdownContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist (though workspace should exist)
|
||||||
|
if err := os.MkdirAll(t.workspace, 0o755); err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("failed to create workspace directory: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to file
|
||||||
|
f, err := os.OpenFile(skillFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("failed to open SKILL.md for writing: %v", err))
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := f.WriteString(contentToAppend); err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("failed to append to SKILL.md: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
output := fmt.Sprintf("Successfully learned and updated SKILL.md.\n\nAnalysis: %s\nSkills Improved: %s\n", analysis, skillsToImprove)
|
||||||
|
|
||||||
|
// The response is passed back to the LLM.
|
||||||
|
// We also populate the ForUser field to notify the user.
|
||||||
|
res := SilentResult(output)
|
||||||
|
res.ForUser = fmt.Sprintf("I have analyzed our conversation and improved my skills.\n\n**My Analysis**:\n%s\n\n**Skills I've Improved/Added**:\n%s\n\nI have saved these learnings to `SKILL.md`.", analysis, skillsToImprove)
|
||||||
|
return res
|
||||||
|
}
|
||||||
69
pkg/tools/skills_update_test.go
Normal file
69
pkg/tools/skills_update_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateSkillTool_Name(t *testing.T) {
|
||||||
|
tool := NewUpdateSkillTool(t.TempDir())
|
||||||
|
assert.Equal(t, "update_skill", tool.Name())
|
||||||
|
assert.NotEmpty(t, tool.Description())
|
||||||
|
assert.NotNil(t, tool.Parameters())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSkillTool_Execute(t *testing.T) {
|
||||||
|
workspace := t.TempDir()
|
||||||
|
tool := NewUpdateSkillTool(workspace)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Missing args
|
||||||
|
res := tool.Execute(ctx, map[string]any{})
|
||||||
|
assert.True(t, res.IsError)
|
||||||
|
assert.Contains(t, res.ForLLM, "required")
|
||||||
|
|
||||||
|
// Success case
|
||||||
|
args := map[string]any{
|
||||||
|
"analysis": "I was slow to find the file.",
|
||||||
|
"skills_to_improve": "Use grep more effectively.",
|
||||||
|
"markdown_content": "Always use `grep -rn` when searching for strings.",
|
||||||
|
}
|
||||||
|
|
||||||
|
res = tool.Execute(ctx, args)
|
||||||
|
assert.False(t, res.IsError)
|
||||||
|
assert.Contains(t, res.ForLLM, "Successfully learned and updated SKILL.md")
|
||||||
|
assert.Contains(t, res.ForUser, "I was slow to find the file.")
|
||||||
|
|
||||||
|
// Check file contents
|
||||||
|
skillFilePath := filepath.Join(workspace, "SKILL.md")
|
||||||
|
content, err := os.ReadFile(skillFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
contentStr := string(content)
|
||||||
|
assert.Contains(t, contentStr, "Learned Skill")
|
||||||
|
assert.Contains(t, contentStr, "I was slow to find the file.")
|
||||||
|
assert.Contains(t, contentStr, "Use grep more effectively.")
|
||||||
|
assert.Contains(t, contentStr, "Always use `grep -rn` when searching for strings.")
|
||||||
|
|
||||||
|
// Test append
|
||||||
|
args2 := map[string]any{
|
||||||
|
"analysis": "Another analysis.",
|
||||||
|
"skills_to_improve": "Another skill.",
|
||||||
|
"markdown_content": "Another markdown.",
|
||||||
|
}
|
||||||
|
|
||||||
|
res2 := tool.Execute(ctx, args2)
|
||||||
|
assert.False(t, res2.IsError)
|
||||||
|
|
||||||
|
content2, err2 := os.ReadFile(skillFilePath)
|
||||||
|
assert.NoError(t, err2)
|
||||||
|
|
||||||
|
contentStr2 := string(content2)
|
||||||
|
assert.Contains(t, contentStr2, "I was slow to find the file.") // Old content still there
|
||||||
|
assert.Contains(t, contentStr2, "Another analysis.") // New content added
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue