mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 23:44:41 +02:00
Sans contexte, le modèle (qwen3-8b) hallucinait sur le homelab (« stockage décentralisé », « worker nodes »…) car il ignore tout du cluster réel. Le cerveau interroge désormais la collection funk-docs (la doc admin/ indexée) et injecte les passages pertinents au prompt → il répond à partir de la vraie doc. Coût ~nul : funk-docs et stt-memory partagent le même embedder nomic (:1238, 768 dim) → le vecteur de requête calculé pour le recall mémoire est réutilisé pour la recherche doc (une embed, deux recherches). Dégrade en silence si Qdrant/embed down (réponse sans doc plutôt qu'échec). Module knowledge.py (STT_DOCS_RAG, TOPK=6, MIN_SCORE=0.45). Log /v1/ask expose docs=<n>. Serveur 0.2.1 → 0.3.0. Validé : « c'est quoi le nom des nœuds ? » → avant « worker/master nodes » (inventé) ; après « Kubernetes ; nœuds storage-01, compute-01, compute-02 » (tiré de la doc). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
3.7 KiB
Python
96 lines
3.7 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,
|
||
knowledge: 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 knowledge:
|
||
# Contexte AUTORITAIRE : extraits de la doc Funk (RAG funk-docs). Le modèle doit
|
||
# répondre à partir de ça pour les questions sur le homelab (sinon il hallucine).
|
||
extraits = "\n\n".join(knowledge)
|
||
system += (
|
||
"\n\n=== Documentation Funk (réponds À PARTIR de ces extraits ; "
|
||
"si l'info n'y est pas, dis-le) ===\n" + extraits
|
||
)
|
||
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ésilience : la chaîne LiteLLM→llama-server (ROCm) se fige par à-coups. Une 2ᵉ tentative
|
||
# récupère un blip transitoire (ex. connexion keep-alive périmée) au lieu de renvoyer 502.
|
||
# Timeout PAR tentative (`request_timeout`) → on ne dépasse pas ~2× ce budget au pire.
|
||
last_exc: Exception | None = None
|
||
for attempt in range(2):
|
||
try:
|
||
r = await _get_client().post(
|
||
settings.litellm_url,
|
||
json=payload,
|
||
headers=headers,
|
||
timeout=settings.request_timeout,
|
||
)
|
||
r.raise_for_status()
|
||
break
|
||
except httpx.HTTPError as e:
|
||
last_exc = e
|
||
await aclose() # repart sur une connexion neuve (pool potentiellement vicié)
|
||
else:
|
||
raise last_exc # type: ignore[misc]
|
||
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
|