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 }