feat(studio): add tool execution and hide AI thinking tags
All checks were successful
Beta Release / beta (push) Successful in 40s
All checks were successful
Beta Release / beta (push) Successful in 40s
Changes:
- Hide <think> tags from user in Studio chat
- Add tool call detection [TOOL_CALL:{...}] in AI responses
- Execute crush tool when requested by AI
- Show loading animation while AI is thinking
The AI can now:
1. Respond directly to user
2. Request tool execution via [TOOL_CALL:{"tool":"crush","task":"..."}]
The system automatically executes the tool and includes results.
Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
@@ -2,11 +2,17 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/muyue/muyue/internal/orchestrator"
|
||||
)
|
||||
|
||||
var toolCallRegex = regexp.MustCompile(`\[TOOL_CALL:\{[^\}]+\}\]`)
|
||||
|
||||
func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
@@ -36,13 +42,27 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
orb.SetSystemPrompt(`Tu es l'orchestrateur IA de Muyue Studio. Tu aides l'utilisateur dans ses tâches de développement logiciel. Tu peux :
|
||||
|- Créer et gérer des plans de développement étape par étape
|
||||
|- Proposer des agents (outils comme Crush, Claude Code, etc.) pour exécuter des tâches spécifiques
|
||||
|- Suivre la progression de tâches multi-étapes
|
||||
|- Suggérer des modifications de fichiers, des revues de code, et des décisions d'architecture
|
||||
orb.SetSystemPrompt(`Tu es l'assistant IA de Muyue Studio. Tu as accès à un outil "crush" pour exécuter des tâches complexes sur l'ordinateur de l'utilisateur.
|
||||
|
||||
Sois concis, actionnable, et structuré. Quand tu proposes un plan, utilise des étapes numérotées claires. Quand tu références des fichiers, utilise des chemins relatifs. Tu es intégré dans l'application desktop Muyue.`)
|
||||
RÈGLES ABSOLUES:
|
||||
1. Tu as DEUX possibilités ONLY:
|
||||
- Répondre directement à l'utilisateur avec tes connaissances
|
||||
- Demander l'exécution d'une tâche via crush en utilisant ce format EXACT:
|
||||
[TOOL_CALL:{"tool":"crush","task":"description de la tâche"}]
|
||||
|
||||
2. Quand tu utilises [TOOL_CALL:...], le système exécutera la tâche et te donnera le résultat.
|
||||
Tu peux ensuite répondre à l'utilisateur avec ce résultat.
|
||||
|
||||
3. SOIS CONCIS - pas de blabla, vais droit au but.
|
||||
|
||||
4. L'utilisateur ne voit PAS tes pensées entre <think> tags.
|
||||
|
||||
5. EXEMPLES d'utilisation de tool:
|
||||
- "cherche tous les fichiers .md dans le projet" → [TOOL_CALL:{"tool":"crush","task":"Recherche les fichiers .md dans le projet courant"}]
|
||||
- "aide-moi à déboguer cette erreur" → tu peux répondre directement si tu as assez d'info, sinon utiliser tool
|
||||
- "quelle est la météo?" → [TOOL_CALL:{"tool":"crush","task":"Cherche la météo actuelle"}]
|
||||
|
||||
6. Ne fais PAS de multi-step tool calls dans une seule réponse. Attends le résultat avant de continuer.`)
|
||||
|
||||
if body.Stream {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
@@ -53,6 +73,10 @@ Sois concis, actionnable, et structuré. Quand tu proposes un plan, utilise des
|
||||
flusher, canFlush := w.(http.Flusher)
|
||||
|
||||
result, err := orb.SendStream(body.Message, func(chunk string) {
|
||||
// Skip thinking tags - user doesn't see them
|
||||
if strings.HasPrefix(chunk, "<think") {
|
||||
return
|
||||
}
|
||||
data, _ := json.Marshal(map[string]string{"content": chunk})
|
||||
w.Write([]byte("data: " + string(data) + "\n\n"))
|
||||
if canFlush {
|
||||
@@ -68,7 +92,9 @@ Sois concis, actionnable, et structuré. Quand tu proposes un plan, utilise des
|
||||
return
|
||||
}
|
||||
|
||||
s.convStore.Add("assistant", result)
|
||||
// Process tool calls if any
|
||||
cleanResult := processToolCalls(result)
|
||||
s.convStore.Add("assistant", cleanResult)
|
||||
|
||||
data, _ := json.Marshal(map[string]string{"done": "true"})
|
||||
w.Write([]byte("data: " + string(data) + "\n\n"))
|
||||
@@ -83,8 +109,64 @@ Sois concis, actionnable, et structuré. Quand tu proposes un plan, utilise des
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.convStore.Add("assistant", result)
|
||||
writeJSON(w, map[string]string{"content": result})
|
||||
cleanResult := processToolCalls(result)
|
||||
s.convStore.Add("assistant", cleanResult)
|
||||
writeJSON(w, map[string]string{"content": cleanResult})
|
||||
}
|
||||
|
||||
func processToolCalls(content string) string {
|
||||
matches := toolCallRegex.FindAllString(content, -1)
|
||||
if len(matches) == 0 {
|
||||
return cleanThinkingTags(content)
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
clean := content
|
||||
|
||||
for _, match := range matches {
|
||||
// Extract tool and task from [TOOL_CALL:{...}]
|
||||
inner := strings.TrimPrefix(match, "[TOOL_CALL:")
|
||||
inner = strings.TrimSuffix(inner, "]}") + "}"
|
||||
|
||||
var call struct {
|
||||
Tool string `json:"tool"`
|
||||
Task string `json:"task"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(inner), &call); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if call.Tool == "crush" && call.Task != "" {
|
||||
result.WriteString(fmt.Sprintf("> %s\n\n", call.Task))
|
||||
output := executeCrush(call.Task)
|
||||
result.WriteString(output)
|
||||
result.WriteString("\n\n---\n\n")
|
||||
}
|
||||
|
||||
clean = strings.Replace(clean, match, "", 1)
|
||||
}
|
||||
|
||||
clean = cleanThinkingTags(clean)
|
||||
|
||||
if result.Len() > 0 {
|
||||
clean = strings.TrimSpace(clean) + "\n\n" + strings.TrimSpace(result.String())
|
||||
}
|
||||
|
||||
return clean
|
||||
}
|
||||
|
||||
func cleanThinkingTags(content string) string {
|
||||
re := regexp.MustCompile(`(?s)<think[^>]*>.*?</think>`)
|
||||
return re.ReplaceAllString(content, "")
|
||||
}
|
||||
|
||||
func executeCrush(task string) string {
|
||||
cmd := exec.Command("crush", "run", task)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Erreur: %v\n%s", err, string(output))
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func (s *Server) autoSummarize() {
|
||||
@@ -139,4 +221,4 @@ func (s *Server) handleChatClear(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
s.convStore.Clear()
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user