Files
MuyueWorkspace/internal/workflow/planner.go
Augustin cb3d35756a
All checks were successful
Beta Release / beta (push) Successful in 1m3s
feat: terminal sudo blocking, token tracking, mermaid & consumption UI
- Block sudo/doas commands when not running as root
- Add real token counting from API responses
- Track and display consumption by provider/day
- Add Mermaid diagram rendering in Shell and Studio
- Add copy-to-clipboard buttons for code blocks
- Support tables in AI message rendering
- Update system prompt with context (date, time, root status)

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-26 12:43:15 +02:00

176 lines
4.6 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 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