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"}

View File

@@ -175,6 +175,55 @@ class ReplayLearner:
self.record(outcome)
def record_human_correction(
self,
session_id: str,
action: Dict[str, Any],
correction: Dict[str, Any],
) -> None:
"""Enregistrer une correction humaine (mode apprentissage supervisé).
L'humain a montré à Léa où cliquer. On stocke cette correction
dans target_memory.db pour que la prochaine fois, Léa sache.
"""
target_spec = action.get("target_spec", {})
by_text = target_spec.get("by_text", "")
window_title = target_spec.get("window_title", "")
x_pct = correction.get("x_pct", 0.0)
y_pct = correction.get("y_pct", 0.0)
# Enregistrer dans le JSONL d'apprentissage
outcome = ActionOutcome(
session_id=session_id,
action_id=action.get("action_id", ""),
action_type="click",
target_description=by_text,
window_title=window_title,
resolution_method="human_supervised",
resolution_score=1.0, # Confiance maximale — l'humain a montré
success=True,
)
self.record(outcome)
# Stocker dans target_memory.db pour le lookup futur
try:
from .replay_memory import get_target_memory_store
store = get_target_memory_store()
if store:
store.record_success(
screen_signature="human_correction",
target_spec=target_spec,
resolved_position={"x_pct": x_pct, "y_pct": y_pct},
method="human_supervised",
score=1.0,
)
logger.info(
f"[APPRENTISSAGE] Correction stockée dans target_memory : "
f"'{by_text}' → ({x_pct:.4f}, {y_pct:.4f})"
)
except Exception as e:
logger.warning(f"Learning: échec stockage target_memory: {e}")
def query_similar(
self,
target_description: str = "",