Files
MuyueWorkspace/internal/skills/auto_create.go
Augustin cb525e6598
All checks were successful
Beta Release / beta (push) Successful in 5m9s
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:01:08 +02:00

283 lines
6.7 KiB
Go

package skills
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
type PatternMatch struct {
Pattern string
Count int
LastSeen time.Time
ExampleText string
}
type AutoCreateProposal struct {
Name string
Description string
SuggestedTags []string
Category string
Patterns []PatternMatch
Confidence float64
CreatedFrom string
}
type ConversationSnippet struct {
Role string `json:"role"`
Content string `json:"content"`
Timestamp time.Time `json:"timestamp"`
}
func AnalyzeConversation(snippets []ConversationSnippet) []AutoCreateProposal {
patterns := detectPatterns(snippets)
var proposals []AutoCreateProposal
for _, p := range patterns {
if p.Count < 3 {
continue
}
name := generateSkillName(p.Pattern)
proposal := AutoCreateProposal{
Name: name,
Description: fmt.Sprintf("Auto-detected skill for recurring pattern: %s", p.Pattern),
SuggestedTags: extractTags(p.Pattern),
Category: categorize(p.Pattern),
Patterns: []PatternMatch{p},
Confidence: computeConfidence(p),
CreatedFrom: "conversation",
}
proposals = append(proposals, proposal)
}
return proposals
}
func CreateFromProposal(proposal *AutoCreateProposal) (*Skill, error) {
skill := &Skill{
Name: proposal.Name,
Description: proposal.Description,
Author: "muyue-auto",
Version: "0.1.0",
Tags: proposal.SuggestedTags,
Category: proposal.Category,
Target: "both",
CreatedFrom: proposal.CreatedFrom,
AutoImprove: true,
Content: buildAutoSkillContent(proposal),
}
return skill, Create(skill)
}
func LoadProposals() ([]AutoCreateProposal, error) {
dir, err := proposalsDir()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var proposals []AutoCreateProposal
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
continue
}
var p AutoCreateProposal
if err := json.Unmarshal(data, &p); err != nil {
continue
}
proposals = append(proposals, p)
}
return proposals, nil
}
func SaveProposal(proposal *AutoCreateProposal) error {
dir, err := proposalsDir()
if err != nil {
return err
}
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(proposal, "", " ")
if err != nil {
return err
}
path := filepath.Join(dir, proposal.Name+".json")
return os.WriteFile(path, data, 0644)
}
func DeleteProposal(name string) error {
dir, err := proposalsDir()
if err != nil {
return err
}
path := filepath.Join(dir, name+".json")
return os.Remove(path)
}
func proposalsDir() (string, error) {
dir, err := SkillsDir()
if err != nil {
return "", err
}
return filepath.Join(filepath.Dir(dir), ".muyue", "proposals"), nil
}
func detectPatterns(snippets []ConversationSnippet) []PatternMatch {
commandPatterns := make(map[string]*PatternMatch)
for _, s := range snippets {
if s.Role != "assistant" {
continue
}
lines := strings.Split(s.Content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if isCommandPattern(line) {
key := extractPatternKey(line)
if key == "" {
continue
}
if existing, ok := commandPatterns[key]; ok {
existing.Count++
if s.Timestamp.After(existing.LastSeen) {
existing.LastSeen = s.Timestamp
existing.ExampleText = truncate(line, 200)
}
} else {
commandPatterns[key] = &PatternMatch{
Pattern: key,
Count: 1,
LastSeen: s.Timestamp,
ExampleText: truncate(line, 200),
}
}
}
}
}
var patterns []PatternMatch
for _, p := range commandPatterns {
patterns = append(patterns, *p)
}
return patterns
}
func isCommandPattern(line string) bool {
toolPrefixes := []string{"go test", "go build", "go run", "npm test", "npm run",
"docker build", "docker run", "git commit", "git push", "kubectl",
"cargo test", "cargo build", "pytest", "make "}
for _, prefix := range toolPrefixes {
if strings.HasPrefix(line, prefix) {
return true
}
}
return false
}
func extractPatternKey(line string) string {
parts := strings.Fields(line)
if len(parts) < 2 {
return ""
}
if len(parts) >= 3 && (parts[0] == "go" || parts[0] == "npm" || parts[0] == "cargo" || parts[0] == "git" || parts[0] == "docker") {
return parts[0] + " " + parts[1]
}
return parts[0]
}
func generateSkillName(pattern string) string {
name := strings.ReplaceAll(pattern, " ", "-")
name = strings.ToLower(name)
if len(name) > 30 {
name = name[:30]
}
h := sha256.Sum256([]byte(pattern))
return fmt.Sprintf("auto-%s-%x", name, h[:4])
}
func extractTags(pattern string) []string {
var tags []string
parts := strings.Fields(pattern)
for _, p := range parts {
if len(p) > 2 {
tags = append(tags, strings.ToLower(p))
}
}
return tags
}
func categorize(pattern string) string {
categories := map[string]string{
"go test": "testing", "go build": "build", "go run": "build",
"npm test": "testing", "npm run": "build",
"docker build": "devops", "docker run": "devops",
"git commit": "workflow", "git push": "workflow",
"kubectl": "devops", "cargo test": "testing",
"cargo build": "build", "pytest": "testing",
"make": "build",
}
for prefix, cat := range categories {
if strings.HasPrefix(pattern, prefix) {
return cat
}
}
return "general"
}
func computeConfidence(p PatternMatch) float64 {
confidence := 0.3
confidence += float64(p.Count) * 0.1
if confidence > 0.95 {
confidence = 0.95
}
return confidence
}
func buildAutoSkillContent(proposal *AutoCreateProposal) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("# %s\n\n", strings.Title(proposal.Name)))
b.WriteString("Auto-generated skill based on recurring patterns detected in conversations.\n\n")
b.WriteString("## Activation\n\n")
b.WriteString("This skill activates when the following patterns are detected:\n\n")
for _, p := range proposal.Patterns {
b.WriteString(fmt.Sprintf("- `%s` (seen %d times)\n", p.Pattern, p.Count))
}
b.WriteString("\n## Instructions\n\n")
b.WriteString("1. Detect the pattern context from the user request\n")
b.WriteString("2. Apply the standard workflow for this pattern\n")
b.WriteString("3. Handle common errors and edge cases\n")
b.WriteString("4. Verify the result\n\n")
b.WriteString("## Error Handling\n\n")
b.WriteString("- If a command fails, check for missing dependencies\n")
b.WriteString("- Suggest alternative approaches when the standard pattern doesn't fit\n")
return b.String()
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen]
}