feat: premier replay E2E + mode apprentissage supervisé

Premier replay fonctionnel de bout en bout (Bloc-notes, Chrome).

Corrections critiques :
- Fix double-lancement agent (Lea.bat start /b + verrou PID)
- Sérialisation replay (threading.Lock dans poll_and_execute)
- Garde UIA bbox >50% écran (rejet conteneurs "Bureau")
- Filtre fenêtres bruit système (systray overflow)
- Auto-nettoyage replays bloqués (paused_need_help)

Cascade visuelle complète dans session_cleaner :
- UIA local (10ms) → template matching (100ms) → serveur docTR/VLM
- Nettoyage bureau pré-replay (clic "Afficher le bureau")
- Crops 80x80 + vlm_description pour chaque clic

Grounding contraint à la fenêtre active :
- Capture croppée à la fenêtre au lieu de l'écran entier
- Conversion coordonnées fenêtre → écran
- Élimine les faux positifs taskbar/systray

Mode apprentissage supervisé (SUPERVISE → capture humaine) :
- Léa passe en mode capture quand elle est perdue
- Capture mini-workflow humain (clics + frappes + combos)
- Fin par Ctrl+Shift+L ou timeout inactivité 10s
- Correction stockée dans target_memory.db via serveur

Deploy Windows complet (grounding.py, policy.py, uia_helper.py).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-13 07:42:50 +02:00
parent 816b37af98
commit 33c198b827
12 changed files with 1561 additions and 60 deletions

View File

@@ -488,6 +488,8 @@ class ReplayResultReport(BaseModel):
# Champs enrichis pour target_not_found (pause supervisée)
target_description: Optional[str] = None # Description humaine de la cible
target_spec: Optional[Dict[str, Any]] = None # Spec complete de la cible
# Correction humaine (mode apprentissage supervisé)
correction: Optional[Dict[str, Any]] = None # {x_pct, y_pct, uia_snapshot, crop_b64}
class ErrorCallbackConfig(BaseModel):
@@ -1883,6 +1885,26 @@ async def start_raw_replay(request: RawReplayRequest):
resolved_machine_id = target_machine_id or (session_obj.machine_id if session_obj else "default")
with _replay_lock:
# ── Nettoyage : annuler les replays bloqués pour cette machine ──
# Un replay en paused_need_help bloque tous les suivants.
# Quand on lance un nouveau replay, les anciens sont obsolètes.
stale_ids = [
rid for rid, state in _replay_states.items()
if state.get("machine_id") == resolved_machine_id
and state["status"] in ("paused_need_help", "running")
]
for rid in stale_ids:
old_state = _replay_states[rid]
old_sid = old_state.get("session_id", "")
old_state["status"] = "cancelled"
# Vider la queue associée
if old_sid in _replay_queues:
_replay_queues.pop(old_sid, None)
logger.info(
f"Replay {rid} annulé (remplacé par {replay_id}) — "
f"était {old_state.get('completed_actions', 0)}/{old_state.get('total_actions', 0)}"
)
_replay_queues[session_id] = list(actions)
_replay_states[replay_id] = _create_replay_state(
replay_id=replay_id,
@@ -3032,6 +3054,26 @@ async def report_action_result(report: ReplayResultReport):
except Exception as e:
logger.debug(f"Learning: échec enregistrement: {e}")
# === Correction humaine (mode apprentissage supervisé) ===
# L'humain a montré à Léa où cliquer. On stocke cette correction
# dans target_memory pour que la prochaine fois, Léa sache toute seule.
if report.correction and original_action:
try:
corr = report.correction
target_spec = original_action.get("target_spec", {})
logger.info(
f"[APPRENTISSAGE] Correction humaine reçue : "
f"({corr.get('x_pct', 0):.4f}, {corr.get('y_pct', 0):.4f}) "
f"pour '{target_spec.get('by_text', '?')}'"
)
_replay_learner.record_human_correction(
session_id=session_id,
action=original_action,
correction=corr,
)
except Exception as e:
logger.warning(f"Learning: échec stockage correction humaine: {e}")
# === Audit Trail : traçabilité complète pour conformité hospitalière ===
try:
_action = original_action or {"action_id": action_id, "type": "unknown"}