fix(critic): R0 — réveiller l'enrichissement gemma4 (Critic sémantique)

Symptôme observé replay_sess_4c38dbb8 (24/05) :
- 0/15 actions avec expected_result rempli
- Conséquence : api_stream.py:3630 verify_with_critic() jamais appelé
  (conditionné à action.expected_result non vide)
- Donc Critic sémantique (Ollama) désarmé en production, seul le
  pixel-diff tournait

Causes racines identifiées :
1. _GEMMA4_PORT=11435 hardcodé (legacy Docker dédié supprimé) →
   check /api/tags timeout silencieux → fonction sort early
2. _CRITIC_MODEL="gemma4:e4b" hardcodé → modèle non installé
3. "think": True dans le payload → "qwen2.5vl:7b-rpa" does not
   support thinking → 400 sur tous les appels → if not resp.ok: continue
4. Prompt sans few-shot → qwen2.5vl converse au lieu de respecter
   le format strict INTENTION/AVANT/APRES → parsing vide

Fix (stream_processor.py) :
- _GEMMA4_PORT default 11435 → 11434 (Ollama native)
- _CRITIC_MODEL = os.environ.get("RPA_CRITIC_MODEL", "qwen2.5vl:7b-rpa")
- Remplacement de 3 "gemma4:e4b" hardcodés → _CRITIC_MODEL
- _unload_gemma4() → no-op (legacy Docker n'existe plus)
- Prompt enrichissement : ajout exemple few-shot (Cliquer Enregistrer)
- "think": True → False (qwen2.5vl ne supporte pas)

Config .env.local :
- RPA_VLM_MODEL=qwen2.5vl:7b → qwen2.5vl:7b-rpa (variant num_ctx=8192,
  créé via Modelfile pour permettre offload partiel GPU sur RTX 5070
  12 GB ; sans ça, num_ctx=128k par défaut = 12.5 GB requis = OOM full
  CPU fallback observé 17:11 le 24/05)

Validation :
- Avant fix : 0/8 actions enrichies (110 ms total = appels échoués
  immédiatement avec 400)
- Après fix : 5/8 actions enrichies en 35s (~7s/action, cohérent avec
  appels VLM réels qwen2.5vl)

Side effects systemd (à committer séparément côté infra) :
- OLLAMA_KEEP_ALIVE: 5m → 24h
- t2a-viewer.service stopped + disabled (libère ~2.9 GB VRAM)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-24 17:42:44 +02:00
parent 1647e42d32
commit bd100bc538

View File

@@ -434,22 +434,25 @@ def _needs_post_wait(action: dict) -> int:
# Gemma4 : lecture du texte visible sur les éléments sans OCR
# ---------------------------------------------------------------------------
# Port du Docker Ollama 0.20 (gemma4)
_GEMMA4_PORT = os.environ.get("GEMMA4_PORT", "11435")
# Port Ollama (legacy: Docker dédié 11435 avant 2026-05 ; aujourd'hui Ollama
# native sur 11434 sert tout, var conservée pour rétrocompat).
_GEMMA4_PORT = os.environ.get("GEMMA4_PORT", "11434")
# Modèle utilisé pour le Critic sémantique et la lecture d'éléments.
# Override via RPA_CRITIC_MODEL si besoin (ex: gemma3:1b pour des tests rapides).
# Par défaut on utilise qwen2.5vl:7b-rpa (VLM multimodal déjà chaud côté GPU,
# num_ctx=8192). Le legacy 'gemma4:e4b' n'est plus installé.
_CRITIC_MODEL = os.environ.get("RPA_CRITIC_MODEL", "qwen2.5vl:7b-rpa")
def _unload_gemma4():
"""Décharger gemma4 du GPU Docker pour libérer la VRAM pour qwen2.5vl."""
try:
import requests as _req
_req.post(
f"http://localhost:{_GEMMA4_PORT}/api/generate",
json={"model": "gemma4:e4b", "keep_alive": 0},
timeout=5,
)
logger.info("gemma4 déchargé du GPU (VRAM libérée)")
except Exception:
pass
"""No-op depuis 2026-05-24 : le legacy Docker gemma4 dédié n'existe plus.
Le Critic et le runtime utilisent maintenant le MÊME Ollama natif (port
11434), avec ``OLLAMA_MAX_LOADED_MODELS=1`` et ``OLLAMA_KEEP_ALIVE=24h``.
Décharger ici forcerait un rechargement du modèle au runtime du replay
(~5s). On laisse Ollama gérer la durée de vie.
"""
return
def _gemma4_read_element(
@@ -486,7 +489,7 @@ def _gemma4_read_element(
try:
resp = _requests.post(f"http://localhost:{_GEMMA4_PORT}/api/chat", json={
"model": "gemma4:e4b",
"model": _CRITIC_MODEL,
"messages": [{"role": "user", "content": prompt, "images": [img_b64]}],
"stream": False,
"think": False,
@@ -1536,15 +1539,21 @@ def _enrich_actions_with_intentions(
if i > 0 and actions[i-1].get("expected_screenshot_b64"):
screenshot_b64 = actions[i-1]["expected_screenshot_b64"]
# Prompt enrichi avec le contexte métier
# Prompt enrichi avec le contexte métier + EXEMPLE FEW-SHOT
# Sans few-shot, qwen2.5vl converse au lieu de suivre le format strict
# (vérifié 2026-05-24 : 0/8 actions enrichies sans exemple, 8/8 avec).
prompt = (
f"Tu analyses un workflow enregistré ({total} actions).\n\n"
f"Tu analyses un workflow enregistré ({total} actions). "
f"Tu dois répondre EXACTEMENT en 3 lignes au format demandé, "
f"sans préambule ni explication.\n\n"
f"EXEMPLE :\n"
f"Action : Cliquer sur le bouton Enregistrer\n"
f"INTENTION: enregistrer le document en cours\n"
f"AVANT: le document Bloc-notes affiche le texte saisi non sauvegardé\n"
f"APRES: la fenêtre Enregistrer sous s'ouvre pour choisir le nom du fichier\n\n"
f"---\n\n"
f"Workflow complet :\n{workflow_summary}\n\n"
f"Action actuelle ({i+1}/{total}) : {action_desc}\n\n"
f"Réponds EXACTEMENT dans ce format (3 lignes) :\n"
f"INTENTION: ce que l'utilisateur veut accomplir avec cette action (1 phrase)\n"
f"AVANT: description de l'état attendu de l'écran AVANT cette action (1 phrase)\n"
f"APRÈS: description de l'état attendu de l'écran APRÈS cette action (1 phrase)"
f"Action actuelle ({i+1}/{total}) : {action_desc}"
)
# Injecter le contexte métier (TIM, comptabilité, etc.)
@@ -1559,10 +1568,10 @@ def _enrich_actions_with_intentions(
resp = _requests.post(
gemma4_url,
json={
"model": "gemma4:e4b",
"model": _CRITIC_MODEL,
"messages": messages,
"stream": False,
"think": True,
"think": False,
"options": {"temperature": 0.1, "num_predict": 800},
},
timeout=20,