Compare commits
16 Commits
demo/ght-2
...
demo-stabl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eeaa806bb | ||
|
|
df5ad59330 | ||
|
|
bfbf0f9c3e | ||
|
|
ecc5a233a7 | ||
|
|
293e54b4e6 | ||
|
|
0d7bcd18ac | ||
|
|
4df1ba5779 | ||
|
|
e9702b4df9 | ||
|
|
e0b47e4518 | ||
|
|
5dc20cc85b | ||
|
|
88ed103de5 | ||
|
|
194853cebb | ||
|
|
626823d327 | ||
|
|
2e76b44ff3 | ||
|
|
731b5bcae2 | ||
|
|
8648e375fe |
@@ -4509,15 +4509,25 @@ async def resolve_target(request: ResolveTargetRequest):
|
||||
# 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).
|
||||
#
|
||||
# 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.
|
||||
# Pré-check OCR — RÉACTIVÉ le 8 mai 2026
|
||||
# Calibrage : radius_px=280, min_token_ratio=0.50
|
||||
# Désactivable via RPA_ENABLE_TEXT_PRECHECK=false
|
||||
#
|
||||
# Historique :
|
||||
# - 6-7 mai 2026 : assouplissements progressifs des garde-fous
|
||||
# (SoM, mémoire visuelle, exemptions drift) pendant prépa démo GHT
|
||||
# - 8 mai 2026 (matin) : flag défaut "false" posé sur ce pré-check
|
||||
# pour stabiliser (calibrage trop strict — faux rejets sur
|
||||
# onglets à 2 tokens : "Examens cliniques", "Synthèse Urgences")
|
||||
# - 8 mai 2026 (après-midi) : réactivé après calibrage chirurgical
|
||||
# (radius_px 200→280, min_token_ratio 0.60→0.50)
|
||||
#
|
||||
# Si futurs faux rejets observés :
|
||||
# - vérifier d'abord radius_px (élargir si textes longs coupés)
|
||||
# - puis min_token_ratio (abaisser si OCR fragmente)
|
||||
# - NE PAS désactiver sans entrée DECISIONS.md datée
|
||||
_text_precheck_enabled = os.environ.get(
|
||||
"RPA_ENABLE_TEXT_PRECHECK", "false"
|
||||
"RPA_ENABLE_TEXT_PRECHECK", "true"
|
||||
).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()
|
||||
@@ -4531,6 +4541,17 @@ async def resolve_target(request: ResolveTargetRequest):
|
||||
effective_w,
|
||||
effective_h,
|
||||
)
|
||||
logger.info(
|
||||
"[REPLAY] Pre-check OCR ACTIF : '%s' attendu @ (%.4f, %.4f) "
|
||||
"via %s — observed='%s' is_valid=%s (%.0fms)",
|
||||
_by_text[:40],
|
||||
float(result.get("x_pct", 0) or 0),
|
||||
float(result.get("y_pct", 0) or 0),
|
||||
result.get("method", "?"),
|
||||
_observed[:80],
|
||||
_is_valid,
|
||||
_ocr_ms,
|
||||
)
|
||||
if not _is_valid:
|
||||
logger.warning(
|
||||
"[REPLAY] Pre-check OCR REJET : '%s' attendu @ (%.4f, %.4f) "
|
||||
|
||||
@@ -26,6 +26,8 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.grounding.bbox_parser import parse_bbox_to_norm, parse_bbox_to_norm_validated
|
||||
|
||||
logger = logging.getLogger("api_stream")
|
||||
|
||||
|
||||
@@ -833,51 +835,8 @@ def _resolve_by_grounding(
|
||||
|
||||
elapsed = time.time() - t0
|
||||
|
||||
# Parser la réponse — supporte bbox_2d en pixels, JSON %, arrays bruts
|
||||
x_pct, y_pct = None, None
|
||||
|
||||
# Format 1 : bbox_2d en pixels [x, y] ou [x1, y1, x2, y2]
|
||||
bbox_match = re.search(r'"bbox_2d"\s*:\s*\[([^\]]+)\]', content)
|
||||
if bbox_match:
|
||||
coords = [float(v.strip()) for v in bbox_match.group(1).split(",")]
|
||||
if len(coords) == 2:
|
||||
x_pct = coords[0] / small_w
|
||||
y_pct = coords[1] / small_h
|
||||
elif len(coords) >= 4:
|
||||
x_pct = (coords[0] + coords[2]) / 2 / small_w
|
||||
y_pct = (coords[1] + coords[3]) / 2 / small_h
|
||||
|
||||
# Format 2 : JSON {"x": 0.XX, "y": 0.YY}
|
||||
if x_pct is None:
|
||||
json_match = re.search(r'"x"\s*:\s*([\d.]+).*?"y"\s*:\s*([\d.]+)', content)
|
||||
if json_match:
|
||||
x_val, y_val = float(json_match.group(1)), float(json_match.group(2))
|
||||
# Si > 1, c'est en pixels
|
||||
if x_val > 1:
|
||||
x_pct = x_val / small_w
|
||||
y_pct = y_val / small_h
|
||||
else:
|
||||
x_pct = x_val
|
||||
y_pct = y_val
|
||||
|
||||
# Format 3 : {"x_pct": 0.XX, "y_pct": 0.YY}
|
||||
if x_pct is None:
|
||||
pct_match = re.search(r'"x_pct"\s*:\s*([\d.]+).*?"y_pct"\s*:\s*([\d.]+)', content)
|
||||
if pct_match:
|
||||
x_pct = float(pct_match.group(1))
|
||||
y_pct = float(pct_match.group(2))
|
||||
|
||||
# Format 4 : array brut [x1, y1, x2, y2] ou [x, y]
|
||||
if x_pct is None:
|
||||
arr_match = re.search(r'\[[\s]*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+)\s*,\s*([\d.]+))?\s*\]', content)
|
||||
if arr_match:
|
||||
vals = [float(v) for v in arr_match.groups() if v is not None]
|
||||
if len(vals) >= 4:
|
||||
x_pct = (vals[0] + vals[2]) / 2 / small_w
|
||||
y_pct = (vals[1] + vals[3]) / 2 / small_h
|
||||
elif len(vals) == 2:
|
||||
x_pct = vals[0] / small_w
|
||||
y_pct = vals[1] / small_h
|
||||
# Parser la réponse — délégué à core.grounding.bbox_parser
|
||||
x_pct, y_pct = parse_bbox_to_norm(content, small_w, small_h)
|
||||
|
||||
if x_pct is None or y_pct is None:
|
||||
# Fallback multi-image : screenshot + crop → grounding sans description
|
||||
@@ -900,21 +859,12 @@ def _resolve_by_grounding(
|
||||
content2 = resp2.json().get("message", {}).get("content", "")
|
||||
elapsed = time.time() - t0
|
||||
|
||||
# Parser tous les formats
|
||||
arr2 = re.search(r'\[[\s]*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+)\s*,\s*([\d.]+))?\s*\]', content2)
|
||||
if arr2:
|
||||
vals = [float(v) for v in arr2.groups() if v is not None]
|
||||
if len(vals) >= 4:
|
||||
x_pct = (vals[0] + vals[2]) / 2 / small_w
|
||||
y_pct = (vals[1] + vals[3]) / 2 / small_h
|
||||
elif len(vals) == 2:
|
||||
x_pct = vals[0] / small_w
|
||||
y_pct = vals[1] / small_h
|
||||
if x_pct is None:
|
||||
json2 = re.search(r'"x"\s*:\s*([\d.]+).*?"y"\s*:\s*([\d.]+)', content2)
|
||||
if json2:
|
||||
x_pct = float(json2.group(1)) / small_w
|
||||
y_pct = float(json2.group(2)) / small_h
|
||||
# Parser la réponse — délégué à core.grounding.bbox_parser
|
||||
# Restriction aux 2 formats attendus par le prompt retry multi-image
|
||||
# (cf. prompt_mi qui demande {"x": NNN, "y": NNN} en pixels).
|
||||
x_pct, y_pct = parse_bbox_to_norm(
|
||||
content2, small_w, small_h, formats={"xy_json", "raw_array"}
|
||||
)
|
||||
if x_pct is not None:
|
||||
logger.info("Grounding multi-image OK (%.1fs)", elapsed)
|
||||
except Exception as e:
|
||||
@@ -2243,10 +2193,10 @@ def _validate_text_at_position(
|
||||
expected_text: str,
|
||||
screen_width: int,
|
||||
screen_height: int,
|
||||
radius_px: int = 200,
|
||||
radius_px: int = 280,
|
||||
) -> tuple:
|
||||
"""Pré-check sémantique : OCR sur une zone autour de (x_pct, y_pct) et
|
||||
vérifie que `expected_text` y est présent (substring ou fuzzy 60%).
|
||||
vérifie que `expected_text` y est présent (substring ou fuzzy 50%).
|
||||
|
||||
Retourne (is_valid: bool, observed_text: str, elapsed_ms: float).
|
||||
|
||||
@@ -2282,7 +2232,7 @@ def _validate_text_at_position(
|
||||
results = reader.readtext(np.array(crop))
|
||||
observed = " ".join(r[1] for r in results if r and len(r) >= 2)
|
||||
elapsed_ms = (time.time() - t0) * 1000
|
||||
is_valid = _text_match_fuzzy(expected_text, observed, min_token_ratio=0.60)
|
||||
is_valid = _text_match_fuzzy(expected_text, observed, min_token_ratio=0.50)
|
||||
return is_valid, observed, elapsed_ms
|
||||
except Exception as e:
|
||||
logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e)
|
||||
@@ -2563,21 +2513,16 @@ def _locate_popup_button(
|
||||
|
||||
content = resp.json().get("message", {}).get("content", "")
|
||||
|
||||
# Parser bbox_2d — qwen2.5vl retourne des coordonnées en pixels
|
||||
# relatifs à l'image envoyée, PAS sur une grille 1000x1000.
|
||||
# Format JSON : [{"bbox_2d": [x1, y1, x2, y2], "label": "..."}]
|
||||
bbox_match = re.search(
|
||||
r'"bbox_2d"\s*:\s*\[\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\]',
|
||||
content,
|
||||
# Parser bbox_2d — délégué à core.grounding.bbox_parser
|
||||
# Restriction au format bbox_2d attendu par le prompt
|
||||
# (cf. prompt qui demande "bounding box"). qwen2.5vl retourne
|
||||
# des coordonnées en pixels relatifs à l'image envoyée.
|
||||
cx, cy = parse_bbox_to_norm_validated(
|
||||
content, screen_width, screen_height, formats={"bbox_2d"}
|
||||
)
|
||||
if bbox_match:
|
||||
x1, y1, x2, y2 = [int(bbox_match.group(i)) for i in range(1, 5)]
|
||||
# Normaliser par les dimensions de l'écran (pixels → 0-1)
|
||||
cx = (x1 + x2) / 2 / screen_width
|
||||
cy = (y1 + y2) / 2 / screen_height
|
||||
if 0.0 <= cx <= 1.0 and 0.0 <= cy <= 1.0:
|
||||
logger.info(f"Observer : bouton '{button_text}' localisé à ({cx:.3f}, {cy:.3f})")
|
||||
return {"x_pct": cx, "y_pct": cy}
|
||||
if cx is not None:
|
||||
logger.info(f"Observer : bouton '{button_text}' localisé à ({cx:.3f}, {cy:.3f})")
|
||||
return {"x_pct": cx, "y_pct": cy}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Observer grounding bouton erreur : {e}")
|
||||
|
||||
120
core/grounding/bbox_parser.py
Normal file
120
core/grounding/bbox_parser.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Parser des réponses VLM de grounding (bbox_2d, x/y, x_pct/y_pct, array brut).
|
||||
|
||||
Centralise le parsing des coordonnées retournées par les modèles VLM
|
||||
(Qwen-VL via Ollama, vLLM ou Transformers direct) vers une représentation
|
||||
normalisée (x_pct, y_pct).
|
||||
|
||||
Module pur : regex + arithmétique, sans dépendance lourde.
|
||||
|
||||
Convention des diviseurs (DETTE-006 ouverte) : actuellement les call sites
|
||||
passent les dimensions de l'image envoyée au VLM (PRE-resize). C'est la
|
||||
source du bug d'échelle pixel grounding — sera corrigé au commit 3/5 du
|
||||
fix DETTE-006 en passant les dimensions POST-smart_resize.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
_ALL_FORMATS = frozenset({"bbox_2d", "xy_json", "xy_pct", "raw_array"})
|
||||
|
||||
|
||||
def parse_bbox_to_norm(
|
||||
content: str,
|
||||
divisor_w: int | float,
|
||||
divisor_h: int | float,
|
||||
*,
|
||||
formats: set[str] | None = None,
|
||||
) -> tuple[float | None, float | None]:
|
||||
"""Parse une réponse VLM en (x_pct, y_pct) normalisés.
|
||||
|
||||
Cascade des formats (premier qui matche gagne) :
|
||||
1. ``"bbox_2d"`` : ``{"bbox_2d": [x, y]}`` ou ``[x1, y1, x2, y2]``
|
||||
2. ``"xy_json"`` : ``{"x": ..., "y": ...}`` (heuristique x>1 → pixels)
|
||||
3. ``"xy_pct"`` : ``{"x_pct": ..., "y_pct": ...}``
|
||||
4. ``"raw_array"`` : array brut ``[...]`` 2 ou 4 coords
|
||||
|
||||
Args:
|
||||
content: réponse texte du VLM.
|
||||
divisor_w, divisor_h: dimensions normalisant les pixels en pct.
|
||||
formats: ensemble des formats à essayer. Si ``None`` (défaut),
|
||||
cascade complète des 4. Pour restreindre, passer un sous-ensemble
|
||||
de ``{"bbox_2d", "xy_json", "xy_pct", "raw_array"}``.
|
||||
|
||||
Returns:
|
||||
``(x_pct, y_pct)`` ou ``(None, None)`` si aucun format ne matche.
|
||||
"""
|
||||
enabled = _ALL_FORMATS if formats is None else formats
|
||||
x_pct, y_pct = None, None
|
||||
|
||||
# Format 1 : bbox_2d en pixels [x, y] ou [x1, y1, x2, y2]
|
||||
if "bbox_2d" in enabled:
|
||||
bbox_match = re.search(r'"bbox_2d"\s*:\s*\[([^\]]+)\]', content)
|
||||
if bbox_match:
|
||||
coords = [float(v.strip()) for v in bbox_match.group(1).split(",")]
|
||||
if len(coords) == 2:
|
||||
x_pct = coords[0] / divisor_w
|
||||
y_pct = coords[1] / divisor_h
|
||||
elif len(coords) >= 4:
|
||||
x_pct = (coords[0] + coords[2]) / 2 / divisor_w
|
||||
y_pct = (coords[1] + coords[3]) / 2 / divisor_h
|
||||
|
||||
# Format 2 : JSON {"x": 0.XX, "y": 0.YY}
|
||||
if x_pct is None and "xy_json" in enabled:
|
||||
json_match = re.search(r'"x"\s*:\s*([\d.]+).*?"y"\s*:\s*([\d.]+)', content)
|
||||
if json_match:
|
||||
x_val, y_val = float(json_match.group(1)), float(json_match.group(2))
|
||||
if x_val > 1:
|
||||
x_pct = x_val / divisor_w
|
||||
y_pct = y_val / divisor_h
|
||||
else:
|
||||
x_pct = x_val
|
||||
y_pct = y_val
|
||||
|
||||
# Format 3 : JSON {"x_pct": 0.XX, "y_pct": 0.YY}
|
||||
if x_pct is None and "xy_pct" in enabled:
|
||||
pct_match = re.search(r'"x_pct"\s*:\s*([\d.]+).*?"y_pct"\s*:\s*([\d.]+)', content)
|
||||
if pct_match:
|
||||
x_pct = float(pct_match.group(1))
|
||||
y_pct = float(pct_match.group(2))
|
||||
|
||||
# Format 4 : array brut [x1, y1, x2, y2] ou [x, y]
|
||||
if x_pct is None and "raw_array" in enabled:
|
||||
arr_match = re.search(
|
||||
r'\[[\s]*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+)\s*,\s*([\d.]+))?\s*\]',
|
||||
content,
|
||||
)
|
||||
if arr_match:
|
||||
vals = [float(v) for v in arr_match.groups() if v is not None]
|
||||
if len(vals) >= 4:
|
||||
x_pct = (vals[0] + vals[2]) / 2 / divisor_w
|
||||
y_pct = (vals[1] + vals[3]) / 2 / divisor_h
|
||||
elif len(vals) == 2:
|
||||
x_pct = vals[0] / divisor_w
|
||||
y_pct = vals[1] / divisor_h
|
||||
|
||||
return x_pct, y_pct
|
||||
|
||||
|
||||
def parse_bbox_to_norm_validated(
|
||||
content: str,
|
||||
divisor_w: int | float,
|
||||
divisor_h: int | float,
|
||||
*,
|
||||
formats: set[str] | None = None,
|
||||
) -> tuple[float | None, float | None]:
|
||||
"""Idem :func:`parse_bbox_to_norm` + validation domaine [0, 1].
|
||||
|
||||
Retourne ``(None, None)`` si le résultat parsé est hors ``[0, 1]`` sur
|
||||
l'un des deux axes — comportement de ``_locate_popup_button``
|
||||
(cf. resolve_engine.py:2569-2580).
|
||||
|
||||
Implémentation : appelle :func:`parse_bbox_to_norm` puis valide. Pas
|
||||
de duplication de la logique de parsing.
|
||||
"""
|
||||
x_pct, y_pct = parse_bbox_to_norm(content, divisor_w, divisor_h, formats=formats)
|
||||
if x_pct is None or y_pct is None:
|
||||
return None, None
|
||||
if not (0.0 <= x_pct <= 1.0 and 0.0 <= y_pct <= 1.0):
|
||||
return None, None
|
||||
return x_pct, y_pct
|
||||
77
core/grounding/smart_resize.py
Normal file
77
core/grounding/smart_resize.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Smart resize officiel Qwen3-VL (algorithme commun Qwen2-VL/Qwen3-VL pour images).
|
||||
|
||||
Source de référence : transformers.models.qwen2_vl.image_processing_qwen2_vl.smart_resize
|
||||
(transformers 4.57.3). Qwen3-VL utilise Qwen2VLImageProcessor pour les images via
|
||||
Qwen3VLProcessor.image_processor_class — la formule est donc commune Qwen2-VL/Qwen3-VL
|
||||
sur le pipeline image.
|
||||
|
||||
Conditions garanties par smart_resize :
|
||||
1. height et width retournés divisibles par `factor` (par défaut 28).
|
||||
2. Total pixels dans l'intervalle [min_pixels, max_pixels].
|
||||
3. Aspect ratio conservé au plus près.
|
||||
|
||||
Module image-only. Pour traitement vidéo Qwen3-VL (factor=32, autres bornes),
|
||||
module dédié à créer si besoin futur.
|
||||
"""
|
||||
|
||||
# DETTE-007 — Trois implémentations smart_resize coexistent dans le repo
|
||||
# (core/grounding/server.py:15, core/grounding/infigui_worker.py:99, ce module).
|
||||
# Unification post-démo Kerella.
|
||||
|
||||
import math
|
||||
|
||||
|
||||
FACTOR_DEFAULT = 28
|
||||
MIN_PIXELS_DEFAULT = 56 * 56 # 3136
|
||||
MAX_PIXELS_DEFAULT = 14 * 14 * 4 * 1280 # 1_003_520
|
||||
MAX_RATIO_DEFAULT = 200
|
||||
|
||||
|
||||
def _round_by_factor(number: int, factor: int) -> int:
|
||||
"""Closest integer to `number` divisible by `factor`."""
|
||||
return round(number / factor) * factor
|
||||
|
||||
|
||||
def _floor_by_factor(number: int, factor: int) -> int:
|
||||
"""Largest integer ≤ `number` divisible by `factor`."""
|
||||
return math.floor(number / factor) * factor
|
||||
|
||||
|
||||
def _ceil_by_factor(number: int, factor: int) -> int:
|
||||
"""Smallest integer ≥ `number` divisible by `factor`."""
|
||||
return math.ceil(number / factor) * factor
|
||||
|
||||
|
||||
def smart_resize(
|
||||
height: int,
|
||||
width: int,
|
||||
factor: int = FACTOR_DEFAULT,
|
||||
min_pixels: int = MIN_PIXELS_DEFAULT,
|
||||
max_pixels: int = MAX_PIXELS_DEFAULT,
|
||||
) -> tuple[int, int]:
|
||||
"""Rescale (height, width) to satisfy the three conditions of the module docstring.
|
||||
|
||||
Raises:
|
||||
ValueError: if max(height, width) / min(height, width) > MAX_RATIO_DEFAULT
|
||||
(aspect ratio out of supported domain).
|
||||
|
||||
Returns:
|
||||
(resized_height, resized_width).
|
||||
"""
|
||||
if max(height, width) / min(height, width) > MAX_RATIO_DEFAULT:
|
||||
raise ValueError(
|
||||
f"absolute aspect ratio must be smaller than {MAX_RATIO_DEFAULT}, "
|
||||
f"got {max(height, width) / min(height, width)}"
|
||||
)
|
||||
h_bar = round(height / factor) * factor
|
||||
w_bar = round(width / factor) * factor
|
||||
if h_bar * w_bar > max_pixels:
|
||||
beta = math.sqrt((height * width) / max_pixels)
|
||||
h_bar = max(factor, math.floor(height / beta / factor) * factor)
|
||||
w_bar = max(factor, math.floor(width / beta / factor) * factor)
|
||||
elif h_bar * w_bar < min_pixels:
|
||||
beta = math.sqrt(min_pixels / (height * width))
|
||||
h_bar = math.ceil(height * beta / factor) * factor
|
||||
w_bar = math.ceil(width * beta / factor) * factor
|
||||
return h_bar, w_bar
|
||||
859
docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md
Normal file
859
docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md
Normal file
@@ -0,0 +1,859 @@
|
||||
# AUDIT — Contrôles débranchés (serveur)
|
||||
Date : 2026-05-08
|
||||
Branche : feature/qw-suite-mai
|
||||
HEAD : 56e869c46
|
||||
Périmètre : agent_v0/server_v1/ + core/* importés. Client exclu.
|
||||
|
||||
## 1. Inventaire des fichiers audités
|
||||
|
||||
| Fichier | Lignes |
|
||||
|---|---|
|
||||
| agent_v0/server_v1/__init__.py | 0 |
|
||||
| agent_v0/server_v1/visual_wait.py | 54 |
|
||||
| agent_v0/server_v1/monitor_router.py | 99 |
|
||||
| agent_v0/server_v1/replay_failure_logger.py | 143 |
|
||||
| agent_v0/server_v1/vm_controller.py | 143 |
|
||||
| agent_v0/server_v1/loop_detector.py | 154 |
|
||||
| agent_v0/server_v1/worker_stream.py | 172 |
|
||||
| agent_v0/server_v1/workflow_replay.py | 185 |
|
||||
| agent_v0/server_v1/safety_checks_provider.py | 195 |
|
||||
| agent_v0/server_v1/session_worker.py | 253 |
|
||||
| agent_v0/server_v1/agent_registry.py | 296 |
|
||||
| agent_v0/server_v1/replay_memory.py | 323 |
|
||||
| agent_v0/server_v1/execution_plan_runner.py | 373 |
|
||||
| agent_v0/server_v1/audit_trail.py | 393 |
|
||||
| agent_v0/server_v1/replay_learner.py | 395 |
|
||||
| agent_v0/server_v1/run_worker.py | 397 |
|
||||
| agent_v0/server_v1/live_session_manager.py | 464 |
|
||||
| agent_v0/server_v1/task_planner.py | 596 |
|
||||
| agent_v0/server_v1/chat_interface.py | 622 |
|
||||
| agent_v0/server_v1/replay_verifier.py | 632 |
|
||||
| agent_v0/server_v1/domain_context.py | 1020 |
|
||||
| agent_v0/server_v1/replay_engine.py | 1643 |
|
||||
| agent_v0/server_v1/resolve_engine.py | 2585 |
|
||||
| agent_v0/server_v1/stream_processor.py | 5137 |
|
||||
| agent_v0/server_v1/api_stream.py | 5445 |
|
||||
|
||||
Total serveur : 21 719 lignes.
|
||||
|
||||
Modules core/ effectivement importés par le serveur :
|
||||
- core.detection.omniparser_adapter (resolve_engine.py:272)
|
||||
- core.detection.ollama_client, core.detection.vlm_config (resolve_engine.py:502-503, api_stream.py:790)
|
||||
- core.detection.som_engine (resolve_engine.py:977)
|
||||
- core.embedding.clip_embedder (resolve_engine.py:1658)
|
||||
- core.anonymisation (api_stream.py:47)
|
||||
- core.auth.credential_vault, core.auth.auth_handler (api_stream.py:84-85)
|
||||
- core.llm.ocr_extractor (api_stream.py:824)
|
||||
- core.models.workflow_graph (api_stream.py:845)
|
||||
- core.workflow.shadow_observer, shadow_validator, execution_plan, execution_compiler, ir_builder (api_stream.py:1601-2656)
|
||||
- core.federation.learning_pack, faiss_global (api_stream.py:4647-4690)
|
||||
- core.learning.target_memory_store (replay_memory.py:62)
|
||||
|
||||
|
||||
## 2. Findings par catégorie
|
||||
|
||||
### 2.1 Validations désactivées ou non consommées
|
||||
|
||||
**F2.1.1 — Pré-check OCR sémantique (`_validate_text_at_position`) gardé par flag off-by-default**
|
||||
- `agent_v0/server_v1/api_stream.py:4519-4533`
|
||||
- Citation :
|
||||
```
|
||||
_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
|
||||
_is_valid, _observed, _ocr_ms = _validate_text_at_position(
|
||||
tmp_path,
|
||||
float(result.get("x_pct", 0) or 0),
|
||||
float(result.get("y_pct", 0) or 0),
|
||||
_by_text,
|
||||
effective_w,
|
||||
effective_h,
|
||||
)
|
||||
```
|
||||
- Statut : off-by-default — l'appel à `_validate_text_at_position` ne s'exécute QUE si `RPA_ENABLE_TEXT_PRECHECK=true`. La fonction reste définie en `resolve_engine.py:2239-2289` mais n'est jamais consommée en production tant que la variable env n'est pas positionnée.
|
||||
|
||||
**F2.1.2 — `_validate_text_at_position` retourne `True` en cas d'échec OCR (politique permissive)**
|
||||
- `agent_v0/server_v1/resolve_engine.py:2253-2261, 2280, 2287-2289`
|
||||
- Citation :
|
||||
```
|
||||
Politique en cas d'échec OCR (lib absente, exception) : retourne
|
||||
(True, "", 0.0) pour ne pas bloquer le flux. Mieux vaut un faux positif
|
||||
rare qu'une régression bloquante introduite par la validation elle-même.
|
||||
"""
|
||||
reader = _get_validation_ocr_reader()
|
||||
if reader is None:
|
||||
return True, "", 0.0
|
||||
if not expected_text or not expected_text.strip():
|
||||
return True, "", 0.0
|
||||
[...]
|
||||
if x2 - x1 < 10 or y2 - y1 < 10:
|
||||
return True, "", 0.0
|
||||
[...]
|
||||
except Exception as e:
|
||||
logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e)
|
||||
return True, "", 0.0
|
||||
```
|
||||
- Statut : actif (quand le flag global est on) mais résultat documenté comme intentionnellement permissif sur tout chemin d'erreur. Une erreur OCR = pas de blocage.
|
||||
|
||||
**F2.1.3 — `_pre_check_screen_state` (CLIP) bascule `match=True` sur exception**
|
||||
- `agent_v0/server_v1/replay_engine.py:1374-1379`
|
||||
- Citation :
|
||||
```
|
||||
except Exception as e:
|
||||
# Ne jamais bloquer le replay en cas d'erreur du pre-check
|
||||
logger.error(f"Pre-check échoué (non bloquant): {e}")
|
||||
result["match"] = True # Fallback permissif
|
||||
result["reason"] = f"precheck_error: {e}"
|
||||
```
|
||||
- Statut : actif mais permissif explicitement (commentaire `# Fallback permissif`). Toute exception interne du pre-check CLIP renvoie `match=True` et l'action passe.
|
||||
|
||||
**F2.1.4 — Vérification post-action (`verify_action`/`verify_with_critic`) skippée pour type/key_combo/wait et popup gérée**
|
||||
- `agent_v0/server_v1/api_stream.py:3394-3399`
|
||||
- Citation :
|
||||
```
|
||||
action_type_for_verify = (original_action or {}).get("type", "unknown")
|
||||
skip_verify = action_type_for_verify in ("type", "key_combo", "wait")
|
||||
# Skip aussi la vérification serveur si l'agent a déjà géré la popup
|
||||
skip_verify = skip_verify or agent_handled_popup
|
||||
verification = None
|
||||
if report.success and screenshot_after and not skip_verify:
|
||||
```
|
||||
- Statut : actif. La vérification visuelle post-action ne tourne que pour les click et seulement si `agent_handled_popup` est faux.
|
||||
|
||||
**F2.1.5 — `_validate_match_context` consommé uniquement dans la branche template strict**
|
||||
- `agent_v0/server_v1/resolve_engine.py:201, 1864`
|
||||
- Le seul appel est `resolve_engine.py:1864` à l'intérieur du mode strict, pour le fallback template. Pas appelé dans `_resolve_with_precompiled_order` (V4) ni dans le mode classique.
|
||||
- Statut : actif sur un seul chemin de la cascade.
|
||||
|
||||
|
||||
### 2.2 Garde-fous court-circuités (seuils, flags, conditions)
|
||||
|
||||
**F2.2.1 — Drift > 0.20 ignoré quand `template_matching ≥ 0.95` ou `hybrid_text_direct ≥ 0.80`**
|
||||
- `agent_v0/server_v1/resolve_engine.py:2367-2390`
|
||||
- Citation :
|
||||
```
|
||||
if dx > _RESOLUTION_MAX_DRIFT or dy > _RESOLUTION_MAX_DRIFT:
|
||||
[...]
|
||||
_high_confidence_method = (
|
||||
(method.startswith("template_matching") and score >= 0.95)
|
||||
or (method == "hybrid_text_direct" and score >= 0.80)
|
||||
)
|
||||
if _high_confidence_method:
|
||||
logger.info(
|
||||
"[REPLAY] Drift (%.3f, %.3f) > %.2f IGNORÉ : score=%.3f "
|
||||
"sur %s — résultat visuel fiable, on l'utilise",
|
||||
dx, dy, _RESOLUTION_MAX_DRIFT, score, method,
|
||||
)
|
||||
return result
|
||||
```
|
||||
- Statut : actif. Exemption introduite par 35b27ae49 (template ≥ 0.95) puis élargie par 40440f1ca à hybrid_text_direct ≥ 0.80. La garde de drift est neutralisée pour deux familles de méthodes.
|
||||
|
||||
**F2.2.2 — Drift check inactif si fallback x/y_pct ressemblent à un placeholder 0.5/0.5 ou 0.0/0.0**
|
||||
- `agent_v0/server_v1/resolve_engine.py:2359-2363`
|
||||
- Citation :
|
||||
```
|
||||
_has_recorded_coords = (
|
||||
fallback_x_pct > 0.001
|
||||
and fallback_y_pct > 0.001
|
||||
and not (abs(fallback_x_pct - 0.5) < 0.001 and abs(fallback_y_pct - 0.5) < 0.001)
|
||||
)
|
||||
if _has_recorded_coords:
|
||||
```
|
||||
- Statut : actif. Sans coords enregistrées exploitables, la garde drift est inerte.
|
||||
|
||||
**F2.2.3 — Self-healing Win+D au retry 1 désactivé (revert)**
|
||||
- `agent_v0/server_v1/replay_engine.py` (commit 22c0a2ba6, branche `next_retry == 2` conservée seule)
|
||||
- Citation post-revert :
|
||||
```
|
||||
if next_retry == 2:
|
||||
# Retry 2 : injecter un wait de 2s avant l'action
|
||||
wait_action = {
|
||||
"action_id": f"wait_retry_{uuid.uuid4().hex[:6]}",
|
||||
"type": "wait",
|
||||
"duration_ms": 2000,
|
||||
}
|
||||
actions_to_insert.append(wait_action)
|
||||
```
|
||||
- Statut : retiré. Aucune injection de gesture de récupération avant le retry 1 — boucle directe sur la même action.
|
||||
|
||||
**F2.2.4 — Pre-check skip si heartbeat > 10s ou timeout > 500ms**
|
||||
- `agent_v0/server_v1/api_stream.py:999-1001, 3100-3130`
|
||||
- Citation :
|
||||
```
|
||||
_HEARTBEAT_MAX_AGE_SECONDS = 10.0
|
||||
_PRECHECK_SIMILARITY_THRESHOLD = 0.85
|
||||
[...]
|
||||
if age <= _HEARTBEAT_MAX_AGE_SECONDS:
|
||||
[...]
|
||||
precheck_result = await asyncio.wait_for(
|
||||
loop.run_in_executor(...),
|
||||
timeout=0.5, # Max 500ms pour le pre-check
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(...)
|
||||
precheck_result = None
|
||||
```
|
||||
- Statut : actif. Le pre-check CLIP est skip silencieusement si heartbeat trop ancien ou si l'embed prend > 500ms.
|
||||
|
||||
**F2.2.5 — VLM Quick Find : confidence < 0.3 → ignoré (résultat valide perdu sous le seuil)**
|
||||
- `agent_v0/server_v1/resolve_engine.py:655-662`
|
||||
- Citation :
|
||||
```
|
||||
if x_pct is None or y_pct is None or confidence < 0.3:
|
||||
logger.info(
|
||||
"VLM Quick Find : élément non trouvé ou confiance trop basse "
|
||||
"(%.1fs, confidence=%.2f) pour '%s'",
|
||||
```
|
||||
- Statut : actif. Tout retour VLM avec confidence < 0.3 est dropé.
|
||||
|
||||
**F2.2.6 — Image client tronquée → remplacement silencieux par dernier heartbeat**
|
||||
- `agent_v0/server_v1/api_stream.py:4422`
|
||||
- Citation :
|
||||
```
|
||||
if img.height < 800 or img.width < 1200:
|
||||
logger.warning(
|
||||
"[RESOLVE_TARGET] Image client tronquée %dx%d (declared %dx%d) — "
|
||||
"fallback heartbeat full screen",
|
||||
```
|
||||
- Statut : actif. Toute image reçue < 1200x800 est remplacée par un screenshot heartbeat (mémoire ou disque) avant cascade. Seuil élargi par 7233df2bb (était 400x200 avant).
|
||||
|
||||
**F2.2.7 — CLIP mismatch < 0.75 retourne resolved=False mais ne bloque qu'en mode strict avec embedding fourni**
|
||||
- `agent_v0/server_v1/resolve_engine.py:1655-1691`
|
||||
- Citation :
|
||||
```
|
||||
clip_embedding = target_spec.get("clip_embedding")
|
||||
if clip_embedding:
|
||||
[...]
|
||||
if clip_sim < 0.75:
|
||||
logger.warning(
|
||||
f"CLIP MISMATCH : sim={clip_sim:.3f} < 0.75 — "
|
||||
f"écran actuel trop différent de l'enregistrement"
|
||||
)
|
||||
return {
|
||||
"resolved": False,
|
||||
"method": "clip_mismatch",
|
||||
```
|
||||
- Statut : actif uniquement si `clip_embedding` fourni ET mode strict. Pour les workflows qui n'embarquent pas l'embedding, ce filet est inerte.
|
||||
|
||||
|
||||
### 2.3 Flags d'environnement avec défaut permissif
|
||||
|
||||
**F2.3.1 — `RPA_ENABLE_TEXT_PRECHECK`, défaut `"false"`**
|
||||
- `agent_v0/server_v1/api_stream.py:4519-4521`
|
||||
- Citation :
|
||||
```
|
||||
_text_precheck_enabled = os.environ.get(
|
||||
"RPA_ENABLE_TEXT_PRECHECK", "false"
|
||||
).lower() in ("true", "1", "yes")
|
||||
```
|
||||
- Statut : off par défaut. Sans surcharge en environnement, le pré-check OCR ne s'exécute jamais.
|
||||
|
||||
**F2.3.2 — `RPA_AUTH_DISABLED`, défaut absent (auth obligatoire) mais permet de tout débrayer**
|
||||
- `agent_v0/server_v1/api_stream.py:107-119`
|
||||
- Citation :
|
||||
```
|
||||
_AUTH_DISABLED = os.environ.get("RPA_AUTH_DISABLED", "").lower() in (
|
||||
"1", "true", "yes",
|
||||
)
|
||||
[...]
|
||||
if _AUTH_DISABLED:
|
||||
logger.warning(
|
||||
"[SÉCURITÉ] RPA_AUTH_DISABLED=true — authentification Bearer DÉSACTIVÉE. ..."
|
||||
)
|
||||
API_TOKEN = _API_TOKEN_ENV or secrets.token_hex(32)
|
||||
```
|
||||
- Statut : par défaut auth obligatoire, mais flag explicite documenté pour la débrayer.
|
||||
|
||||
**F2.3.3 — `RPA_LOOP_DETECTOR_ENABLED`, défaut `"1"` (activé)**
|
||||
- `agent_v0/server_v1/loop_detector.py:42-47, 78-79`
|
||||
- Citation :
|
||||
```
|
||||
def _env_bool_enabled(name: str) -> bool:
|
||||
val = os.environ.get(name, "1").strip().lower()
|
||||
return val not in ("0", "false", "no", "off", "")
|
||||
[...]
|
||||
if not _env_bool_enabled("RPA_LOOP_DETECTOR_ENABLED"):
|
||||
return LoopVerdict(detected=False)
|
||||
```
|
||||
- Statut : on par défaut, désactivable via `RPA_LOOP_DETECTOR_ENABLED=0`.
|
||||
|
||||
**F2.3.4 — `RPA_SAFETY_CHECKS_LLM_ENABLED`, défaut `"1"` (activé)**
|
||||
- `agent_v0/server_v1/safety_checks_provider.py:42-44, 70`
|
||||
- Citation :
|
||||
```
|
||||
if safety_level == "medical_critical" and _env_bool_enabled("RPA_SAFETY_CHECKS_LLM_ENABLED"):
|
||||
```
|
||||
- Statut : on par défaut, mais ne tourne que si `safety_level == "medical_critical"` dans l'action.
|
||||
|
||||
**F2.3.5 — `RPA_PII_BLUR_SERVER`, défaut `"true"` (activé)**
|
||||
- `agent_v0/server_v1/api_stream.py:1023`
|
||||
- Citation :
|
||||
```
|
||||
_PII_BLUR_ENABLED = os.environ.get("RPA_PII_BLUR_SERVER", "true").lower() in ("true", "1", "yes")
|
||||
```
|
||||
- Statut : on par défaut.
|
||||
|
||||
|
||||
### 2.4 Étapes de cascade neutralisées
|
||||
|
||||
**F2.4.1 — `_resolve_by_yolo` défini, importé, jamais appelé**
|
||||
- Définition : `agent_v0/server_v1/resolve_engine.py:293`
|
||||
- Import : `agent_v0/server_v1/api_stream.py:4363`
|
||||
- Recherche `_resolve_by_yolo(` dans le serveur entier : 0 site d'appel.
|
||||
- Statut : fonction morte. La détection OmniParser/YOLO n'est plus dans la cascade exécutée.
|
||||
|
||||
**F2.4.2 — `_resolve_with_precompiled_order` (V4) appelé seulement si `target_spec["resolve_order"]` présent**
|
||||
- `agent_v0/server_v1/resolve_engine.py:1613-1635`
|
||||
- Citation :
|
||||
```
|
||||
resolve_order = target_spec.get("resolve_order")
|
||||
if resolve_order and isinstance(resolve_order, list):
|
||||
[...]
|
||||
result = _resolve_with_precompiled_order(...)
|
||||
if result and result.get("resolved"):
|
||||
return result
|
||||
[...]
|
||||
logger.info(
|
||||
"V4 resolve : toutes les méthodes pré-compilées ont échoué, "
|
||||
"fallback cascade legacy"
|
||||
)
|
||||
```
|
||||
- Statut : actif quand un plan V4 est compilé ; sinon inerte. Fallback cascade legacy systématique en cas d'échec.
|
||||
|
||||
**F2.4.3 — Étape grounding VLM directe conditionnée à `by_text_source ∈ {ocr, vlm}` ET `has_window`**
|
||||
- `agent_v0/server_v1/resolve_engine.py:1696-1715`
|
||||
- Citation :
|
||||
```
|
||||
by_text_source = target_spec.get("by_text_source", "")
|
||||
has_window = bool(target_spec.get("window_capture", {}).get("rect"))
|
||||
|
||||
if by_text_strict and by_text_source in ("ocr", "vlm") and has_window:
|
||||
grounding_result = _resolve_by_grounding(...)
|
||||
```
|
||||
- Statut : actif sur ces deux conditions seulement. Si `by_text_source` est vide ou autre, ou si `window_capture.rect` absent, le grounding direct est sauté.
|
||||
|
||||
**F2.4.4 — `_resolve_by_ocr_text` (hybrid_text_direct) reconnecté le 2026-05-06 dans la cascade strict (commit 1cbec2806)**
|
||||
- `agent_v0/server_v1/resolve_engine.py:1750-1790`
|
||||
- Citation du commit :
|
||||
```
|
||||
fix(resolve): rebrancher hybrid_text_direct dans _resolve_target_sync
|
||||
[...] la fonction _resolve_by_ocr_text (resolve_engine.py:1447) existait
|
||||
déjà mais [...] n'était appelée QUE depuis le runtime V4 [...]
|
||||
```
|
||||
- Statut : actif depuis 1cbec2806. Avant : étape OCR direct n'était pas dans la cascade strict pour les workflows non-V4.
|
||||
|
||||
**F2.4.5 — Template matching mode strict : seuil 0.90 (étape 2 fallback)**
|
||||
- `agent_v0/server_v1/resolve_engine.py:1733, 1847-1875`
|
||||
- Citation :
|
||||
```
|
||||
result = _resolve_by_template_matching(
|
||||
[...]
|
||||
confidence_threshold=0.90,
|
||||
)
|
||||
if result:
|
||||
score = result.get("score", 0)
|
||||
# Score >= 0.95 : match quasi-parfait, pas besoin de valider le contexte
|
||||
if score >= 0.95:
|
||||
[...]
|
||||
return result
|
||||
elif _validate_match_context(result, fallback_x_pct, fallback_y_pct, target_spec):
|
||||
[...]
|
||||
```
|
||||
- Statut : actif. `_validate_match_context` skippé si score ≥ 0.95.
|
||||
|
||||
|
||||
### 2.5 Fonctions améliorantes définies mais non appelées
|
||||
|
||||
**F2.5.1 — `_resolve_by_yolo` (résolution OmniParser+template, défini resolve_engine.py:293, jamais appelé)**
|
||||
- Voir F2.4.1.
|
||||
|
||||
**F2.5.2 — `_fuzzy_match` importé dans api_stream.py mais jamais appelé**
|
||||
- Définition : `agent_v0/server_v1/resolve_engine.py:2086`
|
||||
- Import : `agent_v0/server_v1/api_stream.py:4372`
|
||||
- Recherche `_fuzzy_match(` dans api_stream.py : 0 appel hors la ligne d'import.
|
||||
- Statut : import mort. Le fuzzy match utilisé en runtime est `_text_match_fuzzy` (resolve_engine.py:2213), distinct.
|
||||
|
||||
**F2.5.3 — `_get_omniparser`, `_build_target_description` importés dans api_stream.py mais non appelés directement**
|
||||
- `agent_v0/server_v1/api_stream.py:4362, 4365`
|
||||
- Statut : imports utilisés indirectement via `_resolve_target_sync` qui les appelle en interne. Pas un finding bloquant — pas de fonction améliorante hors usage interne.
|
||||
|
||||
|
||||
### 2.6 Marqueurs de dette (TODO/FIXME/disabled/démo) dans le serveur
|
||||
|
||||
**F2.6.1 — TODO `task_planner.py:400`**
|
||||
- Citation : `# Boucle : TODO — lister les éléments puis itérer`
|
||||
- Statut : commentaire de dette dans `task_planner.py`.
|
||||
|
||||
**F2.6.2 — Commentaire « 8 mai 2026 : désactivé par défaut pour la démo GHT »**
|
||||
- `agent_v0/server_v1/api_stream.py:4512`
|
||||
- Citation :
|
||||
```
|
||||
# 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.
|
||||
```
|
||||
- Statut : marqueur démo explicite.
|
||||
|
||||
**F2.6.3 — Mention « Fallback permissif » dans `_pre_check_screen_state`**
|
||||
- `agent_v0/server_v1/replay_engine.py:1377` — `result["match"] = True # Fallback permissif`
|
||||
|
||||
**F2.6.4 — Ré-introduction explicite « non-bloquant » dans `_validate_text_at_position`**
|
||||
- `agent_v0/server_v1/resolve_engine.py:2288` — `logger.warning("[REPLAY] _validate_text_at_position erreur (%s) — pas de blocage", e)`
|
||||
|
||||
**F2.6.5 — Mode autonome → pause_for_human ignorée silencieusement**
|
||||
- `agent_v0/server_v1/api_stream.py:3011-3017`
|
||||
- Citation :
|
||||
```
|
||||
# Mode autonome sans safety_checks → skip (comportement legacy)
|
||||
logger.info(
|
||||
"pause_for_human ignorée (mode autonome) — replay %s continue",
|
||||
owning_replay["replay_id"] if owning_replay else "?"
|
||||
)
|
||||
queue.pop(0)
|
||||
_replay_queues[session_id] = queue
|
||||
continue
|
||||
```
|
||||
- Statut : actif. La supervision n'est utilisée que si `execution_mode != "autonomous"` ou si `safety_level`/`safety_checks` déclarés. Ce câblage `execution_mode → supervised` a été corrigé par 7233df2bb.
|
||||
|
||||
**F2.6.6 — Commentaire `# Fallback permissif`/`pas de blocage` cumulés**
|
||||
- Présents dans 3 fonctions de validation : `_pre_check_screen_state` (replay_engine.py:1377), `_validate_text_at_position` (resolve_engine.py:2288), et politique de `_get_validation_ocr_reader` (resolve_engine.py:2196).
|
||||
|
||||
|
||||
## 3. Commits récents qui ont désactivé des contrôles
|
||||
|
||||
(Sur les 20 derniers commits du dossier `agent_v0/server_v1/`)
|
||||
|
||||
- `56e869c46` (8 mai) — `fix(replay): bug TypeError log + flag pré-check OCR off par défaut (démo GHT)` : ajoute `RPA_ENABLE_TEXT_PRECHECK` (default `"false"`) qui débraye intégralement l'appel à `_validate_text_at_position`.
|
||||
- `40440f1ca` (7 mai) — `fix(replay): cure régression b584bbabc — fallback recorded_coords aveugle` : restaure `resolved=False` sur drift trop grand (annule le fallback aveugle introduit par b584bbabc) ; étend l'exemption drift à `hybrid_text_direct ≥ 0.80` (resolve_engine.py:2380-2390).
|
||||
- `7233df2bb` (7 mai) — `fix(replay): câblage execution_mode supervised + seuil large fallback heartbeat` : élargit le seuil de détection image tronquée à `< 1200×800` (était `< 400×200`) → fallback heartbeat plus fréquent ; force `execution_mode='supervised'` par défaut quand non précisé.
|
||||
- `f62fda575` (7 mai) — `fix(stream): /resolve_target — fallback heartbeat full si image client tronquée` : introduit le remplacement silencieux de l'image client par un heartbeat disque/mémoire si tronquée.
|
||||
- `22c0a2ba6` (6 mai) — `revert: désactiver self-healing Win+D auto (cercle vicieux)` : retire l'injection automatique de Win+D au retry 1 sur `verification_failed`/`no_screen_change`.
|
||||
- `c969f93a2` (6 mai) — `fix(replay): self-healing Win+D auto au retry 1` : commit de la fonctionnalité, reverté par 22c0a2ba6.
|
||||
- `1cbec2806` (6 mai) — `fix(resolve): rebrancher hybrid_text_direct dans _resolve_target_sync` : reconnecte `_resolve_by_ocr_text` dans la cascade strict, qui auparavant n'était appelé que par le chemin V4 pré-compilé (non actif dans la majorité des workflows).
|
||||
- `b584bbabc` (1 mai) — `fix(stream): robustesse proxy VWB→streaming + ciblage textuel pour démo UHCD` : avait remplacé le rejet strict du drift par un `fallback_recorded_coords` (resolved=True). Reverté factuellement par 40440f1ca le 7 mai.
|
||||
- `35b27ae49` (2 mai) — `fix(stream+vwb): chaîne replay robuste — auth, anchor type_text, lock async, drift, prompt LLM` : introduit l'exemption drift pour `template_matching ≥ 0.95` (point d'entrée du finding F2.2.1).
|
||||
|
||||
|
||||
## 4. Pistes prioritaires (P0 « pré-check OCR rejette systématiquement »)
|
||||
|
||||
Findings directement reliés au P0 (motif factuel : « pré-check OCR rejette systématiquement, contrôles débranchés suite à checkout antérieur ») :
|
||||
|
||||
1. **F2.3.1 + F2.6.2** — `agent_v0/server_v1/api_stream.py:4519-4521` : flag `RPA_ENABLE_TEXT_PRECHECK` à défaut `"false"`. C'est le geste d'extinction explicite mentionné dans le commit `56e869c46` ; le pré-check OCR ne peut rejeter quoi que ce soit en l'état tant que la variable d'environnement n'est pas positionnée à `"true"` côté service rpa-streaming. Confirme directement la piste signalée.
|
||||
|
||||
2. **F2.1.1** — `agent_v0/server_v1/resolve_engine.py:2239-2289` (`_validate_text_at_position`) + `agent_v0/server_v1/api_stream.py:4525-4533` (point d'appel) : la fonction est conservée en place mais son point d'appel est conditionné par F2.3.1. Tant que le flag est off, la fonction est définie mais non consommée — état exact « contrôle débranché ».
|
||||
|
||||
3. **F2.6.5** — `agent_v0/server_v1/api_stream.py:3011-3017` : `pause_for_human ignorée (mode autonome) — replay continue`. Si `execution_mode` n'est pas propagé jusqu'au replay (cf. commit 7233df2bb qui corrige ce câblage), la pause supervisée censée intercepter un rejet OCR est skipée et la file d'actions continue. Combiné à F2.3.1, donne le motif observé : « rejets pré-check silencieux → cascade locale Léa → clic au pif » (extrait commit message `56e869c46`).
|
||||
|
||||
|
||||
## 5. Findings côté CLIENT (`agent_v0/agent_v1/core/executor.py`)
|
||||
|
||||
Fichier audité : `agent_v0/agent_v1/core/executor.py` (2893 lignes). Aucune occurrence de `RPA_ENABLE_*`, `RPA_DISABLE_*`, `if False:` ou `if 0:` dans ce fichier.
|
||||
|
||||
### 5.1 Validations désactivées ou non consommées
|
||||
|
||||
Aucun finding (le client ne contient aucun bloc de validation neutralisé : la pré-vérif titre fenêtre, `_check_and_pause_on_system_dialog` et la cascade Observer→Policy sont toutes appelées en flux nominal).
|
||||
|
||||
### 5.2 Garde-fous court-circuités (seuils, flags, conditions)
|
||||
|
||||
**F5.2.1 — `_check_and_pause_on_system_dialog` fail-closed sur exception (durcissement, pas une désactivation)**
|
||||
- `agent_v0/agent_v1/core/executor.py:2001-2043`
|
||||
- Citation :
|
||||
```
|
||||
except Exception as e:
|
||||
# Fix P0-D : fail-closed (principe "faux positif tolérable,
|
||||
# faux négatif catastrophique"). [...]
|
||||
self._system_dialog_pause = {
|
||||
"category": "unknown_check_failed",
|
||||
[...]
|
||||
}
|
||||
[...]
|
||||
return True
|
||||
```
|
||||
- Statut : actif. C'est un fail-closed (toute erreur de détection → pause supervisée), pas un fallback permissif. Listé pour traçabilité.
|
||||
|
||||
**F5.2.2 — Seuil template-matching `_find_text_on_screen` durci à 0.75**
|
||||
- `agent_v0/agent_v1/core/executor.py:2367`
|
||||
- Citation :
|
||||
```
|
||||
threshold = 0.75 # Démo GHT 8 mai — éviter faux positifs (placeholders italiques, tabs voisins). En dessous, mieux vaut tomber en mode apprentissage humain qu'un clic au pif.
|
||||
```
|
||||
- Statut : actif. Seuil élevé en démo GHT (commit `7847a0e82`, 7 mai) — under threshold = pas de match retourné. Pas une désactivation, un durcissement.
|
||||
|
||||
**F5.2.3 — Skip conditional_on_window : action acquittée success=True quand le dialogue n'est pas apparu**
|
||||
- `agent_v0/agent_v1/core/executor.py:567-592`
|
||||
- Citation :
|
||||
```
|
||||
if not match:
|
||||
[...]
|
||||
print(
|
||||
f" [SKIP] Dialogue '{cond_window}' absent → action skippée"
|
||||
)
|
||||
result["success"] = True
|
||||
result["warning"] = "conditional_skipped"
|
||||
return result
|
||||
```
|
||||
- Statut : actif. Comportement attendu (skip explicite avec `warning=conditional_skipped`) mais l'action est rapportée `success=True` au serveur — pas une erreur côté replay engine.
|
||||
|
||||
**F5.2.4 — `wrong_window_skipped` : action skippée silencieusement après timeout apprentissage**
|
||||
- `agent_v0/agent_v1/core/executor.py:754-764`
|
||||
- Citation :
|
||||
```
|
||||
else:
|
||||
# Timeout ou pas d'action → skipper cette action
|
||||
# L'état est peut-être déjà correct (ex: Ctrl+S
|
||||
# a sauvé sans dialogue → action de dialogue inutile)
|
||||
result["success"] = True
|
||||
result["warning"] = "wrong_window_skipped"
|
||||
logger.info(
|
||||
f"[LEA] Wrong window sans correction → skip "
|
||||
f"(l'état est peut-être déjà atteint)"
|
||||
)
|
||||
```
|
||||
- Statut : actif. Si l'humain ne corrige pas dans les 120s (`_capture_human_correction(timeout_s=120)`), l'action est marquée success=True. Mêmes lignes pour `policy_skip` (`executor.py:993-996`).
|
||||
|
||||
**F5.2.5 — Polling timeout REPLAY étendu à 30s pour démo GHT**
|
||||
- `agent_v0/agent_v1/core/executor.py:1786-1794`
|
||||
- Citation :
|
||||
```
|
||||
# 8 mai 2026 — démo GHT : 5s → 30s. Le serveur peut exécuter
|
||||
# extract_text (5-7s) PUIS dispatcher l'action suivante dans
|
||||
# la même réponse HTTP. À 5s, le client coupait avant la
|
||||
# réponse [...]
|
||||
timeout=30,
|
||||
```
|
||||
- Statut : actif. Marqueur démo, modification non commitée à HEAD (status `Not Committed Yet 2026-05-08`). Pas un contrôle débranché, contournement coûts d'IO.
|
||||
|
||||
### 5.3 Flags d'environnement avec défaut permissif
|
||||
|
||||
**F5.3.1 — `RPA_OLLAMA_HOST`, défaut `"localhost"`**
|
||||
- `agent_v0/agent_v1/core/executor.py:2224` (et autres sites)
|
||||
- Citation :
|
||||
```
|
||||
ollama_host = os.environ.get("RPA_OLLAMA_HOST", "localhost")
|
||||
```
|
||||
- Statut : configuration uniquement, pas un garde-fou.
|
||||
|
||||
Aucun autre flag environnemental dans le client : pas de `RPA_ENABLE_*`/`RPA_DISABLE_*` côté agent V1.
|
||||
|
||||
### 5.4 Étapes de cascade neutralisées
|
||||
|
||||
**F5.4.1 — Self-healing désactivé côté client (revert miroir du serveur)**
|
||||
- Le client n'embarque pas de logique self-healing autonome — l'injection Win+D était purement serveur (cf. F2.2.3, commit `22c0a2ba6`). Côté client, la branche d'apprentissage humain (`_capture_human_correction`) reste l'unique recours en cas d'échec retry.
|
||||
- Statut : pas un finding spécifique au client.
|
||||
|
||||
### 5.5 Fonctions améliorantes définies mais non appelées
|
||||
|
||||
**F5.5.1 — `_handle_possible_popup` (legacy clavier Enter/Escape/Tab+Enter) toujours définie**
|
||||
- `agent_v0/agent_v1/core/executor.py:2430-2472`
|
||||
- Citation :
|
||||
```
|
||||
def _handle_possible_popup(self) -> bool:
|
||||
"""Tenter de gerer une popup imprevue.
|
||||
[...]
|
||||
Strategie simple (non bloquante, max ~3s) :
|
||||
1. Essayer Enter (valide le bouton par defaut de la popup)
|
||||
2. Si ca ne marche pas, essayer Escape (ferme la popup)
|
||||
3. Si ca ne marche pas, essayer Tab + Enter [...]
|
||||
```
|
||||
- Recherche `_handle_possible_popup(` dans le client : 1 site (la définition). 0 site d'appel hors la définition.
|
||||
- Statut : fonction morte côté client. Le chemin actif est `_handle_popup_vlm` (executor.py:2102) + Observer/Policy. La version "clavier seul" est conservée mais non câblée.
|
||||
|
||||
### 5.6 Marqueurs de dette (TODO/FIXME/disabled/démo) dans le client
|
||||
|
||||
**F5.6.1 — Marqueur explicite « démo GHT » multiple**
|
||||
- `agent_v0/agent_v1/core/executor.py:1786, 1813, 1835, 2367` : 4 commentaires « 8 mai 2026 — démo GHT » documentant des changements ciblés (timeout polling, plan B pause UX, threshold FIND-TEXT 0.75).
|
||||
|
||||
**F5.6.2 — Plusieurs `except Exception: pass` silencieux**
|
||||
- `agent_v0/agent_v1/core/executor.py:455-456, 722-723, 958-959, 1017-1018, 1127-1128, 1244-1245, 1286-1287, 2619-2620`
|
||||
- Statut : 8 sites, tous sur des chemins best-effort (notification, snapshot UIA, log d'apprentissage). Aucun ne masque une décision de sécurité.
|
||||
|
||||
|
||||
## 6. Autres fichiers .py modifiés < 14 jours (hors serveur/client déjà audités)
|
||||
|
||||
Périmètre : 23 fichiers Python modifiés depuis 2026-04-24 hors `tests/`, `docs/`, `visual_workflow_builder/`, `web_dashboard/`, `agent_chat/`, `_archive/`, `tools/` et le dossier `agent_v0/server_v1/` (déjà audité §1-§4) et `agent_v0/agent_v1/core/executor.py` (déjà audité §5).
|
||||
|
||||
### 6.1 `core/execution/observe_reason_act.py` (2008 lignes)
|
||||
|
||||
**F6.1.1 — Bloc `if False:` désactivant le pre-check VLM par-clic**
|
||||
- `core/execution/observe_reason_act.py:1704-1713`
|
||||
- Citation :
|
||||
```
|
||||
# --- Pas de pre-check VLM (le pipeline FAST→SMART→THINK a déjà validé) ---
|
||||
if False:
|
||||
try:
|
||||
pre_check = self._verify_pre_click(x, y, target_text, target_desc)
|
||||
if not pre_check:
|
||||
print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}")
|
||||
```
|
||||
- Statut : désactivation explicite par `if False:`. Le commentaire justifie : « le pipeline FAST→SMART→THINK a déjà validé ».
|
||||
|
||||
**F6.1.2 — `_verify_pre_click` : `return True` permissif sur erreur HTTP/exception**
|
||||
- `core/execution/observe_reason_act.py:1917, 1921`
|
||||
- Citation :
|
||||
```
|
||||
return True # En cas d'erreur HTTP, on laisse passer
|
||||
[...]
|
||||
return True # En cas d'erreur, on laisse passer
|
||||
```
|
||||
- Statut : fonction conservée mais devenue inerte (cf. F6.1.1). Si réactivée, retourne `True` (passe le clic) sur toute erreur Ollama/réseau.
|
||||
|
||||
**F6.1.3 — `_act_type` : texte vide → `return True`**
|
||||
- `core/execution/observe_reason_act.py:1740-1742`
|
||||
- Citation :
|
||||
```
|
||||
if not decision.value:
|
||||
logger.warning("🎯 [ORA/type] Pas de texte à saisir")
|
||||
return True # Vide = rien à faire, pas un échec
|
||||
```
|
||||
- Statut : actif. Comportement documenté.
|
||||
|
||||
**F6.1.4 — Cascade post-shortcut : timeout retourne `True` après ≥1 dialog géré**
|
||||
- `core/execution/observe_reason_act.py:1547-1550`
|
||||
- Citation :
|
||||
```
|
||||
if _elapsed() >= total_timeout:
|
||||
print(f"⏳ [ORA/post-shortcut] Timeout cascade ({total_timeout:.0f}s, "
|
||||
f"{dialogs_handled} dialog(s) géré(s))")
|
||||
return True # au moins un dialog traité → considéré OK
|
||||
```
|
||||
- Statut : actif. Politique permissive sur timeout cascade dialogues.
|
||||
|
||||
**F6.1.5 — Flag `RPA_USE_FAST_PIPELINE`, défaut `"1"` (activé)**
|
||||
- `core/execution/observe_reason_act.py:1634`
|
||||
- Citation :
|
||||
```
|
||||
_use_fast = os.environ.get('RPA_USE_FAST_PIPELINE', '1') == '1'
|
||||
```
|
||||
- Statut : on par défaut. Désactivable via env.
|
||||
|
||||
### 6.2 `core/grounding/fast_pipeline.py` (216 lignes)
|
||||
|
||||
**F6.2.1 — Expression mort-née `if False else screenshot_pil` dans appel arbiter**
|
||||
- `core/grounding/fast_pipeline.py:163`
|
||||
- Citation :
|
||||
```
|
||||
screenshot_pil=screenshot_pil or snapshot.elements[0] if False else screenshot_pil,
|
||||
```
|
||||
- Statut : à cause du `if False`, l'expression est équivalente à `screenshot_pil=screenshot_pil`. La branche `snapshot.elements[0]` n'est jamais évaluée. Probable reliquat d'expérimentation.
|
||||
|
||||
### 6.3 `core/grounding/title_verifier.py` (174 lignes)
|
||||
|
||||
**F6.3.1 — `has_title_changed` retourne `True` si un seul titre est vide**
|
||||
- `core/grounding/title_verifier.py:73-74`
|
||||
- Citation :
|
||||
```
|
||||
if not title_before or not title_after:
|
||||
return True # Un des deux est vide = changement
|
||||
```
|
||||
- Statut : actif. Politique documentée — `not bloquante` (échec lecture titre = signal de changement).
|
||||
|
||||
### 6.4 `core/grounding/ui_tars_grounder.py` (288 lignes)
|
||||
|
||||
**F6.4.1 — `available` toujours `True` sans vérifier le worker**
|
||||
- `core/grounding/ui_tars_grounder.py:135-137`
|
||||
- Citation :
|
||||
```
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True # Toujours disponible — le script se lance à la demande
|
||||
```
|
||||
- Statut : actif. Pas de probe socket — la disponibilité est assumée et l'erreur réelle remonte au moment de `ground()`.
|
||||
|
||||
### 6.5 Fichiers sans finding
|
||||
|
||||
Audit pattern par pattern (`if False`, `return True/False` suspects, `RPA_ENABLE_/RPA_DISABLE_`, `# disabled`, `# bypass`, `# TODO re-enable`, marqueurs démo, blocs `try` swallow exception sur fonction de validation) :
|
||||
|
||||
| Fichier | Findings |
|
||||
|---|---|
|
||||
| `agent_v0/agent_v1/main.py` | aucun finding |
|
||||
| `agent_v0/agent_v1/network/feedback_bus.py` | aucun finding |
|
||||
| `agent_v0/agent_v1/ui/chat_window.py` | aucun finding (le marqueur `# démo GHT` ligne 846 documente uniquement un comportement UX) |
|
||||
| `agent_v0/agent_v1/ui/notifications.py` | aucun finding (idem ligne 143) |
|
||||
| `agent_v0/agent_v1/ui/paused_toast.py` | aucun finding |
|
||||
| `agent_v0/agent_v1/vision/capturer.py` | aucun finding |
|
||||
| `core/execution/input_handler.py` | aucun finding |
|
||||
| `core/grounding/dialog_handler.py` | aucun finding |
|
||||
| `core/grounding/element_signature.py` | aucun finding |
|
||||
| `core/grounding/fast_detector.py` | aucun finding |
|
||||
| `core/grounding/infigui_worker.py` | aucun finding |
|
||||
| `core/grounding/pipeline.py` | aucun finding |
|
||||
| `core/grounding/server.py` | aucun finding |
|
||||
| `core/grounding/shadow_learning_hook.py` | aucun finding |
|
||||
| `core/grounding/smart_matcher.py` | aucun finding |
|
||||
| `core/grounding/template_matcher.py` | aucun finding |
|
||||
| `core/grounding/think_arbiter.py` | aucun finding (`available = True` ligne 38 même pattern que F6.4.1, mais arbiter délègue au grounder qui détient F6.4.1) |
|
||||
| `core/knowledge/ui_patterns.py` | aucun finding |
|
||||
| `core/llm/ocr_extractor.py` | aucun finding |
|
||||
| `core/llm/t2a_decision.py` | aucun finding |
|
||||
|
||||
|
||||
## 7. Datation git des findings (toutes sections confondues)
|
||||
|
||||
| Finding | Fichier:ligne | Commit | Date | Message commit (raccourci) |
|
||||
|---|---|---|---|---|
|
||||
| F2.1.1 | api_stream.py:4519-4533 | `56e869c46` (gate) + `40440f1ca` (corps) | 2026-05-08 / 2026-05-07 | flag pré-check OCR off / cure régression b584bbabc |
|
||||
| F2.1.2 | resolve_engine.py:2253-2289 | `40440f1ca` | 2026-05-07 | cure régression b584bbabc |
|
||||
| F2.1.3 | replay_engine.py:1374-1379 | `4509038bf` | 2026-04-09 | refactor api_stream.py 6400→3350 |
|
||||
| F2.1.4 | api_stream.py:3394-3399 | `d5deac302` (corps) + `ae65be255` (3398) | 2026-03-26 / 2026-03-18 | feat replay visuel VLM-first |
|
||||
| F2.1.5 | resolve_engine.py:201, 1864 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.2.1 | resolve_engine.py:2367-2390 | `35b27ae49` (intro) → `40440f1ca` (élargi) | 2026-05-02 / 2026-05-07 | chaîne replay robuste / cure régression |
|
||||
| F2.2.2 | resolve_engine.py:2359-2363 | `a21f1ea9f` | 2026-04-11 | garde qualité résolution |
|
||||
| F2.2.3 | replay_engine.py (revert) | `c969f93a2` (intro) → `22c0a2ba6` (revert) | 2026-05-06 | self-healing Win+D / désactivation cercle vicieux |
|
||||
| F2.2.4 | api_stream.py:999-1001 | `d5deac302` | 2026-03-26 | feat replay visuel VLM-first |
|
||||
| F2.2.5 | resolve_engine.py:655-662 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.2.6 | api_stream.py:4422 | `7233df2bb` | 2026-05-07 | câblage execution_mode + seuil heartbeat élargi |
|
||||
| F2.2.7 | resolve_engine.py:1655-1691 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.3.1 | api_stream.py:4519-4521 | `56e869c46` | 2026-05-08 | flag pré-check OCR off par défaut (démo GHT) |
|
||||
| F2.3.2 | api_stream.py:107-119 | `93ef93e56` | 2026-04-14 | API streaming fail-closed |
|
||||
| F2.3.3 | loop_detector.py:42-47 | `2a51a844b` | 2026-05-05 | LoopDetector composite |
|
||||
| F2.3.4 | safety_checks_provider.py:42-44 | `7c6945171` | 2026-05-05 | SafetyChecksProvider hybride |
|
||||
| F2.3.5 | api_stream.py:1023 | `93ef93e56` | 2026-04-14 | API streaming fail-closed |
|
||||
| F2.4.1 | resolve_engine.py:293 (def) | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.4.2 | resolve_engine.py:1613-1635 | `f6ad5ff2b` | 2026-04-10 | runtime V4 honore resolve_order |
|
||||
| F2.4.3 | resolve_engine.py:1696-1715 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.4.4 | resolve_engine.py:1750-1790 | `1cbec2806` | 2026-05-06 | rebrancher hybrid_text_direct |
|
||||
| F2.4.5 | resolve_engine.py:1733, 1847-1875 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.5.1 | resolve_engine.py:293 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.5.2 | api_stream.py:4372 (import mort) | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.5.3 | api_stream.py:4362, 4365 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.6.1 | task_planner.py:400 | `99041f011` | 2026-04-09 | pipeline complet MACRO/MÉSO/MICRO |
|
||||
| F2.6.2 | api_stream.py:4512 | `56e869c46` | 2026-05-08 | flag pré-check OCR off par défaut |
|
||||
| F2.6.3 | replay_engine.py:1377 | `4509038bf` | 2026-04-09 | refactor api_stream.py |
|
||||
| F2.6.4 | resolve_engine.py:2288 | `40440f1ca` | 2026-05-07 | cure régression b584bbabc |
|
||||
| F2.6.5 | api_stream.py:3011-3017 | `964856ab3` (intro) → `35b27ae49` / `65da55731` (raffinements) | 2026-04-29 / 2026-05-02 / 2026-05-05 | extract_text serveur / chaîne replay robuste / SafetyChecksProvider |
|
||||
| F2.6.6 | replay_engine.py:1377 + resolve_engine.py:2196, 2288 | `4509038bf` + `40440f1ca` | 2026-04-09 / 2026-05-07 | refactor / cure régression |
|
||||
| F5.2.1 | executor.py:2001-2043 | (déjà fail-closed antérieur) | — | durcissement, pas désactivation |
|
||||
| F5.2.2 | executor.py:2367 | `7847a0e82` | 2026-05-07 | toast paused supervisée + threshold FIND-TEXT 0.75 |
|
||||
| F5.2.3 | executor.py:567-592 | (antérieur) | — | (chemin conditionnel intégré) |
|
||||
| F5.2.4 | executor.py:754-764 | (antérieur) | — | mode apprentissage humain |
|
||||
| F5.2.5 | executor.py:1786-1794 | non commité (workdir) | 2026-05-08 | démo GHT (uncommitted change) |
|
||||
| F5.5.1 | executor.py:2430-2472 | (antérieur) | — | legacy popup handler |
|
||||
| F5.6.1 | executor.py:1786, 1813, 1835, 2367 | `7847a0e82` + workdir | 2026-05-07 / 2026-05-08 | démo GHT |
|
||||
| F6.1.1 | observe_reason_act.py:1705 | `e2046837c` | 2026-04-25 | Phase 5 — pipeline FAST→SMART→THINK dans ORA |
|
||||
| F6.1.2 | observe_reason_act.py:1917, 1921 | `8903f3543` | 2026-04-22 | feat ORA — vérification pré-action VLM |
|
||||
| F6.1.3 | observe_reason_act.py:1742 | `0c5fffe95` | 2026-04-22 | boucle ORA observe→raisonne→agit |
|
||||
| F6.1.4 | observe_reason_act.py:1550 | `487bcb861` | 2026-04-26 | cascade post-raccourci DialogHandler/OCR |
|
||||
| F6.1.5 | observe_reason_act.py:1634 | `e2046837c` | 2026-04-25 | Phase 5 — FAST→SMART→THINK dans ORA |
|
||||
| F6.2.1 | fast_pipeline.py:163 | `b30d4b665` | 2026-04-25 | Phase 4 — Pipeline orchestré FAST→SMART→THINK |
|
||||
| F6.3.1 | title_verifier.py:73-74 | `343d6fbe9` | 2026-04-26 | EasyOCR remplace docTR (FastDetector + TitleVerifier) |
|
||||
| F6.4.1 | ui_tars_grounder.py:137 | `487bcb861` | 2026-04-26 | cascade post-raccourci DialogHandler/OCR |
|
||||
|
||||
|
||||
## 8. Code original avant désactivation (quand récupérable)
|
||||
|
||||
### F2.1.1 / F2.3.1 / F2.6.2 — Pré-check OCR (`api_stream.py:4519-4533`)
|
||||
|
||||
**Avant** (commit `40440f1ca`, 2026-05-07) — pré-check appelé inconditionnellement après résolution :
|
||||
```python
|
||||
if 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
|
||||
_is_valid, _observed, _ocr_ms = _validate_text_at_position(
|
||||
tmp_path,
|
||||
float(result.get("x_pct", 0) or 0),
|
||||
float(result.get("y_pct", 0) or 0),
|
||||
_by_text,
|
||||
effective_w,
|
||||
effective_h,
|
||||
)
|
||||
```
|
||||
|
||||
**Après** (HEAD `56e869c46`, 2026-05-08) — pré-check gardé par flag off-by-default :
|
||||
```python
|
||||
_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
|
||||
_is_valid, _observed, _ocr_ms = _validate_text_at_position(...)
|
||||
```
|
||||
|
||||
### F2.2.3 — Self-healing Win+D au retry 1 (revert)
|
||||
|
||||
**Avant** (commit `c969f93a2`, 2026-05-06) — code introduit (non récupéré ici via `git show` car branche restaurée par revert immédiat).
|
||||
|
||||
**Après** (HEAD via `22c0a2ba6`, 2026-05-06) — branche `next_retry == 1` retirée, seule `next_retry == 2` (wait 2s) conservée :
|
||||
```python
|
||||
if next_retry == 2:
|
||||
wait_action = {
|
||||
"action_id": f"wait_retry_{uuid.uuid4().hex[:6]}",
|
||||
"type": "wait",
|
||||
"duration_ms": 2000,
|
||||
}
|
||||
actions_to_insert.append(wait_action)
|
||||
```
|
||||
|
||||
### F2.2.6 — Seuil image tronquée
|
||||
|
||||
**Avant** (`f62fda575`, 2026-05-07) — seuil minimal `< 400×200` (placeholders triviaux) :
|
||||
```python
|
||||
if img.height < 200 or img.width < 400:
|
||||
[...]
|
||||
```
|
||||
**Après** (`7233df2bb`, 2026-05-07) — seuil élargi à `< 1200×800` :
|
||||
```python
|
||||
if img.height < 800 or img.width < 1200:
|
||||
[...]
|
||||
```
|
||||
|
||||
### F2.4.4 — Reconnect `hybrid_text_direct` dans cascade strict
|
||||
|
||||
**Avant** (avant `1cbec2806`, ≤ 2026-05-05) — `_resolve_by_ocr_text` n'était appelée QUE depuis le runtime V4 pré-compilé (extrait commit message). Code original non re-extrait ligne par ligne (commit message factuel suffit).
|
||||
|
||||
**Après** (HEAD via `1cbec2806`, 2026-05-06) — appel ajouté dans `_resolve_target_sync` (cascade strict, resolve_engine.py:1750-1790).
|
||||
|
||||
### F6.1.1 — Désactivation pre-check VLM par-clic (`observe_reason_act.py:1704-1713`)
|
||||
|
||||
**Avant** (commit `8903f3543`, 2026-04-22) :
|
||||
```python
|
||||
# --- Vérification pré-action (skip si UI-TARS a déjà validé visuellement) ---
|
||||
if target_text and method_used not in ('template', 'ui_tars') and MSS_AVAILABLE and PIL_AVAILABLE:
|
||||
try:
|
||||
pre_check = self._verify_pre_click(x, y, target_text, target_desc)
|
||||
if not pre_check:
|
||||
print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}")
|
||||
```
|
||||
|
||||
**Après** (HEAD via `e2046837c`, 2026-04-25) :
|
||||
```python
|
||||
# --- Pas de pre-check VLM (le pipeline FAST→SMART→THINK a déjà validé) ---
|
||||
if False:
|
||||
try:
|
||||
pre_check = self._verify_pre_click(x, y, target_text, target_desc)
|
||||
if not pre_check:
|
||||
print(f"⛔ [ORA/pre-check] L'élément à ({x}, {y}) ne correspond PAS à '{target_text}' — abandon du clic")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"⚠️ [ORA/pre-check] Erreur vérification: {e}")
|
||||
```
|
||||
|
||||
### Findings sans version « avant » récupérable (≤ 14 jours)
|
||||
|
||||
Les findings suivants n'ont pas de version « activée » dans la fenêtre 14 jours (la désactivation est antérieure ou native dès l'introduction du code) :
|
||||
- F2.1.3, F2.1.4, F2.1.5 (commits `4509038bf` du 2026-04-09 et `d5deac302` du 2026-03-26)
|
||||
- F2.2.2, F2.2.4, F2.2.5, F2.2.7 (commits 2026-04-09 à 2026-04-11)
|
||||
- F2.3.2, F2.3.5 (commit `93ef93e56` du 2026-04-14)
|
||||
- F2.4.1, F2.4.3, F2.4.5, F2.5.1, F2.5.2, F2.5.3 (commit `4509038bf`)
|
||||
- F2.6.1 (commit `99041f011` du 2026-04-09)
|
||||
- F6.1.2, F6.1.3, F6.1.4 (commits 2026-04-22 à 2026-04-26 — `return True` permissif natif)
|
||||
- F6.2.1, F6.3.1, F6.4.1 (commits 2026-04-25 à 2026-04-26 — états natifs)
|
||||
- F5.2.1, F5.2.3, F5.2.4, F5.5.1 (chemins clients antérieurs, non touchés < 14 jours)
|
||||
|
||||
Pour ces findings, le motif factuel est « introduction native du contrôle déjà à l'état permissif/désactivé », pas une bascule postérieure.
|
||||
40
docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md
Normal file
40
docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Bug — Pré-check OCR spatialement aveugle
|
||||
|
||||
## Date de découverte
|
||||
2026-05-08, lors du test live du commit 2e76b44ff (log instrumentation)
|
||||
|
||||
## Constat
|
||||
Sur les onglets adjacents (Imagerie / Notes médicales à 3 px d'écart dans la maquette urgences), le pré-check OCR valide à tort des clics sur le mauvais onglet.
|
||||
|
||||
## Cause technique
|
||||
La fonction _text_match_fuzzy (resolve_engine.py:2285) vérifie la PRÉSENCE du texte attendu dans le crop OCR (radius_px=280, soit zone 560×560 px) mais pas sa POSITION par rapport au point cliqué.
|
||||
|
||||
Le crop englobe plusieurs onglets adjacents → n'importe lequel des onglets matchant valide le clic.
|
||||
|
||||
## Logs exemples
|
||||
Trois logs représentatifs du test du 2026-05-08 :
|
||||
|
||||
```
|
||||
mai 08 22:16:26.977119 [REPLAY] Pre-check OCR ACTIF : 'Examens cliniques' attendu @ (0.2280, 0.2805) via hybrid_text_direct — observed='Al Voice Generator Bookmarks archivtech com Maquette POC ssier en cours Codage ' is_valid=True (115ms)
|
||||
|
||||
mai 08 22:16:33.048260 [REPLAY] Pre-check OCR ACTIF : 'Imagerie' attendu @ (0.2305, 0.2676) via hybrid_text_direct — observed='9 Al Voice Generator_ Bookmarks archivtech.com Maquette POC sier en cours Statis' is_valid=True (86ms)
|
||||
|
||||
mai 08 22:16:43.045705 [REPLAY] Pre-check OCR ACTIF : 'Notes médicales' attendu @ (0.2020, 0.2800) via template_matching — observed='18 Al Voice Generator_ Bookmarks archivtech. Urgences Maquette POC Dossier en co' is_valid=True (99ms)
|
||||
```
|
||||
|
||||
## Options de fix
|
||||
**Option A — Réduire radius_px**
|
||||
- Pro : simple (1 ligne)
|
||||
- Con : casse la validation des textes longs ("Synthèse Urgences")
|
||||
|
||||
**Option B — Bboxes individuelles EasyOCR + vérification distance**
|
||||
- Pro : robuste, gère textes longs et onglets serrés
|
||||
- Con : changement de logique de _text_match_fuzzy, pas tweak
|
||||
|
||||
## Décision
|
||||
Reporté à post-démo Kerella. À traiter à froid.
|
||||
Préférence : Option B (plus pérenne).
|
||||
|
||||
## Commits liés
|
||||
- 731b5bcae : réactivation pré-check OCR avec calibrage chirurgical
|
||||
- 2e76b44ff : instrumentation log positif qui a révélé le bug
|
||||
277
docs/CARTE_FONCTIONNELLE_2026-05-08.md
Normal file
277
docs/CARTE_FONCTIONNELLE_2026-05-08.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Carte fonctionnelle RPA Vision V3 — 2026-05-08
|
||||
|
||||
Branche : `feature/qw-suite-mai` | HEAD : `731b5bcae`
|
||||
Vue produit (pas code). Inventaire des fonctionnalités telles qu'elles existent réellement dans le repo à cette date.
|
||||
|
||||
---
|
||||
|
||||
## 1. Modes opérationnels de Léa (agent_v1)
|
||||
|
||||
Le client Léa V1 (`agent_v0/agent_v1/`) n'expose **pas** d'enum `MODE_*` discret. Son comportement runtime est piloté par trois booléens cumulables dans `AgentState` (`ui/shared_state.py`) :
|
||||
|
||||
| Mode | Module(s) concerné(s) | Activation | Statut |
|
||||
|---|---|---|---|
|
||||
| Capture / enregistrement | `ui/shared_state.py:30` (`_recording`), `core/captor.py`, `vision/capturer.py` | Bouton "Démarrer" dans systray (`smart_tray.py:377`) ou ChatWindow → `state.start_recording(name)` | actif |
|
||||
| Replay (polling serveur) | `main.py:130` (`_replay_poll_loop`), `core/executor.py:510-1900` | Boucle daemon permanente, lancée à l'init de `AgentV1` indépendamment de toute session — poll `GET /replay/next` toutes les 1 s sur `agent_{user_id}` | actif |
|
||||
| Heartbeat permanent (background) | `main.py:131` (`_background_heartbeat_loop`) | Daemon permanent, screenshot toutes les 5 s vers `POST /traces/stream/image` (session `bg_{machine_id}`) | actif |
|
||||
| Heartbeat session | `main.py:434` (`_heartbeat_loop`) | Démarre seulement quand `session_id` actif (pendant un enregistrement) | actif |
|
||||
| Watchdog fichier `command.json` | `main.py:247` (`_command_watchdog_loop`) | Poll fichier `C:\rpa_vision\command.json` toutes les 1 s, exécute `execute_normalized_order` | actif (legacy GHOST replay) |
|
||||
| Capture à la demande HTTP | `ui/capture_server.py` | Mini-serveur HTTP local port 5006 lancé au boot | actif |
|
||||
| Auto-stop session | `main.py:160` (`_auto_stop_loop`) | Notifie 10 min avant et stoppe à `MAX_SESSION_DURATION_S` | actif |
|
||||
|
||||
**Modes "shadow / copilot / assisté / autonomous"** : ils n'existent **pas** côté client Léa V1. Côté serveur, `execution_mode` est un paramètre de replay (`"autonomous"` par défaut, voir `api_stream.py:2969`, `replay_engine.py:1520`). Les valeurs détectées : `"autonomous"`, `"verified"`, `"supervised"` (déduit du test `_exec_mode != "autonomous"` à `api_stream.py:2974`). Le frontend VWB définit en plus `'basic' | 'intelligent' | 'debug' | 'verified'` (`types.ts:15`) — **ce sont des modes VWB, pas des modes Léa**. [À VÉRIFIER PAR DOM]
|
||||
|
||||
Endpoints `/api/v1/shadow/*` (start/stop/feedback/build/understanding) existent côté serveur (`api_stream.py:1661-1820`) mais aucun n'est appelé depuis le client Léa V1 (grep dans `agent_v0/agent_v1/` : zéro hit). [À VÉRIFIER PAR DOM]
|
||||
|
||||
---
|
||||
|
||||
## 2. Capacités du serveur (rpa-streaming + dépendances)
|
||||
|
||||
54 endpoints exposés par `agent_v0/server_v1/api_stream.py`.
|
||||
|
||||
### 2.1 Streaming session / heartbeat
|
||||
- `POST /api/v1/traces/stream/register` — Enregistrer une session (session_id + machine_id)
|
||||
- `POST /api/v1/traces/stream/event` — Pousser un événement clavier/souris/fenêtre
|
||||
- `POST /api/v1/traces/stream/image` — Pousser un screenshot (heartbeat ou shot d'action)
|
||||
- `POST /api/v1/traces/stream/finalize` — Clore une session
|
||||
- `GET /api/v1/traces/stream/processing/status` — État de la file de traitement
|
||||
- `POST /api/v1/traces/stream/processing/requeue` — Re-traiter une session déjà finalisée
|
||||
- `GET /api/v1/traces/stream/stats` — Statistiques globales du serveur
|
||||
- `GET /api/v1/traces/stream/machines` — Liste machines enrôlées
|
||||
- `GET /api/v1/traces/stream/sessions` — Liste sessions (filtrable par machine_id)
|
||||
|
||||
### 2.2 Replay (next/report/resolve_target/pause)
|
||||
- `POST /api/v1/traces/stream/replay` — Lancer un replay depuis un workflow_id
|
||||
- `POST /api/v1/traces/stream/replay/raw` — Lancer un replay depuis une liste d'actions brutes
|
||||
- `POST /api/v1/traces/stream/replay-session` — Re-rejouer une session enregistrée
|
||||
- `POST /api/v1/traces/stream/replay/single` — Enqueuer une action unique
|
||||
- `POST /api/v1/traces/stream/replay/plan` — Lancer depuis un ExecutionPlan (V4)
|
||||
- `POST /api/v1/traces/stream/workflow/compile` — Compiler session → WorkflowIR + ExecutionPlan
|
||||
- `GET /api/v1/traces/stream/replay/next` — Action suivante à exécuter (pollée par Léa)
|
||||
- `POST /api/v1/traces/stream/replay/result` — Rapport d'exécution d'une action
|
||||
- `POST /api/v1/traces/stream/replay/error_callback` — Callback erreur configurable
|
||||
- `GET /api/v1/traces/stream/replay/{replay_id}` — État d'un replay
|
||||
- `GET /api/v1/traces/stream/replays` — Liste des replays
|
||||
- `POST /api/v1/traces/stream/replay/{replay_id}/resume` — Reprendre après pause supervisée
|
||||
- `POST /api/v1/traces/stream/replay/{replay_id}/cancel` — Annuler un replay
|
||||
- `POST /api/v1/traces/stream/replay/resolve_target` — Résoudre la position d'une ancre (cascade vLLM/Ollama)
|
||||
- `POST /api/v1/traces/stream/replay/pre_analyze` — Pré-analyse de l'écran avant action
|
||||
|
||||
### 2.3 Extraction (text / table / décision T2A)
|
||||
Pas d'endpoint HTTP dédié — ces actions sont enqueuées côté serveur via le replay et traitées sans round-trip Léa par `replay_engine.py:_handle_extract_text_action / _handle_extract_table_action / _handle_t2a_decision_action` (modules `core/llm/ocr_extractor.py` et `core/llm/t2a_decision.py`).
|
||||
|
||||
### 2.4 Federation / learning packs
|
||||
- `GET /api/v1/traces/stream/learning-pack/export` — Export anonymisé (par client_id)
|
||||
- `POST /api/v1/traces/stream/learning-pack/import` — Import + merge dans FAISS global
|
||||
|
||||
### 2.5 Health / monitoring
|
||||
- `GET /health` — Healthcheck simple
|
||||
- `GET /api/v1/traces/stream/workflows` — Liste workflows visibles
|
||||
- `POST /api/v1/traces/stream/reload-workflows` — Rechargement à chaud
|
||||
- `GET /api/v1/traces/stream/workflow/{workflow_id}` — Détail workflow
|
||||
- `GET /api/v1/traces/stream/session/{session_id}` — Détail session
|
||||
- `GET /api/v1/audit/history` — Historique audit (RGPD/IA Act)
|
||||
- `GET /api/v1/audit/summary` — Résumé audit
|
||||
- `GET /api/v1/audit/export` — Export audit
|
||||
|
||||
### 2.6 Autres
|
||||
- `POST /api/v1/shadow/start` — Démarrer un observateur shadow (existe, voir §1)
|
||||
- `POST /api/v1/shadow/stop` — Arrêter
|
||||
- `POST /api/v1/shadow/feedback` — Feedback humain sur une étape observée
|
||||
- `GET /api/v1/shadow/{session_id}/understanding` — Lire la compréhension construite
|
||||
- `POST /api/v1/shadow/build` — Compiler en workflow
|
||||
- `POST /api/v1/task` — Tâche planifiée (TaskPlanner)
|
||||
- `GET /api/v1/task/capabilities` — Capacités déclarées (action types)
|
||||
- `POST /api/v1/chat/session` — Créer une session de chat serveur
|
||||
- `POST /api/v1/chat/{session_id}/message` — Envoyer message
|
||||
- `GET /api/v1/chat/{session_id}/history` — Historique
|
||||
- `POST /api/v1/chat/{session_id}/confirm` — Confirmer un plan
|
||||
- `GET /api/v1/chat/sessions` — Liste sessions chat
|
||||
- `POST /api/v1/agents/enroll` — Enrôler un nouvel agent (nouvelle machine)
|
||||
- `POST /api/v1/agents/uninstall` — Désenrôler
|
||||
- `GET /api/v1/agents/fleet` — État de la flotte
|
||||
|
||||
---
|
||||
|
||||
## 3. Stack VLM / grounding active
|
||||
|
||||
Synthèse de `docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md` (§1, §6).
|
||||
|
||||
| Modèle | Backend | Module appelant | Statut |
|
||||
|---|---|---|---|
|
||||
| `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 | Transformers in-process (Flask) | `core/grounding/server.py` (port 8200) | câblé mais inactif (pas dans la cascade actuelle) |
|
||||
| `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 | Transformers subprocess one-shot | `core/grounding/infigui_worker.py` | câblé mais inactif (utilisé en fallback de la socket) |
|
||||
| `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 | Transformers daemon Unix socket `/run/rpa/grounding.sock` | `core/grounding/infigui_server.py` (service systemd `rpa-grounding.service`) | service présent — `rpa-grounding.service.parked` détecté dans `/etc/systemd/system/` [À VÉRIFIER PAR DOM] |
|
||||
| `Qwen/Qwen2.5-VL-7B-Instruct-AWQ` | vLLM HTTP OpenAI-compat (port `VLLM_PORT=8100`) | `agent_v0/server_v1/resolve_engine.py:785-816` (`_resolve_by_grounding`) | utilisé en prod — essai 1 (fallback Ollama) [À VÉRIFIER PAR DOM si vLLM tourne] |
|
||||
| `qwen2.5vl:7b` | Ollama HTTP `/api/chat` | `resolve_engine.py:818-832` (fallback de vLLM) | utilisé en prod — fallback principal de la cascade |
|
||||
| `qwen2.5vl:7b` | Ollama HTTP `/api/chat` | `resolve_engine.py:2536-2585` (`_locate_popup_button`) | utilisé en prod — cas spécifique popup |
|
||||
| `qwen3-vl:8b`, `gemma4:e4b` | Ollama HTTP | `core/detection/ollama_client.py` (utilisé par `ui_detector.py`, `som_engine.py`, `vram_orchestrator.py`) | utilisé en prod — détection UI + SoM côté core/detection |
|
||||
| `qwen2.5vl:3b` | Ollama HTTP | `visual_workflow_builder/backend/api_v3/capture.py:245` (description anchor) | utilisé en prod — chaîne capture VWB |
|
||||
| `qwen3-vl:8b` | Ollama HTTP | `visual_workflow_builder/backend/api_v3/dag_execute.py:468` (LLMActionHandler) | utilisé en prod — DAG executor LLM |
|
||||
| `qwen2.5vl` | Ollama HTTP | `visual_workflow_builder/backend/catalog_routes_v2_vlm.py` | utilisé en prod — catalog UI |
|
||||
| OpenAI-compat cloud (OpenAI/Gemini/Anthropic) | HTTP cloud (opt-in `VLM_ALLOW_CLOUD=true`) | `visual_workflow_builder/backend/vlm_provider.py` | câblé mais inactif (cloud désactivé par défaut, contraire à la directive 100% local) |
|
||||
| `cckevinn/SeeClick` (Qwen-VL) | Transformers in-process | `core/detection/seeclick_adapter.py` | téléchargé non utilisé (signalé "cassé" par commit `d1b556b6c`, exporté par `__init__.py` mais zéro call site actif) |
|
||||
| `Owlv2` (Google OWL-v2) | Transformers in-process | `core/detection/owl_detector.py` (via `ui_detector.py:31,113,126`) | câblé mais inactif (présent dans la chaîne de détection — bench récent inconnu) [À VÉRIFIER PAR DOM] |
|
||||
| `ByteDance-Seed/UI-TARS-1.5-7B` | Transformers (référencé) | `tools/start_grounding_server.sh` | référencé en doc seulement (modèle remplacé par InfiGUI dans le code par commit `77faa03ec`) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Capacités du VWB (visual_workflow_builder)
|
||||
|
||||
### 4.1 Modes de construction de workflows
|
||||
Trois voies coexistent :
|
||||
1. **Capture interactive** : sélection de zones/ancres via `POST /api/v3/capture/screen` + `POST /api/v3/capture/select` (frontend `CapturePanel.tsx`, `CaptureLibrary.tsx`).
|
||||
2. **Édition manuelle dans le canvas** : ajout d'étapes via `POST /api/v3/workflow/{id}/step` (frontend `StepNode.tsx`, `ToolPalette.tsx`, `PropertiesPanel.tsx`).
|
||||
3. **Import de workflow appris par Léa** : `POST /api/v3/learned-workflows/{id}/import` lit les workflows produits côté streaming server (sessions enregistrées) et les insère en SQLite VWB.
|
||||
|
||||
### 4.2 Types d'actions supportées
|
||||
36 types listés dans `frontend_v4/src/types.ts:40-82` (constante `ACTIONS`).
|
||||
|
||||
**Souris** :
|
||||
- `click_anchor` — Clic gauche sur élément visuel — needs anchor : oui
|
||||
- `double_click_anchor` — Double-clic — needs anchor : oui
|
||||
- `right_click_anchor` — Clic droit (menu contextuel) — needs anchor : oui
|
||||
- `hover_anchor` — Survol — needs anchor : oui
|
||||
- `drag_drop_anchor` — Glisser-déposer vers cible — needs anchor : oui
|
||||
- `scroll_to_anchor` — Défiler jusqu'à élément — needs anchor : oui
|
||||
- `focus_anchor` — Donner focus clavier — needs anchor : oui
|
||||
|
||||
**Clavier** :
|
||||
- `type_text` — Saisir texte (templating `{{var}}`) — needs anchor : non
|
||||
- `type_secret` — Saisir secret depuis coffre-fort — needs anchor : non
|
||||
- `keyboard_shortcut` — Combinaison touches — needs anchor : non
|
||||
|
||||
**Attente** :
|
||||
- `wait_for_anchor` — Attendre apparition élément — needs anchor : oui
|
||||
|
||||
**Données** :
|
||||
- `extract_text` — OCR EasyOCR fr+en sur dernier screenshot → variable — needs anchor : non
|
||||
- `extract_table` — OCR + filtre regex → liste structurée → variable — needs anchor : oui
|
||||
- `screenshot_evidence` — Capture preuve — needs anchor : non
|
||||
- `download_to_folder` — Télécharger fichier — needs anchor : non
|
||||
- `db_save_data` / `db_read_data` — BDD locale — needs anchor : non
|
||||
- `import_excel` / `db_foreach` — Boucle Excel/CSV → BDD — needs anchor : non
|
||||
|
||||
**Logique** :
|
||||
- `visual_condition` — Branchement si ancre trouvée — needs anchor : oui (hidden : true)
|
||||
- `loop_visual` — Boucle tant qu'ancre visible — needs anchor : oui (hidden : true)
|
||||
- `pause_for_human` — Pause supervisée + safety_checks (QW4) — needs anchor : non
|
||||
- `t2a_decision` — Analyse DPI urgences via LLM local (qwen2.5:7b par défaut) — needs anchor : non
|
||||
|
||||
**IA (Ollama vision/text)** :
|
||||
- `ai_ocr` — OCR IA sur ancre — needs anchor : oui
|
||||
- `ai_summarize` — Résumé LLM — needs anchor : non
|
||||
- `ai_extract` — Extraction structurée IA — needs anchor : oui
|
||||
- `ai_classify` — Classification — needs anchor : non
|
||||
- `ai_analyze_text` — Analyse libre — needs anchor : non
|
||||
- `ai_custom` — Appel IA libre avec system prompt — needs anchor : non
|
||||
|
||||
**LLM via DAGExecutor (parallèle)** :
|
||||
- `llm_analyze` / `llm_translate` / `llm_extract_data` / `llm_generate` — needs anchor : non
|
||||
|
||||
**Fichiers** :
|
||||
- `file_list_dir` / `file_create_dir` / `file_move` / `file_copy` / `file_sort_by_ext` — needs anchor : non
|
||||
|
||||
**Validation** :
|
||||
- `verify_element_exists` — needs anchor : oui
|
||||
- `verify_text_content` — needs anchor : oui
|
||||
|
||||
### 4.3 Intégration avec Léa
|
||||
Le VWB **ne pousse pas** un workflow à Léa : il l'**enregistre** côté streaming server. Mécanisme :
|
||||
1. Workflow sauvé en SQLite VWB (`workflows.db`).
|
||||
2. `POST /api/v3/workflow/{id}/export-for-lea` (`learned_workflows.py:413`) sérialise et envoie au streaming server (proxy `STREAMING_SERVER_URL=http://localhost:5005`).
|
||||
3. Lancement : frontend appelle `POST /api/v3/execute/start` (`execute.py:1528`) qui transite vers `POST /api/v1/traces/stream/replay` côté streaming server.
|
||||
4. Léa V1 récupère ensuite les actions une à une via son polling `GET /replay/next` (cf. §1).
|
||||
|
||||
### 4.4 Bibliothèque de captures
|
||||
Disponible. Architecture v2 (avril 2026) :
|
||||
- PNG HD écrit dans `data/library_captures/{id}.png` (source de vérité)
|
||||
- `data/capture_library.json` = métadonnées + thumbnail base64 640×360 q85 (rapide à charger pour la grille)
|
||||
- Endpoints : `GET/POST /api/v3/capture/library`, `POST /api/v3/capture/library/upload`, `GET /api/v3/capture/library/{id}/full`, `DELETE /api/v3/capture/library/{id}`
|
||||
- Permet à l'utilisateur de réutiliser des captures (ancres) entre workflows sans recapturer.
|
||||
|
||||
---
|
||||
|
||||
## 5. Capacités de l'agent_chat
|
||||
|
||||
### 5.1 Endpoints
|
||||
23 routes Flask dans `agent_chat/app.py` :
|
||||
- `GET /` — UI principale chat
|
||||
- `GET /classic` — UI classique
|
||||
- `GET /api/status` — Statut serveur
|
||||
- `GET /api/workflows` — Liste workflows disponibles
|
||||
- `POST /api/workflows/refresh` — Recharger
|
||||
- `GET /api/machines` — Liste machines
|
||||
- `POST /api/search` — Recherche workflow
|
||||
- `POST /api/execute` — Exécuter un workflow nommé
|
||||
- `GET /api/history` — Historique conversations
|
||||
- `POST /api/chat` — Endpoint chat principal (routage NLP)
|
||||
- `POST /api/gpu/<action>` — Contrôle GPU (start/stop/status)
|
||||
- `GET /api/llm/status` — Statut Ollama
|
||||
- `POST /api/llm/model` — Changer modèle actif
|
||||
- `POST /api/agent/plan` — Planifier (autonomous_planner)
|
||||
- `POST /api/agent/execute` — Lancer plan
|
||||
- `GET /api/agent/status` — Statut agent
|
||||
- `GET /api/gestures` — Catalogue de gestures réflexes
|
||||
- `POST /api/chat/upload` — Upload pièce jointe
|
||||
- `GET /api/help` — Aide
|
||||
- `POST /api/urgences/parse` — Parsing intent "traite N dossiers" (gemma3:1b)
|
||||
- `POST /api/urgences/start` — Démarre l'orchestrateur urgences
|
||||
- `GET /api/urgences/status/<orch_id>` — État orchestrateur
|
||||
- `GET /api/urgences/list` — Liste orchestrations en cours
|
||||
|
||||
### 5.2 Cas d'usage métier
|
||||
- **Orchestration urgences GHT** (`urgences_orchestrator.py`) : reçoit "traite N dossiers" en chat, parse via `gemma3:1b`, ouvre Chrome (Win+R) sur la maquette Easily Assure via `/replay/raw`, extrait la liste IPP avec `extract_table`, puis pour chaque IPP lance le workflow `Urgence_unit` via `/replay` avec `variables={"patient_id": ipp}`. Synthèse finale postée dans le chat. État pollable via `/api/urgences/status/<id>`.
|
||||
- **Recherche/exécution workflow par nom naturel** (`/api/search` + `/api/execute`) — résolution sémantique nom utilisateur → workflow_id.
|
||||
- **Plan autonome** (`/api/agent/plan` + `/api/agent/execute`) — `autonomous_planner.py` planifie un workflow inédit à partir d'un objectif libre via `qwen2.5:7b`.
|
||||
|
||||
### 5.3 Modèles LLM utilisés
|
||||
- `gemma3:1b` — NLP intent parsing urgences (`urgences_orchestrator.py:58`, env `LEA_NLP_MODEL`)
|
||||
- `qwen2.5:7b` — chat principal + autonomous_planner (`app.py:229,319`, `intent_parser.py:283,690`)
|
||||
- `qwen3:8b` — modèle Léa par défaut env `LEA_LLM_MODEL` (`app.py:675`), avec `think=False` désactivé (qwen3)
|
||||
|
||||
---
|
||||
|
||||
## 6. Modules orphelins (code présent mais non câblé)
|
||||
|
||||
| Module | Chemin | Pourquoi orphelin (factuel) | Mentionné en commit ou doc ? |
|
||||
|---|---|---|---|
|
||||
| `core/grounding/fast_pipeline.py` | `core/grounding/` | Référencé uniquement par `core/execution/observe_reason_act.py:1639` (lui-même semi-orphelin, voir ci-dessous). Zéro import depuis `agent_v0/server_v1/`, `visual_workflow_builder/backend/`, `agent_chat/`. | commit `b30d4b665` (Phase 4 FAST→SMART→THINK) |
|
||||
| `core/grounding/fast_detector.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `ea36bba5c` (Phase 1-2) |
|
||||
| `core/grounding/smart_matcher.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `ea36bba5c` |
|
||||
| `core/grounding/think_arbiter.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `e4a48e78b` (Phase 3) |
|
||||
| `core/grounding/shadow_learning_hook.py` | `core/grounding/` | Zéro import dans `agent_v0/server_v1/`, `visual_workflow_builder/`, `agent_chat/`. | commit `73cea2385` (Phase 6) ; mémoire mentionne "ShadowLearningHook non branché" |
|
||||
| `core/grounding/template_matcher.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `9da589c8c` (création) |
|
||||
| `core/grounding/pipeline.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `9da589c8c` |
|
||||
| `core/grounding/element_signature.py` | `core/grounding/` | Zéro import depuis le code actif. | commit `e4a48e78b` |
|
||||
| `core/grounding/server.py` (Flask 8200) | `core/grounding/` | Daemon Flask single-thread alternatif à `infigui_server.py` (Unix socket). Pas de service systemd actif pointant dessus. | doc `tools/start_grounding_server.sh` (toujours obsolète, pointe UI-TARS) |
|
||||
| `core/detection/seeclick_adapter.py` | `core/detection/` | Encore exporté par `core/detection/__init__.py` mais zéro call site actif. Signalé "cassé" par commit `d1b556b6c`. | commit `21bfa3b33` (création) ; `d1b556b6c` (suppression call site) |
|
||||
| `core/learning/target_memory_store.py` | `core/learning/` | Référencé par `agent_v0/server_v1/replay_memory.py:62` et `replay_learner.py:210` — **pas orphelin** (semi-actif via replay_memory). | mémoire "V3/V4 découplés au runtime" |
|
||||
| `core/learning/continuous_learner.py` | `core/learning/` | Zéro import depuis code actif. | — |
|
||||
| `core/learning/feedback_processor.py` | `core/learning/` | Zéro import. | — |
|
||||
| `core/learning/versioned_store.py` | `core/learning/` | Zéro import. | — |
|
||||
| `core/execution/target_resolver.py` | `core/execution/` | Zéro import depuis code actif. | mémoire "TargetResolver cross-frame bug" |
|
||||
| `core/execution/target_memory.py` | `core/execution/` | Zéro import. | — |
|
||||
| `core/execution/action_executor.py` | `core/execution/` | Zéro import. | — |
|
||||
| `core/execution/execution_robustness.py` | `core/execution/` | Zéro import. | — |
|
||||
| `core/execution/recovery_strategies.py` | `core/execution/` | Zéro import. | — |
|
||||
| `core/execution/observe_reason_act.py` | `core/execution/` | Importé par `visual_workflow_builder/backend/api_v3/execute.py:1431,1955` (mode `verified` ORALoop). Pas pleinement orphelin mais activé seulement dans ce mode. | mémoire "Phase 5 intégration FAST→SMART→THINK dans ORA" |
|
||||
| `core/healing/healing_engine.py` + `confidence_scorer.py` + `recovery_logger.py` + `learning_repository.py` | `core/healing/` | Zéro import depuis `agent_v0/server_v1/` et `visual_workflow_builder/backend/api_v3/`. Seul `execution_integration.py` est référencé (2 occurrences). | — |
|
||||
| Service systemd `rpa-streaming.service` | `deploy/systemd/` | Présent dans le repo mais `/etc/systemd/system/rpa-streaming.service.parked` → **inactif** sur la machine de Dom. | — |
|
||||
| Service systemd `rpa-grounding.service` | `deploy/systemd/` | `/etc/systemd/system/rpa-grounding.service.parked` → **inactif**. | commit `3d6868f02` |
|
||||
| Service systemd `rpa-vision-v3-api.service` / `rpa-vision-v3-worker.service` / `rpa-vision-v3-dashboard.service` / `rpa-agent-chat.service` | `deploy/systemd/` | Tous trouvés `.parked` dans `/etc/systemd/system/`. | — |
|
||||
| `agent_v0/server_v1/safety_checks_provider.py` | `agent_v0/server_v1/` | **Pas orphelin** : importé par `api_stream.py:2980` (QW4 build_pause_payload). Listé pour contexte. | mémoire "QW4 safety_checks hybrides" |
|
||||
| Endpoints `/api/v1/shadow/*` | `agent_v0/server_v1/api_stream.py:1661-1820` | Définis côté serveur, **aucun appelant identifié** dans `agent_v0/agent_v1/` (Léa client) ni dans `visual_workflow_builder/`, ni dans `agent_chat/`. | — [À VÉRIFIER PAR DOM] |
|
||||
|
||||
---
|
||||
|
||||
## 7. À vérifier avec Dom (synthèse des `[À VÉRIFIER PAR DOM]`)
|
||||
|
||||
- **Modes Léa "shadow / copilot / assisté"** : seuls les booléens `_recording` / `_replay_active` existent côté client. Confirme-tu qu'aujourd'hui Léa V1 n'a effectivement que ces deux modes runtime (et que les modes VWB `basic/intelligent/debug/verified` ne pilotent rien du client) ?
|
||||
- **Endpoints `/api/v1/shadow/*`** côté serveur : zéro appelant identifié dans le repo. Sont-ils consommés par un script externe / une démo, ou candidats à archivage ?
|
||||
- **Service `rpa-grounding.service`** : présent dans `deploy/systemd/` mais en `.parked` sur la machine. La cascade vLLM→Ollama tourne donc sans `infigui_server.py` ? Confirmer que le grounding Transformers est bien désactivé en prod actuellement.
|
||||
- **Service `rpa-streaming.service`** : trouvé `.parked` dans `/etc/systemd/system/`. Le streaming server tourne-t-il via `svc.sh` / `run.sh` au lieu de systemd ?
|
||||
- **`core/detection/seeclick_adapter.py`** : encore exporté par `__init__.py` mais signalé cassé. Sortir de l'export ou tenter une réparation pour Qwen3-VL ?
|
||||
- **`core/detection/owl_detector.py` (Owlv2)** : câblé via `ui_detector.py` mais aucun bench récent. Encore appelé en prod ou candidat à l'archivage ?
|
||||
- **vLLM (port 8100)** : code prêt dans `resolve_engine.py:785-816`. Confirmer si vLLM tourne actuellement ou si la cascade saute systématiquement à Ollama.
|
||||
- **Mode VWB `verified`** : seul mode qui active `core/execution/observe_reason_act.py` (ORALoop). Est-il utilisé en démo GHT ou réservé au debug ?
|
||||
37
docs/DETTE_TECHNIQUE.md
Normal file
37
docs/DETTE_TECHNIQUE.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Registre de dette technique
|
||||
|
||||
Registre central des dettes techniques identifiées sur le projet.
|
||||
Règle : toute désactivation de contrôle ou contournement assumé fait l'objet d'une entrée. Revue par défaut à création + 14 jours.
|
||||
|
||||
## Statuts
|
||||
- OPEN : à traiter
|
||||
- IN_PROGRESS : en cours de résolution
|
||||
- RESOLVED : résolu (date résolution + commit ref)
|
||||
- ACCEPTED : assumé définitivement, pas de résolution prévue
|
||||
|
||||
## Sévérités
|
||||
P0 / P1 / P2 / P3 (alignées sur convention handoffs)
|
||||
|
||||
## Entrées
|
||||
|
||||
| ID | Date création | Date revue | Sévérité | Statut | Description | Origine |
|
||||
|----|---------------|------------|----------|--------|-------------|---------|
|
||||
| DETTE-001 | 2026-05-08 | 2026-05-22 | P1 | OPEN | Pré-check OCR spatialement aveugle | docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md |
|
||||
| DETTE-002 | 2026-05-08 | 2026-05-22 | P2 | OPEN | Exemption drift > 0.20 si template_matching ≥ 0.95 ou hybrid_text_direct ≥ 0.80 (resolve_engine.py:2367-2390) | docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md (F2.2.1) |
|
||||
| DETTE-003 | 2026-05-08 | 2026-05-22 | P2 | OPEN | Self-healing Win+D au retry 1 retiré (revert 22c0a2ba6, replay_engine.py) | docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md (F2.2.3) |
|
||||
| DETTE-004 | 2026-05-08 | 2026-05-22 | P2 | OPEN | Cascade OmniParser/YOLO neutralisée — `_resolve_by_yolo` défini, importé, jamais appelé (resolve_engine.py:293) | docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md (F2.4.1) |
|
||||
| DETTE-005 | 2026-05-08 | 2026-05-22 | P2 | OPEN | Sous-système mémoire visuelle orphelin — `VisualEmbeddingManager` + `ScreenshotValidationManager` (core/visual/*) définis mais jamais instanciés en runtime | docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md |
|
||||
| DETTE-006 | 2026-05-08 | 2026-05-23 | P0 | IN_PROGRESS | Bug échelle pixel grounding Ollama smart_resize non-déterministe | docs/MIGRATION_VLM_PLAN_2026-05-09.md |
|
||||
| DETTE-007 | 2026-05-09 | 2026-05-23 | P3 | OPEN | Trois implémentations smart_resize coexistent (server.py, infigui_worker.py, nouveau module officiel). Unification post-démo Kerella. | commit feat(grounding): module smart_resize officiel |
|
||||
| DETTE-008 | 2026-05-09 | 2026-05-23 | P2 | OPEN | Pre-check VLM par-clic désactivé via `if False:` (observe_reason_act.py:1704-1713) | docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md (F6.1.1) |
|
||||
| DETTE-009 | 2026-05-09 | 2026-05-23 | P3 | OPEN | `ShadowLearningHook` (core/grounding/shadow_learning_hook.py) défini mais jamais instancié — Phase 6 du pipeline FAST→SMART→THINK non câblée à l'observation Shadow | docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md |
|
||||
| DETTE-010 | 2026-05-09 | 2026-05-10 | P1 | IN_PROGRESS | preprocessor_config.json du checkpoint Qwen3-VL-8B-Instruct lu (snapshot 0c351dd01ed87...) : `image_processor_type=Qwen2VLImageProcessorFast` (variant différent de Qwen2VLImageProcessor lue ce matin), `patch_size=16` (vs 14 hypothèse matin → factor probable 32 au lieu de 28), `size={longest_edge: 16_777_216, shortest_edge: 65_536}` (convention différente de min_pixels/max_pixels), `min_pixels`/`max_pixels` absents du config. Investigation requise demain matin : lire transformers.models.qwen2_vl.image_processing_qwen2_vl_fast pour comprendre défauts effectifs et sémantique. Conséquence pour module smart_resize.py : peut nécessiter ajustement (factor, bornes, sémantique). Étape 2 (validation grounding isolée) DOIT être précédée de cette investigation. | docs/MIGRATION_VLM_PLAN_2026-05-09.md + commit 0d7bcd18a (smart_resize) + investigation 2026-05-09 |
|
||||
| DETTE-011 | 2026-05-09 | 2026-05-23 | P2 | OPEN | Bug `cv2.gapi.wip.draw.Text` manquant en Python 3.12 (déclenché par import `agent_v0.server_v1` dans tests/unit/conftest.py:26). Bloque pytest-cov sur tous les tests qui importent la chaîne. Contournement actuel : stub cv2 + coverage API directe. Investigation : version cv2 vs Python 3.12 compat, ou import conditionnel dans conftest. | session 2026-05-09 (découvert pendant TDD smart_resize) |
|
||||
| DETTE-012 | 2026-05-09 | 2026-05-23 | P3 | OPEN | Migration backend grounding vers vLLM (option mentionnée dans plan migration mais infra absente : pas d'install vLLM, pas de service systemd dédié). Choix Transformers direct retenu pour fix DETTE-006. Migration vLLM à instruire séparément si bénéfice mesuré post-démo Kerella. | docs/MIGRATION_VLM_PLAN_2026-05-09.md + investigation infra session 2026-05-09 |
|
||||
| DETTE-013 | 2026-05-09 | 2026-05-23 | P2 | OPEN | Environnement de tests dev local cassé : pytest tests/unit/ déclenche sys.exit(1) via import api_stream sans RPA_API_TOKEN/RPA_AUTH_DISABLED définis (api_stream.py:135, fail-closed sécurité commit 93ef93e56). Combiné avec DETTE-011 (cv2 dans conftest), la batterie de tests unitaires complète n'est pas exécutable en dev local sans configuration environnement spécifique. À documenter (env vars requises) ou refactor (découpler tests purs des tests chargeant api_stream). | session 2026-05-09 (découvert pendant validation refactor bbox_parser) |
|
||||
| DETTE-014 | 2026-05-09 | 2026-05-10 | P1 | OPEN | Module core/grounding/smart_resize.py commité ce matin (commit 0d7bcd18a) calé sur la référence transformers.qwen2_vl.image_processing_qwen2_vl (factor=28, max_pixels=1_003_520). Le checkpoint Qwen3-VL-8B-Instruct utilise en réalité Qwen2VLImageProcessorFast avec patch_size=16 (factor probable 32) et convention size.longest_edge/shortest_edge. À réaligner après investigation DETTE-010 demain. Module pur, testé à 100% sur la convention actuelle — la convention reste valide en référence, mais ne s'applique pas à ce checkpoint. | commit 0d7bcd18a + investigation DETTE-010 du 2026-05-09 |
|
||||
|
||||
## Convention de référencement
|
||||
|
||||
- Dans les messages de commit : `refs DETTE-NNN` en pied
|
||||
- Dans le code : `# DETTE-NNN` en commentaire au-dessus de la ligne concernée (pour les contournements localisables)
|
||||
191
docs/HANDOFF_SESSION_20260506.md
Normal file
191
docs/HANDOFF_SESSION_20260506.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Handoff session 2026-05-06 — démo GHT Sud 95 (J-2)
|
||||
|
||||
**Auteur :** Claude (session précédente, contexte saturé)
|
||||
**Pour :** Claude (nouvelle session)
|
||||
**Démo :** dans ~2 jours, pour DSI Carvella + DIM/TIM/DG GHT Sud 95
|
||||
**Branche git :** `feature/feedback-bus`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Objectif de la nouvelle session
|
||||
|
||||
Finir la prep démo : terminer corrections `data.js` + structurer onglet Imagerie + accompagner Dom sur l'enregistrement workflow Urgence_unit + tests E2E.
|
||||
|
||||
---
|
||||
|
||||
## 📊 État actuel — ce qui est FAIT
|
||||
|
||||
### Code & infrastructure
|
||||
- ✅ **Maquette aiva-vision** déployée : `https://urgence.labs.laurinebazin.design/codage.html?id=XXX` + service systemd `rpa-mockup-easily` (Flask, port 8765)
|
||||
- ✅ **Backend `/api/analyse`** branché sur `core.llm.t2a_decision.analyze_dpi` (qwen2.5:7b par défaut)
|
||||
- ✅ **Maquette codage.html refondue** avec layout aiva-vision 2 colonnes + auto-trigger paste
|
||||
- ✅ **5e onglet "Imagerie"** ajouté côté UI (HTML + JS) — mais **`data.js` pas encore alimenté** (sauf le champ vide qui est traité comme "Aucun examen")
|
||||
- ✅ **Pipeline Léa orchestrateur** dans `agent_chat/urgences_orchestrator.py` (NLP gemma3:1b + thread + boucle + synthèse) — fonctionne en bout-en-bout, validé matin du 2026-05-05
|
||||
- ✅ **`extract_table` backend** dans `core/llm/ocr_extractor.py` + `agent_v0/server_v1/replay_engine.py` (regex IPP `^25\d{6}$`) — testé OK 11/11 sur capture liste patients
|
||||
- ✅ **Préchargement EasyOCR au boot** du streaming server (3.8s, log confirmé) — fini le cold start qui bloquait 2 min
|
||||
- ✅ **Templating `{{patient_id}}`** sur `by_text` dans replay_engine + variables runtime initiales via `ReplayRequest.variables`
|
||||
- ✅ **Agent V1 mis à jour** sur PC Windows (32 fichiers .py, hashes vérifiés, compile clean) — 2026-05-04
|
||||
- ✅ **Catalogue de réflexes** (`gesture_catalog.py`) utilisé par l'orchestrateur (composition réflexes + workflows appris, pas de hardcode)
|
||||
|
||||
### Bench LLM — 18 modèles testés
|
||||
- ✅ Rapport complet : `docs/BENCH_T2A_DECISION_11DOSSIERS.md`
|
||||
- 🥇 **`gemma3:27b-cloud` : 8/11 (73%)** sur vérité-terrain corrigée — recommandé démo
|
||||
- 🥈 `qwen3:8b` : 7/11 (64%) — backup local, 7.6s/dossier, 5 GB → tient large dans 12 GB GPU
|
||||
- ⚠️ Bench fait sur DPI partiellement fictifs (cf. revue Pauline) → ré-évaluer après corrections data.js
|
||||
|
||||
### Documentation produite
|
||||
- `docs/BENCH_T2A_DECISION_11DOSSIERS.md` — bench 18 modèles
|
||||
- `docs/BENCH_MINI_LLM_NLP.md` — bench gemma3:1b vainqueur (NLP commande chat)
|
||||
- `docs/REVUE_DOSSIERS_PAULINE.md` — revue 11 dossiers vs captures (fait par sous-agent, **avec quelques inexactitudes** : "médecins du sport 0559447669" remonté comme inventé, en réalité présent dans la capture)
|
||||
- `docs/POINTS_SUSPECTS_PAULINE.md` — synthèse pour visio Pauline (méthodo : on ne signale PAS les noms anonymisés, on focus hallucinations cliniques + constantes + imagerie)
|
||||
- `docs/MAIL_PAULINE_AVANT_VISIO.md` — mail-template à adapter
|
||||
|
||||
### Mémoires importantes (`~/.claude/.../memory/`)
|
||||
- `feedback_anonymisation_stricte.md` ⭐ — règle absolue : anonymiser = remplacement chirurgical des identités, **JAMAIS réécrire le contenu clinique**. Erreur historique : "anhydrose" vs "ankylose" sur 25003475
|
||||
- `feedback_lea_reflexes_catalog.md` — utiliser `gesture_catalog.py`, ne pas hardcoder Win+R+type+Enter
|
||||
- `feedback_auth_dialogs_runtime.md` — Windows Hello / Basic Auth bloquent le replay, anticiper avant chaque démo client
|
||||
- + toutes les feedbacks existantes dans `MEMORY.md`
|
||||
|
||||
### Corrections data.js DÉJÀ FAITES
|
||||
1. **25003475** (UHCD aura migraineuse) — 3 hallucinations cliniques graves corrigées :
|
||||
- `symptomes_orientation` : "Migraines de membre" → "**Faiblesse** de membre"
|
||||
- `notes_paramedicales[0]` (03:09) : "Pansement compressif possible si perfusé" → "**--> RAD possible. dé perfusé**"
|
||||
- `notes_medicales[1]` (Histoire maladie) : "anhydrose au talon supérieur" → "**ankylose du membre supérieur gauche**"
|
||||
2. **25151530** (Forfait colique néphrétique) :
|
||||
- "TDM sans injection" → "**TDM avec injection**" (2 occurrences)
|
||||
- Histoire de la maladie enrichie avec **ATCD RGO + TTT ESOMEPRAZOLE**
|
||||
3. **`server.py` `VERITES_TERRAIN`** : 25003284 reclassé `FORFAIT_URGENCE` (sortie domicile en 3h37, J12.1 VRS)
|
||||
4. **`app.js` signes_vitaux** : adapté pour itérer sur N colonnes dynamiquement (au lieu de v1/v2 fixe). **MAIS** `data.js` a toujours 2 cols partout → rétrocompatible.
|
||||
|
||||
---
|
||||
|
||||
## ❌ Ce qui RESTE à faire (priorité décroissante)
|
||||
|
||||
### 🔴 PRIORITÉ 1 — corrections data.js (1-2h)
|
||||
|
||||
#### Constantes vitales tronquées (4 dossiers)
|
||||
Captures Pauline source : `/home/dom/Téléchargements/Exemples Dossiers UHCD - Forfaits (1)/Exemples Dossiers UHCD - Forfaits/[UHCD|FORFAITS]/<IPP>/`
|
||||
|
||||
| IPP | Actuel data.js | Capture | Cols à ajouter | Priorité |
|
||||
|---|---|---|---|---|
|
||||
| 25003364 | 2 cols (21:02 + 14:45) | 4 cols | 19:45 (volume miction 700) + 18:44 (T 71, FC 87, PA 168/92, débit O2 2L) | 🟠 |
|
||||
| 25005866 | 2 cols (10:56 + 23:01) | 5 cols | 08:20, 06:25, 02:00 (surveillance neuro post-trauma) | 🔴 grave |
|
||||
| 25048485 | 2 cols (12:09 + 10:52) | 5 cols | 10:58, 10:54, 10:53 + ligne PA Latéralité | 🔴 grave |
|
||||
| 25151530 | 2 cols (06:41 + 03:25) | 7 cols | 08:15, 07:37, 06:00, 04:45, 04:01 (évolution douleur EN: 7→0→5→10→6→4) | 🔴 très grave |
|
||||
|
||||
**Format à adopter** : passer de `{item, v1, v2}` à `{item, v1, v2, v3, v4, ...}`. `app.js` itère déjà dynamiquement sur N cols.
|
||||
|
||||
#### Imagerie à structurer (7 dossiers)
|
||||
Le champ `imagerie: [{date, type, par, role, horodatage, contenu}, ...]` est lu par `renderImagerie()` dans `app.js` (déjà branché).
|
||||
|
||||
| IPP | CR à déplacer | Source actuelle data.js |
|
||||
|---|---|---|
|
||||
| 25003284 | RX thorax (signé Dr LAURENT Charles) | `notes_medicales[3]` |
|
||||
| 25003364 | RX pulmonaire (foyer condensation lobaire D) | `notes_medicales[0]` |
|
||||
| 25003475 | Scanner cérébral sans injection (normal) | `notes_medicales[0]` |
|
||||
| 25005866 | 3 examens : TDMc 01:53 + RX thorax 01:54 + TDMc contrôle 10:18 | `notes_medicales[1, 2]` |
|
||||
| 25012257 | TDM AP sans injection (allergie iode) + ECG | `notes_medicales[0]` |
|
||||
| 25056615 | Scanner AP avec injection (CR complet) | `notes_medicales[0]` |
|
||||
| 25151530 | Scanner AP avec injection | `notes_medicales[1]` |
|
||||
|
||||
**Règle** : extraire le CR du `notes_medicales` (ou autre source), le placer dans `imagerie` **mot pour mot** (procédure stricte). Le retirer de `notes_medicales` si entièrement déplacé OU laisser une mention "voir onglet Imagerie".
|
||||
|
||||
#### Enrichir 25048485 (2 motifs CTCG)
|
||||
Captures montrent 2 motifs distincts le **même jour 28/02/2025** : 1ère CTCG le matin 9h15, 2e CTCG l'après-midi (récidive). data.js modélise UN seul passage 10:40→17:30 → cohérent (le patient a été gardé entre les 2). **Action** : enrichir l'histoire de la maladie pour mentionner explicitement les 2 épisodes (matin + après-midi). **Pas une question Pauline** — juste clarification de présentation.
|
||||
|
||||
### 🟠 PRIORITÉ 2 — re-bench T2A après corrections (30 min)
|
||||
|
||||
Après corrections data.js, relancer `bench_t2a_cloud.py` (top 5 modèles seulement) pour avoir les chiffres réels. Les scripts existent dans `/tmp/bench_t2a*.py`. Mettre à jour `BENCH_T2A_DECISION_11DOSSIERS.md`.
|
||||
|
||||
### 🟠 PRIORITÉ 3 — workflow Urgence_unit (Dom, sur PC Windows)
|
||||
|
||||
Dom enregistre le workflow VWB qui traite 1 dossier de bout-en-bout :
|
||||
1. Click sur lien IPP `{{patient_id}}` (variabilisé)
|
||||
2. Navigation dans les onglets dossier (Motif, Examens, Imagerie, Notes médicales, Synthèse)
|
||||
3. extract_text par onglet → DPI consolidé
|
||||
4. Click "Coder >" → arrive sur aiva-vision
|
||||
5. type_text DPI dans `#dpi-input` (auto-trigger analyse)
|
||||
6. Wait + extract_text décision aiva-vision
|
||||
7. Click "Liste patients" pour revenir
|
||||
|
||||
**Tu peux le faire MAINTENANT** (data.js stable, aiva-vision opérationnelle, agent V1 à jour). Mais **attendre que les corrections data.js soient finies** est plus sûr (sinon les anchors visuels peuvent se déplacer si la table signes vitaux gagne des colonnes).
|
||||
|
||||
### 🟡 PRIORITÉ 4 — visio Pauline (pour les questions ouvertes)
|
||||
|
||||
Pauline doit répondre à 4-5 questions critiques :
|
||||
1. **25048485** : confirmer 2 épisodes le même jour ✓ (pas urgent, je gère seul)
|
||||
2. **25005866** : "médecins du sport 0559447669" — Pauline avait dit inventé, le sous-agent a vu présent → contradiction à trancher
|
||||
3. **25003284 étiquette workflow** : "UHCD asthme" → "Pneumopathie VRS" ?
|
||||
4. **Onglet Imagerie** : niveau de détail attendu
|
||||
|
||||
Mail-template prêt : `docs/MAIL_PAULINE_AVANT_VISIO.md`
|
||||
|
||||
### 🟡 PRIORITÉ 5 — Tests E2E (J-1, dernier jour)
|
||||
|
||||
10 répétitions du scénario démo complet (chat Léa "traite-moi 3 dossiers" → orchestration → boucle → synthèse).
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Contraintes critiques
|
||||
|
||||
1. **Procédure d'anonymisation stricte** (cf. `feedback_anonymisation_stricte.md`) : pour toute correction data.js, **NE JAMAIS reformuler/synthétiser** le contenu clinique. Remplacer chirurgicalement les identités/dates uniquement. Erreur historique grave : "anhydrose"/"ankylose".
|
||||
|
||||
2. **Catalogue de réflexes** (cf. `feedback_lea_reflexes_catalog.md`) : utiliser `gesture_catalog.py` pour les raccourcis natifs (Win+R, etc.), ne pas hardcoder.
|
||||
|
||||
3. **Pas de cloud LLM dans le projet** sauf pour la démo (Ollama Cloud via clés Dom). Prod 100% local.
|
||||
|
||||
4. **Captures Pauline = source de vérité** : `/home/dom/Téléchargements/Exemples Dossiers UHCD - Forfaits (1)/...` (8 dossiers) + `/tmp/captures_pauline_3manquants/` (3 dossiers extraits du docx). En cas de conflit avec rapport revue, **la capture prime**.
|
||||
|
||||
5. **Noms substitués = anonymisation volontaire** : ne pas signaler comme erreur, ne pas chercher à les réaligner avec captures.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Fichiers clés à connaître
|
||||
|
||||
- `/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/data.js` — base 11 dossiers (~2100 lignes)
|
||||
- `/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/server.py` — backend Flask + `/api/analyse` + `VERITES_TERRAIN`
|
||||
- `/home/dom/ai/rpa_vision_v3/docs/clients/ght_sud_95/mockup_easily_assure/app.js` — rendering frontend (signes_vitaux dynamique appliqué)
|
||||
- `/home/dom/ai/rpa_vision_v3/agent_v0/server_v1/api_stream.py` — streaming server (boot avec préchargement EasyOCR)
|
||||
- `/home/dom/ai/rpa_vision_v3/agent_chat/urgences_orchestrator.py` — orchestrateur démo
|
||||
- `/home/dom/ai/rpa_vision_v3/core/llm/t2a_decision.py` — décision T2A LLM
|
||||
- `/home/dom/ai/rpa_vision_v3/core/llm/ocr_extractor.py` — `extract_text_from_image` + `extract_table_from_image`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Commandes utiles
|
||||
|
||||
```bash
|
||||
# Vérifier syntaxe data.js
|
||||
node -e "var fs=require('fs'); var c=fs.readFileSync('docs/clients/ght_sud_95/mockup_easily_assure/data.js','utf-8'); c=c.replace(/^const /gm, 'var '); var s={}; require('vm').runInContext(c, require('vm').createContext(s)); console.log('OK', Object.keys(s.DOSSIERS).length);"
|
||||
|
||||
# Restart streaming server
|
||||
cd /home/dom/ai/rpa_vision_v3 && ./svc.sh restart streaming
|
||||
|
||||
# Restart maquette
|
||||
sudo systemctl restart rpa-mockup-easily
|
||||
|
||||
# Re-bench T2A (après corrections data.js)
|
||||
node /tmp/extract_dpi.js > /tmp/dpis.json
|
||||
/home/dom/ai/rpa_vision_v3/.venv/bin/python /tmp/bench_t2a_cloud.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⏱ Timeline démo
|
||||
|
||||
- **Aujourd'hui** : finir corrections data.js + visio Pauline
|
||||
- **Demain (J-2 / 2026-05-07)** : enregistrement workflow Urgence_unit sur PC Windows
|
||||
- **J-1 (2026-05-08)** : tests E2E répétés
|
||||
- **Jour J** : démo GHT Sud 95
|
||||
|
||||
---
|
||||
|
||||
## 📌 Ce qui marche aujourd'hui (pas casser)
|
||||
|
||||
- Maquette aiva-vision visible et fonctionnelle
|
||||
- Pipeline orchestrateur opérationnel (testé matin 05/05)
|
||||
- Préchargement EasyOCR opérationnel (3.8s au boot)
|
||||
- Bench complet avec rapport
|
||||
- Mail-template Pauline prêt
|
||||
|
||||
**Ne pas relancer les services systemd sans raison**, ne pas modifier `server.py` sauf pour les corrections explicites.
|
||||
175
docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md
Normal file
175
docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Historique des implémentations VLM — Audit 2026-05-08
|
||||
|
||||
Branche : `feature/qw-suite-mai`
|
||||
HEAD : `731b5bcae`
|
||||
Périmètre : tout backend VLM (Ollama, vLLM, Transformers, services dédiés) — code actif, archivé, ou disparu de l'historique.
|
||||
|
||||
---
|
||||
|
||||
## 1. Implémentations VLM actuellement actives
|
||||
|
||||
### 1.1. Transformers in-process (Qwen2.5-VL family)
|
||||
|
||||
| Fichier | Fonction(s) | Modèle / Backend | Commentaire |
|
||||
|---|---|---|---|
|
||||
| `core/grounding/server.py` | `load_model`, `ground` (Flask `/ground`) | `InfiX-ai/InfiGUI-G1-3B` 4-bit NF4 via `Qwen2_5_VLForConditionalGeneration` + `qwen_vl_utils.process_vision_info` | Serveur Flask single-thread port 8200, contient `_smart_resize` (factor 28, MIN_PIXELS=100·28², MAX_PIXELS=5600·28²). |
|
||||
| `core/grounding/infigui_worker.py` | `load_model`, `infer`, `main` (one-shot stdin/stdout) | Idem (`InfiX-ai/InfiGUI-G1-3B` 4-bit NF4, transformers + qwen_vl_utils) | Mode subprocess one-shot : lit JSON sur stdin, écrit sur stdout. Pas de `_smart_resize` complet (formule courte L99-L101 sans clamp min/max). |
|
||||
| `core/grounding/infigui_server.py` | `InfiGUIServer.start`, `_do_ground`, `_do_ping` | Réutilise `infigui_worker.load_model` / `infer` | Daemon Unix socket (`/run/rpa/grounding.sock`), protocole length-prefixed JSON. Service systemd `rpa-grounding.service`. |
|
||||
| `core/grounding/ui_tars_grounder.py` | `UITarsGrounder.ground`, `_send_socket_request`, fallback subprocess | Client : socket → fallback subprocess (`python -m core.grounding.infigui_worker`) | Ne charge plus rien in-process. Coordonne socket+subprocess. Fichier mis à jour 2026-05-05. |
|
||||
| `core/grounding/think_arbiter.py` | `ThinkArbiter.arbitrate` | Délègue à `UITarsGrounder` | Layer THINK du pipeline FAST→SMART→THINK. |
|
||||
| `core/detection/owl_detector.py` | `OwlDetector` | `Owlv2Processor` + `Owlv2ForObjectDetection` (Google OWL-v2) via transformers | Câblé dans `core/detection/ui_detector.py` (L31, L113, L126). Pas un VLM grounding GUI mais détecteur open-vocabulary. |
|
||||
| `core/detection/seeclick_adapter.py` | `SeeClickAdapter._load_model`, `ground` | `cckevinn/SeeClick` (Qwen-VL) via `AutoModelForCausalLM` + `AutoTokenizer` | Encore exporté par `core/detection/__init__.py` mais signalé "cassé" par le commit `d1b556b6c` (avril 2026) qui l'a retiré de `intelligent_executor.py`. Pas d'autre call site actif. |
|
||||
|
||||
### 1.2. HTTP OpenAI-compatible (vLLM)
|
||||
|
||||
| Fichier | Fonction | Détails |
|
||||
|---|---|---|
|
||||
| `agent_v0/server_v1/resolve_engine.py` (L785-L816) | `_resolve_by_grounding` | Essai 1 vLLM `http://localhost:${VLLM_PORT}/v1/chat/completions`, modèle `Qwen/Qwen2.5-VL-7B-Instruct-AWQ` (env `VLLM_PORT=8100`, `VLLM_MODEL`). Format : POST OpenAI chat.completions avec `image_url: data:image/jpeg;base64`. Fallback Ollama si échec. |
|
||||
|
||||
Verbatim L789-L816 :
|
||||
> ```
|
||||
> # Port vLLM configurable via env
|
||||
> _vllm_port = os.environ.get("VLLM_PORT", "8100")
|
||||
> _vllm_model = os.environ.get("VLLM_MODEL", "Qwen/Qwen2.5-VL-7B-Instruct-AWQ")
|
||||
>
|
||||
> # Essai 1 : vLLM (API OpenAI-compatible, GPU)
|
||||
> try:
|
||||
> vllm_resp = _requests.post(
|
||||
> f"http://localhost:{_vllm_port}/v1/chat/completions",
|
||||
> json={
|
||||
> "model": _vllm_model,
|
||||
> "messages": [
|
||||
> {"role": "system", "content": "You locate UI elements on screenshots. Return coordinates."},
|
||||
> {"role": "user", "content": [
|
||||
> {"type": "text", "text": prompt},
|
||||
> {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{shot_b64}"}},
|
||||
> ]},
|
||||
> ],
|
||||
> "temperature": 0.1,
|
||||
> "max_tokens": 80,
|
||||
> },
|
||||
> timeout=30,
|
||||
> )
|
||||
> if vllm_resp.ok:
|
||||
> content = vllm_resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
> if content:
|
||||
> logger.debug("Grounding via vLLM OK")
|
||||
> except Exception as e:
|
||||
> logger.debug("vLLM non disponible (%s), fallback Ollama", e)
|
||||
> ```
|
||||
|
||||
### 1.3. HTTP Ollama (état dominant en prod aujourd'hui)
|
||||
|
||||
| Fichier | Fonction | Modèle |
|
||||
|---|---|---|
|
||||
| `agent_v0/server_v1/resolve_engine.py` (L818-L832) | `_resolve_by_grounding` (fallback de vLLM) | `qwen2.5vl:7b` via `/api/chat` Ollama. |
|
||||
| `agent_v0/server_v1/resolve_engine.py` (L2536-L2585) | `_locate_popup_button` | `qwen2.5vl:7b` via `/api/chat`. |
|
||||
| `core/detection/ollama_client.py` | `OllamaClient` | `qwen3-vl:8b`, `gemma4:e4b`, etc. — utilisé par `core/detection/ui_detector.py`, `core/detection/som_engine.py`, `core/cognition/vram_orchestrator.py`. |
|
||||
| `core/detection/vlm_config.py` | `FALLBACK_VLM_MODELS` | `["qwen3-vl:8b", "0000/ui-tars-1.5-7b-q8_0:7b"]` |
|
||||
| `visual_workflow_builder/backend/vlm_provider.py` | `VLMProvider.detect_ui_element` | Hub Ollama prioritaire + cloud opt-in (OpenAI/Gemini/Anthropic) si `VLM_ALLOW_CLOUD=true`. |
|
||||
| `visual_workflow_builder/backend/api_v3/capture.py` (L245) | description anchor | `qwen2.5vl:3b` |
|
||||
| `visual_workflow_builder/backend/api_v3/dag_execute.py` (L468) | LLMActionHandler | `qwen3-vl:8b` |
|
||||
| `visual_workflow_builder/backend/catalog_routes_v2_vlm.py` | détection visuelle catalog | `qwen2.5vl` |
|
||||
| `core/llm/ocr_extractor.py`, `core/llm/t2a_decision.py` | LLM text only | Ollama (modèles non-vision). |
|
||||
|
||||
---
|
||||
|
||||
## 2. Implémentations VLM archivées dans le filesystem
|
||||
|
||||
| Chemin | Taille | Mtime | Backend identifié |
|
||||
|---|---|---|---|
|
||||
| `_archive/dead_code_20260424/...` (9 fichiers, ~6300 lignes) | divers | 2026-04-24 | **Aucun fichier VLM** — il s'agit de modules workflow/visual non liés à un backend VLM (ex. `visual_persistence_manager.py`, `workflow_simulation_report.py`). Recherche `vllm|transformers|smart_resize|InfiGUI|UI-TARS|qwen2.5vl|qwen2-vl` : 0 hit. |
|
||||
| `archive/business_docs/`, `archive/historical_recall/` | 3 fichiers .md | 2026-01 / 2026-05-04 | Pas de code (Markdown business / mémoire). |
|
||||
|
||||
Aucun fichier `*_old.py`, `*_v1.py`, `*_backup.py`, `*.py.bak` ou `tests_disabled/` détecté. Les seuls `*_v1.py` existants sont `agent_v0/run_agent_v1.py` (non VLM).
|
||||
|
||||
---
|
||||
|
||||
## 3. Commits historiques mentionnant VLM/vLLM/Transformers/grounding
|
||||
|
||||
Liste chronologique inverse (≤ 25 commits pertinents). SHA court · date · message · fichiers VLM touchés (résumé `--stat`).
|
||||
|
||||
| SHA | Date | Message | Fichiers VLM clé |
|
||||
|---|---|---|---|
|
||||
| `487bcb861` | 2026-04-26 | feat(execution): cascade post-raccourci pilotée par DialogHandler/OCR | `core/grounding/{dialog_handler,infigui_worker,think_arbiter,ui_tars_grounder}.py` |
|
||||
| `3d6868f02` | 2026-04-26 | docs: cartographie + worker InfiGUI fichiers | `core/grounding/{server,ui_tars_grounder,infigui_worker,dialog_handler}.py` (création worker, refonte server.py de 494→124 lignes) |
|
||||
| `343d6fbe9` | 2026-04-26 | perf(ocr): EasyOCR remplace docTR | `core/grounding/{fast_detector,title_verifier}.py` |
|
||||
| `cc6443973` | 2026-04-26 | feat(grounding): vérification titre OCR post-action | `core/grounding/title_verifier.py` (+158) |
|
||||
| `90007cc7c` | 2026-04-26 | perf(grounding): réflexe pHash-only + max_new_tokens 64 | `core/grounding/server.py` |
|
||||
| `77faa03ec` | 2026-04-26 | feat(grounding): InfiGUI-G1-3B remplace UI-TARS 7B | `core/grounding/server.py` (-75/+67) |
|
||||
| `73cea2385` | 2026-04-25 | feat(grounding): Phase 6 Shadow Learning Hook | `core/grounding/shadow_learning_hook.py` (+156) |
|
||||
| `e2046837c` | 2026-04-25 | feat(grounding): Phase 5 intégration FAST→SMART→THINK dans ORA | `core/execution/observe_reason_act.py` |
|
||||
| `b30d4b665` | 2026-04-25 | feat(grounding): Phase 4 pipeline orchestré FAST→SMART→THINK | `core/grounding/fast_pipeline.py` |
|
||||
| `e4a48e78b` | 2026-04-25 | feat(grounding): Phase 3 ThinkArbiter + SignatureStore | `core/grounding/{think_arbiter,element_signature}.py` |
|
||||
| `ea36bba5c` | 2026-04-25 | feat(grounding): Phase 1-2 FAST→SMART détection + matching | `core/grounding/{fast_detector,smart_matcher,fast_types}.py` |
|
||||
| `9da589c8c` | 2026-04-25 | feat(grounding): pipeline centralisé + serveur UI-TARS transformers | **Création** `core/grounding/{server,pipeline,template_matcher,ui_tars_grounder,target,__init__}.py` + `tools/start_grounding_server.sh`. server.py FastAPI port 8200, modèle `ByteDance-Seed/UI-TARS-1.5-7B` 4-bit NF4. |
|
||||
| `73ddcdb29` | 2026-04-21 | feat: chaîne de grounding 3 niveaux + refonte capture | `core/execution/input_handler.py`, `visual_workflow_builder/.../execute.py` |
|
||||
| `d1b556b6c` | 2026-04-21 | fix(grounding): supprimer SeeClick cassé | `intelligent_executor.py` (-46) |
|
||||
| `91614fbff` | 2026-04-04 | fix: prompt natif bbox_2d Qwen2.5-VL | `agent_v0/server_v1/api_stream.py` |
|
||||
| `c1ce6a396` | (avril) | fix: séparer grounding (qwen2.5vl) et compréhension (gemma4) | api_stream.py |
|
||||
| `394342be7` | 2026-03-31 | **feat: support vLLM (GPU) comme moteur de grounding, Ollama en fallback** | `agent_v0/server_v1/api_stream.py` (+47/-14) — c'est l'unique commit qui ajoute vLLM. |
|
||||
| `d99b17394` | 2026-03-31 | feat: VLM grounding direct (Qwen2.5-VL) — nouvelle stratégie de résolution | `agent_v0/server_v1/api_stream.py` (+230) |
|
||||
| `cbe8dc95d` | (mars) | feat(cognition): timing + auto-apprentissage Shadow + VLM qwen2.5vl | — |
|
||||
| `ad15237fe` | (mars) | feat: smart systray Léa + support qwen3-vl | — |
|
||||
| `38966de0d` | (antérieur) | Feat: Action analyser_avec_ia (Ollama qwen2.5-vl) | — |
|
||||
| `728fac3b5` | (antérieur) | Feat: Actions validation avec OCR Ollama (qwen2.5-vl:7b) | — |
|
||||
| `21bfa3b33` | 2026-01-24 | feat(vwb): SeeClick + Self-Healing | `core/detection/seeclick_adapter.py` (+) |
|
||||
| `4509038bf` | 2026-04-09 | refactor: éclater api_stream.py 6400→3350 | déplace le code vLLM/Ollama vers `agent_v0/server_v1/resolve_engine.py` |
|
||||
|
||||
Sur `git reflog | head -100` : aucune trace d'opération destructive (pas de `reset --hard`, pas de checkout détruit) qui aurait perdu un commit lié au VLM. Toutes les opérations sont des commits propres.
|
||||
|
||||
---
|
||||
|
||||
## 4. Code dans des stashes ou branches non mergées
|
||||
|
||||
`git stash list` : **aucun stash**.
|
||||
|
||||
Branches existantes :
|
||||
- `main`
|
||||
- `master` (remote gitea uniquement)
|
||||
- `feature/qw-suite-mai` (HEAD courant)
|
||||
- `feature/feedback-bus`
|
||||
- `backup/pre-qw-suite-mai-2026-05-05`
|
||||
- `demo/ght-2026-05-08`
|
||||
- `dev/ia-tools-improvement`
|
||||
|
||||
Aucune branche divergente n'apparaît dans `git log --all -S "vllm"` au-delà des deux commits déjà recensés (tous accessibles depuis `feature/qw-suite-mai`). Idem pour `Qwen2_5_VL`, `smart_resize`, `qwen_vl_utils`, `BitsAndBytesConfig` : tous les commits qui les introduisent ou les modifient sont accessibles depuis HEAD.
|
||||
|
||||
→ Aucun code VLM unique perdu dans une branche divergente ou un stash.
|
||||
|
||||
---
|
||||
|
||||
## 5. Code potentiellement perdu (commits de suppression VLM)
|
||||
|
||||
| SHA | Date | Action | Résumé |
|
||||
|---|---|---|---|
|
||||
| `d1b556b6c` | 2026-04-21 | suppression | Retire SeeClick de `intelligent_executor.py` (-46). Le **fichier `core/detection/seeclick_adapter.py` n'a jamais été supprimé** : il vit toujours dans `core/detection/` (11 421 octets, mtime 2026-01-24) et est encore exporté par `core/detection/__init__.py`. → Code utilisable mais signalé "cassé" (config QWenConfig incompatible). |
|
||||
| `3d6868f02` | 2026-04-26 | refonte | Réduit `core/grounding/server.py` de 494 → 124 lignes en sortant la logique d'inférence vers `infigui_worker.py`. La logique transformers complète est conservée dans `infigui_worker.py` (et reprise par `infigui_server.py`). → Aucune perte. |
|
||||
| `77faa03ec` | 2026-04-26 | remplacement modèle | UI-TARS-1.5-7B remplacé par InfiGUI-G1-3B dans `core/grounding/server.py`. Le **prompt UI-TARS officiel** (`Thought:/Action: click(start_box='(x1, y1)')`) et la fonction `_evict_ollama_models()` ont disparu mais restent récupérables via `git show 9da589c8c:core/grounding/server.py`. |
|
||||
| `9da589c8c` | 2026-04-25 | nettoyage | "9 fichiers morts archivés dans `_archive/` (~6300 lignes)". Vérifié : aucun fichier VLM dans `_archive/dead_code_20260424/`. Ces 9 fichiers sont du visual/workflow, pas du grounding. → Aucune perte VLM. |
|
||||
| (autres) | | | Pas d'autre commit qui supprime du code VLM exploitable. |
|
||||
|
||||
→ **Pas de code VLM utile irrémédiablement perdu** : tout est récupérable via `git show`. Le seul élément à signaler est le **prompt officiel UI-TARS** présent dans la version `9da589c8c:core/grounding/server.py`, utile si on veut comparer un modèle UI-TARS reload.
|
||||
|
||||
---
|
||||
|
||||
## 6. Synthèse factuelle
|
||||
|
||||
- **Nombre d'implémentations distinctes ayant existé** :
|
||||
- 7 implémentations actives aujourd'hui (cf. §1.1 + §1.2 + §1.3 modèles distincts).
|
||||
- 2 implémentations historiques fortes ayant été remplacées en-place : UI-TARS-1.5-7B (transformers) → InfiGUI-G1-3B ; SoM+VLM intermédiaire → grounding direct Qwen2.5-VL.
|
||||
- **Backends testés au fil du temps** : Ollama (HTTP), vLLM (HTTP OpenAI-compat), Transformers in-process (Flask `server.py`, subprocess one-shot `infigui_worker.py`, daemon Unix socket `infigui_server.py`), HuggingFace direct (SeeClick standalone, OWL-v2 standalone), Cloud opt-in (OpenAI/Gemini/Anthropic via `vlm_provider.py`).
|
||||
- **Code directement utilisable pour la migration vers vLLM ou Transformers** :
|
||||
- **Oui pour Transformers** : `core/grounding/server.py` (loader + `_smart_resize` complet avec MIN/MAX_PIXELS) et `core/grounding/infigui_worker.py` (`load_model`, `infer` mode classique + fusion image+anchor) sont quasi clé-en-main pour Qwen2.5-VL / Qwen3-VL. Il suffit de changer `MODEL_ID` (env `GROUNDING_MODEL` déjà supporté).
|
||||
- **Oui pour vLLM** : `agent_v0/server_v1/resolve_engine.py` lignes 785-816 contient déjà l'appel HTTP OpenAI-compatible avec `image_url: data:image/jpeg;base64`. Il manque uniquement le passage explicite de `resized_width`/`resized_height` (extension OpenAI vLLM) — le bug d'échelle bbox_2d documenté dans `docs/MIGRATION_VLM_PLAN_2026-05-09.md`.
|
||||
- L'infrastructure socket persistant + fallback subprocess (`infigui_server.py` + `ui_tars_grounder.py`) est réutilisable telle quelle pour servir un autre modèle Transformers ou pour wrapper un client vLLM.
|
||||
|
||||
---
|
||||
|
||||
## 7. À clarifier avec Dom
|
||||
|
||||
1. **`core/detection/seeclick_adapter.py`** est encore exporté par `core/detection/__init__.py` mais le commit `d1b556b6c` indique qu'il est cassé. Faut-il le sortir de l'import et l'archiver, ou tenter de le réparer pour Qwen3-VL ?
|
||||
2. **`core/detection/owl_detector.py`** (Owlv2) est câblé via `core/detection/ui_detector.py` (L31, L113, L126) mais aucun trace de bench récent. Est-il encore appelé en prod ou candidat à l'archivage ?
|
||||
3. **`tools/start_grounding_server.sh`** parle encore de `UI-TARS-1.5-7B` dans son banner alors que le serveur charge InfiGUI depuis le commit `77faa03ec`. Doc obsolète mais sans impact runtime — à fixer si on documente la migration.
|
||||
4. **`core/grounding/server.py` (Flask port 8200)** vs **`core/grounding/infigui_server.py` (Unix socket)** vs **`core/grounding/infigui_worker.py` (subprocess one-shot)** : trois entry-points distincts pour la même logique transformers. Le service systemd `rpa-grounding.service` ne lance que `infigui_server`. Confirmer que `server.py` (Flask) est conservé volontairement comme alternative dev / test.
|
||||
5. **Modèle vLLM par défaut hardcodé** `Qwen/Qwen2.5-VL-7B-Instruct-AWQ` (resolve_engine L791) alors que le plan migration cible Qwen3-VL — env `VLLM_MODEL` permet le switch sans toucher au code, à confirmer comme méthode de migration.
|
||||
76
docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md
Normal file
76
docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Investigation — Mémoire visuelle orpheline
|
||||
|
||||
Date : 2026-05-09
|
||||
Branche : feature/qw-suite-mai
|
||||
Périmètre : modules de cache d'embeddings, validation visuelle continue, et apprentissage par observation, identifiés en code mais non instanciés en runtime.
|
||||
|
||||
Hors scope de `docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md` (audit borné à `agent_v0/server_v1/` + `core/*` importés par le serveur). Ces modules ne sont pas importés par le serveur.
|
||||
|
||||
## 1. Modules concernés
|
||||
|
||||
| Module | Lignes | Commit d'origine | Dernier commit touchant le code | Sites d'instanciation runtime |
|
||||
|---|---|---|---|---|
|
||||
| `core/visual/visual_embedding_manager.py` | 651 | `a27b74cf2` (2026-01-29, v1.0 stable) | `36737cfe9` (2026-04-14, refactor sécurité serializer) | 0 |
|
||||
| `core/visual/screenshot_validation_manager.py` | 571 | `a27b74cf2` (2026-01-29, v1.0 stable) | `a27b74cf2` (idem) | 0 |
|
||||
| `core/grounding/shadow_learning_hook.py` | 156 | `73cea2385` (2026-04-25, "Phase 6 — Shadow Learning Hook") | idem | 0 (seul match = exemple docstring du fichier) |
|
||||
|
||||
Vérifications effectuées :
|
||||
- `grep -rn "VisualEmbeddingManager(" --include="*.py"` hors module et hors `tests/` : 0 hit.
|
||||
- `grep -rn "ScreenshotValidationManager("` idem : 0 hit.
|
||||
- `grep -rn "ShadowLearningHook("` idem : 0 hit.
|
||||
- `core/visual/__init__.py` re-exporte les deux managers, mais aucun consommateur n'importe `from core.visual import …`.
|
||||
|
||||
## 2. Rôle déduit du code
|
||||
|
||||
### `VisualEmbeddingManager` (core/visual/visual_embedding_manager.py)
|
||||
Cache d'embeddings CLIP indexés par signature, métriques de similarité multiples (cosine, euclidean, normalized correlation, score combiné), API `MatchResult`. S'interface avec `core.embedding.fusion_engine.FusionEngine` et utilise `core.security.signed_serializer` pour persister le cache (HMAC signé).
|
||||
|
||||
Vise à fournir une recherche de correspondances rapide pour le grounding visuel (intro module : « système RPA 100% visuel », exigences 3.3, 3.4 — référencement non retrouvé dans le repo).
|
||||
|
||||
### `ScreenshotValidationManager` (core/visual/screenshot_validation_manager.py)
|
||||
Validation périodique automatique d'éléments visuels enregistrés. Statuts `VALID/WARNING/ERROR/UNKNOWN/VALIDATING`, `ValidationReport` avec `ValidationIssue` et `RecoveryAction` proposées (auto-fixable ou non). Dépend de `VisualEmbeddingManager` + `ScreenCapturer` + `UIDetector`.
|
||||
|
||||
Vise à alimenter une UI de monitoring de santé des targets visuels (intro : « indicateurs vert/orange/rouge »).
|
||||
|
||||
### `ShadowLearningHook` (core/grounding/shadow_learning_hook.py)
|
||||
Pont entre observation Shadow (l'humain clique) et `SignatureStore` (base de signatures d'éléments UI). À chaque clic observé : détection de l'élément sous le clic via `FastDetector`, enrichissement de la base. Présenté comme « hook optionnel (callback) » à brancher dans `ShadowObserver` ou l'API de capture.
|
||||
|
||||
## 3. Pourquoi non-câblés (analyse historique)
|
||||
|
||||
### Sous-système `core/visual/` (VEM + SVM)
|
||||
Introduits le 2026-01-29 dans le commit `a27b74cf2` (« v1.0 - Version stable »), antérieur de plusieurs mois aux refontes grounding d'avril 2026 (commits `9da589c8c` pipeline centralisé, `77faa03ec` UI-TARS→InfiGUI, phases 1-6 FAST→SMART→THINK). Le pipeline grounding actuel passe par `core/grounding/*` (`fast_pipeline.py`, `infigui_server.py`, `think_arbiter.py`) qui implémente sa propre logique de signature (`SignatureStore` dans `element_signature.py`) — fonctionnellement redondant avec VEM côté cache d'embeddings, et orthogonal à SVM côté validation continue.
|
||||
|
||||
Le `screenshot_validation_manager.py` n'a connu aucun commit depuis sa création (mtime 2026-01-07). Pas de trace de point d'appel envisagé dans les commits ultérieurs.
|
||||
|
||||
### `ShadowLearningHook`
|
||||
Introduit le 2026-04-25 (`73cea2385`) au sein du pipeline FAST→SMART→THINK (phase 6 sur 6 documentée du commit `b30d4b665`). Le hook attend d'être appelé sur événement clic — mais le `ShadowObserver` (`core/workflow/shadow_observer.py`) ne configure aucun callback de ce type, et `api_stream.py` instancie `ShadowValidator` (validation post-action côté workflow) sans déclencher d'enrichissement de `SignatureStore`.
|
||||
|
||||
Le commit suivant (`e2046837c` 2026-04-25, « Phase 5 intégration FAST→SMART→THINK dans ORA ») câble les phases 1-5 dans `observe_reason_act.py` mais ne mentionne pas la phase 6. Probable fin de batch d'implémentation sans pas de câblage côté capture clic.
|
||||
|
||||
## 4. Recommandation préliminaire (à statuer en revue +14j, soit 2026-05-23)
|
||||
|
||||
| Module | Recommandation | Justification |
|
||||
|---|---|---|
|
||||
| `VisualEmbeddingManager` | **ACCEPTED + archivage `_archive/`** sauf cas d'usage prod identifié | Redondant avec `SignatureStore` + cache d'embeddings côté `fusion_engine.py`. Maintien sans utilisateur = bruit. |
|
||||
| `ScreenshotValidationManager` | **ACCEPTED + archivage** | Pas d'UI de monitoring associée dans `web_dashboard/` ni `visual_workflow_builder/`. Composant prévu pour une fonctionnalité non aboutie. |
|
||||
| `ShadowLearningHook` | **CÂBLER** post-démo Kerella | Fonctionnalité cohérente avec la vision aiva-vision (apprentissage progressif). Coût faible : un callback `on_click` dans `ShadowObserver` ou côté API capture. Bénéfice élevé si apprentissage devient prio post-démo. |
|
||||
|
||||
L'archivage VEM/SVM ne tranche pas la question plus large d'une unification du cache d'embeddings (VEM + `fusion_engine.py` + `embedding_cache.py`). Cette unification, si pertinente, fait l'objet d'une dette dédiée à instruire séparément, hors scope de cette investigation.
|
||||
|
||||
## 5. Tests dépendants à traiter en même temps que l'archivage
|
||||
|
||||
Si la revue +14j retient l'archivage de VEM/SVM, les tests suivants doivent être archivés ou réécrits dans le même commit (sinon CI casse ou faux positif vert) :
|
||||
|
||||
- `tests/property/test_visual_embedding_manager_properties.py` — dédié VEM (à archiver avec).
|
||||
- `tests/integration/test_visual_rpa_checkpoint.py` — couvre VEM + SVM en intégration.
|
||||
- `tests/property/test_visual_capture_properties.py` — couvre VEM + SVM.
|
||||
- `tests/property/test_interactive_preview_area_properties.py` — référence VEM ; à examiner avant archivage car peut couvrir d'autres composants encore vivants.
|
||||
- `tests/property/test_realtime_validation_properties.py` — importe `core.visual` (à examiner : peut viser `VisualTargetManager` qui, lui, est instancié côté VWB et ne doit pas être archivé).
|
||||
|
||||
`ShadowLearningHook` n'a aucun test associé.
|
||||
|
||||
## 6. Dette identifiée hors scope de cette investigation
|
||||
|
||||
L'investigation actuelle s'est limitée au sous-système mémoire visuelle. Le constat que des modules orphelins existent au-delà du périmètre de l'AUDIT serveur du 2026-05-08 (cf. modules identifiés en sections 1-2) suggère fortement que d'autres orphelins existent ailleurs dans le repo (VWB, agent_chat, demo/, deploy/, autres).
|
||||
|
||||
Un audit exhaustif des modules orphelins du repo constitue une dette identifiée à instruire post-démo Kerella. Numéro de dette non réservé à ce stade (instruction préalable nécessaire pour cadrer le périmètre).
|
||||
73
docs/MIGRATION_VLM_PLAN_2026-05-09.md
Normal file
73
docs/MIGRATION_VLM_PLAN_2026-05-09.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Migration grounding VLM — qwen2.5vl Ollama → vLLM/Transformers (Qwen3-VL)
|
||||
|
||||
Date plan : 2026-05-09 (rédigé le 2026-05-08 au soir)
|
||||
Branche cible : feature/<à-créer-demain>
|
||||
État actuel : grounding passe par Ollama, qwen2.5vl:7b en split CPU/GPU 42/58.
|
||||
|
||||
## 1. Constat
|
||||
|
||||
Deux problèmes structurels relevés le 8 mai 2026.
|
||||
|
||||
### 1.1. VRAM saturée
|
||||
|
||||
`qwen2.5vl:7b` chargé via Ollama pèse 14 GB en mémoire totale alors que la machine n'a que 12 GB de VRAM. Ollama bascule en split CPU/GPU 42/58. Latence mesurée par appel grounding : ~11 s. À titre de référence, `qwen3-vl:8b` (6 GB) tient en full GPU et descend à 1.7 s sur le même cas.
|
||||
|
||||
### 1.2. Bug d'échelle bbox_2d (root cause documentée)
|
||||
|
||||
La doc officielle Qwen2.5-VL précise que les coordonnées renvoyées sont dans la résolution post-`smart_resize`, pas dans la résolution de l'image envoyée par le client. Or Ollama applique son propre `smart_resize` en interne sans exposer la taille effective au client. Conséquence : le code prod divise par `small_w = orig_w` (taille envoyée) au lieu de `resized_w` → coordonnées toutes shiftées vers le top-left. Bug systémique présent dans 4 occurrences de `agent_v0/server_v1/resolve_engine.py` et dans `_locate_popup_button` (L:2576).
|
||||
|
||||
Tant que le grounding passe par Ollama, le fix ne peut être qu'une rustine (resize forcé côté serveur AVANT envoi pour matcher la convention que Ollama va appliquer ensuite — fragile, dépend de la version Ollama).
|
||||
|
||||
Source : [HF discussion #13 sur Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct/discussions/13)
|
||||
Citation mainteneur : « The bbox_2d coordinates ... will be relative to your **resized image size** if you are resizing. »
|
||||
Citation discussion : « **resized dimensions parameter is not supported in OLLAMA**, which complicates coordinate translation. »
|
||||
|
||||
## 2. Rappel bench 8 mai 2026
|
||||
|
||||
Screenshot fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png` (2560×1600, boîte de dialogue OK/Cancel).
|
||||
|
||||
| Modèle / config | Latence | Format bbox_2d | Parse regex prod | Coords cohérentes |
|
||||
|---|---|---|---|---|
|
||||
| qwen2.5vl:7b Ollama (num_predict=50, prod) | 11.0 s | `{"bbox_2d":[422,604,462,624]}` | oui | non (cx ≈ 0.17, top-left) |
|
||||
| qwen3-vl:8b Ollama (params prod stricts) | 8.0 s | vide (50 tokens absorbés par thinking) | non | n/a |
|
||||
| qwen3-vl:8b Ollama (think:false, num_predict=256) | 1.7 s | liste nue `[332,487,362,507]` | non (regex attend `"bbox_2d":[...]`) | n/a |
|
||||
| qwen3-vl:8b Ollama (prompt JSON explicite) | 1.8 s | `{"bbox_2d":[...]}` | oui | non (même bug d'échelle) |
|
||||
|
||||
## 3. Chemin technique cible
|
||||
|
||||
vLLM ou Transformers direct, avec passage explicite de `resized_width` et `resized_height` au modèle (paramètre supporté par les deux backends), garantissant que les `bbox_2d` retournés sont dans la même résolution que celle qu'on a passée. Le service `core/grounding/server.py` (déjà en place pour InfiGUI via Unix socket, avec `_smart_resize`, `MIN_PIXELS=100*28*28`, `MAX_PIXELS=5600*28*28`) sert de référence architecturale.
|
||||
|
||||
Modèle pressenti : `qwen3-vl:8b` (6 GB en VRAM, full GPU possible), avec :
|
||||
- `think:false` (désactiver le mode thinking par défaut)
|
||||
- `num_predict >= 128` (50 insuffisant : tokens absorbés par thinking quand activé)
|
||||
- prompt imposant le format JSON `[{"bbox_2d":[...],"label":"..."}]`
|
||||
- preprocessing image côté serveur : `_smart_resize` officiel (max_size=1280, multiples de 28, code dans la discussion HF)
|
||||
|
||||
## 4. Étapes de migration (5)
|
||||
|
||||
1. **[Setup] Choix du backend grounding** : vLLM, Transformers direct, ou réorientation de `core/grounding/server.py` vers Qwen3-VL plutôt que démarrer une 4ème stack. Décision à acter en début de chantier.
|
||||
2. **[Preprocessing] Implémenter `_smart_resize` côté `resolve_engine.py`** avec la formule officielle (max_size=1280, multiples de 28). Capturer la nouvelle taille (`resized_w`, `resized_h`) et la passer au backend choisi.
|
||||
3. **[Parser] Adapter les 4 occurrences `bbox_2d` parsing** (`resolve_engine.py:840-848`, 870-880, 908-917, et `_locate_popup_button` L:2576) pour diviser par `resized_w`/`resized_h` et non plus `small_w/h` (= `orig_w/h`). Centraliser dans une seule fonction utilitaire.
|
||||
4. **[Prompt] Imposer le format JSON `bbox_2d`** dans le prompt envoyé (Qwen3-VL ne le sort pas spontanément, sortie liste nue par défaut). Ajouter `think:false` et `num_predict>=128` aux options.
|
||||
5. **[Test bbox_2d cible] Refaire le bench du 8 mai** avec la convention contrôlée. Critère de validation : sur le screenshot heartbeat de référence, le bouton OK doit être localisé à un `cx ≈ 0.45-0.55` (mid-screen) et non plus 0.17.
|
||||
|
||||
## 5. Test bbox_2d à refaire (post-migration)
|
||||
|
||||
Reproduire le test du 8 mai avec :
|
||||
- Même screenshot fixture : `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png`
|
||||
- Même cible : bouton "OK" de la boîte de dialogue
|
||||
- Backend nouveau (vLLM ou Transformers + `smart_resize` côté client)
|
||||
|
||||
Sortie attendue : `bbox_2d` en pixels de l'image redimensionnée connue → division par `resized_w` → `cx` visuellement ≈ centre du bouton (vérification par overlay sur le screenshot).
|
||||
|
||||
## 6. Hors scope du chantier migration (à traiter après)
|
||||
|
||||
- Nettoyage de la logique « décharger gemma4 pour qwen2.5vl » dans `agent_v0/server_v1/stream_processor.py:442,1742` — devient inutile une fois qwen2.5vl écarté.
|
||||
- Nettoyage des 9 mentions/commentaires `qwen2.5vl` dans le code.
|
||||
- Décision sur le maintien d'une compat fallback Ollama (utile en dev sans GPU) ou abandon complet.
|
||||
|
||||
## 7. Risques / points de vigilance
|
||||
|
||||
- Si vLLM tourne sur le même GPU que streaming/agent-chat, vérifier que la VRAM tient : qwen3-vl 6 GB + streaming 0.8 GB + agent-chat 0.8 GB = 7.6 GB → marge OK sur 12 GB.
|
||||
- Le bench du 8 mai montrait des coordonnées visuellement incorrectes MÊME avec un format `bbox_2d` valide. La résolution du bug d'échelle est nécessaire mais peut-être pas suffisante. Vérifier après l'étape 5.
|
||||
- `_locate_popup_button` (`resolve_engine.py:2536-2585`) est plus simple à migrer que la cascade principale. Faire un POC sur cette fonction avant d'attaquer la cascade.
|
||||
375
docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md
Normal file
375
docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Diagnostic — Replay `replay_free_68ca51ab` bloqué sur l'onglet Imagerie
|
||||
|
||||
Date : 8 mai 2026 (matin avant démo GHT Sud 95)
|
||||
Workflow : `Urgence_aiva_demo` (`wf_a38aeebea5e6_1778162737`)
|
||||
Replay : `replay_free_68ca51ab` (cancellé manuellement à 10:34, après pause supervisée step 18)
|
||||
Auteur : ingénieur senior debug RPA / vision
|
||||
|
||||
> **TL;DR — il y a deux causes simultanées, l'une explique l'autre.**
|
||||
>
|
||||
> 1. **Cause primaire (réseau) :** le client Léa V1 Windows utilise un `read_timeout=5 s` sur `GET /replay/next`. Sur cette même connexion, le serveur exécute parfois un `extract_text` (5–7 s) PUIS dispatche un `click` dans le même appel. Le client coupe avant la réponse. L'action `click` était déjà *poppée* de la queue serveur (`_retry_pending`), donc **elle est perdue silencieusement** — pas de retry automatique, pas de re-dispatch.
|
||||
> C'est ce qui s'est passé pour les steps 10, 12, 14 et 17 (clic Imagerie, Notes médicales, Synthèse Urgences, Codage). Le client a poursuivi son polling, mais quand il est revenu à l'écoute, le serveur en était déjà au step 18 (`Coller ou saisir le dossier patient`) — qui n'existe que sur la maquette aiva-vision (`codage.html`), donc échec `target_not_found` → pause supervisée.
|
||||
>
|
||||
> 2. **Cause aggravante (vision) :** la cascade `OCR-DIRECT` (`_resolve_by_ocr_text`) renvoie le **centre de la ligne entière** quand le `target_text` n'est qu'un sous-fragment (`score=0.8`). Pour la barre de tabs Easily, docTR détecte les 5 tabs comme une seule ligne. Conséquence : `Imagerie`, `Notes médicales` et `Synthèse Urgences` retournent quasiment les mêmes coordonnées (~0.23, 0.28) — c'est-à-dire le centre de la rangée de tabs (qui tombe sur Imagerie). Même si le client avait reçu chaque action, le clic aurait probablement raté la cible.
|
||||
|
||||
---
|
||||
|
||||
## 1. Reconstruction temporelle
|
||||
|
||||
Workflow `Urgence_aiva_demo` (steps issues de la DB SQLite, ordre 1→22) :
|
||||
|
||||
| Order | id (court) | type | label |
|
||||
|-------|----------------|------------------|-----------------------------------------------|
|
||||
| 1 | 43ab3c1417d3 | extract_table | Lire liste patients (IPP) |
|
||||
| 2 | 1dada40f6a44 | pause_for_human | Confirmer démarrage |
|
||||
| 3 | 288d0bceea90 | click_anchor | Ouvrir dossier MOREL (25003284) |
|
||||
| 4 | 5388268582d6 | extract_text | Lire Motif d'admission (`t_motif`) |
|
||||
| 5 | b18e530526bb | keyboard_shortcut| Scroll fin de page (End) |
|
||||
| 6 | b425b17b37f6 | extract_text | Lire bas de Motif (`t_motif_bas`) |
|
||||
| 7 | fc4cf0a78b65 | keyboard_shortcut| Retour haut (Home) |
|
||||
| 8 | 45f5d7fb7456 | click_anchor | Onglet Examens cliniques |
|
||||
| 9 | 4148c9e8caa4 | extract_text | Lire Examens cliniques (`t_examens`) |
|
||||
| 10 | 4c0663941f22 | click_anchor | Onglet Imagerie |
|
||||
| 11 | 93cf4c6651f3 | extract_text | Lire Imagerie (`t_imagerie`) |
|
||||
| 12 | **3b13c973d737** | click_anchor | **Onglet Notes médicales** |
|
||||
| 13 | a5840d6bf8ed | extract_text | Lire Notes médicales (`t_notes`) |
|
||||
| 14 | 8767d8e2e221 | click_anchor | Onglet Synthèse Urgences |
|
||||
| 15 | 835e5dd54bb7 | extract_text | Lire Synthèse (`t_synthese`) |
|
||||
| 16 | fc5a9676af55 | t2a_decision | Décision T2A (LLM) |
|
||||
| 17 | 156d7cd29ebb | click_anchor | Onglet « Codage > » (vers maquette aiva) |
|
||||
| 18 | 36346c1c40b9 | click_anchor | Cliquer textarea DPI (sur codage.html) |
|
||||
| ... | | | |
|
||||
|
||||
### Logs serveur — `journalctl --user -u rpa-streaming`
|
||||
|
||||
Filtrage `replay_free_68ca51ab` + `RESOLVE_*` + `REPORT` + `extract_text`. Extraits pertinents :
|
||||
|
||||
```
|
||||
10:25:46 RESOLVE_ENTRY by_text='Examens cliniques' strict_mode=True screen=2560x1490 has_anchor=True
|
||||
10:25:48 Strict resolve OCR-DIRECT : OK 'Examens cliniques' → (0.2305, 0.2676) score=0.80
|
||||
10:25:48 RESOLVE_EXIT resolved=True method='hybrid_text_direct' coords=(0.2305, 0.2676) score=0.8
|
||||
10:25:49 REPORT step_45f5d7fb7456 success=True actual_position=(0.2305, 0.2798)
|
||||
|
||||
10:25:55 extract_text → variable 't_examens' (1689 chars)
|
||||
10:25:55 DISPATCH action_id=step_4c0663941f22 (click) by_text='Imagerie' ← client a déjà timeout
|
||||
|
||||
10:26:01 extract_text → variable 't_imagerie' (1084 chars)
|
||||
10:26:01 DISPATCH action_id=step_3b13c973d737 (click) by_text='Notes médicales' ← perdu
|
||||
|
||||
10:26:08 extract_text → variable 't_notes' (1084 chars)
|
||||
10:26:08 DISPATCH action_id=step_8767d8e2e221 (click) by_text='Synthèse Urgences' ← perdu
|
||||
|
||||
10:26:17 extract_text → variable 't_synthese' (1084 chars)
|
||||
10:26:27 t2a_decision → variable 'dec' decision=FORFAIT_URGENCE (10.0s)
|
||||
10:26:27 DISPATCH action_id=step_156d7cd29ebb (click) by_text='Codage' ← perdu (concurrence de polls)
|
||||
10:26:27 DISPATCH action_id=step_36346c1c40b9 (click) by_text='Coller ou saisir le dossier patient'
|
||||
|
||||
10:26:28 RESOLVE_ENTRY by_text='Coller ou saisir le dossier patient' strict_mode=True
|
||||
10:26:30 Strict resolve OCR-DIRECT : 'Coller ou saisir le dossier patient' non trouvé, passage VLM
|
||||
10:26:36 RESOLVE_EXIT resolved=False method='strict_vlm_template_failed'
|
||||
…
|
||||
10:28:47 REPORT step_36346c1c40b9 success=False error='target_not_found' warning='visual_resolve_failed'
|
||||
10:34:00 Replay annulé manuellement
|
||||
```
|
||||
|
||||
**Ce qui ne figure PAS dans les logs serveur** : aucun `RESOLVE_ENTRY` pour `by_text='Imagerie'`, `'Notes médicales'`, `'Synthèse Urgences'` ou `'Codage'` côté replay live. La cascade de résolution n'a JAMAIS été appelée pour ces tabs. → Le client n'a jamais frappé `/resolve_target` ni reçu l'action.
|
||||
|
||||
### Logs client — `C:\rpa_vision\agent_debug.log`
|
||||
|
||||
```
|
||||
10:25:44.710 Action de replay recue : click (id=step_45f5d7fb7456 — Examens cliniques)
|
||||
10:25:47.448 Server resolve OK [hybrid_text_direct] score=0.80
|
||||
10:25:48.008 Replay click [VISUAL] : (0.230, 0.280) -> (590, 447) sur (2560x1600)
|
||||
10:25:48.324 Ecran change apres ~200ms
|
||||
10:25:48.537 Resultat rapporte : replay_status=running, restant=14
|
||||
10:25:53.771 WARNING : HTTPConnectionPool(host=192.168.1.40, port=5005): Read timed out (read timeout=5)
|
||||
10:25:53.771 Replay termine - retour en mode capture
|
||||
10:25:53.780 shared_state Replay termine
|
||||
← 33 s de silence
|
||||
10:26:26.608 Action de replay recue : click (id=step_36346c1c40b9 — Coller ou saisir...)
|
||||
10:26:35.409 Server resolve échoué : vlm_and_template_all_failed
|
||||
10:26:39.096 Server resolve échoué : no_target_criteria
|
||||
10:26:44.178 Server resolve échoué : vlm_and_template_all_failed
|
||||
10:26:45.585 ERROR [LEA] Léa a besoin d'aide: Je n'y arrive pas (« Coller ou saisir... »)
|
||||
…
|
||||
10:28:45.762 [APPRENTISSAGE] Timeout global → 0 actions capturées
|
||||
10:28:46.231 Replay termine
|
||||
```
|
||||
|
||||
→ Confirmation directe : le client a sauté **9 actions serveur+visuelles** entre l'OK Examens cliniques (10:25:48) et la réception de step 18 (10:26:26).
|
||||
|
||||
---
|
||||
|
||||
## 2. Diagnostic causal
|
||||
|
||||
### Chaîne de responsabilité
|
||||
|
||||
```
|
||||
+---------------------------------------------------------------------------+
|
||||
| Hyp #1 (cascade serveur foire) — INFIRMÉE |
|
||||
| La cascade serveur n'est même jamais invoquée pour ces 4 tabs. |
|
||||
| |
|
||||
| Hyp #2 (cascade locale Léa V1 prend le relais) — INFIRMÉE |
|
||||
| Le client n'a pas reçu d'action → rien à résoudre localement. |
|
||||
| |
|
||||
| Hyp #3 (coords brutes du record obsolètes) — INFIRMÉE |
|
||||
| L'ancre `anchor_0438bd2d9bdd_1778161174` (« Notes médicales ») a |
|
||||
| bbox (444, 424, 146, 48) qui dans l'image de référence pointe sur |
|
||||
| « Imagerie » et NON Notes médicales (le crop le confirme : |
|
||||
| /tmp/anchor_0438bd2d9bdd_1778161174_bbox.png montre « Imagerie »). |
|
||||
| Pareil pour anchor_6a2591e7c51c (« Synthèse Urgences ») dont la |
|
||||
| bbox (580, 423, 192, 47) crop « Notes médicales ». |
|
||||
| → Les bboxes des tabs sont décalées d'un cran à gauche dans la DB, |
|
||||
| mais ce n'est PAS la cause du blocage actuel : le mode strict + OCR- |
|
||||
| DIRECT ignore la bbox et part de by_text. Anomalie cosmétique à |
|
||||
| nettoyer hors-démo. |
|
||||
| |
|
||||
| Hyp #4 (offset écran live vs record) — PARTIELLEMENT VRAIE |
|
||||
| Voir §3. |
|
||||
| |
|
||||
| Hyp #5 (event onclick JS) — INFIRMÉE |
|
||||
| Voir §3. |
|
||||
| |
|
||||
| Hyp #6 (cache client/serveur) — INFIRMÉE |
|
||||
| Aucun `from_memory=True` dans les logs ; TargetMemoryStore pas hit. |
|
||||
| |
|
||||
| Cause primaire = HTTP TIMEOUT 5 s côté client |
|
||||
| + actions serveur lentes (extract_text 5-7 s, t2a_decision 10 s) |
|
||||
| + pas de watchdog d'orphelins dans `_retry_pending` |
|
||||
| |
|
||||
| Cause aggravante = OCR-DIRECT center-of-line bug |
|
||||
| score=0.8 → coords = centre de la ligne docTR entière, pas du span. |
|
||||
+---------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Mécanique exacte du timeout
|
||||
|
||||
`agent_v0/server_v1/api_stream.py` (`get_next_action`, lignes 2816-3083) :
|
||||
1. Acquiert `_replay_lock` avec `acquire_timeout=4.5 s`. Sinon retourne `{server_busy: True}` — **OK**.
|
||||
2. Une fois le lock pris, boucle `while queue:` qui exécute toutes les actions « serveur » (`extract_text`, `extract_table`, `t2a_decision`, `pause_for_human` non bloquant) **dans le même appel HTTP**, jusqu'à tomber sur une action visuelle (`click`/`type`/`key_combo`) qu'il dispatch et retourne.
|
||||
3. `extract_text` est wrappé dans `loop.run_in_executor(...)` (timeout 180 s) pour ne pas bloquer l'event loop FastAPI — bon design.
|
||||
4. **Mais le client appelle ce endpoint avec `timeout=5` (executor.py:1786).** Si la chaîne `extract_text + dispatch_click` prend plus de 5 s, la réponse arrive après que le client ait fermé sa socket. La réponse contient le `click` action et est perdue.
|
||||
5. Côté serveur (ligne 3209-3224), l'action est déjà *poppée* de la queue et stockée dans `_retry_pending[action_id]` au moment du dispatch. Pas de retry automatique tant que le client ne renvoie pas un report (qui ne viendra pas).
|
||||
6. Le client repasse en `_replay_active=False` (`main.py:331`) — *cosmétique* — puis continue de poller. Au poll suivant, la queue est passée à l'action suivante (`extract_text`), idem boucle.
|
||||
|
||||
**Aucun watchdog ne ré-énonce `_retry_pending` au client.** L'unique chemin pour récupérer une action perdue serait que le client envoie un report avec `success=False` (jamais le cas ici puisqu'il n'a pas reçu l'action).
|
||||
|
||||
### Mécanique exacte de l'OCR-DIRECT center-of-line
|
||||
|
||||
`agent_v0/server_v1/resolve_engine.py:1447-1527` :
|
||||
|
||||
```python
|
||||
# Match exact > contient > mot par mot
|
||||
score = 0.0
|
||||
if target_lower == line_lower: score = 1.0
|
||||
elif target_lower in line_lower: score = 0.8
|
||||
elif any(target_lower == w.value.lower() for w in line_obj.words): score = 0.9
|
||||
|
||||
if score > best_score:
|
||||
box = line_obj.geometry # bbox de la LIGNE ENTIÈRE
|
||||
cx = (box[0][0] + box[1][0]) / 2
|
||||
cy = (box[0][1] + box[1][1]) / 2
|
||||
```
|
||||
|
||||
Quand docTR voit la barre de tabs Easily comme **une seule ligne** : `"Motif d'admission Examens cliniques Imagerie Notes médicales Synthèse Urgences Codage >"`, il retourne pour CHAQUE `target` qui est un sous-fragment de cette ligne **le centre de la rangée entière** (~50 % en x, ~28 % en y). Les coords ne dépendent pas du tab demandé.
|
||||
|
||||
Preuve archivée dans les logs e2e_singleshot du 8 mai à 09:58 (single screenshot, donc résultats reproductibles) :
|
||||
- `'Imagerie'` → `(0.2305, 0.2805)` score 0.8
|
||||
- `'Notes médicales'` → `(0.2285, 0.2805)` score 0.8
|
||||
- `'Synthèse Urgences'` → `(0.2285, 0.2805)` score 0.8
|
||||
- (delta ~5 px, trois tabs visuellement à 4-5 cm d'écart les uns des autres)
|
||||
|
||||
→ Si le client avait reçu les 4 actions, il aurait cliqué 4 fois quasiment au même endroit (vers Imagerie). Bug latent indépendant du timeout.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vérification des hypothèses 4 et 5
|
||||
|
||||
### Hyp #4 — offset écran live vs record
|
||||
|
||||
Géométrie réelle du rendu Easily Assure dans une fenêtre Edge fullscreen 2560×1600 :
|
||||
- Edge title bar : ~40 px (offset_y de la fenêtre = 49 d'après le log « Grounding contraint à la fenêtre : 2560x1490 (0, 49) »)
|
||||
- Edge tabs/URL/bookmarks : ~250 px
|
||||
- `.app-header` Easily (bleu) : 36 px (padding 8 + font 18)
|
||||
- `.menu-bar` (Patients/Planning/...) : 32 px
|
||||
- `.patient-banner` (IPP MOREL...) : ~50 px
|
||||
- `.tabs` (Motif/Examens/...) : 36 px (height CSS) → **y range ≈ 410-450 dans l'image 2560x1600**
|
||||
|
||||
Le crop de référence (ancre `anchor_0438bd2d9bdd_1778161174`) à y=420-480 montre exactement la rangée de tabs (cf. `/tmp/tabs_row_full.png`). Pas d'offset majeur entre record et live. Une éventuelle dérive ±10-30 px est gérable par un click au pixel central.
|
||||
|
||||
→ **Hypothèse 4 partiellement vraie** : il y a effectivement un offset, mais il n'est pas la cause du bug. Et il est dégradé par le bug OCR-DIRECT center-of-line (cause #2) puisque le centre de la ligne tombe au milieu de la barre, pas sur le tab demandé.
|
||||
|
||||
### Hyp #5 — event onclick JS de la maquette
|
||||
|
||||
`docs/clients/ght_sud_95/mockup_easily_assure/dossier.html:36-43` :
|
||||
|
||||
```html
|
||||
<div class="tabs">
|
||||
<a class="tab active" data-tab="motif">Motif d'admission</a>
|
||||
<a class="tab" data-tab="examens">Examens cliniques</a>
|
||||
<a class="tab" data-tab="imagerie">Imagerie</a>
|
||||
<a class="tab" data-tab="notes">Notes médicales</a>
|
||||
<a class="tab" data-tab="synthese">Synthèse Urgences</a>
|
||||
<a class="tab" id="tab-vers-codage" href="codage.html">Codage ></a>
|
||||
</div>
|
||||
```
|
||||
|
||||
`app.js:377-401` :
|
||||
|
||||
```js
|
||||
function installTabs() {
|
||||
const tabs = document.querySelectorAll('.tabs .tab[data-tab]');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const target = tab.getAttribute('data-tab');
|
||||
history.replaceState(null, '', '#' + target + location.search);
|
||||
activate(target);
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
→ Mécanisme propre. `addEventListener('click')` directement sur les `<a class="tab">`. Aucun overlay, aucun event swallow. Un MouseEvent (Win32 SendInput → Windows Edge → DOM) sur le pixel d'un tab DÉCLENCHE le listener. Le tab `Codage` est un lien `href="codage.html"` → navigation native. Aucun problème côté maquette.
|
||||
|
||||
→ **Hypothèse 5 infirmée**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Reproduction en isolation
|
||||
|
||||
Données déjà disponibles via le test e2e_singleshot du 8 mai 09:58 (`session=e2e_singleshot_1778227119_1fe686`), qui appelle `/resolve_target` sur un screenshot fixe (probablement l'onglet Imagerie ouvert) :
|
||||
|
||||
| target_spec.by_text | Résolution | x_pct | y_pct | score |
|
||||
|------------------------------|-----------------------|--------|--------|-------|
|
||||
| `25003284` | hybrid_text_direct | 0.0312 | 0.3539 | 1.00 |
|
||||
| `Examens cliniques` | hybrid_text_direct | 0.0610 | 0.3195 | 1.00 |
|
||||
| `Imagerie` | hybrid_text_direct | 0.2305 | 0.2805 | 0.80 |
|
||||
| `Notes médicales` | hybrid_text_direct | 0.2285 | 0.2805 | 0.80 |
|
||||
| `Synthèse Urgences` | hybrid_text_direct | 0.2285 | 0.2805 | 0.80 |
|
||||
| `Codage` | hybrid_text_direct | 0.1279 | 0.1641 | 0.80 |
|
||||
| `Coller ou saisir le dossier patient` | hybrid_text_direct | 0.0630 | 0.4125 | 1.00 |
|
||||
| `Justification de la décision`| template_matching | 0.5000 | 0.5000 | 1.00 |
|
||||
|
||||
→ Reproduction confirmée : trois tabs (Imagerie / Notes / Synthèse) renvoyés à coords pratiquement identiques. Le test du 10:01 (1920x1080) reproduit la même chose : `Notes médicales` → (0.2227, 0.1259), `Imagerie` → (0.2256, 0.1267), même row.
|
||||
|
||||
Pour reproduire en CLI sans flask :
|
||||
```bash
|
||||
cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate
|
||||
python -c "
|
||||
from agent_v0.server_v1.resolve_engine import _resolve_by_ocr_text
|
||||
img='/home/dom/ai/rpa_vision_v3/visual_workflow_builder/backend/data/anchors/anchor_0438bd2d9bdd_1778161174_full.png'
|
||||
for t in ['Imagerie','Notes médicales','Synthèse Urgences','Codage','Examens cliniques']:
|
||||
r = _resolve_by_ocr_text(img, t, 2560, 1600)
|
||||
print(f'{t:25s} -> {r}')
|
||||
"
|
||||
```
|
||||
|
||||
Ce smoke test (offline, ~30 s la 1re fois pour télécharger le modèle docTR) prouve la cause #2 sans dépendre du PC Windows.
|
||||
|
||||
---
|
||||
|
||||
## 5. Trois correctifs proposés (sans appliquer)
|
||||
|
||||
### Quick fix démo (5–10 min) — **passer le client en `timeout=30` pour `/replay/next`**
|
||||
|
||||
Fichier : `agent_v1/core/executor.py:1786` côté Windows.
|
||||
|
||||
Changer :
|
||||
```python
|
||||
timeout=5,
|
||||
```
|
||||
en :
|
||||
```python
|
||||
timeout=30,
|
||||
```
|
||||
|
||||
Justification : la borne dure côté serveur est déjà 180 s par action serveur ; le serveur retourne aussi `server_busy=True` au plus tard à 4.5 s. Un timeout client à 30 s laisse passer un `extract_text` de 5-10 s + dispatch d'un click sans couper la connexion. Pas d'effet de bord majeur — au pire le client attend 30 s en cas de mort serveur, déjà couvert par la backoff.
|
||||
|
||||
**Effet** :
|
||||
- Le client ne loupera plus aucun click même si extract_text/t2a_decision est devant.
|
||||
- Le bug OCR-DIRECT center-of-line reste, mais `Examens cliniques` (score 1.0) et `Codage` (autre ligne) seront correctement résolus → la maquette aiva-vision finira par s'afficher.
|
||||
- Imagerie/Notes/Synthèse cliqueront tous les trois sur le centre de la rangée (en pratique au-dessus d'Imagerie). C'est cosmétiquement faux mais **t_imagerie/t_notes/t_synthese seront tous identiques** ; il faut prévenir Amina qu'on n'aura qu'une seule lecture du DPI multi-onglets.
|
||||
|
||||
**Risque** : très bas. Modifier un seul littéral. Redéploiement SSH du fichier executor.py.
|
||||
|
||||
### Quick fix démo bis (10–15 min) — **boost OCR-DIRECT pour ne renvoyer que le centre du span matché**
|
||||
|
||||
Fichier : `agent_v0/server_v1/resolve_engine.py:1486-1519`.
|
||||
|
||||
Idée : quand le score est 0.8 (substring match) ou 0.9 (mot exact dans la ligne), recalculer `cx, cy` à partir des **bboxes des words** qui composent le `target_text`, pas de la ligne entière.
|
||||
|
||||
Pseudo-patch (à appliquer après-démo) :
|
||||
```python
|
||||
elif target_lower in line_lower:
|
||||
score = 0.8
|
||||
# Recalculer la bbox du span uniquement
|
||||
matched_words = [w for w in line_obj.words if w.value.lower() in target_lower]
|
||||
if matched_words:
|
||||
xs = [pt[0] for w in matched_words for pt in w.geometry]
|
||||
ys = [pt[1] for w in matched_words for pt in w.geometry]
|
||||
cx = (min(xs) + max(xs)) / 2
|
||||
cy = (min(ys) + max(ys)) / 2
|
||||
```
|
||||
|
||||
**Effet** : chaque tab résolu à son propre centre, plus de collision.
|
||||
|
||||
**Risque** : moyen — il faut tester avec docTR pour vérifier que les `geometry` des words sont normalisées dans le même repère que celui de la ligne. Possible que le nettoyage du substring matching soit tordu par les accents/casse. À NE PAS appliquer à la chaude pour la démo, mais pour le runner 2.
|
||||
|
||||
> **Combo conseillé pour la démo** : appliquer SEULEMENT le fix #1 (timeout 30 s). Le bug center-of-line tab fait que t_imagerie/t_notes/t_synthese seront tous = même contenu (Imagerie). Si Amina utilise déjà `t_imagerie ∪ t_notes ∪ t_synthese` dans le prompt T2A, ça reste exploitable (juste moins de variété). Le clic Codage > marchera (autre ligne docTR).
|
||||
|
||||
### Fix moyen terme (30–60 min) — **watchdog `_retry_pending` côté serveur**
|
||||
|
||||
Ajout d'une boucle background dans `api_stream.py` qui scanne `_retry_pending` toutes les 10 s et :
|
||||
- Si une action a été dispatchée il y a > 30 s sans `REPORT` → la repush en tête de queue (avec un `_resent=True` flag pour stats).
|
||||
- Émission `[BUS] lea:dispatch_orphan_resent`.
|
||||
|
||||
Justification : aujourd'hui une action perdue (timeout, kill client, déconnexion réseau) est perdue silencieusement. C'est un trou de fiabilité indépendamment de la démo GHT. Le watchdog garantit la reprise sans intervention manuelle.
|
||||
|
||||
**Risque** : moyen — il faut bien gérer la concurrence avec le client qui pourrait finalement renvoyer le report tardivement. Idempotence des reports déjà gérée dans `report_action_result` (line 3356 : `_retry_pending.pop(action_id)`), donc resend = réponse éventuelle ignorée.
|
||||
|
||||
### Fix structurel (post-démo, refonte) — **Server-Sent Events (SSE) ou WebSocket pour le push d'actions**
|
||||
|
||||
Le pattern « pull avec long poll 5 s » est intrinsèquement fragile dès que les étapes serveur sont imprévisibles. Solutions architecturales :
|
||||
|
||||
1. **SSE** (`text/event-stream`) : connexion persistante, le serveur push chaque action quand prête. Pas de timeout client à régler. Reconnexion automatique gérée nativement par EventSource. Plus simple à implémenter que WebSocket en FastAPI.
|
||||
2. **WebSocket** : full duplex, idéal pour heartbeat + actions + monitoring. Plus de code mais futur-proof.
|
||||
3. **HTTP/2 server push + chunked responses** : entre les deux. Pas standard côté requests Python.
|
||||
|
||||
Bénéfices :
|
||||
- Suppression du bug timeout pour de bon.
|
||||
- `_retry_pending` devient quasi inutile (push ack-based).
|
||||
- Réduction du trafic (pas de poll inutile ~1/s).
|
||||
- Détection immédiate de déconnexion client → déclenche pause supervisée serveur.
|
||||
|
||||
Coût : 1-2 jours dev + tests E2E.
|
||||
|
||||
---
|
||||
|
||||
## 6. Notes annexes (à nettoyer hors démo)
|
||||
|
||||
1. **Anomalie d'ancrage DB** : les ancres `anchor_0438bd2d9bdd_1778161174` (Notes médicales label) et `anchor_6a2591e7c51c_1778229076` (Synthèse Urgences label) ont des bboxes pointant un cran à gauche du tab nommé. Ce n'est pas la cause du bug (mode strict + OCR-DIRECT bypass la bbox) mais c'est trompeur en debug. À reposer en VWB record session post-démo.
|
||||
|
||||
2. **`target_text` mal-OCRisé en DB** : le champ `target_text` de l'ancre Notes médicales contient `"ine Né(e) le 14/03/1947 I 77 ans es Imagerie Notes médical J scan, echograj phie"`. C'est un OCR brut de la zone capturée — utile en debug, à ne pas confondre avec un identifiant fiable.
|
||||
|
||||
3. **Pré-check OCR post-cascade désactivé** (`RPA_ENABLE_TEXT_PRECHECK=false`) : pour la démo c'est OK. Mais à activer post-démo car il aurait peut-être attrapé le cas (clic sur centre de rangée = OCR autour ne voit pas exactement le by_text demandé). À recalibrer (radius_px et min_token_ratio) pour ne pas faux-rejeter sur les tabs à 2 tokens.
|
||||
|
||||
4. **Pas de `RESOLVE_ENTRY` dans les logs serveur du replay live pour les tabs perdus** : confirme que `/resolve_target` n'est PAS appelé tant que le client n'a pas reçu l'action. Aucun chemin caché côté serveur.
|
||||
|
||||
5. **Concurrence de polls vue à 10:26:27** : deux DISPATCHes en 0.6 s pour 2 polls quasi-simultanés. C'est cohérent avec deux requêtes en attente sur l'acquire lock + une qui retourne `server_busy` puis une qui acquiert. Le bug fundamental reste le timeout client trop court, pas la concurrence.
|
||||
|
||||
---
|
||||
|
||||
## Synthèse (≤ 400 mots)
|
||||
|
||||
Le replay s'est bloqué non pas à cause d'un échec de résolution visuelle, mais à cause d'une **désynchronisation client-serveur silencieuse**.
|
||||
|
||||
À 10:25:48, le client Léa Windows a cliqué avec succès « Examens cliniques » et reporté `success=True`. Cinq secondes plus tard (10:25:53.771), il poste un nouveau `GET /replay/next` qui timeout à 5 s — parce que côté serveur l'appel commence par `extract_text` (~5–7 s pour récupérer `t_examens` 1689 chars) puis dispatche le click `Imagerie`. Le serveur a déjà *poppé* l'action de la queue et stocké dans `_retry_pending`, mais la réponse HTTP arrive après que le client ait fermé sa socket. **L'action est perdue.** Aucun watchdog côté serveur ne la republie. Le client repasse en mode capture cosmétique mais continue à poller. Pendant 33 s, à chaque /next il aspire de l'action serveur (extract Imagerie, dispatch Notes — perdu, extract Notes, dispatch Synthèse — perdu, extract Synthèse, t2a_decision 10 s, dispatch Codage — perdu) jusqu'à recevoir directement step 18 (« Coller ou saisir le dossier patient ») qui n'existe que sur la maquette `codage.html`. Échec `target_not_found` → pause supervisée → l'utilisateur cancel.
|
||||
|
||||
C'est pour ça que `t_examens`, `t_imagerie`, `t_notes`, `t_synthese` ont tous le même contenu (1689 puis 1084 chars répétés) : l'écran n'a jamais changé d'onglet ; le DPI envoyé à T2A est mutilé.
|
||||
|
||||
Bug aggravant **indépendant** : `_resolve_by_ocr_text` (resolve_engine.py:1447) renvoie le **centre de la ligne docTR entière** quand le `target_text` est un sous-fragment (score 0.8). docTR détecte la barre de tabs comme une ligne unique → Imagerie/Notes/Synthèse renvoient tous (0.23, 0.28). Confirmé par le test e2e_singleshot du même jour à 09:58. Même si le client recevait les actions, le clic raterait la cible. Latent dès que plusieurs tokens cibles partagent la même ligne docTR.
|
||||
|
||||
**Recommandation démo** : passer le `read_timeout` client de 5 s à 30 s (`agent_v1/core/executor.py:1786`). Quick win, zéro risque, suffit pour que le pipeline aboutisse à `codage.html` et que la maquette aiva-vision se remplisse. Accepter pour la démo que les 3 tabs Imagerie/Notes/Synthèse cliqueront tous au centre de la rangée (le DPI multi-onglets sera dégradé mais le t2a_decision restera exploitable car `t_motif` et `t_motif_bas` portent l'essentiel du diagnostic).
|
||||
|
||||
**Priorité post-démo** : (1) watchdog `_retry_pending`, (2) fix OCR-DIRECT center-of-span, (3) refonte SSE/WebSocket.
|
||||
174
docs/UX_PAUSE_BUBBLES_FIX_2026-05-08.md
Normal file
174
docs/UX_PAUSE_BUBBLES_FIX_2026-05-08.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# UX Fix — bulle pause Léa (1 affichage, scroll, boutons fonctionnels)
|
||||
|
||||
**Date** : 2026-05-08 (matin, J démo GHT Sud 95)
|
||||
**Branche** : `feature/qw-suite-mai`
|
||||
**Périmètre** : `agent_v0/agent_v1/` uniquement (client Windows). Pas de modif serveur, pas de modif VWB.
|
||||
|
||||
## 1. Constat utilisateur
|
||||
|
||||
Verbatim Dom (chat avant démo) :
|
||||
|
||||
> "Pour le thinker, il y en a 3 maintenant ! un en haut à droite de l'écran, l'autre dans le chat (qui n'a pas de scroll donc message tronqué) puis dans le vwb. Je précise également que les boutons annulé sur toutes les questions ne fonctionnent pas !"
|
||||
|
||||
Quatre bugs UX simultanés au déclenchement d'un `pause_for_human` :
|
||||
|
||||
1. **Trois affichages parallèles** — toast Tkinter `paused_toast` en haut à droite, bulle dans la fenêtre chat Tkinter, popup PauseDialog dans le frontend VWB React.
|
||||
2. **Message tronqué dans la bulle chat** — pas de scroll interne, donc un `pause_message` long (>200 chars, fréquent côté serveur) déborde de la fenêtre.
|
||||
3. **Bouton "Annuler" inopérant** — clic sans effet visuel.
|
||||
4. **Bouton "Continuer" sans feedback** — pas de plainte explicite mais aucune confirmation visuelle après clic.
|
||||
|
||||
## 2. Diagnostic technique
|
||||
|
||||
### Bug 1 — 3 toasts paused_toast en parallèle
|
||||
|
||||
`grep -rn "show_paused_toast" agent_v0/agent_v1/` a remonté **trois call sites** qui se déclenchent en cascade pour la même pause :
|
||||
|
||||
| # | Fichier | Ligne | Contexte |
|
||||
|---|---|---|---|
|
||||
| (a) | `core/executor.py` | 1831 | Plan B polling — `replay_paused=True` détecté |
|
||||
| (b) | `ui/chat_window.py` | 860 | `_add_paused_bubble` appelait aussi le toast en complément |
|
||||
| (c) | `ui/notifications.py` | 156 | `notify_message(BLOCAGE)` déclenché en parallèle par `executor` (mécanisme legacy) |
|
||||
|
||||
Chacun des trois chemins force le toast topmost ⇒ Dom voit 3 popups + 1 bulle = 4 éléments UI. Confirmé dans `agent_debug.log` Windows :
|
||||
|
||||
```
|
||||
2026-05-08 10:24:37,750 [paused_toast] INFO: paused_toast scheduled on existing Tk root
|
||||
2026-05-08 10:26:45,586 [paused_toast] INFO: paused_toast scheduled on existing Tk root
|
||||
2026-05-08 10:28:46,217 [paused_toast] INFO: paused_toast scheduled on existing Tk root
|
||||
```
|
||||
|
||||
Plus la PauseDialog du VWB côté Linux (non concernée par la démo Windows mais visible si on ouvre le navigateur).
|
||||
|
||||
### Bug 2 — bulle non scrollable
|
||||
|
||||
La fenêtre chat dispose déjà d'un Canvas + Scrollbar global (`_build_messages_area`, ligne 464-507). Mais `_render_paused_bubble` n'appelait pas `_scroll_to_bottom()` après le `pack()`, donc une bulle insérée en bas de la zone restait potentiellement masquée. Et le contenu de la bulle (`reason`) était rendu via un `tk.Label` à `wraplength` fixe : pas de scroll interne pour les messages très longs (>300 chars).
|
||||
|
||||
### Bug 3 — bouton "Annuler" inopérant
|
||||
|
||||
Lecture de `_on_paused_abort` (ligne 975) :
|
||||
|
||||
```python
|
||||
self._bus.abort_replay(replay_id) # émet lea:replay_abort
|
||||
if self._active_paused_bubble:
|
||||
self._active_paused_bubble["btn_resume"].config(state="disabled")
|
||||
self._active_paused_bubble["btn_abort"].config(state="disabled")
|
||||
```
|
||||
|
||||
Le bus émet bien `lea:replay_abort`. Côté serveur (`agent_chat/app.py:1720`), le handler met `execution_status["running"] = False` et émet `lea:abort_acked`. Mais :
|
||||
- `_on_lea_event` ligne 768 ignore explicitement `lea:abort_acked` (silencieux côté UI).
|
||||
- Aucun `lea:resumed` n'est émis pour un abort (ce serait sémantiquement faux).
|
||||
- Donc `_close_active_paused_bubble` n'est jamais déclenché ⇒ la bulle reste affichée avec ses boutons disabled, sans aucun message de fermeture. Pour Dom, "rien ne se passe".
|
||||
|
||||
### Bug 4 — bouton "Continuer" sans feedback immédiat
|
||||
|
||||
Même mécanisme : le clic émet `lea:replay_resume`, le serveur relaie en POST HTTP vers le streaming server. La fermeture de la bulle ne survient qu'à la réception ultérieure de `lea:resumed` (plusieurs secondes plus tard). Pas de feedback sur le clic lui-même.
|
||||
|
||||
## 3. Solution implémentée
|
||||
|
||||
### 3.1 Un seul affichage canonique : la bulle chat
|
||||
|
||||
`core/executor.py` — Plan B simplifié :
|
||||
|
||||
```python
|
||||
# UX fix 8 mai 2026 : un seul affichage. La bulle ChatWindow EST l'affichage
|
||||
# canonique (force show + topmost + bell sonore). Plus de paused_toast en double.
|
||||
chat_window = getattr(self, "_chat_window_ref", None)
|
||||
if chat_window is not None:
|
||||
chat_window._add_paused_bubble(payload)
|
||||
else:
|
||||
# Fallback headless / tests : toast Tkinter custom
|
||||
from ..ui.paused_toast import show_paused_toast
|
||||
show_paused_toast(title=..., message=pause_msg[:300])
|
||||
```
|
||||
|
||||
`ui/chat_window.py:_add_paused_bubble` — suppression du `show_paused_toast` en complément, remplacé par un `root.bell()` natif Tkinter + `attributes("-topmost", True)` + `lift()` pour la mise au premier plan.
|
||||
|
||||
`ui/notifications.py:notify_message` — suppression du `show_paused_toast` BLOCAGE (devenu redondant). Plyer reste actif comme notification système Windows discrète.
|
||||
|
||||
### 3.2 Scroll dans la bulle pour messages longs
|
||||
|
||||
`_render_paused_bubble` — remplacement du `tk.Label` par un `tk.Text` read-only avec hauteur calculée dynamiquement (2-8 lignes selon longueur), et scrollbar interne au-delà de 280 caractères :
|
||||
|
||||
```python
|
||||
approx_lines = max(2, min(8, (len(reason_str) // 60) + 1))
|
||||
reason_text = tk.Text(msg_frame, height=approx_lines, wrap=tk.WORD, ...)
|
||||
reason_text.insert("1.0", reason_str)
|
||||
reason_text.configure(state="disabled")
|
||||
if len(reason_str) > 280:
|
||||
reason_scroll = tk.Scrollbar(msg_frame, command=reason_text.yview, ...)
|
||||
reason_text.configure(yscrollcommand=reason_scroll.set)
|
||||
```
|
||||
|
||||
Ajout d'un appel `self._scroll_to_bottom()` à la fin de `_render_paused_bubble` ET de `_render_action_bubble` pour que la bulle apparaisse toujours dans la zone visible.
|
||||
|
||||
### 3.3 Fermeture immédiate sur Annuler + feedback visuel
|
||||
|
||||
`_on_paused_abort` :
|
||||
|
||||
```python
|
||||
emitted = self._bus.abort_replay(replay_id) if (self._bus and self._bus.connected) else False
|
||||
self._disable_paused_buttons()
|
||||
self._update_paused_feedback("✗ Annulé" if emitted else "✗ Annulé (bus indisponible)")
|
||||
self._close_active_paused_bubble(reason="abort_local") # NEW : fermeture locale immédiate
|
||||
```
|
||||
|
||||
`_on_paused_resume` : même structure avec feedback `"→ Reprise demandée…"`. La bulle reste visible avec boutons disabled jusqu'à réception de `lea:resumed` qui déclenche `_close_active_paused_bubble("lea:resumed")`.
|
||||
|
||||
Helpers ajoutés : `_disable_paused_buttons()` et `_update_paused_feedback(text)`. Un `feedback_label` (label vide) est intégré dans la bulle au render et mis à jour à chaque clic.
|
||||
|
||||
## 4. Test isolé
|
||||
|
||||
Script ajouté : `agent_v0/agent_v1/tools/test_lea_pause_flow.py` (déployé `C:\rpa_vision\agent_v1\tools\`).
|
||||
|
||||
Commande exacte sur le PC Windows :
|
||||
|
||||
```cmd
|
||||
cd C:\rpa_vision
|
||||
.venv\Scripts\python.exe -m agent_v1.tools.test_lea_pause_flow
|
||||
```
|
||||
|
||||
Le script ouvre une ChatWindow, simule un `paused_need_help` avec un message de 350 chars (« Je n'arrive pas à trouver le champ Numéro de dossier patient... »), et garde la fenêtre ouverte 30s pour validation visuelle. Vérifications attendues :
|
||||
|
||||
1. **UN SEUL popup** (la bulle chat dans la fenêtre Léa, pas de toast Tkinter en plus).
|
||||
2. Message long visible avec scroll interne si débordement.
|
||||
3. Boutons Continuer / Annuler fonctionnels.
|
||||
4. Clic Annuler ⇒ bulle fermée + feedback `✗ Annulé`.
|
||||
|
||||
## 5. Tests automatisés exécutés
|
||||
|
||||
```bash
|
||||
$ pytest tests/unit/test_lea_notifications.py
|
||||
101 passed in 0.69s
|
||||
|
||||
$ pytest tests/integration/test_chat_window_templates.py tests/integration/test_feedback_bus_client.py
|
||||
35 passed
|
||||
```
|
||||
|
||||
Aucune régression. Les tests existants vérifient `notify_message(BLOCAGE)` retourne True — le retour reste True via `notify(...)` (le toast en complément a juste été retiré).
|
||||
|
||||
## 6. Déploiement Windows
|
||||
|
||||
| Fichier | MD5 Linux | MD5 Windows | Match |
|
||||
|---|---|---|---|
|
||||
| `agent_v1/ui/chat_window.py` | `50597f1f7531ab8e15fdc91e3a03e98a` | identique | OK |
|
||||
| `agent_v1/ui/notifications.py` | `8382ce3cbbc819af0e1a25fc708a0596` | identique | OK |
|
||||
| `agent_v1/core/executor.py` | `dfec3a9da28ef44019fd705404d670a5` | identique | OK |
|
||||
| `agent_v1/tools/test_lea_pause_flow.py` | `edd66b613430d10e1fce8c50f478c90c` | identique | OK |
|
||||
|
||||
Cache `__pycache__` purgé sur Windows :
|
||||
|
||||
```powershell
|
||||
Get-ChildItem -Recurse -Path C:\rpa_vision\agent_v1 -Include *.pyc | Remove-Item -Force
|
||||
Get-ChildItem -Recurse -Path C:\rpa_vision\agent_v1 -Include __pycache__ -Directory | Remove-Item -Recurse -Force
|
||||
```
|
||||
|
||||
**Action restante avant démo** : redémarrer l'agent Léa V1 sur le PC Windows (le client doit recharger les modules). Procédure standard : tray icon Léa → Quitter, puis `Win+R` → `C:\rpa_vision\start_lea.cmd` (ou équivalent dans la doc reference_windows_pc.md).
|
||||
|
||||
## 7. Synthèse fichiers modifiés
|
||||
|
||||
- `agent_v0/agent_v1/ui/chat_window.py` : `_add_paused_bubble`, `_render_paused_bubble`, `_on_paused_resume`, `_on_paused_abort` + helpers `_disable_paused_buttons`, `_update_paused_feedback`. Auto-scroll ajouté à `_render_action_bubble` aussi.
|
||||
- `agent_v0/agent_v1/ui/notifications.py` : `notify_message` — suppression du déclenchement `show_paused_toast` BLOCAGE.
|
||||
- `agent_v0/agent_v1/core/executor.py` : Plan B polling — suppression du `show_paused_toast` direct, remplacé par fallback uniquement si `_chat_window_ref` est None.
|
||||
- `agent_v0/agent_v1/tools/test_lea_pause_flow.py` : nouveau script de smoke test.
|
||||
|
||||
Aucun fichier serveur ni VWB modifié. Conforme `feedback_agent_frozen.md` (modif client validée par Dom ce matin).
|
||||
64
docs/handoffs/2026-05-08_session_audit.md
Normal file
64
docs/handoffs/2026-05-08_session_audit.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# HANDOFF — Session 2026-05-08 (clôture)
|
||||
|
||||
## 1. ÉTAT FACTUEL DU REPO
|
||||
- Branche : feature/qw-suite-mai
|
||||
- 3 commits du jour (les plus récents en HEAD) :
|
||||
- 626823d32 docs(bug): pré-check OCR spatialement aveugle
|
||||
- 2e76b44ff feat(observability): log positif pré-check OCR
|
||||
- 731b5bcae fix(replay): réactivation pré-check OCR
|
||||
- Pas de push (tous commits locaux)
|
||||
- Working directory : modifications antérieures intactes
|
||||
(à traiter dans une autre session, hors scope aujourd'hui)
|
||||
|
||||
## 2. DÉCISIONS PRISES CETTE SESSION
|
||||
- Réactivation pré-check OCR avec calibrage radius_px=280,
|
||||
min_token_ratio=0.50 (validé Dom)
|
||||
- Démo interne reportée (validé Dom le matin)
|
||||
- Migration vLLM reportée à demain matin (validé Dom)
|
||||
- Bug spatial pré-check : reporté post-démo Kerella,
|
||||
Option B préférée (validé Dom)
|
||||
- 4 contrôles débranchés sur 5 laissés en l'état pour aujourd'hui
|
||||
(audit AUDIT_CONTROLES_DEBRANCHES disponible)
|
||||
|
||||
## 3. PROBLÈMES OUVERTS
|
||||
- [P0] Bug d'échelle pixel grounding (Ollama smart_resize
|
||||
non-déterministe) — fix prévu demain matin via vLLM +
|
||||
resized_width/height explicite
|
||||
- [P1] Bug pré-check OCR spatialement aveugle — documenté,
|
||||
reporté post-démo Kerella
|
||||
- [P2] 4 contrôles débranchés à réévaluer un par un
|
||||
(SoM, mémoire visuelle, exemptions drift)
|
||||
- [P3] Démo Kerella à préparer (date à confirmer)
|
||||
|
||||
## 4. PROCHAINE ACTION CONCRÈTE (demain matin)
|
||||
Action unique : appliquer le fix smart_resize au payload vLLM
|
||||
selon docs/MIGRATION_VLM_PLAN_2026-05-09.md
|
||||
|
||||
Critère de succès : workflow rpa complet sur 1 dossier MOREL
|
||||
sans clic aberrant, latence acceptable.
|
||||
|
||||
## 5. CE QUE LE PROCHAIN CLAUDE DOIT LIRE
|
||||
- @docs/handoffs/2026-05-08_session_audit.md (ce fichier)
|
||||
- @docs/MIGRATION_VLM_PLAN_2026-05-09.md (plan de demain)
|
||||
- @docs/HISTORIQUE_VLM_IMPLEMENTATIONS_2026-05-08.md
|
||||
(vLLM + Transformers déjà câblés, ne pas réinventer)
|
||||
- @docs/CARTE_FONCTIONNELLE_2026-05-08.md (focus sections [À VÉRIFIER])
|
||||
- @docs/AUDIT_CONTROLES_DEBRANCHES_2026-05-08.md (les 4 autres
|
||||
contrôles à traiter plus tard)
|
||||
- @docs/BUG_PRECHECK_SPATIAL_BLINDNESS_2026-05-08.md
|
||||
(dette identifiée ce soir)
|
||||
|
||||
NE PAS LIRE :
|
||||
- recall.md (à archiver post-démo, obsolète)
|
||||
- MEMORY.md (saturé, à refondre post-démo)
|
||||
- ANALYSE_MOAT_RPA_VISION_V3.md / PITCH_INVESTISSEURS_*.md
|
||||
(déplacés vers archive/business_docs/)
|
||||
|
||||
## 6. RÈGLES DE LA JOURNÉE À RESPECTER DEMAIN
|
||||
- "Il faut prendre le temps d'aller vite" (maxime Dom)
|
||||
- "Rustine interdite" (maxime Dom)
|
||||
- "On lit la doc avant de faire quoi que ce soit" (maxime Dom)
|
||||
- Git destructeur : jamais en combo, atomique, validation explicite
|
||||
- Un commit = une intention
|
||||
- Datage : forcer `date '+%Y-%m-%d %H:%M %Z'` avant tout commentaire daté
|
||||
- Désactivation contrôle : flag env + log WARNING + entrée dette + revue +14j
|
||||
168
docs/handoffs/2026-05-09_session_audit.md
Normal file
168
docs/handoffs/2026-05-09_session_audit.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# HANDOFF — Session 2026-05-09 (clôture)
|
||||
|
||||
Rédigé le 2026-05-09 15:46 CEST.
|
||||
|
||||
## 1. ÉTAT FACTUEL DU REPO
|
||||
|
||||
- Branche : `feature/qw-suite-mai`
|
||||
- 10 commits du jour (les plus récents en HEAD) :
|
||||
- `df5ad5933` docs(dette): MAJ DETTE-010 (config trouvé, divergences) + création DETTE-014 (smart_resize calé sur mauvaise référence)
|
||||
- `bfbf0f9c3` refactor(grounding): centralise parser bbox_2d
|
||||
- `ecc5a233a` docs(dette): création DETTE-013 env tests dev local
|
||||
- `4df1ba577` docs(dette): création DETTE-011 bug cv2 Python 3.12
|
||||
- `0d7bcd18a` feat(grounding): module smart_resize officiel Qwen3-VL
|
||||
- `e9702b4df` docs(dette): création DETTE-010 vérif preprocessor_config Qwen3-VL
|
||||
- `293e54b4e` docs(dette): création DETTE-012 (vLLM hors scope) + maj DETTE-010 (cible Transformers + AWQ)
|
||||
- `e0b47e451` docs(refs): commit groupé docs de référence session 2026-05-08
|
||||
- `5dc20cc85` docs(dette): rectif mapping DETTE-005 + DETTE-008/009 + investigation mémoire visuelle orpheline
|
||||
- `88ed103de` docs(dette): création registre dette technique + 7 entrées rétroactives
|
||||
- Pas de push (tous commits locaux)
|
||||
- Working directory : modifications antérieures intactes
|
||||
(héritées du handoff 2026-05-08, hors scope cette session)
|
||||
- Checkpoint Qwen3-VL-8B-Instruct fp16 téléchargé localement
|
||||
(`~/.cache/huggingface/hub/models--Qwen--Qwen3-VL-8B-Instruct/`,
|
||||
17 GB, 16/16 fichiers, 0 `.incomplete`)
|
||||
- Total : 2 modules purs créés (smart_resize, bbox_parser) + 100%
|
||||
coverage (58 tests unitaires) + registre dette créé avec 14
|
||||
entrées + −96 lignes nettes sur resolve_engine.py.
|
||||
|
||||
## 2. DÉCISIONS PRISES CETTE SESSION
|
||||
|
||||
- **Cible backend grounding** : Transformers direct (pas vLLM —
|
||||
vLLM absent de la machine, infra inexistante, cf. DETTE-012).
|
||||
Cohérent avec `core/grounding/server.py` déjà câblé en
|
||||
Transformers et service systemd `rpa-grounding.service`.
|
||||
- **Modèle cible** : `Qwen/Qwen3-VL-8B-Instruct` fp16 (16 GB safetensors)
|
||||
+ chargement Transformers avec `BitsAndBytesConfig(load_in_4bit=True,
|
||||
bnb_4bit_quant_type="nf4")` à la volée (~5-6 GB VRAM runtime).
|
||||
Pas d'AWQ officiel disponible pour Qwen3-VL-8B
|
||||
(`Qwen/Qwen3-VL-8B-Instruct-AWQ` → 404 HF, repo inexistant).
|
||||
- **T2A v2 désactivé** durant la session pour libérer la VRAM
|
||||
partagée du RTX 5070 (12 GB).
|
||||
- **Strict no-op sémantique sur le refactor parser bbox_2d**
|
||||
(commit 2/5 `bfbf0f9c3`) : préservation des contrats sémantiques
|
||||
des 3 sites d'appel via paramètre `formats=` (pas seulement
|
||||
préservation comportementale).
|
||||
- **Investigation DETTE-010 en fin de journée a révélé une
|
||||
divergence majeure** : `factor` effectif probable 32 (vs 28
|
||||
hypothèse matin) car `patch_size=16` dans le `preprocessor_config.json`
|
||||
du checkpoint Qwen3-VL-8B-Instruct ; convention
|
||||
`size.longest_edge`/`shortest_edge` différente de `min_pixels`/`max_pixels`.
|
||||
→ Arrêt avant commit 3/5 du fix DETTE-006 pour reprendre demain
|
||||
matin frais avec investigation propre, plutôt que coder à
|
||||
l'aveugle sur fin de journée.
|
||||
|
||||
## 3. PROBLÈMES OUVERTS (par criticité)
|
||||
|
||||
- **[P0] DETTE-006 — Bug d'échelle pixel grounding**
|
||||
fix en cours, 2/5 commits faits (smart_resize + refactor parser),
|
||||
3/5 bloqués par DETTE-010 + DETTE-014 (alignement convention
|
||||
Qwen3-VL effective).
|
||||
- **[P1] DETTE-010 IN_PROGRESS** — investigation
|
||||
`Qwen2VLImageProcessorFast` + sémantique `longest_edge`/`shortest_edge`
|
||||
+ `patch_size=16` requise demain matin. Bloquant Étape 2
|
||||
(validation grounding isolée). Date revue : 2026-05-10.
|
||||
- **[P1] DETTE-014 OPEN** — module `core/grounding/smart_resize.py`
|
||||
calé sur `Qwen2VLImageProcessor` (factor=28, max=1_003_520) alors
|
||||
que le checkpoint utilise `Qwen2VLImageProcessorFast` avec
|
||||
`patch_size=16` (factor probable 32). À réaligner après DETTE-010.
|
||||
- **[P2] Autres dettes (P2/P3)** listées dans
|
||||
`docs/DETTE_TECHNIQUE.md` (DETTE-001 à 014) — pas urgent, revues
|
||||
par défaut +14 jours sauf P1 listées ci-dessus.
|
||||
|
||||
## 4. PROCHAINE ACTION CONCRÈTE (demain matin)
|
||||
|
||||
- **Étape A** : lire
|
||||
`transformers.models.qwen2_vl.image_processing_qwen2_vl_fast`
|
||||
pour comprendre les défauts effectifs (`min_pixels`, `max_pixels`,
|
||||
interaction `patch_size × merge_size` → `factor`) et la sémantique
|
||||
de `size.longest_edge`/`shortest_edge` (côté max ou produit total ?).
|
||||
- **Étape B** : tester le chargement runtime via
|
||||
`AutoProcessor.from_pretrained('Qwen/Qwen3-VL-8B-Instruct')` puis
|
||||
inspecter ses attributs (`processor.image_processor.size`,
|
||||
`processor.image_processor.patch_size`, etc.) pour confirmer la
|
||||
convention réelle au runtime.
|
||||
- **Étape C** : tirer la convention validée et décider de
|
||||
l'ajustement de `core/grounding/smart_resize.py` :
|
||||
- soit ajuster `FACTOR_DEFAULT` / bornes du module existant
|
||||
(et adapter les tests),
|
||||
- soit créer un nouveau module dédié Qwen3-VL si la sémantique
|
||||
diverge trop (ex: `core/grounding/smart_resize_qwen3.py`).
|
||||
- **Étape D** : commit 3/5 du fix DETTE-006 avec convention validée
|
||||
(les 4 sites de `resolve_engine.py` parseront enfin par
|
||||
`resized_w/h` au lieu de `small_w/h`).
|
||||
- **Critère de succès Étape 2** (validation grounding isolée) :
|
||||
sur fixture `data/training/live_sessions/bg_DESKTOP-58D5CAC_windows/shots/heartbeat_1773792436.png`
|
||||
(2560×1600), bouton OK localisé à `cx ≈ 0.45-0.55` (vs 0.17 le 8 mai).
|
||||
- **Critère de succès Étape 3** (replay e2e) : workflow rpa complet
|
||||
sur 1 dossier MOREL sans clic aberrant, latence acceptable.
|
||||
|
||||
## 5. CE QUE LE PROCHAIN CLAUDE DOIT LIRE (ordre)
|
||||
|
||||
- @docs/handoffs/2026-05-09_session_audit.md (ce fichier)
|
||||
- @docs/DETTE_TECHNIQUE.md (en particulier DETTE-010 et DETTE-014, P1 bloquants)
|
||||
- @/home/dom/.cache/huggingface/hub/models--Qwen--Qwen3-VL-8B-Instruct/snapshots/0c351dd01ed87e9c1b53cbc748cba10e6187ff3b/preprocessor_config.json
|
||||
(config réel du checkpoint, pivot pour DETTE-010)
|
||||
- @core/grounding/smart_resize.py (module à éventuellement réaligner)
|
||||
- @core/grounding/bbox_parser.py (commit 2/5 OK strict no-op, à conserver)
|
||||
- @docs/MIGRATION_VLM_PLAN_2026-05-09.md (plan original toujours
|
||||
valide pour la suite, mais cible backend ajustée :
|
||||
Transformers direct, pas vLLM)
|
||||
- @docs/handoffs/2026-05-08_session_audit.md (handoff veille,
|
||||
pour contexte si besoin)
|
||||
|
||||
## 6. RÈGLES DE LA JOURNÉE À RESPECTER DEMAIN
|
||||
|
||||
- Maximes Dom : « il faut prendre le temps d'aller vite »,
|
||||
« rustine interdite », « on lit la doc avant de faire quoi que ce soit ».
|
||||
- **Investigation infra** : ne jamais conclure à l'absence d'un
|
||||
composant sans avoir vérifié N endroits possibles
|
||||
(PATH, autres venvs, conda, Docker, système, autres caches).
|
||||
Leçon DETTE-012 vLLM : conclusion hâtive « stack vLLM absente »
|
||||
sur fouille partielle, rectifié par Dom. Présenter les
|
||||
vérifications négatives comme partielles et demander
|
||||
confirmation, pas comme une absence définitive.
|
||||
- **Refactor de code mature** : préserver les contrats sémantiques
|
||||
en plus des comportements techniques. Leçon refactor parser
|
||||
Occ 3 + Occ 4 : strict no-op via paramètre `formats=` plutôt
|
||||
que centralisation laxe qui élargit silencieusement les formats
|
||||
acceptés.
|
||||
- **Validation explicite par étape**, stop pour relecture diff
|
||||
sur tout code prod. Pas d'enchaînement A→B→C sans GO entre les
|
||||
étapes structurantes.
|
||||
- **Datage** : forcer `date '+%Y-%m-%d %H:%M %Z'` avant tout
|
||||
commentaire daté.
|
||||
- **Désactivation contrôle** : flag env + log WARNING + entrée
|
||||
dette + revue +14j (et registre central
|
||||
`docs/DETTE_TECHNIQUE.md`).
|
||||
- **Git destructeur** : jamais en combo, atomique, validation
|
||||
explicite.
|
||||
- **Un commit = une intention** (respecté sur les 10 commits du jour).
|
||||
|
||||
## 7. NOTES POUR LA PROCHAINE SESSION
|
||||
|
||||
- **Action pré-prochaine session — hygiène git** : commit
|
||||
`chore(docs): déplacement business_docs vers archive/`
|
||||
pour consolider en 1 commit la suppression à la racine
|
||||
(ANALYSE_MOAT_RPA_VISION_V3.md, PITCH_INVESTISSEURS_RPA_VISION_V3.md
|
||||
→ `D` dans `git status`) et l'ajout dans `archive/business_docs/`
|
||||
(encore en `?? archive/`). Pas de DETTE associée, juste hygiène.
|
||||
- **Mécanisme de délestage VRAM rpa_v3** : à documenter dans le
|
||||
module concerné (path à retrouver), trace de fonctionnalité
|
||||
transversale.
|
||||
- **Bug cv2 (DETTE-011) + env tests (DETTE-013)** : la batterie
|
||||
`pytest tests/unit/` entière n'est pas exécutable en dev local
|
||||
sans configuration spécifique (`RPA_API_TOKEN` ou
|
||||
`RPA_AUTH_DISABLED` + version cv2 compatible Python 3.12).
|
||||
Prévoir une session dédiée pour rétablir l'exécution complète
|
||||
ou découpler les tests purs des tests qui chargent
|
||||
`agent_v0.server_v1.api_stream`.
|
||||
- **Audit exhaustif modules orphelins** (au-delà périmètre AUDIT
|
||||
serveur) : mentionné dans
|
||||
`docs/INVESTIGATION_MEMOIRE_VISUELLE_ORPHELINE_2026-05-09.md` §6,
|
||||
dette identifiée sans numéro réservé. À instruire post-démo
|
||||
Kerella.
|
||||
- **DETTE-010 + DETTE-014 sont la première chose à débloquer
|
||||
demain matin**. Sans cet alignement, le commit 3/5 du fix
|
||||
DETTE-006 et l'Étape 2 (validation grounding isolée) sont
|
||||
impossibles à instruire correctement.
|
||||
267
tests/unit/test_bbox_parser.py
Normal file
267
tests/unit/test_bbox_parser.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
Tests unitaires pour core.grounding.bbox_parser.
|
||||
|
||||
Module pur, indépendant de resolve_engine.py / agent_v0 / cv2.
|
||||
|
||||
Plan :
|
||||
- A. Format 1 (bbox_2d) — 4 cas
|
||||
- B. Format 2 (x/y JSON) — 3 cas
|
||||
- C. Format 3 (x_pct/y_pct) — 1 cas
|
||||
- D. Format 4 (array brut) — 3 cas
|
||||
- E. Cascade et edge cases — 4 cas
|
||||
- E-bis. Filtre formats= (parse_bbox_to_norm + parse_bbox_to_norm_validated) — 4 cas
|
||||
- F. parse_bbox_to_norm_validated — 5 cas
|
||||
- G. Type retour — 2 cas
|
||||
|
||||
Total : 26 cas.
|
||||
"""
|
||||
|
||||
from core.grounding.bbox_parser import (
|
||||
parse_bbox_to_norm,
|
||||
parse_bbox_to_norm_validated,
|
||||
)
|
||||
|
||||
|
||||
# Dimensions de référence pour les divisions
|
||||
W = 2560
|
||||
H = 1600
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# A. Format 1 — bbox_2d
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestFormat1Bbox2d:
|
||||
def test_2_coords_point(self):
|
||||
# bbox_2d [x, y] : pixels divisés
|
||||
content = '{"bbox_2d": [1280, 800], "label": "btn"}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 1280 / W
|
||||
assert y == 800 / H
|
||||
|
||||
def test_4_coords_rect_center(self):
|
||||
# bbox_2d [x1, y1, x2, y2] : centre du rect divisé
|
||||
content = '{"bbox_2d": [100, 200, 300, 400]}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == (100 + 300) / 2 / W
|
||||
assert y == (200 + 400) / 2 / H
|
||||
|
||||
def test_floats(self):
|
||||
content = '{"bbox_2d": [1280.5, 800.25, 1300.0, 850.0]}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == (1280.5 + 1300.0) / 2 / W
|
||||
assert y == (800.25 + 850.0) / 2 / H
|
||||
|
||||
def test_5_coords_uses_first_4(self):
|
||||
# >= 4 coords : prend les 4 premières comme rect (comportement original)
|
||||
content = '{"bbox_2d": [10, 20, 30, 40, 99]}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == (10 + 30) / 2 / W
|
||||
assert y == (20 + 40) / 2 / H
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# B. Format 2 — x/y JSON
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestFormat2XYJson:
|
||||
def test_pixels_x_above_1(self):
|
||||
# x > 1 → divise par W (heuristique pixels)
|
||||
content = '{"x": 1280, "y": 800}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 1280 / W
|
||||
assert y == 800 / H
|
||||
|
||||
def test_already_pct_x_below_1(self):
|
||||
# x <= 1 → considéré comme déjà normalisé, pas de division
|
||||
content = '{"x": 0.5, "y": 0.3}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 0.5
|
||||
assert y == 0.3
|
||||
|
||||
def test_x_exactly_1_treated_as_pct(self):
|
||||
# x == 1 : x > 1 est False → traité comme pct (non divisé)
|
||||
# Comportement original Occ 1+2 — fige la limite.
|
||||
content = '{"x": 1.0, "y": 1.0}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 1.0
|
||||
assert y == 1.0
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# C. Format 3 — x_pct/y_pct
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestFormat3XPctYPct:
|
||||
def test_already_normalized(self):
|
||||
content = '{"x_pct": 0.42, "y_pct": 0.68}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 0.42
|
||||
assert y == 0.68
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# D. Format 4 — array brut
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestFormat4RawArray:
|
||||
def test_2_coords(self):
|
||||
content = "[1280, 800]"
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 1280 / W
|
||||
assert y == 800 / H
|
||||
|
||||
def test_4_coords(self):
|
||||
content = "[100, 200, 300, 400]"
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == (100 + 300) / 2 / W
|
||||
assert y == (200 + 400) / 2 / H
|
||||
|
||||
def test_floats(self):
|
||||
content = "[100.5, 200.25, 300.0, 400.75]"
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == (100.5 + 300.0) / 2 / W
|
||||
assert y == (200.25 + 400.75) / 2 / H
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# E. Cascade et edge cases
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestCascadeAndEdge:
|
||||
def test_bbox_2d_priority_over_array(self):
|
||||
# bbox_2d présent ET array brut présent → bbox_2d gagne (testé en premier)
|
||||
content = '{"bbox_2d": [10, 20], "extra": [9999, 9999]}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x == 10 / W
|
||||
assert y == 20 / H
|
||||
|
||||
def test_empty_content_returns_none(self):
|
||||
x, y = parse_bbox_to_norm("", W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_no_match_returns_none(self):
|
||||
# Texte ne contenant aucun format reconnu
|
||||
x, y = parse_bbox_to_norm("Sorry, I cannot locate this element.", W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_malformed_json_no_coords(self):
|
||||
# JSON sans coordonnées
|
||||
content = '{"label": "ok", "confidence": 0.9}'
|
||||
x, y = parse_bbox_to_norm(content, W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# E-bis. Filtre formats= (paramètre kwarg)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestFormatsFilter:
|
||||
def test_formats_xy_json_only_excludes_bbox_2d(self):
|
||||
# bbox_2d présent dans le content, mais formats=xy_json seul.
|
||||
# Avec ce filtre : bbox_2d skipped, raw_array skipped, xy_json
|
||||
# ne matche pas (regex `"x"\s*:` ne capture pas `"bbox_2d"`).
|
||||
# → (None, None) confirmé.
|
||||
content = '{"bbox_2d": [10, 20]}'
|
||||
x, y = parse_bbox_to_norm(content, W, H, formats={"xy_json"})
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_formats_xy_json_and_raw_array_excludes_xy_pct(self):
|
||||
# Sous-ensemble Occ 3 : restreint à xy_json + raw_array.
|
||||
# Content avec x_pct/y_pct uniquement → format 3 filtré, autres
|
||||
# ne matchent pas → (None, None).
|
||||
content = '{"x_pct": 0.42, "y_pct": 0.68}'
|
||||
x, y = parse_bbox_to_norm(
|
||||
content, W, H, formats={"xy_json", "raw_array"}
|
||||
)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# F. parse_bbox_to_norm_validated
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestValidated:
|
||||
def test_inside_domain_returns_value(self):
|
||||
# x_pct, y_pct ∈ [0, 1] → valeurs retournées
|
||||
content = '{"x_pct": 0.42, "y_pct": 0.68}'
|
||||
x, y = parse_bbox_to_norm_validated(content, W, H)
|
||||
assert x == 0.42
|
||||
assert y == 0.68
|
||||
|
||||
def test_x_negative_returns_none(self):
|
||||
content = '{"x_pct": -0.1, "y_pct": 0.5}'
|
||||
x, y = parse_bbox_to_norm_validated(content, W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_x_above_1_returns_none(self):
|
||||
# bbox_2d en pixels > divisor → x_pct > 1
|
||||
content = '{"bbox_2d": [9999, 800]}'
|
||||
x, y = parse_bbox_to_norm_validated(content, W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_y_out_of_range_returns_none(self):
|
||||
content = '{"x_pct": 0.5, "y_pct": 1.5}'
|
||||
x, y = parse_bbox_to_norm_validated(content, W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_no_parse_returns_none(self):
|
||||
x, y = parse_bbox_to_norm_validated("nope", W, H)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
def test_validated_formats_bbox_2d_only_valid(self):
|
||||
# Sous-ensemble Occ 4 : restreint à bbox_2d, validation [0, 1].
|
||||
# bbox_2d 4-coords valide → coordonnées normalisées dans le domaine.
|
||||
content = '{"bbox_2d": [100, 200, 300, 400]}'
|
||||
x, y = parse_bbox_to_norm_validated(
|
||||
content, W, H, formats={"bbox_2d"}
|
||||
)
|
||||
assert x == (100 + 300) / 2 / W
|
||||
assert y == (200 + 400) / 2 / H
|
||||
|
||||
def test_validated_formats_bbox_2d_only_excludes_xy_json(self):
|
||||
# Sous-ensemble Occ 4 : si le VLM retourne {"x":..., "y":...}
|
||||
# au lieu du bbox_2d demandé, le format est filtré → (None, None).
|
||||
content = '{"x": 1280, "y": 800}'
|
||||
x, y = parse_bbox_to_norm_validated(
|
||||
content, W, H, formats={"bbox_2d"}
|
||||
)
|
||||
assert x is None
|
||||
assert y is None
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# G. Type retour
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestReturnType:
|
||||
def test_tuple_of_two(self):
|
||||
result = parse_bbox_to_norm('{"bbox_2d": [10, 20]}', W, H)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_floats_or_none(self):
|
||||
x, y = parse_bbox_to_norm('{"bbox_2d": [10, 20]}', W, H)
|
||||
assert isinstance(x, float)
|
||||
assert isinstance(y, float)
|
||||
|
||||
x_none, y_none = parse_bbox_to_norm("nope", W, H)
|
||||
assert x_none is None
|
||||
assert y_none is None
|
||||
234
tests/unit/test_smart_resize.py
Normal file
234
tests/unit/test_smart_resize.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Tests unitaires pour core.grounding.smart_resize.
|
||||
|
||||
Référence : transformers.models.qwen2_vl.image_processing_qwen2_vl.smart_resize
|
||||
(transformers 4.57.3). Module image-only (pas de vidéo).
|
||||
|
||||
Plan de tests :
|
||||
- A. Constantes module-level (3 cas)
|
||||
- B. _round_by_factor (8 cas — focus banker's rounding)
|
||||
- C. _floor_by_factor (4 cas)
|
||||
- D. _ceil_by_factor (4 cas)
|
||||
- E. smart_resize public (11 cas, incluant golden bench 8 mai et E.11 limite)
|
||||
- F. smart_resize compat server.py via paramètres explicites (2 cas)
|
||||
|
||||
Total : 32 cas.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from core.grounding.smart_resize import (
|
||||
FACTOR_DEFAULT,
|
||||
MAX_PIXELS_DEFAULT,
|
||||
MAX_RATIO_DEFAULT,
|
||||
MIN_PIXELS_DEFAULT,
|
||||
_ceil_by_factor,
|
||||
_floor_by_factor,
|
||||
_round_by_factor,
|
||||
smart_resize,
|
||||
)
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# A. Constantes module-level
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestConstants:
|
||||
def test_factor_default_is_28(self):
|
||||
assert FACTOR_DEFAULT == 28
|
||||
|
||||
def test_min_pixels_default_is_3136(self):
|
||||
# 56 * 56 — défaut transformers Qwen2VLImageProcessor
|
||||
assert MIN_PIXELS_DEFAULT == 3136
|
||||
|
||||
def test_max_pixels_default_is_1_003_520(self):
|
||||
# 14 * 14 * 4 * 1280 — défaut transformers Qwen2VLImageProcessor
|
||||
# (utilisé par Qwen3VLProcessor pour les images)
|
||||
assert MAX_PIXELS_DEFAULT == 1_003_520
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# B. _round_by_factor — focus banker's rounding (round-half-to-even)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestRoundByFactor:
|
||||
def test_zero(self):
|
||||
assert _round_by_factor(0, 28) == 0
|
||||
|
||||
def test_half_below_factor_rounds_to_zero(self):
|
||||
# 14/28 = 0.5 → banker round vers pair (0)
|
||||
assert _round_by_factor(14, 28) == 0
|
||||
|
||||
def test_just_above_half_rounds_up(self):
|
||||
# 15/28 ≈ 0.535 → 1 → 28
|
||||
assert _round_by_factor(15, 28) == 28
|
||||
|
||||
def test_exact_factor(self):
|
||||
assert _round_by_factor(28, 28) == 28
|
||||
|
||||
def test_one_and_half_factor_banker(self):
|
||||
# 42/28 = 1.5 → banker round vers pair (2) → 56
|
||||
assert _round_by_factor(42, 28) == 56
|
||||
|
||||
def test_two_and_half_factor_banker(self):
|
||||
# 70/28 = 2.5 → banker round vers pair (2) → 56
|
||||
assert _round_by_factor(70, 28) == 56
|
||||
|
||||
def test_three_and_half_factor_banker(self):
|
||||
# 98/28 = 3.5 → banker round vers pair (4) → 112
|
||||
assert _round_by_factor(98, 28) == 112
|
||||
|
||||
def test_fourteen_and_half_factor_banker(self):
|
||||
# 406/28 = 14.5 → banker round vers pair (14) → 392
|
||||
# Piège classique du round Python — fige le comportement.
|
||||
assert _round_by_factor(406, 28) == 392
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# C. _floor_by_factor
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestFloorByFactor:
|
||||
def test_zero(self):
|
||||
assert _floor_by_factor(0, 28) == 0
|
||||
|
||||
def test_below_factor_floors_to_zero(self):
|
||||
assert _floor_by_factor(27, 28) == 0
|
||||
|
||||
def test_exact_factor(self):
|
||||
assert _floor_by_factor(28, 28) == 28
|
||||
|
||||
def test_just_below_two_factor(self):
|
||||
assert _floor_by_factor(55, 28) == 28
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# D. _ceil_by_factor
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestCeilByFactor:
|
||||
def test_zero(self):
|
||||
assert _ceil_by_factor(0, 28) == 0
|
||||
|
||||
def test_one_ceils_to_factor(self):
|
||||
assert _ceil_by_factor(1, 28) == 28
|
||||
|
||||
def test_exact_factor(self):
|
||||
assert _ceil_by_factor(28, 28) == 28
|
||||
|
||||
def test_just_above_factor(self):
|
||||
assert _ceil_by_factor(29, 28) == 56
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# E. smart_resize — API publique
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestSmartResizePublic:
|
||||
def test_idempotence_square(self):
|
||||
# Image déjà multiple de 28, dans bornes : retour identique.
|
||||
assert smart_resize(280, 280) == (280, 280)
|
||||
|
||||
def test_idempotence_rectangle(self):
|
||||
# 560*1120 = 627_200 ∈ [3136, 1_003_520] et tous deux multiples de 28.
|
||||
assert smart_resize(560, 1120) == (560, 1120)
|
||||
|
||||
def test_round_down(self):
|
||||
# 290/28 ≈ 10.357 → round = 10 → 280
|
||||
assert smart_resize(290, 290) == (280, 280)
|
||||
|
||||
def test_round_up(self):
|
||||
# 295/28 ≈ 10.535 → round = 11 → 308
|
||||
assert smart_resize(295, 295) == (308, 308)
|
||||
|
||||
def test_golden_bench_8_mai(self):
|
||||
# Fixture bench du 8 mai : 2560×1600 (heartbeat_1773792436.png).
|
||||
# h=1600, w=2560, defaults officiels Qwen3-VL image (max=1_003_520).
|
||||
# h_bar_init=1596, w_bar_init=2548 ; produit=4_066_608 > max
|
||||
# → resize down via beta = sqrt(4_096_000/1_003_520) ≈ 2.0203
|
||||
# → h_bar=floor(1600/beta/28)*28 = 28*28 = 784
|
||||
# → w_bar=floor(2560/beta/28)*28 = 45*28 = 1260
|
||||
# → 784*1260 = 987_840 ≤ 1_003_520 ✓
|
||||
assert smart_resize(1600, 2560) == (784, 1260)
|
||||
|
||||
def test_clamp_min_pixels(self):
|
||||
# 28*28 = 784 < 3136 → resize up.
|
||||
h, w = smart_resize(28, 28)
|
||||
assert h * w >= MIN_PIXELS_DEFAULT
|
||||
assert h % FACTOR_DEFAULT == 0
|
||||
assert w % FACTOR_DEFAULT == 0
|
||||
|
||||
def test_clamp_max_pixels(self):
|
||||
# 8000*8000 = 64M >> 1_003_520 → resize down.
|
||||
h, w = smart_resize(8000, 8000)
|
||||
assert h * w <= MAX_PIXELS_DEFAULT
|
||||
assert h % FACTOR_DEFAULT == 0
|
||||
assert w % FACTOR_DEFAULT == 0
|
||||
|
||||
def test_extreme_ratio_raises(self):
|
||||
# ratio = 5601/28 ≈ 200.04 > 200 → ValueError.
|
||||
with pytest.raises(ValueError):
|
||||
smart_resize(28, 5601)
|
||||
|
||||
def test_ratio_at_limit_passes(self):
|
||||
# ratio = 5600/28 = 200 exactement → ne lève pas (limite incluse).
|
||||
result = smart_resize(28, 5600)
|
||||
assert isinstance(result, tuple)
|
||||
|
||||
def test_return_type(self):
|
||||
result = smart_resize(560, 1120)
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(x, int) for x in result)
|
||||
|
||||
def test_e11_very_small_image_clamped_up_to_min_pixels(self):
|
||||
"""Très petite image : comportement défini par la formule officielle.
|
||||
|
||||
Hypothèse initiale (lors de la conception du module 2026-05-09) :
|
||||
images avec h*w < min_pixels ET h<factor pourraient produire
|
||||
ZeroDivisionError ou résultat indéfini (h_bar=0 dans step 2 init).
|
||||
|
||||
Vérification TDD : la formule officielle gère proprement ce cas via
|
||||
la branche `< min_pixels` qui rescale upward avec beta = sqrt(min/h*w).
|
||||
Pour (10, 10) : beta=5.6, h_bar = ceil(10 * 5.6 / 28) * 28 = 56.
|
||||
|
||||
Ce test fige le comportement réel et documente que l'hypothèse
|
||||
initiale était trop défensive. Aucune limite mathématique connue
|
||||
sur les petites images dans le domaine factor=28, min_pixels=3136.
|
||||
"""
|
||||
result = smart_resize(10, 10)
|
||||
assert result == (56, 56)
|
||||
h_bar, w_bar = result
|
||||
assert h_bar * w_bar >= MIN_PIXELS_DEFAULT
|
||||
assert h_bar % FACTOR_DEFAULT == 0
|
||||
assert w_bar % FACTOR_DEFAULT == 0
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# F. smart_resize — compat server.py via paramètres explicites
|
||||
# =====================================================================
|
||||
|
||||
|
||||
class TestSmartResizeServerCompat:
|
||||
def test_bench_8_mai_with_server_bounds(self):
|
||||
# Avec defaults server.py prod : min=78400, max=4_390_400.
|
||||
# h_bar_init=1596, w_bar_init=2548 ; produit=4_066_608 ≤ 4_390_400
|
||||
# → pas de rescale → (1596, 2548)
|
||||
assert smart_resize(
|
||||
1600, 2560, min_pixels=78_400, max_pixels=4_390_400
|
||||
) == (1596, 2548)
|
||||
|
||||
def test_large_image_with_server_bounds(self):
|
||||
# Avec defaults server.py serrés (max=4_390_400) : 2560×2560 = 6.55M > max.
|
||||
# → resize down sous le clamp serré.
|
||||
h, w = smart_resize(
|
||||
2560, 2560, min_pixels=78_400, max_pixels=4_390_400
|
||||
)
|
||||
assert h * w <= 4_390_400
|
||||
assert h % FACTOR_DEFAULT == 0
|
||||
assert w % FACTOR_DEFAULT == 0
|
||||
Reference in New Issue
Block a user