Theme neo-brutaliste + panneau raisonnement + repli sans outils

UI : reskin complet vers un theme neo-brutaliste clair (bordures noires 3px,
ombres dures, accent orange, polices Press Start 2P + DM Mono).
- tailwind.config / index.css / index.html : palette, polices, ombres, scrollbars
- TopBar, ActivityBar, LeftPanel, ChatPanel, PreviewPanel, SettingsView,
  ToolCard, ModelSelector, Icon : restyle complet

Chat : panneau « Raisonnement » repliable et redimensionnable affichant le
thinking de l'agent en direct (diffuse depuis le backend, persiste dans meta).

Robustesse Ollama : les echecs de generation de parseur d'outils
(« Unable to generate parser for this template ») declenchent desormais le
repli automatique en conversation simple sans outils, au lieu d'un 400.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-30 14:21:05 +02:00
1 parent e8e6a1446c
commit b4fc8cee3d
16 files changed
+488 -419

No files matched your search

+21 -2
View File
@@ -26,9 +26,22 @@ MAX_TOOL_REPAIR_ATTEMPTS = 2
def _tools_not_supported(exc: OllamaError) -> bool:
"""Détecte l'erreur Ollama renvoyée par un modèle sans function calling."""
"""Détecte un modèle incapable de function calling.
Deux cas :
- Ollama refuse explicitement (« does not support tools ») ;
- Ollama n'arrive pas à dériver un parseur d'appels d'outils du template
du modèle (« Unable to generate parser for this template ») — fréquent
sur des modèles exotiques dont le template Jinja lève une exception.
Dans les deux cas, on retombe sur une conversation simple, sans outils.
"""
message = str(exc).lower()
return "does not support tools" in message or "does not support tool" in message
return (
"does not support tools" in message
or "does not support tool" in message
or "unable to generate parser for this template" in message
or "automatic parser generation failed" in message
)
def _invalid_tool_arguments(exc: OllamaError) -> bool:
@@ -70,6 +83,7 @@ async def run_agent(
tools = None
collected: list[dict] = []
text_parts: list[str] = []
thinking_parts: list[str] = []
active_tools = tools
tool_fallback_used = False
tool_repair_attempts = 0
@@ -107,6 +121,9 @@ async def run_agent(
thinking = msg.get("thinking", "")
if thinking:
thinking_buf += thinking
# Diffuse le raisonnement en direct pour l'afficher
# dans le panneau repliable du chat.
yield {"type": "thinking", "content": thinking}
if not thinking_status_sent:
thinking_status_sent = True
yield {"type": "status", "message": "Réflexion…"}
@@ -179,6 +196,7 @@ async def run_agent(
assistant_turn: dict = {"role": "assistant", "content": content_buf}
if thinking_buf:
assistant_turn["thinking"] = thinking_buf
thinking_parts.append(thinking_buf)
if tool_calls:
assistant_turn["tool_calls"] = tool_calls
convo.append(assistant_turn)
@@ -300,4 +318,5 @@ async def run_agent(
"content": final_content,
"tools": collected,
"stats": final_stats,
"thinking": "\n\n".join(thinking_parts).strip(),
}
+5
View File
@@ -59,6 +59,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
final_content = ""
tools_meta: list[dict] = []
stats_meta: dict | None = None
thinking_meta = ""
error_message = ""
queue: asyncio.Queue[dict | None] = asyncio.Queue()
@@ -101,6 +102,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
etype = ev.pop("type")
if etype in (
"token",
"thinking",
"status",
"notice",
"tool_call",
@@ -115,6 +117,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
final_content = ev["content"]
tools_meta = ev["tools"]
stats_meta = ev.get("stats")
thinking_meta = ev.get("thinking", "")
finally:
if not producer.done():
producer.cancel()
@@ -127,6 +130,8 @@ async def chat(req: ChatRequest) -> StreamingResponse:
meta["tools"] = tools_meta
if stats_meta:
meta["stats"] = stats_meta
if thinking_meta:
meta["thinking"] = thinking_meta
db.add_message(
req.session_id,
"assistant",