Détecte les modèles VLM/grounding « aveugles » (capabilities sans vision, ex.
UI-TARS réimporté sans mmproj) pour éviter le HTTP 500 silencieux masqué par
la cascade de grounding.
- core/detection/model_health.py : has_vision_capability() (cache, fail-open)
+ smoke_check_models()
- core/execution/input_handler.py : gate vision dans _grounding_ui_tars
(skip propre vers niveau 3 si modèle aveugle, plus de 500 silencieux)
- tests/unit/test_model_health.py : 6 tests (vision/aveugle/fail-open/cache/smoke)
Incident 2026-06-08 : UI-TARS sans mmproj -> niveau 2 cascade en 500 silencieux,
non détecté (hors chemin runtime démo + échec avalé par fallback + zéro test).
NB : le smoke non bloquant au démarrage (api_stream.py startup) reste dans le WIP
de la branche, mélangé au préflight non committé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sans env RPA_VLM_MODEL/VLM_MODEL, get_vlm_model() tombait sur le default
gemma4:latest, qui peut etre absent du tunnel DGX (depull) -> 404 Ollama et
echec de tout le pipeline VLM avant un test Lea humain.
- core/detection/vlm_config.py : DEFAULT_VLM_MODEL gemma4:latest -> qwen2.5vl:7b-rpa
(confirme present DGX, deja default reasoning + fallback bbox grounding).
+ DGX_SAFE_VLM_MODELS allow-list documentee.
- tests/unit/test_vlm_default_dgx_safe.py : 5 tests (default != gemma4:latest,
default in allow-list, no-env -> DGX-safe, env garde priorite).
Logique de resolution inchangee, pas d'appel reseau a l'import.
gemma4:latest reste accessible via env explicite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adapter de benchmark isole (hors runtime Lea) ciblant un serveur
/v1/chat/completions a support vision (vLLM/SGLang/TGI), pour comparer
plus tard a Ollama via LeaBench. Ne controle jamais le desktop.
- core/evaluation/openai_compat_lea_bench_adapter.py : payload data-URL
image_url, parsing choices[0].message.content. Reutilise par import la
logique prompt/parse/normalisation de ollama_lea_bench_adapter (zero refactor).
- tools/lea_bench_openai_compat.py : wrapper CLI (--base-url defaut :8001).
- tests/unit/test_openai_compat_lea_bench_adapter.py : 6 tests mockes HTTP
(data URL, pas de fuite expectation/click_region, prediction valide,
abstain safe sur HTTP!=200 et reponse malformee, JSONL rechargeable).
Aucun runtime Lea modifie. Aucun service lance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a private in-flight helper for replay dispatch, block machine retargeting while an action is still pending on the previous session, and warn on duplicate in-flight entries for the same replay triplet.
Freeze the Notepad runtime dialog success path and add integration coverage for single in-flight dispatch, watchdog late-report documentation, and the known concurrent-poll race as an xfail.
Préprocesseur Python qui injecte un bloc FAITS_CALCULÉS en tête du DPI
avant l'appel LLM, pour neutraliser l'hallucination de durée (bug "23h"
sur cas MOREL, confusion avec "depuis 23h" de l'Observ. IDE Urg).
Extrait depuis le bandeau Easily Assure et la Synthèse Urgences :
- âge (dateutil.relativedelta)
- date admission / sortie + durée passage (format humain + décimal)
- CCMU / GEMSA libellé complet (parser multi-ligne)
- priorité IAO, mode de venue / médicalisation / mode d'entrée
- diagnostic principal
- decision_terrain + orientation_terrain (metadata only, jamais injectés
dans le prompt pour ne pas biaiser le LLM)
Retour tuple (dpi_enriched, metadata) pour permettre les garde-fous
serveur Python ↔ LLM au commit 2.
Robustesse :
- re.search 1re occurrence + WARNING si bandeau divergent multi-occurrences
- Synthèse Urgences priorité sur bandeau pour dates
- Valeur exigée sur même ligne que label (évite capture de section title)
- Cas négatif (horaires absents) → "NON CALCULABLE" + parsing_warnings
- Jamais de crash, retour tuple toujours valide
Tests : 4/4 verts (golden MOREL string + metadata, négatif sortie absente,
DPI vide). Pas de régression sur tests/integration/test_t2a_extract.py.
Brief complet : docs/handoffs/2026-05-12_brief_S1_build_dpi_enriched.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : 4 occurrences de parsing en cascade dans resolve_engine.py
(L840-885, L903-915, L2569-2580, ~110 lignes au total).
Après : centralisation dans core/grounding/bbox_parser.py avec
paramètre formats= permettant de filtrer les formats reconnus
selon le contrat sémantique de chaque site d'appel.
Préservation des contrats sémantiques (strict no-op) :
- Occ 1+2 (cascade principale) : tous formats (par défaut)
- Occ 3 (retry multi-image) : formats={"xy_json", "raw_array"}
pour respecter le prompt qui impose {"x": NNN, "y": NNN} in pixels
- Occ 4 (_locate_popup_button) : formats={"bbox_2d"} pour respecter
le prompt qui demande "bounding box"
Notes :
- Mini-bug Occ 3 retry multi-image (division systématique sans
heuristique x>1, produisait coordonnées aberrantes ~0.0004 si
VLM retournait déjà du pourcentage) corrigé incidemment via
centralisation. Pas de régression possible (résultat précédent
aberrant par construction).
- Occ 4 : bbox_2d strict 4-coords élargi à bbox_2d 2 ou 4 coords.
Contrat sémantique "bounding box" respecté ; un point 2-coords
interprété comme centre de bbox.
Tests : 26 cas dans test_bbox_parser.py (tous formats × cascade
+ filtre formats= + validated). 121 PASS / 0 FAIL sur le périmètre
refactor (5 fichiers ciblés).
Net : -96 lignes dans resolve_engine.py, +120 lignes module
+ 250 lignes tests.
refs DETTE-006 (étape 2/5 du fix smart_resize)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Module pur core/grounding/smart_resize.py implémentant la formule
smart_resize officielle (transformers.qwen2_vl.image_processing_qwen2_vl,
utilisée par Qwen3VLProcessor pour les images via wrap Qwen2VLImageProcessor).
Helpers exposés : _round_by_factor, _floor_by_factor, _ceil_by_factor.
Constantes : FACTOR_DEFAULT=28, MIN_PIXELS_DEFAULT=3136,
MAX_PIXELS_DEFAULT=1_003_520, MAX_RATIO_DEFAULT=200.
Tests : tests/unit/test_smart_resize.py — 32 cas, 100% coverage sur le
module (mesure via coverage API directe, pytest-cov bloqué par bug cv2
préexistant tracé dans DETTE-011).
refs DETTE-006 (étape 1/5 du fix smart_resize)
refs DETTE-007 (création de la 3ème implémentation, à unifier post-démo)
refs DETTE-010 (vérif preprocessor_config.json checkpoint Qwen3-VL-8B
bloquante avant Étape 2)
refs DETTE-011 (bug cv2 contourné pour mesure coverage)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architecture grounding complète :
- core/grounding/server.py : serveur FastAPI (port 8200) avec UI-TARS-1.5-7B en 4-bit NF4
Process séparé avec son propre contexte CUDA (résout le crash Flask/CUDA)
- core/grounding/pipeline.py : orchestrateur cascade template→OCR→UI-TARS→static
- core/grounding/template_matcher.py : TemplateMatcher centralisé (remplace 5 copies)
- core/grounding/ui_tars_grounder.py : client HTTP vers le serveur de grounding
- core/grounding/target.py : GroundingTarget + GroundingResult
ORA modifié :
- _act_click() : capture unique de l'écran envoyée au serveur de grounding
- Pre-check VLM skippé pour ui_tars (redondant, et Ollama n'a plus de VRAM)
- verify_level='none' par défaut (vérification titre OCR prévue en Phase 2)
- Détection réponses négatives UI-TARS ("I don't see it" → fallback OCR)
Nettoyage :
- 9 fichiers morts archivés dans _archive/ (~6300 lignes supprimées)
- 21 tests ajoutés pour TemplateMatcher
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Point de sauvegarde incluant les fichiers non committés des sessions
précédentes (systemd, docs, agents, GPU manager).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GraphBuilder construit maintenant des ScreenState enrichis
(ui_elements + detected_text) au lieu de stubs vides, et associe
les clics aux UIElement par proximité spatiale.
Détails :
- __init__ accepte ui_detector, screen_analyzer, enable_ui_enrichment,
element_proximity_max_px (+ lazy resolver via singleton C1)
- _create_screen_states délègue à ScreenAnalyzer.analyze() — remplace
l'appel à _extract_text() qui n'existait plus depuis le Lot C
(bug silencieux : OCR cassé en prod depuis ce jour, caught except)
- _find_clicked_element : bbox contenant strict + fallback proximité
≤50px, préfère le plus petit bbox (form vs button)
- _build_click_target_spec : TargetSpec(by_role, by_text,
selection_policy="by_similarity") avec ancres dans context_hints
(anchor_element_id, anchor_bbox, anchor_center)
- _build_edges propage le ScreenState source aux builders d'action
- WorkflowPipeline passe ui_detector + enable_ui_enrichment au builder
Impact : matching prod 3-5x plus précis, TargetSpec ne sont plus
des "unknown_element" génériques, UIConstraint.required_roles se
remplit correctement via _extract_common_ui_elements (qui marchait
depuis toujours mais sur des state.ui_elements vides).
Tests e2e migrés vers enable_ui_enrichment=False (2.9s vs 67s) —
ils valident le pipeline DBSCAN/edges, pas la détection UI réelle.
15 nouveaux tests, 178 tests passants au total (incluant Lots A-E).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nouvelle méthode match_current_state_from_state(screen_state, workflow_id)
qui utilise directement le ScreenState enrichi (window_title, detected_text,
ui_elements) fourni par ExecutionLoop au lieu de reconstruire un stub
ScreenState("Unknown", ui_elements=[], ...).
Préfère HierarchicalMatcher si workflow chargeable, fallback FAISS sinon.
L'ancienne API match_current_state(screenshot_path, workflow_id) est
convertie en wrapper : appelle ScreenAnalyzer.analyze() puis délègue.
Rétrocompat préservée.
ExecutionLoop._execute_step utilise la nouvelle méthode -> plus de double
analyze() dans le chemin d'exécution (économie latence).
Premier vrai matching context-aware. 11 nouveaux tests + 2 tests
integration loop. 172 tests non-régression verts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avant : clé = phash seul
-> deux contextes différents avec même screenshot partageaient
la même entrée cache -> collisions silencieuses.
Après : clé composite {phash}|{md5(ctx)[:16]} avec ctx =
- window_title
- app_name
- enable_ocr
- enable_ui_detection
- workflow_id (isolation inter-workflows)
get_or_compute() kwargs-only. TTL 2s et éviction LRU inchangés.
invalidate_if_changed() continue de comparer uniquement les phash.
ExecutionLoop propage tout le contexte au cache.
8 nouveaux tests prouvant :
- même image + window différent = miss
- même image + app différent = miss
- même image + flags différents = miss
- même image + workflow_id différent = miss
- même image + même contexte = hit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Retrait de l'état global toxique :
- analyze() : kwargs-only enable_ocr, enable_ui_detection, session_id
- Ne mute JAMAIS self pour les flags (variables locales + branches)
- _resolve_ocr_instance() / _resolve_ui_detector_instance() : lecture seule
- _init_lock par instance pour lazy init concurrent safe
- session_id par appel, plus via mutation singleton
Avant : ExecutionLoop mutait analyzer._ocr, _ui_detector,
_ocr_initialized, _ui_detector_initialized pour désactiver OCR/UI.
Deux loops partageant le singleton se polluaient mutuellement.
Après : deux loops partageant l'analyzer sont complètement isolés.
Preuve par TestAnalyzerIsolationBetweenLoops (3 tests).
Singleton get_screen_analyzer() préservé — garde uniquement les
ressources lourdes, plus de contexte d'exécution.
9 nouveaux tests (3 isolation + 6 kwargs-only/lazy-init).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avant : source_similarity=1.0 hardcodé dans _check_preconditions
-> la contrainte EdgeConstraints.min_source_similarity était
silencieusement désactivée. Un edge passait toujours.
Après : propagation ExecutionLoop -> workflow_pipeline -> EdgeScorer
- select_best/rank/score_edge/_check_preconditions acceptent
source_similarity: float (kwargs-only)
- get_next_action() le propage
- execution_loop passe la confidence issue de match_current_state
La contrainte min_source_similarity est opérationnelle pour la
première fois. Preuve concrète par test_min_source_similarity_fail
et test_low_similarity_blocks_edge (edge rejeté si sim < seuil).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>