package api import ( "context" "encoding/json" "fmt" "net/http" "os" "os/exec" "runtime" "strings" "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" ) type ShellChatRequest struct { Message string `json:"message"` Context string `json:"context,omitempty"` Cwd string `json:"cwd,omitempty"` Platform string `json:"platform,omitempty"` Stream bool `json:"stream"` } func (s *Server) handleShellChat(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } if s.shellConvStore.AtLimit() { writeError(w, "context limit reached, use /clear", http.StatusBadRequest) return } var req ShellChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return } if req.Message == "" { writeError(w, "message is required", http.StatusBadRequest) return } s.shellConvStore.Add("user", req.Message) orb, err := orchestrator.New(s.config) if err != nil { writeError(w, err.Error(), http.StatusServiceUnavailable) return } orb.SetSystemPrompt(s.buildShellSystemPrompt(req)) orb.SetTools(s.shellAgentToolsJSON) if req.Stream { s.handleShellChatStream(w, orb) } else { s.handleShellChatNonStream(w, orb) } } func (s *Server) buildShellSystemPrompt(_ ShellChatRequest) string { var sb strings.Builder sb.WriteString(`Tu es l'Analyste Système de Muyue. Tu es un expert en administration système et développement. Tu aides l'utilisateur à comprendre son système, diagnostiquer des problèmes, et optimiser son environnement. OUTILS DISPONIBLES: - terminal: Exécute des commandes shell sur le système local et retourne le résultat RÈGLES: - Utilise l'outil terminal pour exécuter des commandes quand c'est nécessaire - Analyse les résultats et explique-les clairement - Formate tes réponses en markdown avec des blocs de code quand approprié - Sois concis et technique - Quand tu proposes des commandes alternatives, utilise des blocs de code markdown `) analysis := LoadSystemAnalysis() if analysis != "" { sb.WriteString("=== ANALYSE SYSTÈME ACTUELLE ===\n") sb.WriteString(analysis) sb.WriteString("\n=== FIN DE L'ANALYSE ===\n\n") } sb.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) if hostname, err := os.Hostname(); err == nil { sb.WriteString("Hostname: " + hostname + "\n") } return sb.String() } func (s *Server) handleShellChatStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) { SetupSSEHeaders(w) flusher, canFlush := w.(http.Flusher) sseWriter := NewSSEWriter(w) ctx := context.Background() messages := s.buildShellContextMessages() engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON) engine.OnChunk(func(data map[string]interface{}) { if data == nil { return } sseWriter.Write(data) if canFlush { flusher.Flush() } }) finalContent, allToolCalls, allToolResults, err := engine.RunWithTools(ctx, messages) if err != nil { sseWriter.Write(map[string]interface{}{"error": err.Error()}) return } storeContent := finalContent if len(allToolCalls) > 0 { storeObj := map[string]interface{}{ "content": storeContent, "tool_calls": allToolCalls, "tool_results": allToolResults, } storeJSON, _ := json.Marshal(storeObj) storeContent = string(storeJSON) } s.shellConvStore.Add("assistant", storeContent) sseWriter.Write(map[string]interface{}{ "done": "true", "tokens": s.shellConvStore.ApproxTokens(), }) } func (s *Server) handleShellChatNonStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) { ctx := context.Background() messages := s.buildShellContextMessages() engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON) finalContent, err := engine.RunNonStream(ctx, messages) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } s.shellConvStore.Add("assistant", finalContent) writeJSON(w, map[string]interface{}{ "content": finalContent, "tokens": s.shellConvStore.ApproxTokens(), }) } func (s *Server) buildShellContextMessages() []orchestrator.Message { history := s.shellConvStore.Get() start := 0 const shellContextWindow = 20 if len(history) > shellContextWindow { start = len(history) - shellContextWindow } messages := make([]orchestrator.Message, 0, len(history[start:])) for _, m := range history[start:] { content := m.Content if m.Role == "assistant" { var parsed struct { Content string `json:"content"` ToolCalls []struct { ToolCallID string `json:"tool_call_id"` Name string `json:"name"` Args string `json:"args"` } `json:"tool_calls"` } if err := json.Unmarshal([]byte(content), &parsed); err == nil && parsed.Content != "" { content = parsed.Content } } role := m.Role if role == "system" { continue } messages = append(messages, orchestrator.Message{ Role: role, Content: content, }) } return messages } func (s *Server) handleShellChatHistory(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } messages := s.shellConvStore.Get() writeJSON(w, map[string]interface{}{ "messages": messages, "tokens": s.shellConvStore.ApproxTokens(), "max_tokens": shellMaxTokens, "at_limit": s.shellConvStore.AtLimit(), }) } func (s *Server) handleShellChatClear(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } s.shellConvStore.Clear() writeJSON(w, map[string]interface{}{ "status": "ok", "tokens": 0, }) } func (s *Server) handleShellAnalyze(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { writeError(w, "POST only", http.StatusMethodNotAllowed) return } var sysInfo strings.Builder sysInfo.WriteString("=== INFORMATIONS SYSTÈME ===\n") sysInfo.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) if hostname, err := os.Hostname(); err == nil { sysInfo.WriteString("Hostname: " + hostname + "\n") } if user := os.Getenv("USER"); user != "" { sysInfo.WriteString("User: " + user + "\n") } if data, err := os.ReadFile("/proc/cpuinfo"); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "model name") { sysInfo.WriteString("CPU: " + strings.SplitN(line, ":", 2)[1] + "\n") break } } } if data, err := os.ReadFile("/proc/meminfo"); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "MemTotal:") || strings.HasPrefix(line, "MemAvailable:") { sysInfo.WriteString(strings.TrimSpace(line) + "\n") } } } if out, err := exec.Command("df", "-h", "/").Output(); err == nil { lines := strings.Split(string(out), "\n") if len(lines) >= 2 { sysInfo.WriteString("Disk: " + strings.TrimSpace(lines[1]) + "\n") } } if out, err := exec.Command("ps", "aux", "--sort=-pcpu").Output(); err == nil { lines := strings.Split(string(out), "\n") sysInfo.WriteString(fmt.Sprintf("\nProcessus actifs (%d total):\n", len(lines)-1)) for i := 1; i < len(lines) && i <= 10; i++ { fields := strings.Fields(lines[i]) if len(fields) >= 11 { sysInfo.WriteString(fmt.Sprintf(" %-20s CPU:%-6s MEM:%-6s %s\n", fields[10], fields[2]+"%", fields[3]+"%", fields[0])) } } } if s.scanResult != nil { sysInfo.WriteString("\nOutils installés:\n") for _, t := range s.scanResult.Tools { status := "✗" if t.Installed { status = "✓" } sysInfo.WriteString(fmt.Sprintf(" %s %s %s\n", status, t.Name, t.Version)) } } orb, err := orchestrator.New(s.config) if err != nil { writeError(w, err.Error(), http.StatusServiceUnavailable) return } orb.SetSystemPrompt(agent.StudioSystemPrompt()) analysisPrompt := `Tu es un expert en administration système. Analyse les informations suivantes sur le système de l'utilisateur. Génère un rapport d'analyse concis et structuré en markdown qui inclut: 1. Un résumé de l'état du système 2. Les points d'attention (performance, sécurité, configuration) 3. Des recommandations spécifiques d'optimisation 4. Les outils manquants qui pourraient être utiles 5. L'état du réseau et des connexions Sois concret et technique. Le rapport sera utilisé comme contexte pour un assistant terminal. ` + sysInfo.String() result, err := orb.Send(analysisPrompt) if err != nil { writeError(w, "analysis failed: "+err.Error(), http.StatusInternalServerError) return } SaveSystemAnalysis(result) writeJSON(w, map[string]interface{}{ "status": "ok", "analysis": result, }) } func (s *Server) handleShellAnalysisGet(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) return } analysis := LoadSystemAnalysis() if analysis == "" { writeJSON(w, map[string]interface{}{"analysis": nil}) return } writeJSON(w, map[string]interface{}{"analysis": analysis}) }