From 92c350bdb82623c7b9ef6cca193292d48cfe8893 Mon Sep 17 00:00:00 2001 From: nathaninline Date: Wed, 29 Jul 2026 18:49:53 +0200 Subject: [PATCH] v0.6.3 : texte non coupe autour des outils, garde-fous d'outils retires, retours UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flux : le reliquat retenu par la garde « » est vidé dès qu'un appel d'outil arrive, au lieu de la fin du flux. Sans ça il partait APRÈS l'événement d'outil et la fin de phrase — souvent coupée en plein mot — atterrissait dans une bulle séparée sous l'outil. - Le plafond d'appels d'outils par tour (TOOL_LIMIT) et l'anti-boucle « appel identique répété » sont SUPPRIMÉS : ils coupaient des tours légitimes (une recherche enchaîne facilement des dizaines d'appels). Le bouton stop reste le seul frein. Endpoint /api/agent/tool-limit et son interrupteur retirés. - Prompt système : « You are Jean » (majuscule) — le modèle recopiait la casse et se présentait en minuscule. - UI : titres du menu tous en monospace capitales via une seule règle (la casse vient du CSS, plus du HTML). - UI : le message envoyé s'affiche tout de suite en gris (« envoi… ») et s'éclaircit quand le serveur le confirme ; il ne disparaît plus pendant l'aller-retour, et il est retiré si l'envoi échoue. - UI : voile de chargement du fil (« chargement de la conversation… » / « connexion au serveur… ») — un chat vide ne se confond plus avec une connexion lente. - UI mobile : hauteur de coque 100dvh hors PWA (100vh en PWA), et plus aucun défilement du document. Le mode PWA est détecté par navigator.standalone : le média display-mode ne matche pas, faute de manifeste. --- README.md | 3 +- cmd/jean/resource_windows_amd64.syso | Bin 8340 -> 8340 bytes cmd/jean/resource_windows_arm64.syso | Bin 8340 -> 8340 bytes cmd/jean/versioninfo.json | 6 +- internal/jean/backend_config.go | 23 +--- internal/jean/chat_tools.go | 4 +- internal/jean/llm_client.go | 50 +++---- internal/jean/run.go | 2 +- internal/jean/ui/index.html | 161 ++++++++++++++++++---- internal/jean/ui/src/index.tmpl.html | 13 +- internal/jean/ui/src/js/06-settings.js | 6 - internal/jean/ui/src/js/08-chat-render.js | 17 ++- internal/jean/ui/src/js/09-stream.js | 44 +++++- internal/jean/ui/src/styles.css | 81 +++++++++-- internal/jean/web_api.go | 20 +-- internal/jean/web_server.go | 1 - 16 files changed, 297 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index e2c89b1..460ecdd 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,7 @@ Tout vit sous **`$JEAN_HOME`** (défaut `/etc/jean` sur Linux/macOS, `%ProgramDa | `CUDA_VISIBLE_DEVICES` | GPU à utiliser (réglé par `jean gpu`) | tous | | `KV_TYPE` (`_K`/`_V`) | quantization du cache KV | — | | `REASONING` | passthrough du mode raisonnement | — | -| `REASONING_BUDGET` | plafond de tokens de réflexion passé à llama-server ; `-1` = illimité (l'anti-boucle est géré côté agent) | `-1` | -| `TOOL_LIMIT` | plafond d'appels d'outils par tour en mode agent ; `off` = quasi illimité (réglable aussi depuis l'UI) | activé | +| `REASONING_BUDGET` | plafond de tokens de réflexion passé à llama-server ; `-1` = illimité | `-1` | | `CRAWL4AI_URL` | URL du serveur Crawl4AI pour l'accès internet (réglé par `jean internet url`) | — | | `MEM_MODE` | mode mémoire de l'IA : `off` / `ondemand` / `always` (réglé par `jean memory`) | `always` | | `EXTRA_ARGS` | ajouté tel quel à `llama-server` | — | diff --git a/cmd/jean/resource_windows_amd64.syso b/cmd/jean/resource_windows_amd64.syso index c889c312aeaee24d0318ce16c8313df427a68503..5c5d047f7329fc03789d6fd3d6a825a5b9a1cbe9 100644 GIT binary patch delta 36 ncmbQ@IK^>;lnf^`0~-SfFmG0t5oKaD-fSrs%m}1<<=GhlgnI_u delta 36 ncmbQ@IK^>;lnf^m0~-SfFl|;lnf^`0~-SfFmG0t5oKaD-fSrs%m}1<<=GhlgnI_u delta 36 ncmbQ@IK^>;lnf^m0~-SfFl| // puis le token de fin SANS appeler d'outil (~25-45 % de tours « morts » // mesurés). Une version courte et directe ramène ça à 0 %. NE PAS regonfler. - b.WriteString("You are jean, an expert assistant operating directly on this machine with real tools.") + // « Jean » avec une majuscule : c'est un nom propre, et le modèle recopie + // littéralement la casse d'ici quand il se présente (« je suis jean »). + b.WriteString("You are Jean, an expert assistant operating directly on this machine with real tools.") if caps.Mem == MemAlways { b.WriteString(" You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.") } diff --git a/internal/jean/llm_client.go b/internal/jean/llm_client.go index b219ccf..9405871 100644 --- a/internal/jean/llm_client.go +++ b/internal/jean/llm_client.go @@ -414,10 +414,6 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps // dépassement de la fenêtre de contexte après de gros résultats d'outils), on // compacte l'historique en vol et on rejoue le tour — une seule fois. compactedRetry := false - // Anti-loop net: if the model re-emits the exact same tool call (name+args) - // several times, it's stuck — break instead of spinning to the iteration cap. - lastSig := "" - repeatSig := 0 // Appels d'outil déjà exécutés (clé = nom + arguments bruts) : sert à ne pas // rejouer deux fois exactement la même écriture dans un même échange. doneCalls := map[string]string{} @@ -426,12 +422,11 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps // (ni réponse, ni tool_call). On relance alors UNE fois le tour avec un nudge // explicite au lieu d'afficher « pas de réponse ». nudged := false - // Plafond d'itérations d'appels d'outils. Activé par défaut (8) pour éviter - // qu'un modèle parti en vrille n'enchaîne les appels indéfiniment ; l'anti- - // boucle (lastSig) protège toujours même quand la limite est désactivée via - // l'UI (TOOL_LIMIT=off → plafond très haut, quasi illimité). - maxIter := toolCallLimit() - for iter := 0; iter < maxIter; iter++ { + // Pas de plafond d'itérations ni d'anti-boucle : ils coupaient des tours + // parfaitement légitimes (une recherche enchaîne facilement des dizaines + // d'appels, parfois identiques). Le seul frein est le bouton stop, qui annule + // le contexte — c'est un choix assumé. + for iter := 0; ; iter++ { payload := map[string]any{ "model": "jean", "messages": messages, @@ -557,6 +552,20 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps finishReason = ch.FinishReason } if len(ch.Delta.ToolCalls) > 0 { + // Un appel d'outil clôt le texte : on vide MAINTENANT le reliquat + // retenu par la garde « » (voir plus bas). Sinon il n'était + // émis qu'en fin de flux, donc APRÈS l'événement d'outil, et l'UI + // (qui coupe la bulle en cours à chaque tool_used) affichait la fin + // de la phrase — souvent coupée en plein mot — dans une bulle + // séparée sous l'outil. + if thinkOpen && thinkTail.Len() > 0 { + tail := thinkTail.String() + thinkTail.Reset() + if !cb(StreamEvent{Content: tail}) { + aborted = true + break + } + } for i, tc := range ch.Delta.ToolCalls { // llama.cpp's stream may omit index; fall back to slot i. idx := i @@ -705,25 +714,6 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps } messages = append(messages, assistant) extra = append(extra, assistant) - // Loop guard: same exact call(s) as last turn? Count it; on the 3rd - // identical turn, stop so we don't churn the same command forever. - sig := strings.Builder{} - for _, tc := range tcs { - sig.WriteString(tc.Function.Name) - sig.WriteByte('\x00') - sig.WriteString(tc.Function.Arguments) - sig.WriteByte('\n') - } - if s := sig.String(); s == lastSig { - repeatSig++ - if repeatSig >= 2 { - cb(StreamEvent{Content: "\n\n[stop: appel d'outil répété en boucle — " + tcs[0].Function.Name + "]"}) - return extra, nil - } - } else { - lastSig = s - repeatSig = 0 - } // 2. Execute each tool locally and append a "tool" reply. for _, tc := range tcs { var args map[string]any @@ -887,8 +877,6 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps } return extra, nil } - cb(StreamEvent{Content: "\n\n[stop: trop d'appels d'outils]"}) - return extra, nil } // healthCheck pings llama.cpp's /health endpoint. diff --git a/internal/jean/run.go b/internal/jean/run.go index 422d3dc..5389fbc 100644 --- a/internal/jean/run.go +++ b/internal/jean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.6.2" +const Version = "0.6.3" // Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler). func Main() { diff --git a/internal/jean/ui/index.html b/internal/jean/ui/index.html index a301a92..d2ce372 100644 --- a/internal/jean/ui/index.html +++ b/internal/jean/ui/index.html @@ -14,6 +14,13 @@ (iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. --> + +