"""Mémoire long-terme sémantique (Qdrant). Stocke les tours utilisateur comme vecteurs dans la collection `stt-memory` et retrouve les souvenirs pertinents pour les injecter dans le prompt. Embeddings via Qwen3 (llama-server gpu-01), comme le RAG. **Tout dégrade proprement** : si Qdrant ou l'endpoint d'embedding est injoignable, `recall` renvoie [] et `remember` ne fait rien (la mémoire court-terme de session continue de fonctionner). > Caveat qualité : Qwen3 n'est pas un modèle d'embedding dédié (cosinus uniformément hauts) ; > la recherche est approximative. Voir admin/ia/rag.md pour la piste nomic-embed-text. """ from __future__ import annotations import time import uuid import httpx from stt_server.config import settings class LongTermMemory: def __init__(self) -> None: self.qdrant = settings.qdrant_url.rstrip("/") self.collection = settings.qdrant_collection self.embed_url = settings.embed_url self.embed_model = settings.embed_model self.top_k = settings.memory_top_k self._ready = False async def _embed(self, client: httpx.AsyncClient, text: str) -> list[float]: r = await client.post( self.embed_url, json={"model": self.embed_model, "input": text}, timeout=30, ) r.raise_for_status() return r.json()["data"][0]["embedding"] async def _ensure_collection(self, client: httpx.AsyncClient, dim: int) -> None: if self._ready: return r = await client.get(f"{self.qdrant}/collections/{self.collection}") if r.status_code == 404: await client.put( f"{self.qdrant}/collections/{self.collection}", json={"vectors": {"size": dim, "distance": "Cosine"}}, ) self._ready = True async def recall(self, text: str) -> list[str]: """Souvenirs pertinents (texte) ou [] si indisponible.""" try: async with httpx.AsyncClient(timeout=20) as client: vec = await self._embed(client, text) r = await client.post( f"{self.qdrant}/collections/{self.collection}/points/search", json={"vector": vec, "limit": self.top_k, "with_payload": True}, ) if r.status_code == 404: # collection pas encore créée return [] r.raise_for_status() pts = r.json().get("result", []) return [p["payload"]["text"] for p in pts if p.get("payload", {}).get("text")] except Exception: # noqa: BLE001 — dégrade silencieusement return [] async def remember(self, session_id: str, text: str) -> None: try: async with httpx.AsyncClient(timeout=20) as client: vec = await self._embed(client, text) await self._ensure_collection(client, len(vec)) await client.put( f"{self.qdrant}/collections/{self.collection}/points", json={ "points": [ { "id": str(uuid.uuid4()), "vector": vec, "payload": { "text": text, "session_id": session_id, "ts": time.time(), }, } ] }, ) except Exception: # noqa: BLE001 — dégrade silencieusement pass