Funk-lab/stt/server/stt_server/brain.py
alkatrazz 4f0338dbb1 perf(stt-server): mémoire long-terme hors chemin de réponse + résilience embed
Cause de la latence 30-45s : l'endpoint d'embeddings (gpu-01:1238) peut se
geler ; recall ET remember l'attendaient ~20s chacun (timeout → dégradation
silencieuse), s'ajoutant à la réponse. Refactor :

- store (ex-remember) en BackgroundTasks → APRÈS la réponse, hors latence perçue ;
  suppression de `?wait=true` (pas d'attente du flush Qdrant)
- recall renvoie aussi le vecteur de la requête → store le réutilise (1 embed/tour
  au lieu de 2, le 2ᵉ portait sur le même texte)
- timeout recall serré (4s, STT_MEMORY_RECALL_TIMEOUT) : un embed lent/mort dégrade
  vite (souvenirs vides) au lieu de bloquer ; store tolère 20s en arrière-plan
- clients httpx persistants (pooling/keep-alive) côté brain + longterm, fermés via
  lifespan (plus de handshake TCP par appel)
- log de timing par requête (recall/gen/total/mem) pour diagnostiquer
- bump serveur 0.1.0 → 0.2.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 15:14:44 +02:00

70 lines
2.6 KiB
Python

"""Orchestration AI : route les requêtes des clients vers LiteLLM (s01).
LiteLLM (:4000) est OpenAI-compatible et route lui-même vers Qwen3 (g01) ou Claude
selon l'alias `hermes-default` / `hermes-switch`. L'intégration des outils Hermes
(« agir sur Funk » via le gateway :8080) est une étape ultérieure.
"""
from __future__ import annotations
import httpx
from stt_server.config import settings
# Client persistant (pooling + keep-alive) : évite un handshake TCP vers LiteLLM à chaque tour.
_client: httpx.AsyncClient | None = None
def _get_client() -> httpx.AsyncClient:
global _client
if _client is None or _client.is_closed:
_client = httpx.AsyncClient(timeout=settings.request_timeout)
return _client
async def aclose() -> None:
global _client
if _client is not None and not _client.is_closed:
await _client.aclose()
_client = None
async def ask(
text: str,
model: str | None = None,
history: list[dict] | None = None,
memories: list[str] | None = None,
) -> str:
system = settings.system_prompt
if settings.disable_thinking:
# Qwen3 est un modèle « thinking » : sans ça il dépense tout le budget max_tokens
# en raisonnement (`reasoning_content`) → `content` vide, ou part en réflexion longue
# → timeout (502). Le token de contrôle `/no_think` désactive le mode raisonnement.
# Inoffensif pour les modèles non-Qwen (simple texte ignoré).
system += "\n/no_think"
if memories:
souvenirs = "\n".join(f"- {m}" for m in memories)
system += (
"\n\nÉléments de mémoire long-terme (peuvent aider, ignore si hors-sujet) :\n"
+ souvenirs
)
messages = [{"role": "system", "content": system}]
if history:
messages += history
messages.append({"role": "user", "content": text})
payload = {
"model": model or settings.model,
"messages": messages,
"max_tokens": settings.max_tokens,
"temperature": settings.temperature,
}
headers = {"Authorization": f"Bearer {settings.litellm_key}"}
r = await _get_client().post(settings.litellm_url, json=payload, headers=headers)
r.raise_for_status()
msg = r.json()["choices"][0]["message"]
# Filet de sécurité : si un modèle « thinking » renvoie un content vide (tout parti
# en reasoning_content), on récupère le raisonnement plutôt que de renvoyer "".
content = (msg.get("content") or "").strip()
if not content:
content = (msg.get("reasoning_content") or "").strip()
return content