app/ping.py
Rama main · réplica de lectura del repositorio original.
"""Diagnostic /ping : teste le chemin LLM complet maillon par maillon.
Chaîne : bot Telegram -> API FastAPI -> bridge socat -> Ollama -> modèle.
Chaque maillon est testé indépendamment pour nommer précisément celui
qui est cassé en cas d'échec. Lecture seule, sans coût d'inférence pour
les 4 premiers checks ; le 5e fait une micro-inférence (prompt vide de
outils, 1 token) pour prouver la chaîne d'inférence de bout en bout.
"""
import os
import time
import httpx
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://172.20.0.1:11434")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5:14b")
async def ping_report() -> str:
"""Teste chaque maillon et nomme le premier cassé."""
lines: list[str] = ["🏓 **Vektor /ping** — chemin LLM complet"]
failures: list[str] = []
# 1. Ollama joignable via le bridge (le maillon qui a déjà cassé deux fois)
t0 = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=6) as client:
response = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
dt = (time.perf_counter() - t0) * 1000
if response.status_code == 200:
names = [m.get("name", "") for m in response.json().get("models", [])]
lines.append(f"🟢 Bridge Ollama ({OLLAMA_BASE_URL}) — {dt:.0f} ms, {len(names)} modèles")
else:
failures.append("bridge Ollama")
lines.append(f"🔴 Bridge Ollama : HTTP {response.status_code}")
except httpx.HTTPError as exc:
failures.append("bridge Ollama")
lines.append(
f"🔴 Bridge Ollama ({OLLAMA_BASE_URL}) injoignable "
f"({exc.__class__.__name__}) — vérifier le conteneur "
"vektor-ollama-bridge et OLLAMA_NETBIRD_HOST"
)
return "\n".join(lines + [f"\n❌ Premier maillon cassé : **{failures[0]}**"])
# 2. Le modèle configuré est-il présent ?
model_present = any(OLLAMA_MODEL in name for name in names)
if model_present:
lines.append(f"🟢 Modèle `{OLLAMA_MODEL}` présent côté Ollama")
else:
failures.append("modèle Ollama")
lines.append(
f"🔴 Modèle `{OLLAMA_MODEL}` ABSENT — modèles vus : "
+ ", ".join(names[:5])
+ " — vérifier OLLAMA_MODEL et le tag image (ollama pull)"
)
lines.insert(1, f"⚠️ Premier maillon cassé : **{failures[0]}**")
return "\n".join(lines)
# 3. Le modèle est-il chargé en RAM (keep_alive) ?
try:
async with httpx.AsyncClient(timeout=6) as client:
ps = await client.get(f"{OLLAMA_BASE_URL}/api/ps")
loaded = [m.get("name", "") for m in ps.json().get("models", [])]
if any(OLLAMA_MODEL in name for name in loaded):
lines.append(f"🟢 Modèle chargé en RAM (keep_alive) : réponse immédiate attendue")
else:
lines.append(
"🟡 Modèle NON chargé en RAM — prochaine question LLM : "
"+40-60 s de chargement (pas une panne)"
)
except httpx.HTTPError:
lines.append("🟡 /api/ps indisponible (état RAM inconnu)")
# 4. Inférence réelle de bout en bout (micro-prompt, 1 token attendu)
t0 = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=90) as client:
response = await client.post(
f"{OLLAMA_BASE_URL}/api/generate",
json={
"model": OLLAMA_MODEL,
"prompt": "Réponds uniquement: OK",
"stream": False,
"options": {"num_predict": 5},
},
)
dt = time.perf_counter() - t0
if response.status_code == 200 and response.json().get("response"):
sample = response.json()["response"].strip()[:40]
lines.append(f"🟢 Inférence de bout en bout — {dt:.0f} s (réponse: «{sample}»)")
lines.append("\n✅ **Chemin LLM complet opérationnel**")
else:
failures.append("inférence Ollama")
lines.append(f"🔴 Inférence : HTTP {response.status_code} ou réponse vide")
except httpx.HTTPError as exc:
failures.append("inférence Ollama (timeout 90 s — CPU chargé ?)")
lines.append(f"🔴 Inférence : {exc.__class__.__name__} après 90 s")
if failures:
lines.insert(1, f"⚠️ Maillon dégradé : **{failures[0]}**")
return "\n".join(lines)