fix(replay): bug TypeError log + flag pré-check OCR off par défaut (démo GHT)
Some checks failed
tests / Lint (ruff + black) (push) Successful in 14s
tests / Tests unitaires (sans GPU) (push) Failing after 13s
tests / Tests sécurité (critique) (push) Has been skipped

Diagnostic post-bench E2E (rapport docs/E2E_TEST_RUN_2026-05-08.md) :

1. BUG SILENCIEUX MAJEUR (api_stream.py:4549) — quand le pré-check OCR
   rejette, mon code de rejet hier soir met x_pct=None / y_pct=None.
   Le log structuré faisait result.get('x_pct', 0):.4f → None:.4f →
   TypeError → réponse "analysis_error" qui MASQUE le vrai motif
   "rejected_text_mismatch". Conséquence : pendant toute la session
   du 7 mai soir, les rejets pré-check ont été silencieusement
   transformés en erreurs analyse → cascade locale Léa V1 → clic au pif.
   Fix : `(result.get('x_pct') or 0):.4f` traite None | None | 0
   uniformément.

2. FLAG ENV pré-check OFF par défaut — le pré-check
   _validate_text_at_position introduit hier soir a 2 défauts
   identifiés par le bench E2E sur 8 click_anchor :
   * radius_px=200 trop petit pour les tabs à 2 tokens (Examens
     cliniques, Synthèse Urgences) — OCR voit un crop tronqué
     "Maquette POC ler en cours Codage Statistiques" qui n'inclut
     pas "Examens" → fuzzy match 1/2 = 50% < seuil 0.60 → REJET.
     À radius 300/400 le mot est inclus → match passe.
   * min_token_ratio=0.60 trop strict pour cibles 2 tokens.

   Solution démo : flag env RPA_ENABLE_TEXT_PRECHECK (défaut "false").
   Le pré-check est désactivé par défaut → retour au comportement
   stable d'avant-hier (hybrid_text_direct ≥ 0.80 utilisé direct,
   exemption drift préservée). Code et fonction _validate_text_at_position
   conservés en place pour reprise post-démo après calibrage radius
   adaptatif (≈ 0.17 × min(screen_w, screen_h)) et token_ratio descendu
   à 0.50.

   Pour ré-activer en dev/test : `RPA_ENABLE_TEXT_PRECHECK=true`
   dans .env.local ou env du service rpa-streaming.

Inclus aussi :
- docs/E2E_TEST_RUN_2026-05-08.md (rapport agent test E2E ~1700 mots)
- tests/e2e/urgence_aiva_demo_expected.yaml (tolérances re-écrites)
- tests/e2e/fixtures/urgence_aiva_demo/live/*.png (8 fixtures
  recapturées headless 1920x1080 pour itérer demain)
- _ocr_inventory.json + _run_resolve_results.json (raw runs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-08 10:09:23 +02:00
parent f8dc3c3af4
commit 56e869c467
13 changed files with 970 additions and 82 deletions

View File

@@ -4508,7 +4508,18 @@ async def resolve_target(request: ResolveTargetRequest):
# présent. Attrape les cas où la cascade rend des coords plausibles
# mais pointant sur un autre élément (ex : clic sur "Dossier en cours"
# du menu au lieu de "Synthèse Urgences" du tab plus bas).
if result and result.get("resolved"):
#
# 8 mai 2026 : désactivé par défaut pour la démo GHT. Calibrage du
# radius_px et min_token_ratio à finaliser post-démo (cf. rapport
# docs/E2E_TEST_RUN_2026-05-08.md). Le pré-check était trop strict
# sur les onglets à 2 tokens (Examens cliniques, Synthèse Urgences)
# → faux rejets → cascade locale Léa V1 → clic au pif. Réactivable
# via env RPA_ENABLE_TEXT_PRECHECK=true. Le code et les tests
# restent en place pour reprise post-démo.
_text_precheck_enabled = os.environ.get(
"RPA_ENABLE_TEXT_PRECHECK", "false"
).lower() in ("true", "1", "yes")
if _text_precheck_enabled and result and result.get("resolved"):
_by_text = (request.target_spec.get("by_text") or "").strip()
if _by_text:
from agent_v0.server_v1.resolve_engine import _validate_text_at_position
@@ -4542,11 +4553,17 @@ async def resolve_target(request: ResolveTargetRequest):
}
# [REPLAY] log structuré de sortie résolution (après validation)
# Note: x_pct/y_pct peuvent être None quand le pré-check OCR rejette
# (rejected_text_mismatch). result.get('x_pct', 0) renvoie alors None
# — la clé existe, le default 0 est ignoré — et None:.4f lève
# TypeError. Fix : `(... or 0)` traite None/None/0 uniformément.
_x = result.get('x_pct') if result else None
_y = result.get('y_pct') if result else None
logger.info(
f"[REPLAY] RESOLVE_EXIT session={request.session_id} "
f"resolved={result.get('resolved', False) if result else False} "
f"method='{result.get('method', '?') if result else 'none'}' "
f"coords=({result.get('x_pct', 0):.4f}, {result.get('y_pct', 0):.4f}) "
f"coords=({(_x or 0):.4f}, {(_y or 0):.4f}) "
f"score={result.get('score', 0) if result else 0} "
f"from_memory={bool(result.get('from_memory', False)) if result else False} "
f"reason='{result.get('reason', '') if result else ''}'"