Files
MuyueWorkspace/internal/workflow/planner.go
Augustin 485e085bb0 feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard
Major changes:
- Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version)
- Add LSP registry with health checks, auto-install, and editor config generation
- Add MCP registry with editor detection, status tracking, and per-editor configuration
- Add workflow engine with planner and step execution for automated task chains
- Add conversation search, export (Markdown/JSON), and detailed token counting
- Add streaming shell chat handler with tool call/result events
- Add skill validation, dry-run testing, and export endpoints
- Enrich dashboard with Tools/Activity/Status tabs and tool cards grid
- Add PRD documentation
- Complete i18n for both EN and FR

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-23 19:47:00 +02:00

172 lines
4.4 KiB
Go

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: 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 assistant de planification de workflows pour Muyue. Tu génères des plans d'exécution sous forme de JSON. Chaque plan est une séquence d'étapes (steps) représentant des appels d'outils.
Pour générer un plan:
1. Comprends l'objectif de l'utilisateur
2. Identifie les outils nécessaires
3. Décompose en étapes logiques
4. Spécifie les paramètres de chaque outil
Réponds toujours en JSON valide, sans texte additionnel.`
var _ = plannerSystemPrompt