mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Le déplacement de la préparation (plan, recall RAG) dans le flux SSE laissait un silence total pendant le chargement d'un gros modèle : le reverse proxy coupait la connexion (« Connexion interrompue pendant le chargement du modèle »). Avec la réflexion active, l'appel de plan laissait en plus le modèle penser — silence encore plus long. - ping SSE toutes les 10 s pendant la préparation du contexte - plan / auto-critique / résumé : think=False (repli automatique si le modèle refuse le paramètre) — un appel utilitaire ne réfléchit jamais Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""Mémoire de conversation compressée — le vrai levier des petits modèles.
|
||
|
||
Un petit modèle se noie dans un long historique : il oublie la consigne, part
|
||
en boucle, et un grand num_ctx le fait déborder du GPU. On garde donc :
|
||
[invite système] + [résumé compact des anciens tours] + [N derniers messages]
|
||
|
||
Le résumé est régénéré en arrière-plan (après la réponse, sans latence pour
|
||
l'utilisateur) dès que l'historique dépasse le seuil.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
import httpx
|
||
|
||
from . import db
|
||
from .ollama_client import OllamaError, ollama
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Nombre de messages récents passés tels quels au modèle.
|
||
KEEP_RECENT = 10
|
||
# Au-delà de ce total, les anciens tours sont compressés dans le résumé.
|
||
SUMMARIZE_AFTER = KEEP_RECENT + 6
|
||
|
||
_SUMMARY_PROMPT = (
|
||
"Résume la conversation ci-dessous en français, en 10 lignes maximum. "
|
||
"Conserve impérativement : l'objectif de l'utilisateur, les décisions "
|
||
"prises, les fichiers créés/modifiés et leur rôle, et les points encore "
|
||
"ouverts. Réponds UNIQUEMENT par le résumé."
|
||
)
|
||
|
||
|
||
def build_convo(sid: str, system_prompt: str) -> list[dict]:
|
||
"""Construit le contexte : système + résumé éventuel + messages récents."""
|
||
convo: list[dict] = [{"role": "system", "content": system_prompt}]
|
||
|
||
session = db.get_session(sid) or {}
|
||
summary = (session.get("summary") or "").strip()
|
||
messages = db.list_messages_for_model(sid)
|
||
|
||
if summary and len(messages) > KEEP_RECENT:
|
||
convo.append(
|
||
{
|
||
"role": "system",
|
||
"content": f"Résumé des échanges précédents :\n{summary}",
|
||
}
|
||
)
|
||
convo += messages[-KEEP_RECENT:]
|
||
else:
|
||
convo += messages
|
||
return convo
|
||
|
||
|
||
async def maybe_summarize(
|
||
sid: str,
|
||
model: str,
|
||
*,
|
||
options: dict | None = None,
|
||
keep_alive: str | None = None,
|
||
) -> None:
|
||
"""Compresse les anciens tours dans le résumé (tâche d'arrière-plan).
|
||
|
||
``options`` doit reprendre les options runner du chat (num_ctx…) pour ne
|
||
pas déclencher un rechargement du modèle après chaque réponse.
|
||
"""
|
||
try:
|
||
messages = db.list_messages_for_model(sid)
|
||
if len(messages) <= SUMMARIZE_AFTER:
|
||
return
|
||
|
||
session = db.get_session(sid) or {}
|
||
previous = (session.get("summary") or "").strip()
|
||
old = messages[:-KEEP_RECENT]
|
||
|
||
transcript = "\n".join(
|
||
f"[{m['role']}] {m['content'][:600]}" for m in old
|
||
)[-8000:]
|
||
if previous:
|
||
transcript = f"[résumé existant] {previous}\n{transcript}"
|
||
|
||
messages = [
|
||
{"role": "system", "content": _SUMMARY_PROMPT},
|
||
{"role": "user", "content": transcript},
|
||
]
|
||
opts = {**(options or {}), "temperature": 0.2, "num_predict": 350}
|
||
# think=False : le résumé d'arrière-plan ne doit pas « réfléchir »
|
||
# (latence ×3 sur un modèle thinking). Repli si paramètre refusé.
|
||
think: bool | None = False
|
||
while True:
|
||
text = ""
|
||
try:
|
||
async for chunk in ollama.chat(
|
||
model, messages, options=opts, think=think,
|
||
keep_alive=keep_alive, stream=True,
|
||
):
|
||
text += chunk.get("message", {}).get("content", "")
|
||
if chunk.get("done"):
|
||
break
|
||
break
|
||
except OllamaError as exc:
|
||
if think is False and "think" in str(exc).lower():
|
||
think = None
|
||
continue
|
||
raise
|
||
|
||
if text.strip():
|
||
db.set_session_summary(sid, text.strip())
|
||
except (httpx.HTTPError, OSError) as exc:
|
||
# Best-effort : un échec de résumé ne doit jamais gêner le chat.
|
||
logger.warning("Résumé de session %s impossible : %s", sid, exc)
|