package workflow import ( "context" "encoding/json" "fmt" "strings" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/orchestrator" ) type Planner struct { orchestrator *orchestrator.Orchestrator } func NewPlanner(cfg *config.MuyueConfig) (*Planner, error) { orb, err := orchestrator.New(cfg) if err != nil { return nil, err } orb.SetSystemPrompt(plannerSystemPrompt) return &Planner{orchestrator: orb}, nil } func (p *Planner) GeneratePlan(ctx context.Context, goal string) ([]Step, error) { prompt := buildPlanPrompt(goal) messages := []orchestrator.Message{ {Role: "user", Content: orchestrator.TextContent(prompt)}, } resp, err := p.orchestrator.SendWithTools(messages) if err != nil { return nil, err } if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" { return nil, fmt.Errorf("no plan generated") } content := resp.Choices[0].Message.Content plan, err := parsePlanResponse(content) if err != nil { return nil, err } return plan, nil } func buildPlanPrompt(goal string) string { return fmt.Sprintf(`Tu es un planificateur de workflows pour Muyue. L'utilisateur veut accomplir la tâche suivante: "%s" Analyse cette tâche et génère un plan d'exécution en une série d'étapes. Chaque étape est un appel d'outil. Les outils disponibles sont: - terminal: Exécuter une commande shell - read_file: Lire un fichier - list_files: Lister les fichiers d'un répertoire - search_files: Rechercher des fichiers par pattern - grep_content: Rechercher du texte dans des fichiers - get_config: Lire la configuration Muyue - set_provider: Configurer un provider AI - manage_ssh: Gérer les connexions SSH - web_fetch: Récupérer le contenu d'une URL Réponds UNIQUEMENT avec un JSON valide représentant un tableau d'étapes, sans texte avant ou après: [ {"name": "Nom de l'étape", "tool": "terminal", "args": {"command": "ls -la"}}, {"name": "Lire le fichier config", "tool": "read_file", "args": {"path": "~/.muyue/config.json"}} ] Règles: - Chaque étape doit avoir: name, tool, args - Les args varient selon le tool (voir les définitions) - Sois précis dans les commandes - Sépare en étapes logiques - Ne génère pas plus de 10 étapes`, goal) } func parsePlanResponse(content string) ([]Step, error) { content = strings.TrimSpace(content) var jsonStr string if strings.HasPrefix(content, "```json") { lines := strings.Split(content, "\n") var jsonLines []string for _, line := range lines[1:] { if strings.HasPrefix(line, "```") { break } jsonLines = append(jsonLines, line) } jsonStr = strings.Join(jsonLines, "\n") } else if strings.HasPrefix(content, "```") { lines := strings.Split(content, "\n") var jsonLines []string for _, line := range lines[1:] { if strings.HasPrefix(line, "```") { break } jsonLines = append(jsonLines, line) } jsonStr = strings.Join(jsonLines, "\n") } else { jsonStr = content } var rawSteps []map[string]interface{} if err := json.Unmarshal([]byte(jsonStr), &rawSteps); err != nil { return nil, fmt.Errorf("failed to parse plan JSON: %v\nContent: %s", err, content) } steps := make([]Step, 0, len(rawSteps)) for i, raw := range rawSteps { step := Step{ ID: fmt.Sprintf("step-%d", i), Status: StatusPending, } if name, ok := raw["name"].(string); ok { step.Name = name } else { step.Name = fmt.Sprintf("Step %d", i+1) } if tool, ok := raw["tool"].(string); ok { step.Tool = tool step.Type = TypeToolCall } if args, ok := raw["args"].(map[string]interface{}); ok { argsJSON, err := json.Marshal(args) if err == nil { step.Args = argsJSON } } if tool, ok := raw["type"].(string); ok { switch tool { case "approval": step.Type = TypeApproval case "condition": step.Type = TypeCondition if cond, ok := raw["condition"].(string); ok { step.Condition = cond } default: step.Type = TypeToolCall } } steps = append(steps, step) } return steps, nil } const plannerSystemPrompt = `Tu es un planificateur de workflows pour Muyue. Tu génères des plans d'exécution sous forme de tableaux JSON. RÈGLES : 1. Analyse l'objectif → identifie les outils → décompose en étapes 2. Chaque étape : {"name": string, "tool": string, "args": object} 3. Max 10 étapes par plan 4. Ordonne par dépendances (les lectures avant les écritures) 5. Préfère les commandes non-interactives 6. Utilise crush_run pour les tâches complexes multi-fichiers Outils : terminal, crush_run, read_file, list_files, search_files, grep_content, get_config, set_provider, manage_ssh, web_fetch Réponds UNIQUEMENT en JSON valide, sans texte avant/après.` const _ = plannerSystemPrompt