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:
@@ -434,22 +434,25 @@ def _needs_post_wait(action: dict) -> int:
|
|||||||
# Gemma4 : lecture du texte visible sur les éléments sans OCR
|
# Gemma4 : lecture du texte visible sur les éléments sans OCR
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Port du Docker Ollama 0.20 (gemma4)
|
# Port Ollama (legacy: Docker dédié 11435 avant 2026-05 ; aujourd'hui Ollama
|
||||||
_GEMMA4_PORT = os.environ.get("GEMMA4_PORT", "11435")
|
# 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():
|
def _unload_gemma4():
|
||||||
"""Décharger gemma4 du GPU Docker pour libérer la VRAM pour qwen2.5vl."""
|
"""No-op depuis 2026-05-24 : le legacy Docker gemma4 dédié n'existe plus.
|
||||||
try:
|
|
||||||
import requests as _req
|
Le Critic et le runtime utilisent maintenant le MÊME Ollama natif (port
|
||||||
_req.post(
|
11434), avec ``OLLAMA_MAX_LOADED_MODELS=1`` et ``OLLAMA_KEEP_ALIVE=24h``.
|
||||||
f"http://localhost:{_GEMMA4_PORT}/api/generate",
|
Décharger ici forcerait un rechargement du modèle au runtime du replay
|
||||||
json={"model": "gemma4:e4b", "keep_alive": 0},
|
(~5s). On laisse Ollama gérer la durée de vie.
|
||||||
timeout=5,
|
"""
|
||||||
)
|
return
|
||||||
logger.info("gemma4 déchargé du GPU (VRAM libérée)")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _gemma4_read_element(
|
def _gemma4_read_element(
|
||||||
@@ -486,7 +489,7 @@ def _gemma4_read_element(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp = _requests.post(f"http://localhost:{_GEMMA4_PORT}/api/chat", json={
|
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]}],
|
"messages": [{"role": "user", "content": prompt, "images": [img_b64]}],
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"think": False,
|
"think": False,
|
||||||
@@ -1536,15 +1539,21 @@ def _enrich_actions_with_intentions(
|
|||||||
if i > 0 and actions[i-1].get("expected_screenshot_b64"):
|
if i > 0 and actions[i-1].get("expected_screenshot_b64"):
|
||||||
screenshot_b64 = actions[i-1]["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 = (
|
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"Workflow complet :\n{workflow_summary}\n\n"
|
||||||
f"Action actuelle ({i+1}/{total}) : {action_desc}\n\n"
|
f"Action actuelle ({i+1}/{total}) : {action_desc}"
|
||||||
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)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Injecter le contexte métier (TIM, comptabilité, etc.)
|
# Injecter le contexte métier (TIM, comptabilité, etc.)
|
||||||
@@ -1559,10 +1568,10 @@ def _enrich_actions_with_intentions(
|
|||||||
resp = _requests.post(
|
resp = _requests.post(
|
||||||
gemma4_url,
|
gemma4_url,
|
||||||
json={
|
json={
|
||||||
"model": "gemma4:e4b",
|
"model": _CRITIC_MODEL,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"think": True,
|
"think": False,
|
||||||
"options": {"temperature": 0.1, "num_predict": 800},
|
"options": {"temperature": 0.1, "num_predict": 800},
|
||||||
},
|
},
|
||||||
timeout=20,
|
timeout=20,
|
||||||
|
|||||||
Reference in New Issue
Block a user