diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index 32c2b58..b5ad87d 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -8,8 +8,10 @@ Flux : """ from __future__ import annotations +import asyncio import json import logging +from contextlib import suppress from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse @@ -58,14 +60,43 @@ async def chat(req: ChatRequest) -> StreamingResponse: tools_meta: list[dict] = [] error_message = "" + queue: asyncio.Queue[dict | None] = asyncio.Queue() + + async def produce_events() -> None: + try: + async for event in run_agent( + model, + convo, + options=agent_config.ollama_options(cfg), + enabled_tools=agent_config.enabled_tool_names(cfg), + confirm_shell=cfg.get("confirm_shell", True), + ): + await queue.put(event) + except Exception as exc: + logger.exception("Échec inattendu du flux de chat") + await queue.put( + { + "type": "error", + "message": f"Erreur interne du chat : {exc}", + } + ) + finally: + await queue.put(None) + + producer = asyncio.create_task(produce_events()) try: - async for ev in run_agent( - model, - convo, - options=agent_config.ollama_options(cfg), - enabled_tools=agent_config.enabled_tool_names(cfg), - confirm_shell=cfg.get("confirm_shell", True), - ): + while True: + try: + ev = await asyncio.wait_for(queue.get(), timeout=15.0) + except TimeoutError: + # Empêche OpenResty/Nginx/Cloudflare de fermer le SSE pendant + # le chargement parfois long d'un modèle Ollama. + yield _sse("ping", {"status": "waiting"}) + continue + + if ev is None: + break + etype = ev.pop("type") if etype in ( "token", @@ -82,12 +113,11 @@ async def chat(req: ChatRequest) -> StreamingResponse: elif etype == "final": final_content = ev["content"] tools_meta = ev["tools"] - except Exception as exc: - # Après l'envoi des en-têtes SSE, une exception non gérée coupe le - # socket sans explication et laisse l'interface bloquée. - logger.exception("Échec inattendu du flux de chat") - error_message = f"Erreur interne du chat : {exc}" - yield _sse("error", {"message": error_message}) + finally: + if not producer.done(): + producer.cancel() + with suppress(asyncio.CancelledError): + await producer if final_content or tools_meta: db.add_message( diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e19f808..9a04038 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -190,8 +190,11 @@ export async function streamChat( body: JSON.stringify(body), }); } catch (err) { + const raw = err instanceof Error ? err.message : "serveur Loki injoignable"; handlers.onError( - err instanceof Error ? err.message : "serveur Loki injoignable" + /network error|failed to fetch/i.test(raw) + ? "Serveur Loki injoignable. Vérifiez le reverse proxy et le conteneur." + : raw ); return; } @@ -271,8 +274,11 @@ export async function streamChat( } catch (err) { if (!failed) { failed = true; + const raw = err instanceof Error ? err.message : "connexion interrompue"; handlers.onError( - err instanceof Error ? err.message : "connexion au chat interrompue" + /network error|failed to fetch/i.test(raw) + ? "Connexion interrompue pendant le chargement du modèle. Vérifiez OLLAMA_HOST et le délai du reverse proxy." + : raw ); } }