Files
MuyueWorkspace/internal/skills/improver.go
Augustin 4523bbd42c
All checks were successful
Stable Release / stable (push) Successful in 1m34s
feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
Major additions:
- RAG pipeline (indexing, chunking, search) with sidebar upload button
- Memory system with CRUD API
- Plugins and lessons modules
- MCP discovery and MCP server
- Advanced skills (auto-create, conditional, improver)
- Agent browser/image support, delegate, sessions
- File editor with CodeMirror in split panes
- Markdown rendering via react-markdown + KaTeX + highlight.js
- Raw markdown toggle
- PWA manifest + service worker
- Extension UI redesign with new design tokens and studio-style chat
- Pipeline API for chat streaming
- Mobile responsive layout

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 21:04:11 +02:00

268 lines
7.4 KiB
Go

package skills
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
type ImprovementSuggestion struct {
Type string `json:"type"`
Section string `json:"section"`
Current string `json:"current"`
Suggested string `json:"suggested"`
Reason string `json:"reason"`
Confidence float64 `json:"confidence"`
CreatedAt time.Time `json:"created_at"`
}
type ImprovementHistory struct {
SkillName string `json:"skill_name"`
Version string `json:"version"`
Improvements []ImprovementSuggestion `json:"improvements"`
AppliedAt time.Time `json:"applied_at"`
Result string `json:"result"`
}
type SkillImprover struct {
historyDir string
}
func NewSkillImprover() (*SkillImprover, error) {
dir, err := improvementHistoryDir()
if err != nil {
return nil, err
}
return &SkillImprover{historyDir: dir}, nil
}
func (si *SkillImprover) Analyze(skill *Skill, conversationContext string) ([]ImprovementSuggestion, error) {
var suggestions []ImprovementSuggestion
if skill.Content == "" {
return nil, fmt.Errorf("skill has no content to analyze")
}
suggestions = append(suggestions, si.checkMissingSections(skill)...)
suggestions = append(suggestions, si.checkErrorHandling(skill)...)
suggestions = append(suggestions, si.checkStepCompleteness(skill)...)
suggestions = append(suggestions, si.analyzeContextRelevance(skill, conversationContext)...)
return suggestions, nil
}
func (si *SkillImprover) ApplyImprovement(skillName string, suggestion ImprovementSuggestion) error {
skill, err := Get(skillName)
if err != nil {
return fmt.Errorf("get skill: %w", err)
}
switch suggestion.Section {
case "content":
skill.Content = applyContentSuggestion(skill.Content, suggestion)
case "description":
skill.Description = suggestion.Suggested
default:
skill.Content = applyContentSuggestion(skill.Content, suggestion)
}
now := time.Now()
skill.LastImprovedAt = &now
skill.ImprovementCount++
if err := Update(skill); err != nil {
return fmt.Errorf("update skill: %w", err)
}
history := ImprovementHistory{
SkillName: skillName,
Version: skill.Version,
Improvements: []ImprovementSuggestion{suggestion},
AppliedAt: now,
Result: "applied",
}
return si.saveHistory(&history)
}
func (si *SkillImprover) GetHistory(skillName string) ([]ImprovementHistory, error) {
if err := os.MkdirAll(si.historyDir, 0755); err != nil {
return nil, err
}
entries, err := os.ReadDir(si.historyDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var histories []ImprovementHistory
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
if skillName != "" && !strings.HasPrefix(e.Name(), skillName+"_") {
continue
}
data, err := os.ReadFile(filepath.Join(si.historyDir, e.Name()))
if err != nil {
continue
}
var h ImprovementHistory
if err := json.Unmarshal(data, &h); err != nil {
continue
}
histories = append(histories, h)
}
return histories, nil
}
func (si *SkillImprover) checkMissingSections(skill *Skill) []ImprovementSuggestion {
var suggestions []ImprovementSuggestion
content := strings.ToLower(skill.Content)
requiredSections := []struct {
keyword string
label string
}{
{"error handling", "Error Handling"},
{"steps", "Steps"},
{"when to", "Activation"},
}
for _, req := range requiredSections {
if !strings.Contains(content, req.keyword) {
suggestions = append(suggestions, ImprovementSuggestion{
Type: "missing_section",
Section: "content",
Current: "",
Suggested: fmt.Sprintf("Add a '%s' section", req.label),
Reason: fmt.Sprintf("Skill is missing a '%s' section which is important for completeness", req.label),
Confidence: 0.8,
CreatedAt: time.Now(),
})
}
}
return suggestions
}
func (si *SkillImprover) checkErrorHandling(skill *Skill) []ImprovementSuggestion {
var suggestions []ImprovementSuggestion
content := strings.ToLower(skill.Content)
if !strings.Contains(content, "error") && !strings.Contains(content, "fail") {
suggestions = append(suggestions, ImprovementSuggestion{
Type: "missing_error_handling",
Section: "content",
Current: "",
Suggested: "Add error handling guidance covering common failure modes",
Reason: "Skill lacks error handling guidance, which may lead to poor user experience when things go wrong",
Confidence: 0.85,
CreatedAt: time.Now(),
})
}
return suggestions
}
func (si *SkillImprover) checkStepCompleteness(skill *Skill) []ImprovementSuggestion {
var suggestions []ImprovementSuggestion
lines := strings.Split(skill.Content, "\n")
stepCount := 0
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "1.") || strings.HasPrefix(trimmed, "Step 1") {
stepCount++
}
}
if stepCount == 0 && len(lines) > 10 {
suggestions = append(suggestions, ImprovementSuggestion{
Type: "no_clear_steps",
Section: "content",
Current: "",
Suggested: "Add numbered step-by-step instructions for clarity",
Reason: "Long skill content without clear step-by-step structure can be hard to follow",
Confidence: 0.7,
CreatedAt: time.Now(),
})
}
return suggestions
}
func (si *SkillImprover) analyzeContextRelevance(skill *Skill, context string) []ImprovementSuggestion {
if context == "" {
return nil
}
var suggestions []ImprovementSuggestion
contextLower := strings.ToLower(context)
tags := skill.Tags
relevance := 0
for _, tag := range tags {
if strings.Contains(contextLower, strings.ToLower(tag)) {
relevance++
}
}
if len(tags) > 0 && relevance == 0 && skill.Category != "" && !strings.Contains(contextLower, strings.ToLower(skill.Category)) {
suggestions = append(suggestions, ImprovementSuggestion{
Type: "tag_relevance",
Section: "tags",
Current: strings.Join(tags, ", "),
Suggested: "Review tags for better context matching",
Reason: "Current tags do not match recent conversation context, suggesting tags may need updating",
Confidence: 0.5,
CreatedAt: time.Now(),
})
}
return suggestions
}
func applyContentSuggestion(content string, suggestion ImprovementSuggestion) string {
switch suggestion.Type {
case "missing_section":
return content + "\n\n## " + strings.Title(suggestion.Type) + "\n\n" + suggestion.Suggested + ".\n"
case "missing_error_handling":
return content + "\n\n## Error Handling\n\n- Handle common failure modes gracefully\n- Provide clear error messages\n- Suggest alternative approaches\n"
case "no_clear_steps":
return "## Steps\n\n1. Review the skill context\n2. Apply the appropriate pattern\n3. Verify the result\n\n" + content
default:
return content
}
}
func (si *SkillImprover) saveHistory(history *ImprovementHistory) error {
if err := os.MkdirAll(si.historyDir, 0755); err != nil {
return err
}
filename := fmt.Sprintf("%s_%s.json", history.SkillName, history.AppliedAt.Format("20060102-150405"))
data, err := json.MarshalIndent(history, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(si.historyDir, filename), data, 0644)
}
func improvementHistoryDir() (string, error) {
dir, err := SkillsDir()
if err != nil {
return "", err
}
return filepath.Join(filepath.Dir(dir), ".muyue", "improvements"), nil
}