Compare commits
36 Commits
3ed9798f06
...
feat/push-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c371c9775f | ||
|
|
931cf13217 | ||
|
|
fd9efdbbf5 | ||
|
|
19187e633e | ||
|
|
9a34ecded6 | ||
|
|
bd1c9d2c8a | ||
|
|
6907ecc82f | ||
|
|
7dd5c872df | ||
|
|
bb1ea42318 | ||
|
|
b062e2cca7 | ||
|
|
4cb173a8ec | ||
|
|
882e4e1f3a | ||
|
|
cac965cef9 | ||
|
|
ebed4d7546 | ||
|
|
9a8242add5 | ||
|
|
f9a0531325 | ||
|
|
ab78ae390a | ||
|
|
e59489e2cd | ||
|
|
86e31ada34 | ||
|
|
94fd93ad19 | ||
|
|
50f34b5727 | ||
|
|
a1b3062991 | ||
|
|
a210e5ee32 | ||
|
|
5d235e49f1 | ||
|
|
e679804cfd | ||
|
|
e57b54a100 | ||
|
|
d34c1f2697 | ||
|
|
61664c9a36 | ||
|
|
9ab5ed4671 | ||
|
|
144a5c288a | ||
|
|
e3f61de4ad | ||
|
|
2a1b1ed80e | ||
|
|
f09b8b8cfd | ||
|
|
6a78a0059b | ||
|
|
813b33b47e | ||
|
|
a50057d499 |
32
.gitignore
vendored
32
.gitignore
vendored
@@ -121,12 +121,22 @@ results_vlm_bench.json
|
||||
# Scripts locaux one-shot d'intervention/bench, non réutilisables tels quels.
|
||||
tools/bench_qwen35_evidence.py
|
||||
tools/codex_windows_correction_rapport.py
|
||||
|
||||
tools/diagnostic_lea_chat_win11.ps1
|
||||
tools/poc_lecture_ecran.py
|
||||
tools/watch_emilie_agent.py
|
||||
test_sanitizer_live.py
|
||||
# Verbatims clients (sensibles, à valider avant push)
|
||||
docs/clients/
|
||||
|
||||
.qw-baseline.log
|
||||
# Coordination ephemeral — inbox messages, active decisions, loop state
|
||||
docs/coordination/.loop_state/
|
||||
docs/coordination/.inbox_baseline.txt
|
||||
docs/coordination/.loop_log.txt
|
||||
docs/coordination/inbox_qwen/
|
||||
docs/coordination/inbox_codex/
|
||||
docs/coordination/inbox_claude/
|
||||
docs/coordination/active/
|
||||
|
||||
# Runtime Python embedded pour l'installateur Inno Setup (local, ~11M, non versionné)
|
||||
deploy/installer/python-3.12-embed/
|
||||
@@ -134,3 +144,23 @@ deploy/installer/python-3.12.8-embed-amd64.zip
|
||||
# Artefacts de build installateur (EXE compilés + staging) — non versionnés
|
||||
deploy/releases/*.exe
|
||||
deploy/build/
|
||||
# Embed tgz working (37M, local build artifact)
|
||||
deploy/installer/lea_python_embed_working.tgz
|
||||
|
||||
# Agent/Codex state (local, session-specific)
|
||||
.agents/
|
||||
.codex/
|
||||
agent_chat/state/
|
||||
|
||||
# Graphify tool + generated output (1.2G)
|
||||
graphify/
|
||||
graphify-out/
|
||||
|
||||
# Local PostScript artifact (webbrowser = 11M DSC)
|
||||
webbrowser
|
||||
|
||||
# Bench predictions (generated, not source)
|
||||
benchmarks/computer_use/predictions/
|
||||
|
||||
# DB backups (instance level, runtime artifact)
|
||||
**/instance/*.db.bak*
|
||||
|
||||
12
AGENTS.md
12
AGENTS.md
@@ -10,3 +10,15 @@ Rules:
|
||||
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
|
||||
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
|
||||
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
||||
|
||||
## coordination watcher
|
||||
|
||||
At the beginning of every session, the coordination watcher is mandatory and must be operational for Codex, Claude, and Qwen before coordination work continues.
|
||||
|
||||
Session-start checklist:
|
||||
- Run `docs/coordination/coordination_loop.sh ensure`.
|
||||
- Read every pending message relevant to the current agent.
|
||||
- After messages are processed, run `docs/coordination/coordination_loop.sh ack`.
|
||||
- If the watcher cannot be started or checked, report that blocker immediately in the handoff/status response.
|
||||
|
||||
Every new handoff or restart prompt must include this watcher requirement by default.
|
||||
|
||||
@@ -10,7 +10,9 @@ Tu n'es pas en autonomie. Dom valide avant chaque étape. Tu proposes, il décid
|
||||
|
||||
## Priorité absolue
|
||||
|
||||
**La démo Urgence_aiva_demo doit fonctionner.** Workflow 22+ steps sur Easily Assure, patiente MOREL Catherine, audience mixte DG/DSI/médecins/DIM/TIM. Tout arbitrage technique se tranche par : "est-ce que ça rapproche ou éloigne de la démo qui tourne ?"
|
||||
**Le POC clinique Wallerstein doit tourner.** 5 postes Léa live ; les TIM travaillent sur leurs **vrais logiciels métier en mode web** (navigateur intégré au logiciel / navigateur du PC, instances **RDP** et **Citrix**), sur **2 écrans** → capture de la **fenêtre active**. Objectif produit : Léa **apprend** ces parcours et les **rejoue intelligemment** (pas du record-and-replay). Tout arbitrage technique se tranche par : « est-ce que ça rapproche ou éloigne du POC clinique qui tourne ? »
|
||||
|
||||
> Historique : `Urgence_aiva_demo` (22+ steps) sur la **maquette Easily Assure** (patiente fictive MOREL Catherine) était le banc de démo/test — **maquette abandonnée comme cible** (recadrage Dom 2026-06-25). Ne plus raisonner « Easily ».
|
||||
|
||||
## Méthode obligatoire — non négociable
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ if platform.system() == "Windows":
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
AGENT_VERSION = os.environ.get("RPA_AGENT_VERSION", "1.0.1")
|
||||
AGENT_VERSION = os.environ.get("RPA_AGENT_VERSION", "1.0.2")
|
||||
|
||||
# Identifiant unique de la machine (utilisé pour le multi-machine)
|
||||
# Configurable via variable d'environnement, sinon auto-généré depuis hostname + OS
|
||||
@@ -93,6 +93,27 @@ LOG_SHIP_ENABLED = os.environ.get("RPA_LOG_SHIP_ENABLED", "false").lower() in (
|
||||
# Intervalle de flush du buffer de logs (secondes).
|
||||
LOG_SHIP_INTERVAL_S = float(os.environ.get("RPA_LOG_SHIP_INTERVAL_S", "30"))
|
||||
|
||||
# Mise à jour silencieuse du client Léa (DETTE-022 v2).
|
||||
# Le client interroge le serveur (GET /api/v1/agents/update/check), télécharge
|
||||
# le ZIP en staging et vérifie le SHA256. Le SWAP réel des fichiers / l'édition
|
||||
# de Lea.bat / le redémarrage restent RÉSERVÉS RÉVISION HUMAINE (voir
|
||||
# network/updater.py : stubs apply_update / write_boot_ok_marker).
|
||||
# Défaut PRUDENT = désactivé : activé poste par poste via config.txt / variable
|
||||
# d'environnement, sans rebuild de l'installateur (même esprit que LOG_SHIP).
|
||||
AUTO_UPDATE_ENABLED = os.environ.get("RPA_AUTO_UPDATE_ENABLED", "false").lower() in (
|
||||
"true", "1", "yes", "on",
|
||||
)
|
||||
# Intervalle entre deux interrogations serveur pour une MAJ (secondes).
|
||||
# Défaut 1 h : une MAJ n'est jamais urgente ; on interroge peu pour ne pas
|
||||
# charger le réseau clinique. Le check ne fait de toute façon aucun swap.
|
||||
AUTO_UPDATE_INTERVAL_S = float(os.environ.get("RPA_AUTO_UPDATE_INTERVAL_S", "3600"))
|
||||
# Dossier de STAGING des ZIP d'update (jamais les fichiers vivants). Équivalent
|
||||
# de `Lea_next\\`. Sous LOCALAPPDATA en prod Windows, sinon à côté de l'agent.
|
||||
AUTO_UPDATE_STAGING_DIR = os.environ.get(
|
||||
"RPA_AUTO_UPDATE_STAGING_DIR",
|
||||
str(BASE_DIR / "_update_staging"),
|
||||
)
|
||||
|
||||
# Monitoring
|
||||
PERF_MONITOR_INTERVAL_S = 30
|
||||
LOGS_DIR = BASE_DIR / "logs"
|
||||
|
||||
@@ -18,6 +18,7 @@ from .config import (
|
||||
SESSIONS_ROOT, AGENT_VERSION, SERVER_URL, MACHINE_ID, LOG_RETENTION_DAYS, LOG_FILE,
|
||||
SCREEN_RESOLUTION, DPI_SCALE, OS_THEME, API_TOKEN, MAX_SESSION_DURATION_S,
|
||||
STREAMING_ENDPOINT, LOG_SHIP_ENABLED, LOG_SHIP_INTERVAL_S,
|
||||
AUTO_UPDATE_ENABLED, AUTO_UPDATE_INTERVAL_S, AUTO_UPDATE_STAGING_DIR,
|
||||
)
|
||||
from .core.captor import EventCaptorV1
|
||||
from .core.executor import ActionExecutorV1
|
||||
@@ -158,6 +159,31 @@ class AgentV1:
|
||||
threading.Thread(target=self._replay_poll_loop, daemon=True).start()
|
||||
threading.Thread(target=self._background_heartbeat_loop, daemon=True).start()
|
||||
|
||||
# DETTE-022 v2 : MAJ silencieuse — boucle de check GATED (défaut OFF).
|
||||
# Interroge le serveur (canary-aware) et télécharge en STAGING ; le swap
|
||||
# réel reste réservé révision humaine (updater.apply_update = stub no-op).
|
||||
# Activable poste par poste via RPA_AUTO_UPDATE_ENABLED, sans rebuild.
|
||||
if AUTO_UPDATE_ENABLED:
|
||||
threading.Thread(
|
||||
target=self._auto_update_loop, daemon=True, name="lea-auto-update"
|
||||
).start()
|
||||
|
||||
# MAJ silencieuse — confirmation de boot post-swap. Si Lea.bat vient
|
||||
# d'appliquer une MAJ (marqueur PENDING_BOOT), on désarme le rollback
|
||||
# après ~90 s de tourne STABLE (liveness LOCALE, indépendante du DGX).
|
||||
# Un quit propre avant 90 s confirme aussi (cf. main()). Seul un vrai
|
||||
# crash laisse PENDING_BOOT → rollback au prochain lancement.
|
||||
if _pending_boot_marker_exists():
|
||||
def _boot_confirm():
|
||||
import os as _os
|
||||
import time as _time
|
||||
_time.sleep(float(_os.environ.get("RPA_BOOT_CONFIRM_DELAY_S", "90")))
|
||||
if self.running:
|
||||
_confirm_boot_ok()
|
||||
threading.Thread(
|
||||
target=_boot_confirm, daemon=True, name="lea-boot-confirm"
|
||||
).start()
|
||||
|
||||
# Mini-serveur HTTP pour captures a la demande (port 5006)
|
||||
self._capture_server = CaptureServer()
|
||||
self._capture_server.start()
|
||||
@@ -441,6 +467,67 @@ class AgentV1:
|
||||
logger.debug(f"[HEARTBEAT] Erreur: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def _auto_update_loop(self):
|
||||
"""DETTE-022 v2 — boucle de MAJ silencieuse GATED (défaut OFF).
|
||||
|
||||
Interroge périodiquement le serveur (endpoint canary-aware), et si une
|
||||
MAJ est proposée pour CE poste, la télécharge dans le STAGING après
|
||||
vérif SHA256. Le swap réel N'EST PAS fait ici : `updater.run_update_cycle`
|
||||
s'arrête au staging (apply_update = stub réservé révision humaine + swap
|
||||
hors-process par Lea.bat au prochain démarrage).
|
||||
|
||||
SÉCURITÉ — « au bon moment » : on NE stage PAS pendant un enregistrement
|
||||
ou un replay actif (self.session_id / self._replay_active), pour ne pas
|
||||
perturber le travail utilisateur ni consommer du réseau au mauvais
|
||||
moment. Best-effort : aucune exception ne remonte (ne casse jamais Léa).
|
||||
"""
|
||||
try:
|
||||
from .network.updater import run_update_cycle
|
||||
except Exception as e:
|
||||
logger.warning("[UPDATE] Module updater indisponible : %s", e)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"[UPDATE] Boucle MAJ silencieuse démarrée (intervalle=%.0fs, "
|
||||
"version=%s) — check seul, swap réservé révision humaine",
|
||||
AUTO_UPDATE_INTERVAL_S, AGENT_VERSION,
|
||||
)
|
||||
|
||||
while self.running:
|
||||
# Découpe l'attente pour réagir vite à l'arrêt.
|
||||
waited = 0.0
|
||||
step = 1.0
|
||||
while self.running and waited < AUTO_UPDATE_INTERVAL_S:
|
||||
time.sleep(step)
|
||||
waited += step
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
# « Au bon moment » : jamais en plein travail (enregistrement/replay).
|
||||
if self.session_id or getattr(self, "_replay_active", False):
|
||||
logger.debug("[UPDATE] Report du check (session/replay active)")
|
||||
continue
|
||||
|
||||
try:
|
||||
result = run_update_cycle(
|
||||
local_version=AGENT_VERSION,
|
||||
machine_id=self.machine_id,
|
||||
staging_dir=AUTO_UPDATE_STAGING_DIR,
|
||||
)
|
||||
status = result.get("status")
|
||||
if status == "staged":
|
||||
logger.info(
|
||||
"[UPDATE] MAJ %s téléchargée en staging (SHA256=%s) — "
|
||||
"swap réservé révision humaine, non appliqué",
|
||||
result.get("target_version"),
|
||||
result.get("sha256_verified"),
|
||||
)
|
||||
elif status not in ("up_to_date", "disabled"):
|
||||
logger.debug("[UPDATE] Cycle: %s", result)
|
||||
except Exception as e:
|
||||
# run_update_cycle est déjà best-effort ; double filet ici.
|
||||
logger.debug("[UPDATE] Erreur boucle MAJ : %s", e)
|
||||
|
||||
def stop_session(self):
|
||||
# Sauvegarder le session_id avant de l'annuler (pour les logs)
|
||||
ended_session_id = self.session_id
|
||||
@@ -607,29 +694,20 @@ class AgentV1:
|
||||
def run(self):
|
||||
self.ui.run()
|
||||
|
||||
def _headless_keepalive(agent):
|
||||
"""Maintient le main thread vivant quand l'UI tray ne peut pas tourner.
|
||||
def _install_signal_handlers(agent, watchdog) -> None:
|
||||
"""Installe SIGTERM/SIGINT/SIGBREAK pour un arrêt propre du main thread.
|
||||
|
||||
Sans cela, ``agent.run()`` retourne immédiatement (pystray échoue quand
|
||||
Léa est lancée via SSH sans session interactive Windows), le main thread
|
||||
se termine, et TOUS les daemon threads — y compris ``_replay_poll_loop``
|
||||
— meurent avec lui. Observé 3 fois en 24h les 24/05 :
|
||||
- SSH ``Permission denied`` (1231)
|
||||
- polls morts après relance distante (1620)
|
||||
- polls morts ``replay_sess_506d6fa2`` (1627)
|
||||
|
||||
Le keepalive ne se déclenche QUE si ``agent.run()`` est sorti tout en
|
||||
laissant ``agent.running=True`` (cas anormal). En mode interactif
|
||||
normal, ``pystray.Icon.run()`` ne sort jamais, donc ce code est
|
||||
invisible.
|
||||
Met ``agent.running=False`` (les daemon threads s'arrêtent) et réveille
|
||||
le watchdog (qui sort de sa boucle de surveillance). Sans session
|
||||
interactive (pystray.Icon.stop indisponible), c'est le SEUL moyen
|
||||
d'arrêter Léa proprement : ``kill -TERM <pid>`` ou Ctrl+C.
|
||||
"""
|
||||
import signal as _sig
|
||||
_stop = threading.Event()
|
||||
|
||||
def _handler(sig, frame):
|
||||
logger.info(f"[MAIN] Signal {sig} recu — arret propre")
|
||||
_stop.set()
|
||||
agent.running = False
|
||||
watchdog.stop()
|
||||
|
||||
for sig_name in ("SIGTERM", "SIGINT", "SIGBREAK"):
|
||||
sig_obj = getattr(_sig, sig_name, None)
|
||||
@@ -640,33 +718,78 @@ def _headless_keepalive(agent):
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
"[MAIN] Keepalive headless actif — main thread bloque pour maintenir "
|
||||
"les daemon threads (_replay_poll_loop, heartbeat, capture_server) vivants. "
|
||||
"Pour stopper Lea : kill -TERM <pid> ou Ctrl+C."
|
||||
)
|
||||
|
||||
def _agent_should_live(agent) -> bool:
|
||||
"""Vrai tant que Léa doit vivre : agent actif ET pas de Quitter explicite.
|
||||
|
||||
Un « Quitter » utilisateur (``ui._quit_requested``) doit stopper le
|
||||
watchdog pour de bon ; une simple déconnexion RDP ne met JAMAIS ce flag
|
||||
→ le tray revient tout seul à la reconnexion.
|
||||
"""
|
||||
if not getattr(agent, "running", False):
|
||||
return False
|
||||
ui = getattr(agent, "ui", None)
|
||||
if ui is not None and getattr(ui, "_quit_requested", False):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _pending_boot_marker_exists() -> bool:
|
||||
"""True si Lea.bat a posé PENDING_BOOT (boot post-MAJ à valider)."""
|
||||
try:
|
||||
_stop.wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
agent.running = False
|
||||
logger.info("[MAIN] Keepalive termine — agent.running=False, daemon threads vont s'arreter")
|
||||
from .network.updater import _resolve_app_dir
|
||||
return (_resolve_app_dir(None) / "PENDING_BOOT").exists()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _confirm_boot_ok() -> None:
|
||||
"""Confirme un boot post-MAJ : écrit boot_ok + retire PENDING_BOOT.
|
||||
|
||||
Désarme le rollback de Lea.bat. No-op si pas de PENDING_BOOT (boot normal).
|
||||
Best-effort — ne doit jamais casser l'arrêt/la vie de Léa.
|
||||
"""
|
||||
try:
|
||||
if not _pending_boot_marker_exists():
|
||||
return
|
||||
from .network import updater
|
||||
updater.write_boot_ok_marker(AGENT_VERSION)
|
||||
logger.info("[MAJ] Boot confirmé (v%s) — rollback désarmé", AGENT_VERSION)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.debug("confirm_boot_ok: %s", e)
|
||||
|
||||
|
||||
def main():
|
||||
agent = AgentV1()
|
||||
try:
|
||||
agent.run()
|
||||
except Exception:
|
||||
logger.exception("[MAIN] agent.run() a leve une exception")
|
||||
from .ui.session_watchdog import InteractiveSessionWatchdog
|
||||
|
||||
if getattr(agent, "running", False):
|
||||
logger.warning(
|
||||
"[MAIN] agent.run() est sorti mais agent.running=True — "
|
||||
"probablement pystray sans session interactive (SSH). "
|
||||
"Bascule en keepalive headless."
|
||||
)
|
||||
_headless_keepalive(agent)
|
||||
agent = AgentV1()
|
||||
|
||||
# Résilience RDP/Citrix : au lieu de bloquer le main thread pour toujours
|
||||
# quand pystray sort (session interactive perdue), on surveille la
|
||||
# session et on ré-affiche le tray + le chat à chaque reconnexion.
|
||||
# agent.run() (== agent.ui.run()) est ré-entrant : les threads de fond
|
||||
# ne démarrent qu'une fois, seule l'icône est recréée. Les daemon threads
|
||||
# de capture/heartbeat/replay tournent contre agent.running et restent
|
||||
# uniques — le watchdog n'y touche pas.
|
||||
watchdog = InteractiveSessionWatchdog(
|
||||
run_ui=agent.run,
|
||||
is_running=lambda: _agent_should_live(agent),
|
||||
)
|
||||
_install_signal_handlers(agent, watchdog)
|
||||
|
||||
try:
|
||||
watchdog.run()
|
||||
# Sortie normale du watchdog = quit propre (tray / session) → le boot
|
||||
# était sain : on confirme (couvre un quit AVANT les 90 s, évite un faux
|
||||
# rollback). No-op si ce n'est pas un boot post-MAJ.
|
||||
_confirm_boot_ok()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("[MAIN] Interruption clavier — arret propre")
|
||||
except Exception:
|
||||
logger.exception("[MAIN] Le watchdog de session a leve une exception")
|
||||
finally:
|
||||
agent.running = False
|
||||
logger.info("[MAIN] Sortie — agent.running=False, daemon threads vont s'arreter")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
481
agent_v0/agent_v1/network/updater.py
Normal file
481
agent_v0/agent_v1/network/updater.py
Normal file
@@ -0,0 +1,481 @@
|
||||
# agent_v1/network/updater.py
|
||||
"""NOYAU client de la mise à jour silencieuse de Léa (DETTE-022 v2).
|
||||
|
||||
GATED — flag `RPA_AUTO_UPDATE_ENABLED` (défaut OFF). Tant qu'il est OFF,
|
||||
rien ne se déclenche : l'intégration de ce module au runtime (boucle de poll
|
||||
de `main.py`) ne fait aucune MAJ.
|
||||
|
||||
Ce module ne contient que les parties PURES / testables, sans réseau réel :
|
||||
|
||||
- `parse_version` / `is_newer` (R3) : self-contained (le bundle client
|
||||
n'embarque PAS `server_v1` — duplication assumée, même algorithme).
|
||||
- `should_update(local_version, server_response)` : décide « faut-il
|
||||
updater ? quelle version/type ? » à partir de la réponse serveur. Double
|
||||
garde semver côté client (jamais de downgrade) = défense en profondeur.
|
||||
- `download_update(plan, staging_dir, downloader)` : télécharge le ZIP via un
|
||||
`downloader` callable INJECTABLE (aucun réseau réel en test), vérifie le
|
||||
SHA256, écrit le ZIP dans le **staging** (`Lea_next\\`-like) — JAMAIS dans
|
||||
les fichiers vivants. Retourne un plan d'application.
|
||||
- `auto_update_enabled()` : lit le flag (défaut OFF).
|
||||
|
||||
⚠️ SWAP — répartition claire des responsabilités :
|
||||
`apply_update` / `write_boot_ok_marker` ci-dessous ne font que l'ARMEMENT côté
|
||||
Python (extraction vers `agent_v1_new/` + marqueurs) — ils n'écrasent JAMAIS un
|
||||
fichier vivant. Le remplacement ATOMIQUE (renames), le redémarrage et le
|
||||
rollback sont faits HORS-PROCESS par `Lea.bat` au démarrage (revu ligne à ligne).
|
||||
|
||||
Pattern d'import / résilience aligné sur `log_shipper.py` (même branche).
|
||||
|
||||
Branche feat/push-log-dgx.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Niveaux de livraison (R2). `code-only` par défaut = 99 % des MAJ (~500 Ko).
|
||||
VALID_UPDATE_TYPES = ("code-only", "full")
|
||||
DEFAULT_UPDATE_TYPE = "code-only"
|
||||
|
||||
_FALLBACK_VERSION: Tuple[int, ...] = (0,)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flag d'activation — OFF par défaut (lu à chaque appel pour faciliter tests)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def auto_update_enabled() -> bool:
|
||||
"""True si la MAJ auto client est activée (flag RPA_AUTO_UPDATE_ENABLED).
|
||||
|
||||
Défaut PRUDENT = OFF. On l'active poste par poste via config.txt / variable
|
||||
d'environnement, sans rebuild de l'installateur (même esprit que
|
||||
LOG_SHIP_ENABLED).
|
||||
"""
|
||||
return os.environ.get("RPA_AUTO_UPDATE_ENABLED", "false").lower() in (
|
||||
"true", "1", "yes", "on",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# R3 — parse_version self-contained (le bundle client n'a pas server_v1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def parse_version(v) -> Tuple[int, ...]:
|
||||
"""Parse une version semver en tuple d'entiers. Voir server_v1/update_check.
|
||||
|
||||
"1.0.2" → (1, 0, 2) ; "1.0.10" → (1, 0, 10) ; "v1.2.3" → (1, 2, 3).
|
||||
Tolérant et SANS exception : invalide → fallback `(0,)`.
|
||||
"""
|
||||
if not isinstance(v, str):
|
||||
return _FALLBACK_VERSION
|
||||
s = v.strip().lstrip("vV").strip()
|
||||
if not s:
|
||||
return _FALLBACK_VERSION
|
||||
try:
|
||||
from packaging.version import Version
|
||||
|
||||
return tuple(Version(s).release)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return tuple(int(x) for x in s.split("."))
|
||||
except (ValueError, AttributeError):
|
||||
return _FALLBACK_VERSION
|
||||
|
||||
|
||||
def is_newer(candidate: str, baseline: str) -> bool:
|
||||
"""True si `candidate` strictement plus récent que `baseline` (semver)."""
|
||||
return parse_version(candidate) > parse_version(baseline)
|
||||
|
||||
|
||||
def _normalize_update_type(update_type) -> str:
|
||||
if update_type in VALID_UPDATE_TYPES:
|
||||
return update_type
|
||||
return DEFAULT_UPDATE_TYPE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Décision client : faut-il updater ?
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def should_update(local_version: str, server_response) -> Optional[dict]:
|
||||
"""Décide à partir de la réponse serveur s'il faut updater.
|
||||
|
||||
Args:
|
||||
local_version : version courante du client (config.AGENT_VERSION).
|
||||
server_response : dict renvoyé par l'endpoint serveur
|
||||
{update_available, latest_version, update_type, url, [sha256]}.
|
||||
|
||||
Returns:
|
||||
Un PLAN d'update `{target_version, update_type, url, sha256}` si une MAJ
|
||||
valide est à faire, sinon None.
|
||||
|
||||
Défense en profondeur : même si `update_available` est True, le client
|
||||
REVÉRIFIE en semver (`is_newer`) — il ne descend JAMAIS vers une version
|
||||
<= locale. Tolérant : réponse malformée → None (jamais d'exception).
|
||||
"""
|
||||
if not isinstance(server_response, dict):
|
||||
return None
|
||||
if not server_response.get("update_available"):
|
||||
return None
|
||||
|
||||
target = server_response.get("latest_version")
|
||||
url = server_response.get("url")
|
||||
if not target or not url:
|
||||
return None
|
||||
|
||||
# Double garde semver : pas de downgrade, pas d'égalité.
|
||||
if not is_newer(target, local_version):
|
||||
return None
|
||||
|
||||
return {
|
||||
"target_version": target,
|
||||
"update_type": _normalize_update_type(server_response.get("update_type")),
|
||||
"url": url,
|
||||
"sha256": server_response.get("sha256"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Téléchargement — downloader INJECTABLE, SHA256, staging only
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _default_downloader(url: str) -> bytes:
|
||||
"""Téléchargement réel du ZIP (best-effort, pattern streamer/log_shipper).
|
||||
|
||||
Résout l'URL relative contre SERVER_BASE, ajoute le Bearer si présent.
|
||||
INJECTABLE : remplacé par un fake en test (aucun réseau réel).
|
||||
"""
|
||||
import requests # import tardif (absent de certains envs de test)
|
||||
|
||||
full_url = url
|
||||
headers = {}
|
||||
try:
|
||||
from ..config import SERVER_BASE, API_TOKEN
|
||||
|
||||
if url.startswith("/"):
|
||||
full_url = f"{SERVER_BASE}{url}"
|
||||
if API_TOKEN:
|
||||
headers["Authorization"] = f"Bearer {API_TOKEN}"
|
||||
except Exception:
|
||||
# Hors package (test isolé) : on utilise l'URL telle quelle.
|
||||
pass
|
||||
|
||||
resp = requests.get(full_url, headers=headers, timeout=30, stream=False)
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
|
||||
|
||||
def download_update(
|
||||
plan: dict,
|
||||
staging_dir,
|
||||
downloader: Optional[Callable[[str], bytes]] = None,
|
||||
) -> dict:
|
||||
"""Télécharge le ZIP d'update dans le staging et vérifie son intégrité.
|
||||
|
||||
NE TOUCHE PAS aux fichiers vivants : écrit uniquement dans `staging_dir`
|
||||
(équivalent de `Lea_next\\`). L'application réelle (swap) est un stub
|
||||
réservé révision humaine (voir `apply_update`).
|
||||
|
||||
Args:
|
||||
plan : sortie de `should_update` (target_version, update_type, url, sha256).
|
||||
staging_dir : dossier de staging (créé si absent).
|
||||
downloader : callable `(url) -> bytes` INJECTABLE (défaut = HTTP réel).
|
||||
|
||||
Returns:
|
||||
Succès : {ok: True, staged_zip: str, update_type, target_version,
|
||||
sha256_verified: bool}
|
||||
Échec : {ok: False, error: str}
|
||||
Best-effort : aucune exception ne remonte ; un échec laisse le staging propre
|
||||
(pas de ZIP corrompu).
|
||||
"""
|
||||
dl = downloader if downloader is not None else _default_downloader
|
||||
staging = Path(staging_dir)
|
||||
|
||||
try:
|
||||
data = dl(plan["url"])
|
||||
except Exception as e:
|
||||
logger.warning("Téléchargement update échoué : %s", e)
|
||||
return {"ok": False, "error": f"download_failed: {e}"}
|
||||
|
||||
expected_sha = (plan.get("sha256") or "").strip().lower()
|
||||
sha256_verified = False
|
||||
if expected_sha:
|
||||
actual = hashlib.sha256(data).hexdigest()
|
||||
if actual != expected_sha:
|
||||
logger.warning(
|
||||
"SHA256 mismatch update (attendu=%s, obtenu=%s) — rejeté",
|
||||
expected_sha, actual,
|
||||
)
|
||||
return {"ok": False, "error": "sha256 mismatch — ZIP rejeté"}
|
||||
sha256_verified = True
|
||||
else:
|
||||
# Best-effort : pas de SHA fourni → on accepte mais on le signale.
|
||||
logger.info("Pas de SHA256 fourni pour l'update — intégrité non vérifiée")
|
||||
|
||||
try:
|
||||
staging.mkdir(parents=True, exist_ok=True)
|
||||
target_version = plan.get("target_version", "unknown")
|
||||
staged_zip = staging / f"lea_update_{target_version}.zip"
|
||||
staged_zip.write_bytes(data)
|
||||
except Exception as e:
|
||||
logger.warning("Écriture ZIP staging échouée : %s", e)
|
||||
return {"ok": False, "error": f"staging_write_failed: {e}"}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"staged_zip": str(staged_zip),
|
||||
"update_type": _normalize_update_type(plan.get("update_type")),
|
||||
"target_version": plan.get("target_version"),
|
||||
"sha256_verified": sha256_verified,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interrogation serveur — checker INJECTABLE (GET /agents/update/check)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _default_update_checker(local_version: str, machine_id: str):
|
||||
"""Interroge le serveur : y a-t-il une MAJ ? (best-effort, INJECTABLE).
|
||||
|
||||
GET SERVER_URL/agents/update/check?current_version=..&machine_id=..
|
||||
(endpoint gated côté serveur — 503 si RPA_AUTO_UPDATE_SERVER_ENABLED OFF,
|
||||
auquel cas on renvoie None : pas de MAJ). Bearer si présent. Pattern aligné
|
||||
sur `log_shipper._default_sender`. INJECTABLE : remplacé par un fake en test.
|
||||
|
||||
Returns:
|
||||
Le dict réponse serveur (`should_update` sait le lire), ou None si
|
||||
indisponible / gated / erreur (jamais d'exception ne remonte).
|
||||
"""
|
||||
try:
|
||||
import requests # import tardif
|
||||
|
||||
headers = {}
|
||||
try:
|
||||
from ..config import SERVER_URL, API_TOKEN
|
||||
|
||||
base = SERVER_URL
|
||||
if API_TOKEN:
|
||||
headers["Authorization"] = f"Bearer {API_TOKEN}"
|
||||
except Exception:
|
||||
base = ""
|
||||
url = f"{base}/agents/update/check"
|
||||
resp = requests.get(
|
||||
url,
|
||||
params={"current_version": local_version, "machine_id": machine_id},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
allow_redirects=False,
|
||||
)
|
||||
# 503 = endpoint gated OFF côté serveur → pas de MAJ (silencieux).
|
||||
if resp.status_code == 503:
|
||||
return None
|
||||
if not resp.ok:
|
||||
logger.debug("update/check HTTP %s", resp.status_code)
|
||||
return None
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
logger.debug("update/check indisponible : %s", e)
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Orchestrateur GATED — check → décide → download (staging) → stub apply
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_update_cycle(
|
||||
local_version: str,
|
||||
machine_id: str,
|
||||
staging_dir,
|
||||
checker: Optional[Callable[[str, str], object]] = None,
|
||||
downloader: Optional[Callable[[str], bytes]] = None,
|
||||
app_dir=None,
|
||||
) -> dict:
|
||||
"""Un cycle complet de MAJ silencieuse — GATED, best-effort, SANS swap.
|
||||
|
||||
Enchaîne :
|
||||
1. GATE `auto_update_enabled()` (RPA_AUTO_UPDATE_ENABLED, défaut OFF) —
|
||||
si OFF, ne fait STRICTEMENT rien (aucun appel réseau).
|
||||
2. `checker(local_version, machine_id)` → réponse serveur (canary-aware).
|
||||
3. `should_update(...)` → plan (double garde semver, jamais de downgrade).
|
||||
4. `download_update(...)` → ZIP dans le STAGING + vérif SHA256. Ne touche
|
||||
JAMAIS les fichiers vivants.
|
||||
5. `apply_update` ARME le swap (extraction `agent_v1_new/` + marqueur
|
||||
UPDATE_READY) mais NE swappe PAS : le remplacement atomique + le
|
||||
redémarrage sont faits par Lea.bat au prochain démarrage. `applied`
|
||||
reste False tant que Léa n'a pas redémarré sur la nouvelle version.
|
||||
|
||||
Jamais d'exception ne remonte (ne doit JAMAIS casser Léa). Retourne un dict
|
||||
d'état pour le diagnostic / le log :
|
||||
status ∈ {disabled, check_failed, up_to_date, download_failed, staged}
|
||||
|
||||
Args:
|
||||
checker : callable `(local_version, machine_id) -> dict|None`
|
||||
INJECTABLE (défaut = HTTP réel vers l'endpoint gated).
|
||||
downloader : callable `(url) -> bytes` INJECTABLE (défaut = HTTP réel).
|
||||
"""
|
||||
if not auto_update_enabled():
|
||||
return {"status": "disabled", "applied": False}
|
||||
|
||||
chk = checker if checker is not None else _default_update_checker
|
||||
|
||||
try:
|
||||
server_response = chk(local_version, machine_id)
|
||||
except Exception as e:
|
||||
logger.warning("update check a levé : %s", e)
|
||||
return {"status": "check_failed", "applied": False, "error": str(e)}
|
||||
|
||||
plan = should_update(local_version, server_response)
|
||||
if plan is None:
|
||||
return {"status": "up_to_date", "applied": False}
|
||||
|
||||
staged = download_update(plan, staging_dir, downloader=downloader)
|
||||
if not staged.get("ok"):
|
||||
return {
|
||||
"status": "download_failed",
|
||||
"applied": False,
|
||||
"error": staged.get("error"),
|
||||
}
|
||||
|
||||
# Armement du swap : extraction du ZIP vers agent_v1_new\ + marqueur
|
||||
# UPDATE_READY. Le swap ATOMIQUE (renames) et le redémarrage sont faits
|
||||
# HORS-PROCESS par Lea.bat au prochain démarrage — JAMAIS depuis ici
|
||||
# (on n'écrase pas les fichiers d'un Léa en cours d'exécution).
|
||||
armed = apply_update(staged, app_dir=app_dir)
|
||||
|
||||
return {
|
||||
"status": "armed" if armed.get("armed") else "arm_failed",
|
||||
"applied": False, # le swap effectif est fait par Lea.bat, pas ici
|
||||
"armed": bool(armed.get("armed", False)),
|
||||
"target_version": staged.get("target_version"),
|
||||
"update_type": staged.get("update_type"),
|
||||
"staged_zip": staged.get("staged_zip"),
|
||||
"sha256_verified": staged.get("sha256_verified", False),
|
||||
"marker": armed.get("marker"),
|
||||
"error": armed.get("error"),
|
||||
}
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# SWAP — côté Python : ARMEMENT SEULEMENT (extraction + marqueurs).
|
||||
# Le remplacement ATOMIQUE des fichiers vivants + le redémarrage + le
|
||||
# rollback sont faits HORS-PROCESS par `Lea.bat` au démarrage (renames).
|
||||
# Python n'écrase JAMAIS les fichiers d'un Léa en cours d'exécution.
|
||||
# ===========================================================================
|
||||
|
||||
def _resolve_app_dir(app_dir) -> Path:
|
||||
"""Répertoire d'install (contient `agent_v1/`, `run_agent_v1.py`, `Lea.bat`).
|
||||
|
||||
INJECTABLE (tests : tmp_path). Défaut = parent du package agent_v1.
|
||||
"""
|
||||
if app_dir is not None:
|
||||
return Path(app_dir)
|
||||
try:
|
||||
from ..config import BASE_DIR # BASE_DIR = dossier du package agent_v1
|
||||
return Path(BASE_DIR).parent
|
||||
except Exception:
|
||||
return Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def apply_update(prepared: dict, app_dir=None) -> dict:
|
||||
"""ARME le swap : extrait le ZIP staging vers `agent_v1_new/` + marqueur.
|
||||
|
||||
NE swappe PAS et NE redémarre PAS (c'est le rôle de `Lea.bat`). Écrit
|
||||
uniquement à côté des fichiers vivants (dossier neuf + marqueur), donc
|
||||
l'opération est sûre même sur un Léa en cours d'exécution.
|
||||
|
||||
1. Extrait `prepared["staged_zip"]` → `<app_dir>/agent_v1_new/`
|
||||
(nettoyé au préalable ; garde-fou zip-slip).
|
||||
2. Écrit `<app_dir>/UPDATE_READY` (JSON : version, type, chemins) que
|
||||
`Lea.bat` lira au prochain démarrage pour faire le swap atomique.
|
||||
|
||||
Best-effort : aucune exception ne remonte (ne doit jamais casser Léa).
|
||||
|
||||
Returns:
|
||||
succès : {armed: True, applied: False, target_version, update_type,
|
||||
marker, extracted_to}
|
||||
échec : {armed: False, applied: False, error}
|
||||
"""
|
||||
if not isinstance(prepared, dict):
|
||||
return {"armed": False, "applied": False, "error": "prepared invalide"}
|
||||
staged_zip = prepared.get("staged_zip")
|
||||
target_version = prepared.get("target_version", "unknown")
|
||||
update_type = _normalize_update_type(prepared.get("update_type"))
|
||||
try:
|
||||
root = _resolve_app_dir(app_dir)
|
||||
zip_path = Path(staged_zip) if staged_zip else None
|
||||
if zip_path is None or not zip_path.is_file():
|
||||
return {"armed": False, "applied": False, "error": "ZIP staging introuvable"}
|
||||
|
||||
new_dir = root / "agent_v1_new"
|
||||
if new_dir.exists():
|
||||
shutil.rmtree(new_dir, ignore_errors=True) # nettoie un staging partiel
|
||||
new_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
import zipfile
|
||||
new_root = new_dir.resolve()
|
||||
with zipfile.ZipFile(zip_path) as zf:
|
||||
for name in zf.namelist(): # garde-fou zip-slip (chemins ../)
|
||||
dest = (new_dir / name).resolve()
|
||||
if not str(dest).startswith(str(new_root)):
|
||||
shutil.rmtree(new_dir, ignore_errors=True)
|
||||
return {"armed": False, "applied": False,
|
||||
"error": f"zip-slip refusé : {name}"}
|
||||
zf.extractall(new_dir)
|
||||
|
||||
marker = root / "UPDATE_READY"
|
||||
marker.write_text(json.dumps({
|
||||
"target_version": target_version,
|
||||
"update_type": update_type,
|
||||
"extracted_to": str(new_dir),
|
||||
"staged_zip": str(zip_path),
|
||||
}), encoding="utf-8")
|
||||
|
||||
logger.info(
|
||||
"Update ARMÉ : %s (%s) → %s ; swap au prochain démarrage (Lea.bat)",
|
||||
target_version, update_type, new_dir,
|
||||
)
|
||||
return {"armed": True, "applied": False, "target_version": target_version,
|
||||
"update_type": update_type, "marker": str(marker),
|
||||
"extracted_to": str(new_dir)}
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("apply_update (armement) a échoué : %s", e)
|
||||
return {"armed": False, "applied": False, "error": f"arm_failed: {e}"}
|
||||
|
||||
|
||||
def write_boot_ok_marker(version: str, app_dir=None) -> dict:
|
||||
"""Confirme un boot sain : écrit `boot_ok_{version}` + désarme le rollback.
|
||||
|
||||
Appelé par `main.py` après ~90 s de tourne STABLE (liveness LOCALE,
|
||||
indépendante du DGX — évite un faux rollback quand le réseau est coupé).
|
||||
Retirer `PENDING_BOOT*` dit à `Lea.bat` que la nouvelle version a démarré
|
||||
correctement (sinon, au prochain lancement, Lea.bat rollback vers la
|
||||
version précédente).
|
||||
|
||||
Best-effort : aucune exception ne remonte.
|
||||
"""
|
||||
try:
|
||||
root = _resolve_app_dir(app_dir)
|
||||
marker = root / f"boot_ok_{version}"
|
||||
marker.write_text("ok", encoding="utf-8")
|
||||
cleared = []
|
||||
for p in root.glob("PENDING_BOOT*"):
|
||||
try:
|
||||
p.unlink()
|
||||
cleared.append(p.name)
|
||||
except OSError:
|
||||
pass
|
||||
logger.info("boot_ok écrit (%s) ; PENDING_BOOT retiré : %s",
|
||||
version, cleared or "aucun")
|
||||
return {"written": True, "marker": str(marker), "cleared_pending": cleared}
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("write_boot_ok_marker a échoué : %s", e)
|
||||
return {"written": False, "error": str(e)}
|
||||
@@ -3,6 +3,7 @@ mss>=9.0.1 # Capture d'écran haute performance
|
||||
pynput>=1.7.7 # Clavier/Souris Cross-plateforme
|
||||
Pillow>=10.0.0 # Crops et processing image
|
||||
requests>=2.31.0 # Streaming réseau
|
||||
httpx>=0.27 # Client HTTP orchestrateur Léa (POST /api/learn/start) — brique conversationnelle
|
||||
python-socketio[client]>=5.10,<6.0 # Bus feedback Léa 'lea:*' (compat Flask-SocketIO 5.3.x serveur)
|
||||
psutil>=5.9.0 # Monitoring CPU/RAM
|
||||
screeninfo>=0.8 # QW1 — détection des monitors physiques + offsets
|
||||
|
||||
197
agent_v0/agent_v1/ui/session_watchdog.py
Normal file
197
agent_v0/agent_v1/ui/session_watchdog.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# agent_v1/ui/session_watchdog.py
|
||||
"""Watchdog de session interactive Windows — résilience RDP/Citrix.
|
||||
|
||||
Problème résolu (preuve poste clinique Émilie, 01/07) :
|
||||
09:46:28 [MAIN] agent.run() est sorti mais agent.running=True — probablement
|
||||
pystray sans session interactive (SSH)
|
||||
09:46:28 [MAIN] Keepalive headless actif — main thread bloque...
|
||||
|
||||
Sur les postes cliniques (tous RDP/Citrix), la session interactive
|
||||
disparaît quand l'utilisateur se déconnecte / la session bascule en
|
||||
verrouillage. `pystray.Icon.run()` sort alors immédiatement (plus de
|
||||
bureau interactif `WinSta0\\Default` pour recevoir les entrées et afficher
|
||||
l'icône). L'ancien `_headless_keepalive` bloquait le main thread *pour
|
||||
toujours* : l'icône tray + la fenêtre chat DISPARAISSAIENT et ne
|
||||
revenaient JAMAIS, même après reconnexion RDP. Les soignants croyaient
|
||||
que Léa avait planté (la capture continuait pourtant en fond).
|
||||
|
||||
Solution : un watchdog qui surveille la disponibilité du bureau
|
||||
interactif via `OpenInputDesktop()` (signal Win32 canonique — échoue quand
|
||||
la session est déconnectée/verrouillée, réussit à la reconnexion) et
|
||||
(re)lance l'UI tray dès qu'une session redevient disponible. Les threads
|
||||
de fond (heartbeat, replay poll, capture_server) NE SONT JAMAIS touchés :
|
||||
ils tournent contre `agent.running` et restent uniques. On ne relance
|
||||
JAMAIS un second `AgentV1` — seulement la couche UI (tray + chat).
|
||||
|
||||
État de l'art (recherche 01/07) :
|
||||
- `OpenInputDesktop()` échoue (ERROR_ACCESS_DENIED / ERROR_INVALID_...)
|
||||
quand le processus n'est pas rattaché au windowstation interactif
|
||||
`WinSta0` — c'est exactement le cas quand la session RDP est
|
||||
déconnectée. C'est la méthode fiable recommandée (comparer les
|
||||
*noms* de bureau via GetUserObjectInformation n'apporte rien de plus
|
||||
ici : on a juste besoin d'un booléen « input desktop dispo ? »).
|
||||
- `WTSGetActiveConsoleSessionId` renvoie une pseudo-session même sans
|
||||
login → PAS fiable pour ce besoin.
|
||||
- `pystray.Icon.run()` ne sort jamais en session interactive normale ;
|
||||
il sort immédiatement sinon → c'est notre signal de « session perdue ».
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import platform
|
||||
import threading
|
||||
from typing import Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Intervalle de sondage du bureau interactif (secondes).
|
||||
# 3s = compromis : réactif à la reconnexion sans marteler l'API Win32.
|
||||
POLL_INTERVAL_S = 3.0
|
||||
|
||||
|
||||
def is_interactive_desktop_available() -> bool:
|
||||
"""Retourne True si un bureau interactif Windows est disponible.
|
||||
|
||||
Utilise `OpenInputDesktop()` : succès => le windowstation interactif
|
||||
(`WinSta0\\Default`) est accessible et peut afficher un tray. Échec =>
|
||||
session RDP/Citrix déconnectée ou verrouillée sans bureau d'entrée.
|
||||
|
||||
Hors Windows (Linux/dev/tests) : renvoie toujours True (pas de notion
|
||||
de bureau interactif verrouillable ici — on laisse l'UI tourner).
|
||||
Toute erreur d'appel Win32 est traitée comme « indisponible » (prudent)
|
||||
SAUF l'indisponibilité de l'API elle-même (pywin32 absent) → True pour
|
||||
ne pas priver un poste de son tray à cause d'une dépendance manquante.
|
||||
"""
|
||||
if platform.system() != "Windows":
|
||||
return True
|
||||
|
||||
try:
|
||||
import win32con # type: ignore
|
||||
import win32service # type: ignore
|
||||
except Exception:
|
||||
# pywin32 indisponible : on ne peut pas sonder → on suppose dispo
|
||||
# (comportement historique : tenter l'UI plutôt que la bloquer).
|
||||
logger.debug("pywin32 indisponible — sondage bureau interactif ignoré")
|
||||
return True
|
||||
|
||||
hdesk = None
|
||||
try:
|
||||
# DESKTOP_SWITCHDESKTOP (0x0100) = droit minimal, aligné sur l'usage
|
||||
# documenté pour tester la présence du bureau d'entrée.
|
||||
hdesk = win32service.OpenInputDesktop(0, False, win32con.DESKTOP_SWITCHDESKTOP)
|
||||
return hdesk is not None
|
||||
except Exception:
|
||||
# OpenInputDesktop lève quand aucun bureau d'entrée n'est accessible
|
||||
# (session déconnectée / verrouillée). C'est le cas « indisponible ».
|
||||
return False
|
||||
finally:
|
||||
if hdesk is not None:
|
||||
try:
|
||||
# PyHANDLE se ferme via .Close() (pywin32) ; fallback silencieux.
|
||||
hdesk.Close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class InteractiveSessionWatchdog:
|
||||
"""Surveille la session interactive et (re)lance l'UI tray à la reconnexion.
|
||||
|
||||
Ne détient AUCUN état de capture. Sa seule responsabilité : garantir
|
||||
qu'il existe au plus UN tray vivant à la fois, et le ressusciter quand
|
||||
une session interactive redevient disponible. Les daemon threads de
|
||||
l'agent (heartbeat/replay/capture) sont indépendants et intacts.
|
||||
|
||||
Paramètres :
|
||||
run_ui : callable bloquant qui lance le tray (typiquement
|
||||
``agent.ui.run`` / ``agent.run``). Retourne quand le
|
||||
tray sort (normal en fin de session interactive).
|
||||
is_running : callable -> bool ; True tant que l'agent doit vivre
|
||||
(typiquement ``lambda: agent.running``).
|
||||
is_available : callable -> bool de détection de session (injectable
|
||||
pour les tests). Défaut = is_interactive_desktop_available.
|
||||
poll_interval_s : période de sondage quand la session est absente.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
run_ui: Callable[[], None],
|
||||
is_running: Callable[[], bool],
|
||||
is_available: Optional[Callable[[], bool]] = None,
|
||||
poll_interval_s: float = POLL_INTERVAL_S,
|
||||
) -> None:
|
||||
self._run_ui = run_ui
|
||||
self._is_running = is_running
|
||||
self._is_available = is_available or is_interactive_desktop_available
|
||||
self._poll_interval_s = poll_interval_s
|
||||
self._wake = threading.Event()
|
||||
# Sérialise le lancement de l'UI : jamais deux trays en parallèle.
|
||||
self._ui_lock = threading.Lock()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Réveille le watchdog pour qu'il réévalue ``is_running`` et sorte."""
|
||||
self._wake.set()
|
||||
|
||||
def _run_ui_once(self) -> None:
|
||||
"""Lance l'UI tray une fois (bloquant) sous verrou, avec garde d'erreur.
|
||||
|
||||
Le verrou empêche formellement qu'un second appel démarre un tray
|
||||
alors qu'un premier tourne encore (invariant « un seul tray »).
|
||||
"""
|
||||
with self._ui_lock:
|
||||
try:
|
||||
self._run_ui()
|
||||
except Exception:
|
||||
# Un crash du tray ne doit jamais tuer le watchdog : on log et
|
||||
# on laisse la boucle décider (retry ou sortie selon is_running).
|
||||
logger.exception("[WATCHDOG] Le tray UI a levé une exception")
|
||||
|
||||
def run(self) -> None:
|
||||
"""Boucle principale (bloque le main thread à la place du keepalive).
|
||||
|
||||
Cycle :
|
||||
1. Attendre qu'un bureau interactif soit disponible.
|
||||
2. (Re)lancer le tray — bloque jusqu'à sa sortie (déconnexion RDP).
|
||||
3. Recommencer tant que ``is_running`` est vrai.
|
||||
|
||||
Ne consomme pas de CPU en boucle serrée : sonde toutes les
|
||||
``poll_interval_s`` via un Event interruptible (réveil immédiat au stop).
|
||||
"""
|
||||
logger.info(
|
||||
"[WATCHDOG] Surveillance session interactive active "
|
||||
"(re-affichage auto du tray + chat à la reconnexion RDP/Citrix)."
|
||||
)
|
||||
first_cycle = True
|
||||
|
||||
while self._is_running():
|
||||
if not self._is_available():
|
||||
# Session absente : sonder périodiquement sans brûler le CPU.
|
||||
if first_cycle:
|
||||
logger.warning(
|
||||
"[WATCHDOG] Aucune session interactive — Léa reste active "
|
||||
"en fond (capture/heartbeat), tray masqué. En attente de "
|
||||
"reconnexion RDP/Citrix pour ré-afficher l'interface."
|
||||
)
|
||||
# Event.wait renvoie True si stop() a été appelé → on sort.
|
||||
if self._wake.wait(timeout=self._poll_interval_s):
|
||||
break
|
||||
first_cycle = False
|
||||
continue
|
||||
|
||||
# Session disponible : (re)lancer le tray.
|
||||
if not first_cycle:
|
||||
logger.info(
|
||||
"[WATCHDOG] Session interactive détectée — ré-affichage du "
|
||||
"tray et de la fenêtre chat de Léa."
|
||||
)
|
||||
first_cycle = False
|
||||
|
||||
# Bloque jusqu'à la sortie du tray (fin de session interactive).
|
||||
self._run_ui_once()
|
||||
|
||||
# Le tray est sorti. Si l'agent doit vivre, on reboucle (le
|
||||
# prochain tour re-sondera la session et re-affichera le tray).
|
||||
if not self._is_running():
|
||||
break
|
||||
|
||||
logger.info("[WATCHDOG] Arrêt de la surveillance de session interactive.")
|
||||
@@ -137,6 +137,15 @@ class SmartTrayV1:
|
||||
self._state_lock = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
# Résilience RDP/Citrix : run() peut être rappelé plusieurs fois par le
|
||||
# watchdog de session (ré-affichage du tray à la reconnexion). Les
|
||||
# threads de fond (connexion, cache workflows, hotkey) et l'accueil ne
|
||||
# doivent démarrer QU'UNE fois — sinon on duplique les threads.
|
||||
self._bg_started = False
|
||||
# Signalé quand l'utilisateur a demandé Quitter : le watchdog ne doit
|
||||
# alors PAS relancer le tray.
|
||||
self._quit_requested = False
|
||||
|
||||
# Notifications
|
||||
self._notifier = NotificationManager()
|
||||
|
||||
@@ -709,6 +718,11 @@ class SmartTrayV1:
|
||||
"""Arrete proprement l'agent et quitte."""
|
||||
logger.info("Arret demande par l'utilisateur")
|
||||
|
||||
# Marquer l'arret volontaire : le watchdog de session ne doit PAS
|
||||
# relancer le tray après un Quitter explicite (à distinguer d'une
|
||||
# simple déconnexion RDP où le tray doit revenir tout seul).
|
||||
self._quit_requested = True
|
||||
|
||||
# Arreter la session si en cours
|
||||
if self.is_recording:
|
||||
self.on_stop()
|
||||
@@ -885,17 +899,24 @@ class SmartTrayV1:
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def run(self) -> None:
|
||||
"""Demarre le tray, les threads de fond, et entre dans la boucle principale."""
|
||||
# Notification d'accueil — divulgation IA (Article 50, Reglement IA)
|
||||
self._notifier.greet()
|
||||
"""Demarre (ou ré-affiche) le tray et entre dans la boucle pystray.
|
||||
|
||||
# Enregistrer le hotkey global Ctrl+Shift+L (toggle chat)
|
||||
self._start_hotkey()
|
||||
Ré-entrant : le watchdog de session (session_watchdog.py) rappelle
|
||||
cette méthode à chaque reconnexion RDP/Citrix pour ré-afficher le
|
||||
tray + la fenêtre chat. Les initialisations one-shot (accueil,
|
||||
hotkey, threads de fond connexion/cache) sont protégées par
|
||||
``_bg_started`` pour ne PAS dupliquer les threads. Seule l'icône
|
||||
pystray est recréée à chaque appel (l'ancienne est morte avec la
|
||||
session précédente).
|
||||
"""
|
||||
self._start_background_once()
|
||||
|
||||
# Tooltip avec identifiant machine pour le multi-machine
|
||||
tray_title = f"Agent V1 - {self.machine_id}"
|
||||
|
||||
# Menu statique — reconstruit via _update_icon() quand l'état change
|
||||
# Menu statique — reconstruit via _update_icon() quand l'état change.
|
||||
# Nouvelle icône à chaque (ré)affichage : l'objet pystray précédent
|
||||
# est invalide une fois sa boucle sortie (session interactive perdue).
|
||||
self.icon = pystray.Icon(
|
||||
"AgentV1",
|
||||
self._current_icon(),
|
||||
@@ -903,6 +924,33 @@ class SmartTrayV1:
|
||||
menu=pystray.Menu(*self._get_menu_items()),
|
||||
)
|
||||
|
||||
# Rafraîchir les workflows au (ré)affichage — utile après reconnexion.
|
||||
if self._bg_started and self.server_client is not None:
|
||||
threading.Thread(target=self._fetch_workflows, daemon=True).start()
|
||||
|
||||
# Boucle principale pystray (bloquante). Sort quand la session
|
||||
# interactive disparaît (RDP déconnecté) OU sur _on_quit → le
|
||||
# watchdog décide alors de relancer ou non.
|
||||
logger.info("SmartTrayV1 demarre — entree dans la boucle pystray")
|
||||
self.icon.run()
|
||||
|
||||
def _start_background_once(self) -> None:
|
||||
"""Initialisations one-shot : accueil, hotkey, threads de fond.
|
||||
|
||||
Idempotent : les appels suivants (ré-affichage tray) sont des no-op.
|
||||
Garantit qu'on n'accumule pas de threads connexion/cache à chaque
|
||||
reconnexion RDP.
|
||||
"""
|
||||
if self._bg_started:
|
||||
return
|
||||
self._bg_started = True
|
||||
|
||||
# Notification d'accueil — divulgation IA (Article 50, Reglement IA)
|
||||
self._notifier.greet()
|
||||
|
||||
# Enregistrer le hotkey global Ctrl+Shift+L (toggle chat)
|
||||
self._start_hotkey()
|
||||
|
||||
# Demarrer le thread de verification connexion
|
||||
if self.server_client is not None:
|
||||
conn_thread = threading.Thread(
|
||||
@@ -924,7 +972,3 @@ class SmartTrayV1:
|
||||
threading.Thread(
|
||||
target=self._fetch_workflows, daemon=True
|
||||
).start()
|
||||
|
||||
# Boucle principale pystray (bloquante)
|
||||
logger.info("SmartTrayV1 demarre — entree dans la boucle pystray")
|
||||
self.icon.run()
|
||||
|
||||
110
agent_v0/agent_v1/vision/capture_io.py
Normal file
110
agent_v0/agent_v1/vision/capture_io.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Politique de sauvegarde des captures — réduction du poids disque.
|
||||
|
||||
Constat : tous les shots étaient sauvés en PNG plein écran lossless
|
||||
(``img.save(path, "PNG", quality=...)`` — PNG ignore ``quality``), d'où
|
||||
~90 Go pour 13 sessions. La majorité de ce poids n'a aucune valeur de
|
||||
grounding (full + full_blurred en doublon, heartbeats plein écran).
|
||||
|
||||
Cette politique distingue le **type** de shot et écrit le format adapté :
|
||||
|
||||
- ``crop`` → PNG lossless. C'est la cible de grounding qwen3-vl ; on
|
||||
préserve chaque pixel (perte JPEG = bruit sur de petites icônes). Le crop
|
||||
fait 80×80 → poids négligeable, aucun intérêt à le dégrader.
|
||||
- ``full`` / ``window`` / ``context`` → JPEG ``quality=SCREENSHOT_QUALITY,
|
||||
optimize=True``. Ce sont des vues contextuelles / humaines : la
|
||||
compression JPEG (~5-10x) est sans impact fonctionnel.
|
||||
- ``heartbeat`` → JPEG **downscalé** (largeur max ``HEARTBEAT_MAX_WIDTH``,
|
||||
ratio préservé). C'est de la *liveness* (le serveur vérifie juste qu'un
|
||||
écran a changé), pas du grounding → la pleine résolution est du gaspillage.
|
||||
|
||||
``save_capture`` retourne le chemin RÉELLEMENT écrit, extension ajustée selon
|
||||
le format. L'appelant doit utiliser ce retour (et non un chemin ``.png``
|
||||
présumé) pour streamer / référencer le bon fichier.
|
||||
|
||||
⚠️ Contrat avec le serveur : l'extension du crop NE DOIT PAS changer (le
|
||||
serveur retrouve le crop par basename via ``vision_info.crop`` — voir
|
||||
``stream_processor._extract_crop_b64`` stratégie 1). C'est pourquoi ``crop``
|
||||
reste PNG. Les full/window/context/heartbeat sont retrouvés par
|
||||
``screenshot_id`` avec extension ``.png`` hardcodée côté serveur, mais le
|
||||
serveur réécrit toujours l'upload sous ``{shot_id}.png`` (le suffixe envoyé
|
||||
sur le fil est ignoré) → changer l'extension LOCALE de ces types est sûr.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Iterable
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ..config import SCREENSHOT_QUALITY
|
||||
|
||||
# Types sauvés en JPEG (vue contextuelle / humaine, pas de grounding pixel).
|
||||
_JPEG_KINDS: frozenset = frozenset({"full", "window", "context"})
|
||||
|
||||
# Largeur max d'un heartbeat downscalé. 1280 px suffit largement pour de la
|
||||
# liveness (détecter qu'un écran a changé) ; on divise le poids d'un 2560 px
|
||||
# par ~4 (surface) avant compression JPEG.
|
||||
HEARTBEAT_MAX_WIDTH = 1280
|
||||
|
||||
|
||||
def _ensure_jpeg_ready(img: Image.Image) -> Image.Image:
|
||||
"""Convertit en RGB si nécessaire (JPEG ne supporte ni alpha ni palette)."""
|
||||
if img.mode in ("RGBA", "LA", "P"):
|
||||
return img.convert("RGB")
|
||||
return img
|
||||
|
||||
|
||||
def _downscale_to_width(img: Image.Image, max_width: int) -> Image.Image:
|
||||
"""Réduit l'image à ``max_width`` en préservant le ratio (no-op si plus petite)."""
|
||||
if img.width <= max_width:
|
||||
return img
|
||||
new_height = max(1, round(img.height * max_width / img.width))
|
||||
return img.resize((max_width, new_height), Image.LANCZOS)
|
||||
|
||||
|
||||
def save_capture(img: Image.Image, path_base: str, kind: str) -> str:
|
||||
"""Sauve ``img`` selon la politique du ``kind`` et retourne le chemin écrit.
|
||||
|
||||
Args:
|
||||
img: image PIL à sauvegarder.
|
||||
path_base: chemin SANS extension (ex.
|
||||
``.../shots/shot_0001_full``). L'extension finale (``.png`` ou
|
||||
``.jpg``) est ajoutée par la politique.
|
||||
kind: type de shot — ``"crop"`` | ``"full"`` | ``"window"`` |
|
||||
``"context"`` | ``"heartbeat"``.
|
||||
|
||||
Returns:
|
||||
Le chemin RÉELLEMENT écrit, avec la bonne extension.
|
||||
|
||||
Raises:
|
||||
ValueError: si ``kind`` n'est pas reconnu (fail-closed : on refuse
|
||||
d'écrire un fichier dont la politique est indéterminée).
|
||||
"""
|
||||
if kind == "crop":
|
||||
out_path = f"{path_base}.png"
|
||||
img.save(out_path, "PNG")
|
||||
return out_path
|
||||
|
||||
if kind in _JPEG_KINDS:
|
||||
out_path = f"{path_base}.jpg"
|
||||
_ensure_jpeg_ready(img).save(
|
||||
out_path, "JPEG", quality=SCREENSHOT_QUALITY, optimize=True
|
||||
)
|
||||
return out_path
|
||||
|
||||
if kind == "heartbeat":
|
||||
out_path = f"{path_base}.jpg"
|
||||
small = _downscale_to_width(_ensure_jpeg_ready(img), HEARTBEAT_MAX_WIDTH)
|
||||
small.save(out_path, "JPEG", quality=SCREENSHOT_QUALITY)
|
||||
return out_path
|
||||
|
||||
raise ValueError(
|
||||
f"kind de capture inconnu : {kind!r} "
|
||||
f"(attendu: crop, full, window, context, heartbeat)"
|
||||
)
|
||||
|
||||
|
||||
def known_kinds() -> Iterable[str]:
|
||||
"""Retourne les ``kind`` supportés (utile pour la validation appelant)."""
|
||||
return ("crop", *sorted(_JPEG_KINDS), "heartbeat")
|
||||
@@ -18,8 +18,9 @@ import platform
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from PIL import Image, ImageFilter, ImageStat
|
||||
import mss
|
||||
from ..config import TARGETED_CROP_SIZE, SCREENSHOT_QUALITY, BLUR_SENSITIVE
|
||||
from ..config import TARGETED_CROP_SIZE, BLUR_SENSITIVE
|
||||
from .blur_sensitive import blur_sensitive_regions
|
||||
from .capture_io import save_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -425,6 +426,18 @@ class VisionCapturer:
|
||||
# On ne crée plus self.sct ici car mss n'est pas thread-safe sous Windows
|
||||
self.last_img_hash = None
|
||||
|
||||
def _ensure_shots_dir(self) -> None:
|
||||
"""Garantit l'existence de `shots/` avant toute écriture.
|
||||
|
||||
Le dossier est créé dans `__init__`, mais l'auto-cleanup de
|
||||
`SessionStorage` (`shutil.rmtree` par âge/taille) peut supprimer tout
|
||||
le dossier de session — y compris la session permanente `_background`.
|
||||
Sans ce garde, la capture suivante lève `[Errno 2] No such file or
|
||||
directory` (bug observé poste Émilie). On recrée donc le répertoire
|
||||
cible juste avant chaque sauvegarde.
|
||||
"""
|
||||
os.makedirs(self.shots_dir, exist_ok=True)
|
||||
|
||||
def capture_full_context(self, name_suffix: str, force=False) -> str:
|
||||
"""
|
||||
Capture l'écran complet.
|
||||
@@ -460,9 +473,15 @@ class VisionCapturer:
|
||||
if BLUR_SENSITIVE:
|
||||
blur_sensitive_regions(img)
|
||||
|
||||
path = os.path.join(self.shots_dir, f"context_{int(time.time())}_{name_suffix}.png")
|
||||
img.save(path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||
return path
|
||||
# Politique d'écriture : les heartbeats sont de la liveness pure
|
||||
# (le serveur vérifie juste qu'un écran a changé) → JPEG downscalé.
|
||||
# Les autres contextes (focus_change, result_of_*) → JPEG q85.
|
||||
kind = "heartbeat" if "heartbeat" in name_suffix else "context"
|
||||
self._ensure_shots_dir()
|
||||
path_base = os.path.join(
|
||||
self.shots_dir, f"context_{int(time.time())}_{name_suffix}"
|
||||
)
|
||||
return save_capture(img, path_base, kind)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur Context Capture: {e}")
|
||||
return ""
|
||||
@@ -506,10 +525,10 @@ class VisionCapturer:
|
||||
return result
|
||||
return {}
|
||||
|
||||
full_path = os.path.join(self.shots_dir, f"{screenshot_id}_full.png")
|
||||
full_base = os.path.join(self.shots_dir, f"{screenshot_id}_full")
|
||||
|
||||
# Capture du Crop (Cœur de l'apprentissage qwen3-vl)
|
||||
crop_path = os.path.join(self.shots_dir, f"{screenshot_id}_crop.png")
|
||||
crop_base = os.path.join(self.shots_dir, f"{screenshot_id}_crop")
|
||||
w, h = TARGETED_CROP_SIZE
|
||||
left = max(0, x - w // 2)
|
||||
top = max(0, y - h // 2)
|
||||
@@ -523,8 +542,11 @@ class VisionCapturer:
|
||||
blur_sensitive_regions(img)
|
||||
blur_sensitive_regions(crop_img)
|
||||
|
||||
img.save(full_path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||
crop_img.save(crop_path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||
# Politique d'écriture : full = vue contextuelle → JPEG q85 ;
|
||||
# crop = cible de grounding qwen3-vl → PNG lossless (contrat serveur).
|
||||
self._ensure_shots_dir()
|
||||
full_path = save_capture(img, full_base, "full")
|
||||
crop_path = save_capture(crop_img, crop_base, "crop")
|
||||
|
||||
# Mise à jour du hash pour le prochain heartbeat
|
||||
self.last_img_hash = self._compute_quick_hash(img)
|
||||
@@ -648,11 +670,12 @@ class VisionCapturer:
|
||||
if BLUR_SENSITIVE:
|
||||
blur_sensitive_regions(window_img)
|
||||
|
||||
# Sauvegarde
|
||||
window_path = os.path.join(
|
||||
self.shots_dir, f"{screenshot_id}_window.png"
|
||||
# Sauvegarde — fenêtre = vue contextuelle → JPEG q85 (politique).
|
||||
self._ensure_shots_dir()
|
||||
window_base = os.path.join(
|
||||
self.shots_dir, f"{screenshot_id}_window"
|
||||
)
|
||||
window_img.save(window_path, "PNG", quality=SCREENSHOT_QUALITY)
|
||||
window_path = save_capture(window_img, window_base, "window")
|
||||
|
||||
result = {
|
||||
"window_image": window_path,
|
||||
|
||||
@@ -420,9 +420,11 @@ from .replay_engine import (
|
||||
_edge_to_normalized_actions,
|
||||
_substitute_variables,
|
||||
_resolve_runtime_vars,
|
||||
_coerce_action_coords,
|
||||
_SERVER_SIDE_ACTION_TYPES,
|
||||
_handle_extract_text_action,
|
||||
_handle_extract_table_action,
|
||||
_handle_extract_dossier_action,
|
||||
_handle_t2a_decision_action,
|
||||
_handle_llm_generate_action,
|
||||
_handle_concat_text_vars_action,
|
||||
@@ -435,6 +437,9 @@ from .replay_engine import (
|
||||
_notify_error_callback as _notify_error_callback_impl,
|
||||
)
|
||||
|
||||
# Navigate handler — import direct depuis core/navigation (pas via replay_engine)
|
||||
from core.navigation import _handle_navigate_action
|
||||
|
||||
|
||||
|
||||
# Wrappers pour les fonctions replay_engine qui accèdent aux variables globales du module.
|
||||
@@ -4330,6 +4335,9 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
|
||||
if runtime_vars:
|
||||
action = _resolve_runtime_vars(action, runtime_vars)
|
||||
|
||||
# Coercion coords: cast x_pct/y_pct en float après resolver
|
||||
action = _coerce_action_coords(action)
|
||||
|
||||
type_ = action.get("type")
|
||||
|
||||
# pause_for_human : pause supervisée si safety_level/safety_checks ou mode supervised,
|
||||
@@ -4443,6 +4451,24 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
|
||||
),
|
||||
timeout=180,
|
||||
)
|
||||
elif type_ == "extract_dossier":
|
||||
await asyncio.wait_for(
|
||||
loop.run_in_executor(
|
||||
None,
|
||||
_handle_extract_dossier_action,
|
||||
action, owning_replay, session_id,
|
||||
),
|
||||
timeout=180,
|
||||
)
|
||||
elif type_ == "navigate":
|
||||
await asyncio.wait_for(
|
||||
loop.run_in_executor(
|
||||
None,
|
||||
_handle_navigate_action,
|
||||
action, owning_replay, session_id,
|
||||
),
|
||||
timeout=180,
|
||||
)
|
||||
elif type_ == "t2a_decision":
|
||||
await asyncio.wait_for(
|
||||
loop.run_in_executor(
|
||||
@@ -7830,6 +7856,81 @@ async def lea_screen_analyze(payload: _Phase25ScreenRequest, request: Request):
|
||||
return payload_out
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# DETTE-022 v2 — GET /api/v1/agents/update/check (MAJ silencieuse client Léa)
|
||||
# Flag OFF par défaut (RPA_AUTO_UPDATE_SERVER_ENABLED). Best-effort, additif :
|
||||
# expose la DÉCISION d'update (logique PURE dans update_check.py, testée hors
|
||||
# serveur — DETTE-013). NE FAIT PAS le swap (réservé révision humaine côté
|
||||
# client + Lea.bat).
|
||||
# =========================================================================
|
||||
from .update_check import decide_update as _decide_update # noqa: E402
|
||||
from .update_policy import ( # noqa: E402
|
||||
resolve_target_version_from_env as _resolve_target_version_from_env,
|
||||
)
|
||||
|
||||
|
||||
def _auto_update_server_enabled() -> bool:
|
||||
"""Flag d'activation serveur — lu à chaque appel (faciliter les tests)."""
|
||||
return os.environ.get("RPA_AUTO_UPDATE_SERVER_ENABLED", "").lower() in (
|
||||
"1", "true", "yes", "on",
|
||||
)
|
||||
|
||||
|
||||
def _latest_agent_version(machine_id: Optional[str] = None) -> str:
|
||||
"""Version d'agent cible POUR CE POSTE (canary-aware, DETTE-022 v2).
|
||||
|
||||
⭐ SÉCURITÉ flotte ⭐ — la version servie est résolue PAR MACHINE via la
|
||||
politique canary (`update_policy.resolve_target_version_from_env`) : un
|
||||
poste canary (Émilie `lea-4zbgwxty`) reçoit la nouvelle version en premier ;
|
||||
tous les autres restent sur le floor stable. Piloté 100 % par env, sans
|
||||
rebuild :
|
||||
RPA_AGENT_STABLE_VERSION (défaut 1.0.1) — servi à toute la flotte.
|
||||
RPA_AGENT_CANARY_VERSION — servi AUX SEULS postes canary.
|
||||
RPA_AGENT_CANARY_MACHINES — allow-list CSV des machine_id canary.
|
||||
|
||||
Rétrocompat : si `RPA_AGENT_LATEST_VERSION` (ancienne var globale) est
|
||||
positionnée, elle prime — évite toute régression d'un déploiement existant.
|
||||
"""
|
||||
legacy = os.environ.get("RPA_AGENT_LATEST_VERSION")
|
||||
if legacy:
|
||||
return legacy
|
||||
return _resolve_target_version_from_env(machine_id)
|
||||
|
||||
|
||||
@app.get("/api/v1/agents/update/check")
|
||||
async def check_agent_update(
|
||||
current_version: str,
|
||||
machine_id: Optional[str] = None,
|
||||
update_type: Optional[str] = None,
|
||||
):
|
||||
"""Indiquer au client Léa si une MAJ est disponible (DETTE-022 v2).
|
||||
|
||||
Réponse : {update_available, latest_version, update_type, url}.
|
||||
|
||||
La version cible est résolue PAR MACHINE (canary) : voir
|
||||
`_latest_agent_version`. Un poste hors canary ne se voit JAMAIS proposer la
|
||||
version canary (blast radius borné à la liste canary).
|
||||
|
||||
GATED : si RPA_AUTO_UPDATE_SERVER_ENABLED n'est pas positionné → 503
|
||||
(aucun effet sur le pipeline existant — anti-régression). Auth Bearer
|
||||
requise (dépendance globale `_verify_token`).
|
||||
"""
|
||||
if not _auto_update_server_enabled():
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=(
|
||||
"MAJ auto désactivée (flag RPA_AUTO_UPDATE_SERVER_ENABLED). "
|
||||
"DETTE-022 : endpoint exposé mais OFF par défaut."
|
||||
),
|
||||
)
|
||||
return _decide_update(
|
||||
current_version=current_version,
|
||||
latest_version=_latest_agent_version(machine_id),
|
||||
update_type=update_type,
|
||||
machine_id=machine_id,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ _ALLOWED_ACTION_TYPES = {
|
||||
"extract_text", # OCR serveur sur dernier heartbeat → variable workflow
|
||||
"extract_table", # OCR serveur + filtre regex → liste structurée (boucle)
|
||||
"extract_dossier", # OCR grille structurée → dossier patient persisté (brique 3)
|
||||
"navigate", # Navigation visuelle → coords login/recherche (brique navigation)
|
||||
"extract_text_scroll", # Marker côté graphe — expansé en sous-actions par _edge_to_normalized_actions
|
||||
"_concat_text_vars", # Action serveur interne (générée par expansion extract_text_scroll)
|
||||
"t2a_decision", # Analyse LLM facturation T2A → variable workflow
|
||||
@@ -55,6 +56,7 @@ _SERVER_SIDE_ACTION_TYPES = {
|
||||
"extract_text",
|
||||
"extract_table",
|
||||
"extract_dossier",
|
||||
"navigate",
|
||||
"t2a_decision",
|
||||
"llm_generate",
|
||||
"_concat_text_vars",
|
||||
@@ -1946,6 +1948,21 @@ def _edge_to_normalized_actions(edge, params: Dict[str, Any]) -> List[Dict[str,
|
||||
normalized["parameters"]["temperature"] = action_params.get("temperature")
|
||||
return [normalized]
|
||||
|
||||
elif action_type == "navigate":
|
||||
normalized["type"] = "navigate"
|
||||
normalized["parameters"] = {
|
||||
"action": action_params.get("action", "login"),
|
||||
"login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"),
|
||||
"password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"),
|
||||
"submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"),
|
||||
}
|
||||
login_config_keys = ("login_field", "password_field", "submit_button",
|
||||
"success_elements", "context")
|
||||
for key in login_config_keys:
|
||||
if action_params.get(key) is not None:
|
||||
normalized["parameters"][key] = action_params[key]
|
||||
return [normalized]
|
||||
|
||||
else:
|
||||
logger.warning(f"Type d'action inconnu : {action_type}")
|
||||
return []
|
||||
@@ -2043,6 +2060,38 @@ def _resolve_runtime_vars(value: Any, variables: Dict[str, Any]) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _coerce_action_coords(action: dict) -> dict:
|
||||
"""Cast x_pct/y_pct en float après template resolution par _resolve_runtime_vars.
|
||||
|
||||
Politique : si string non convertible ou template encore present → skip + pause_for_human.
|
||||
Idempotent sur les actions qui ont déjà des floats (mouse_click existant).
|
||||
Jamais fallback 0.0/0.0 — un clic sur coords (0,0) = top-left = potentiellement dangereux.
|
||||
|
||||
Appelé APRÈS _resolve_runtime_vars dans la boucle dispatch (api_stream.py).
|
||||
"""
|
||||
for key in ("x_pct", "y_pct"):
|
||||
val = action.get(key)
|
||||
if val is None:
|
||||
continue
|
||||
if isinstance(val, float):
|
||||
continue # déjà float, idempotent
|
||||
if isinstance(val, str):
|
||||
# Template encore présent = non résolu par _resolve_runtime_vars
|
||||
if val.startswith("{{") and val.endswith("}}"):
|
||||
action["_skip_reason"] = f"coords_var non résolu: {key}={val}"
|
||||
action["type"] = "pause_for_human"
|
||||
action["safety_level"] = "high"
|
||||
return action
|
||||
try:
|
||||
action[key] = float(val)
|
||||
except (ValueError, TypeError):
|
||||
action["_skip_reason"] = f"coords invalide: {key}={val}"
|
||||
action["type"] = "pause_for_human"
|
||||
action["safety_level"] = "high"
|
||||
return action
|
||||
return action
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Handlers pour les actions exécutées côté serveur (extract_text, t2a_decision)
|
||||
# =========================================================================
|
||||
|
||||
138
agent_v0/server_v1/update_check.py
Normal file
138
agent_v0/server_v1/update_check.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# agent_v0/server_v1/update_check.py
|
||||
"""Logique PURE de décision de mise à jour du client Léa (DETTE-022 v2).
|
||||
|
||||
But : centraliser, SANS dépendance FastAPI, le cœur testable de la MAJ
|
||||
silencieuse :
|
||||
|
||||
- `parse_version()` (R3) : parse une version semver en tuple d'entiers, pour
|
||||
une comparaison correcte ("1.0.2" < "1.0.10" — le piège lexicographique
|
||||
classique). Tolérant : préfixe « v », espaces, et format invalide → fallback
|
||||
`(0,)` (la plus basse) SANS jamais lever.
|
||||
- `decide_update()` (R2) : compare la version courante à la dernière dispo,
|
||||
choisit l'`update_type` (`code-only` par défaut, ~500 Ko / `full` ~33 Mo
|
||||
rare) et construit la réponse
|
||||
`{update_available, latest_version, update_type, url}`.
|
||||
|
||||
Ce module est volontairement IMPORTABLE seul (aucun import lourd, pas de
|
||||
`api_stream`) pour être testé sans démarrer le serveur (DETTE-013). Le
|
||||
branchement HTTP (endpoint gated) vit dans `api_stream.py`.
|
||||
|
||||
⚠️ Cette brique ne fait QUE décider. Le swap réel des fichiers, l'édition de
|
||||
Lea.bat et le redémarrage sont HORS de ce module (réservé révision humaine).
|
||||
|
||||
Branche feat/push-log-dgx.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Niveaux de livraison valides (R2). `code-only` par défaut = 99 % des MAJ.
|
||||
VALID_UPDATE_TYPES = ("code-only", "full")
|
||||
DEFAULT_UPDATE_TYPE = "code-only"
|
||||
|
||||
# Fallback de version « la plus basse » pour une chaîne illisible : ainsi une
|
||||
# version valide est toujours > à une version invalide, et une *latest* illisible
|
||||
# ne déclenche jamais de MAJ douteuse.
|
||||
_FALLBACK_VERSION: Tuple[int, ...] = (0,)
|
||||
|
||||
|
||||
def parse_version(v) -> Tuple[int, ...]:
|
||||
"""Parse une version semver en tuple d'entiers (R3).
|
||||
|
||||
"1.0.2" → (1, 0, 2), "1.0.10" → (1, 0, 10), "v1.2.3" → (1, 2, 3).
|
||||
|
||||
Tolérant et SANS exception : préfixe « v/V » et espaces tolérés ; tout
|
||||
format non numérique (vide, None, "abc", "1.x.3") retombe sur `(0,)`.
|
||||
|
||||
Stratégie : `packaging.version` si présent (déjà dans le venv via
|
||||
setuptools/pip), sinon parse manuel. Aucune nouvelle dépendance.
|
||||
"""
|
||||
if not isinstance(v, str):
|
||||
return _FALLBACK_VERSION
|
||||
s = v.strip().lstrip("vV").strip()
|
||||
if not s:
|
||||
return _FALLBACK_VERSION
|
||||
try:
|
||||
from packaging.version import Version
|
||||
|
||||
return tuple(Version(s).release)
|
||||
except Exception:
|
||||
# packaging absent (python-embed minimal) OU version non-PEP440.
|
||||
pass
|
||||
try:
|
||||
return tuple(int(x) for x in s.split("."))
|
||||
except (ValueError, AttributeError):
|
||||
return _FALLBACK_VERSION
|
||||
|
||||
|
||||
def is_newer(candidate: str, baseline: str) -> bool:
|
||||
"""True si `candidate` est strictement plus récent que `baseline` (semver)."""
|
||||
return parse_version(candidate) > parse_version(baseline)
|
||||
|
||||
|
||||
def _normalize_update_type(update_type: Optional[str]) -> str:
|
||||
"""Normalise l'update_type sur un niveau valide (défaut code-only)."""
|
||||
if update_type in VALID_UPDATE_TYPES:
|
||||
return update_type
|
||||
return DEFAULT_UPDATE_TYPE
|
||||
|
||||
|
||||
def build_download_url(
|
||||
machine_id: Optional[str],
|
||||
version: str,
|
||||
update_type: str,
|
||||
) -> str:
|
||||
"""Construit l'URL de téléchargement RELATIVE (R2, 2 niveaux).
|
||||
|
||||
Forme alignée sur les endpoints fleet existants :
|
||||
/api/fleet/download/<machine_id>?type=<update_type>&version=<version>
|
||||
|
||||
On garde une URL relative : le client la résout contre son SERVER_BASE.
|
||||
`machine_id` absent → segment « default » (rétrocompatible).
|
||||
"""
|
||||
mid = (machine_id or "default").strip() or "default"
|
||||
return f"/api/fleet/download/{mid}?type={update_type}&version={version}"
|
||||
|
||||
|
||||
def decide_update(
|
||||
current_version: str,
|
||||
latest_version: str,
|
||||
update_type: Optional[str] = None,
|
||||
machine_id: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Décision PURE de mise à jour (R2 + R3).
|
||||
|
||||
Compare `current_version` à `latest_version` en semver. Si la dernière est
|
||||
strictement plus récente, construit une réponse d'update ; sinon réponse
|
||||
« à jour ». Aucune exception : versions illisibles → pas de MAJ (prudence).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"update_available": bool,
|
||||
"latest_version": str,
|
||||
"update_type": "code-only" | "full" | None, # None si pas de MAJ
|
||||
"url": str | None, # None si pas de MAJ
|
||||
}
|
||||
"""
|
||||
no_update = {
|
||||
"update_available": False,
|
||||
"latest_version": latest_version,
|
||||
"update_type": None,
|
||||
"url": None,
|
||||
}
|
||||
|
||||
# latest illisible → on ne propose RIEN (pas de MAJ douteuse).
|
||||
if parse_version(latest_version) == _FALLBACK_VERSION:
|
||||
return no_update
|
||||
|
||||
if not is_newer(latest_version, current_version):
|
||||
return no_update
|
||||
|
||||
chosen_type = _normalize_update_type(update_type)
|
||||
return {
|
||||
"update_available": True,
|
||||
"latest_version": latest_version,
|
||||
"update_type": chosen_type,
|
||||
"url": build_download_url(machine_id, latest_version, chosen_type),
|
||||
}
|
||||
139
agent_v0/server_v1/update_policy.py
Normal file
139
agent_v0/server_v1/update_policy.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# agent_v0/server_v1/update_policy.py
|
||||
"""Politique de déploiement CANARY de la MAJ silencieuse Léa (DETTE-022 v2).
|
||||
|
||||
⭐ Brique de SÉCURITÉ centrale ⭐ — 10+ postes cliniques live (Wallerstein).
|
||||
|
||||
Une MAJ ratée peut briquer toute la flotte. La règle non négociable : on ne
|
||||
pousse JAMAIS une nouvelle version sur tous les postes d'un coup. On la déploie
|
||||
d'abord sur UN poste (canary = Émilie `lea-4zbgwxty`), on vérifie, puis on
|
||||
élargit. Ce module résout, PAR MACHINE, la version cible :
|
||||
|
||||
- poste dans la liste canary → `canary_version` (la nouvelle) ;
|
||||
- tous les autres postes → `stable_version` (le floor, inchangé).
|
||||
|
||||
Piloté 100 % par variables d'environnement (config serveur, sans rebuild) :
|
||||
RPA_AGENT_STABLE_VERSION — version servie à toute la flotte (défaut floor).
|
||||
RPA_AGENT_CANARY_VERSION — version servie AUX SEULS postes canary (optionnel).
|
||||
RPA_AGENT_CANARY_MACHINES — allow-list CSV des machine_id canary.
|
||||
|
||||
Promotion = quand le canary est validé, on met RPA_AGENT_STABLE_VERSION à la
|
||||
version canary (toute la flotte suit) et on vide RPA_AGENT_CANARY_MACHINES.
|
||||
Rollback canary = on remet RPA_AGENT_CANARY_VERSION à l'ancienne / on vide la
|
||||
liste : le prochain check ne proposera plus la MAJ (le swap réel côté client
|
||||
reste réservé révision humaine — cf. updater.py).
|
||||
|
||||
Module PUR (aucun import FastAPI, aucune IO) → importable et testable seul
|
||||
(DETTE-013). Le branchement HTTP vit dans api_stream.py.
|
||||
|
||||
Branche feat/push-log-dgx.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional, Set
|
||||
|
||||
# Réutilise le comparateur semver de la décision (même module serveur, pas de
|
||||
# duplication) : "1.0.2" < "1.0.10" correctement, tolérant aux formats invalides.
|
||||
try: # import relatif quand chargé comme package
|
||||
from .update_check import is_newer
|
||||
except Exception: # chargé par chemin (tests importlib) : import du voisin
|
||||
import importlib.util as _ilu
|
||||
from pathlib import Path as _Path
|
||||
|
||||
_uc_path = _Path(__file__).resolve().parent / "update_check.py"
|
||||
_spec = _ilu.spec_from_file_location("_rpa_update_check_for_policy", _uc_path)
|
||||
_uc = _ilu.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_uc)
|
||||
is_newer = _uc.is_newer
|
||||
|
||||
|
||||
# Séparateurs tolérés dans l'allow-list canary (CSV, espaces, point-virgule).
|
||||
_CANARY_SEPARATORS = (",", ";")
|
||||
|
||||
|
||||
def parse_canary_machines(raw: Optional[str]) -> Set[str]:
|
||||
"""Parse l'allow-list canary en un ensemble de machine_id.
|
||||
|
||||
Tolérant : virgule / point-virgule / espace comme séparateurs, entrées
|
||||
vides ignorées. `None` ou chaîne vide → ensemble vide (aucun canary).
|
||||
"""
|
||||
if not raw or not isinstance(raw, str):
|
||||
return set()
|
||||
normalized = raw
|
||||
for sep in _CANARY_SEPARATORS:
|
||||
normalized = normalized.replace(sep, " ")
|
||||
return {tok for tok in (t.strip() for t in normalized.split()) if tok}
|
||||
|
||||
|
||||
def resolve_target_version(
|
||||
machine_id: Optional[str],
|
||||
stable_version: str,
|
||||
canary_version: Optional[str],
|
||||
canary_machines: Set[str],
|
||||
) -> str:
|
||||
"""Résout la version cible POUR CE POSTE (cœur canary — sécurité).
|
||||
|
||||
Règles (toutes prudentes par défaut) :
|
||||
1. Poste HORS liste canary → `stable_version` (jamais la nouvelle).
|
||||
2. machine_id absent / liste vide / pas de canary_version → `stable_version`.
|
||||
3. Poste DANS la liste canary ET `canary_version` fournie ET STRICTEMENT
|
||||
plus récente que stable → `canary_version`.
|
||||
4. Garde-fou : si `canary_version` <= `stable_version` (config douteuse,
|
||||
ex. downgrade), on sert quand même `stable_version` (jamais de recul).
|
||||
|
||||
Ne lève jamais. Une version illisible retombe naturellement sur le stable
|
||||
via le comparateur semver tolérant.
|
||||
"""
|
||||
# Cas 1/2 : hors canary → stable.
|
||||
if not machine_id or machine_id not in canary_machines:
|
||||
return stable_version
|
||||
if not canary_version:
|
||||
return stable_version
|
||||
|
||||
# Cas 4 : garde-fou anti-recul — le canary doit être STRICTEMENT plus récent.
|
||||
if not is_newer(canary_version, stable_version):
|
||||
return stable_version
|
||||
|
||||
# Cas 3 : poste canary → nouvelle version.
|
||||
return canary_version
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lecture de la politique depuis l'environnement (pilotage sans rebuild).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Défaut historique aligné sur AGENT_VERSION client (config.py) et sur le
|
||||
# fallback de _latest_agent_version().
|
||||
_DEFAULT_STABLE_VERSION = "1.0.1"
|
||||
|
||||
|
||||
def stable_version_from_env() -> str:
|
||||
"""Version servie à toute la flotte (floor). Défaut = 1.0.1."""
|
||||
return os.environ.get("RPA_AGENT_STABLE_VERSION", _DEFAULT_STABLE_VERSION)
|
||||
|
||||
|
||||
def canary_version_from_env() -> Optional[str]:
|
||||
"""Version canary (nouvelle), servie aux seuls postes canary. Optionnel."""
|
||||
val = os.environ.get("RPA_AGENT_CANARY_VERSION", "").strip()
|
||||
return val or None
|
||||
|
||||
|
||||
def canary_machines_from_env() -> Set[str]:
|
||||
"""Allow-list canary (machine_id) depuis RPA_AGENT_CANARY_MACHINES."""
|
||||
return parse_canary_machines(os.environ.get("RPA_AGENT_CANARY_MACHINES", ""))
|
||||
|
||||
|
||||
def resolve_target_version_from_env(machine_id: Optional[str]) -> str:
|
||||
"""Raccourci : résout la version cible pour `machine_id` d'après l'env.
|
||||
|
||||
C'est le point d'entrée que l'endpoint serveur appelle. Il isole toute la
|
||||
lecture d'environnement ici (testable en injectant les paramètres via
|
||||
`resolve_target_version`).
|
||||
"""
|
||||
return resolve_target_version(
|
||||
machine_id=machine_id,
|
||||
stable_version=stable_version_from_env(),
|
||||
canary_version=canary_version_from_env(),
|
||||
canary_machines=canary_machines_from_env(),
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
{"case_id": "easily_rec_shot_0001_72_538", "screenshot_path": "/tmp/easily_session/shots/shot_0001_full.png", "task": {"intent": "cliquer sur « 25003362 »", "target_text": "25003362", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003362 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.3362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003362", "ocr_dist": 0.0067, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0002_380_919", "screenshot_path": "/tmp/easily_session/shots/shot_0002_full.png", "task": {"intent": "cliquer sur « iméicamentset-substancs »", "target_text": "iméicamentset-substancs", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « iméicamentset-substancs » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1484, "y_pct": 0.5744, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "iméicamentset-substancs", "ocr_dist": 0.0107, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0003_388_380", "screenshot_path": "/tmp/easily_session/shots/shot_0003_full.png", "task": {"intent": "cliquer sur « cliniques »", "target_text": "cliniques", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « cliniques » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1516, "y_pct": 0.2375, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "cliniques", "ocr_dist": 0.0075, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0004_552_381", "screenshot_path": "/tmp/easily_session/shots/shot_0004_full.png", "task": {"intent": "cliquer sur « Imagerie »", "target_text": "Imagerie", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Imagerie » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2156, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Imagerie", "ocr_dist": 0.009, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0005_685_385", "screenshot_path": "/tmp/easily_session/shots/shot_0005_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2676, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0127, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0007_947_381", "screenshot_path": "/tmp/easily_session/shots/shot_0007_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3699, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0057, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0011_72_288", "screenshot_path": "/tmp/easily_session/shots/shot_0011_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.18, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0109, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0012_95_613", "screenshot_path": "/tmp/easily_session/shots/shot_0012_full.png", "task": {"intent": "cliquer sur « 25003451 »", "target_text": "25003451", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003451 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0371, "y_pct": 0.3831, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003451", "ocr_dist": 0.0111, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0014_910_378", "screenshot_path": "/tmp/easily_session/shots/shot_0014_full.png", "task": {"intent": "cliquer sur « Synthèse »", "target_text": "Synthèse", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Synthèse » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3555, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Synthèse", "ocr_dist": 0.0172, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0015_638_381", "screenshot_path": "/tmp/easily_session/shots/shot_0015_full.png", "task": {"intent": "cliquer sur « Notes »", "target_text": "Notes", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Notes » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2492, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Notes", "ocr_dist": 0.0051, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0016_970_393", "screenshot_path": "/tmp/easily_session/shots/shot_0016_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3789, "y_pct": 0.2456, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.008, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0020_695_379", "screenshot_path": "/tmp/easily_session/shots/shot_0020_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2715, "y_pct": 0.2369, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0057, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0021_127_395", "screenshot_path": "/tmp/easily_session/shots/shot_0021_full.png", "task": {"intent": "cliquer sur « d'admission »", "target_text": "d'admission", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « d'admission » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0496, "y_pct": 0.2469, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "d'admission", "ocr_dist": 0.0089, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0022_73_304", "screenshot_path": "/tmp/easily_session/shots/shot_0022_full.png", "task": {"intent": "cliquer sur « IPP: »", "target_text": "IPP:", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP: » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "IPP:", "ocr_dist": 0.0147, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0024_84_269", "screenshot_path": "/tmp/easily_session/shots/shot_0024_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0328, "y_pct": 0.1681, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0018, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0025_67_790", "screenshot_path": "/tmp/easily_session/shots/shot_0025_full.png", "task": {"intent": "cliquer sur « 25012257 »", "target_text": "25012257", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25012257 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0262, "y_pct": 0.4938, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25012257", "ocr_dist": 0.0053, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0028_770_385", "screenshot_path": "/tmp/easily_session/shots/shot_0028_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3008, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0166, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0029_766_378", "screenshot_path": "/tmp/easily_session/shots/shot_0029_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2992, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0196, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0032_954_375", "screenshot_path": "/tmp/easily_session/shots/shot_0032_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3727, "y_pct": 0.2344, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0095, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0037_2028_1290", "screenshot_path": "/tmp/easily_session/shots/shot_0037_full.png", "task": {"intent": "cliquer sur « terminé »", "target_text": "terminé", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « terminé » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7922, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "terminé", "ocr_dist": 0.0395, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0041_2010_1013", "screenshot_path": "/tmp/easily_session/shots/shot_0041_full.png", "task": {"intent": "cliquer sur « mémorisées »", "target_text": "mémorisées", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « mémorisées » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7852, "y_pct": 0.6331, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "mémorisées", "ocr_dist": 0.0454, "needs_human_check": false}}
|
||||
15
benchmarks/computer_use/cases/leabench_easily_clean_v2.jsonl
Normal file
15
benchmarks/computer_use/cases/leabench_easily_clean_v2.jsonl
Normal file
@@ -0,0 +1,15 @@
|
||||
{"case_id": "easily_shot_0001_72_538", "screenshot_path": "/tmp/easily_session/shots/shot_0001_full.png", "task": {"intent": "cliquer sur « 25003362 »", "target_text": "25003362", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003362 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.3362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0003_388_380", "screenshot_path": "/tmp/easily_session/shots/shot_0003_full.png", "task": {"intent": "cliquer sur « cliniques »", "target_text": "cliniques", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « cliniques » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1516, "y_pct": 0.2375, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0004_552_381", "screenshot_path": "/tmp/easily_session/shots/shot_0004_full.png", "task": {"intent": "cliquer sur « Imagerie »", "target_text": "Imagerie", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Imagerie » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2156, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0005_685_385", "screenshot_path": "/tmp/easily_session/shots/shot_0005_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2676, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0012_95_613", "screenshot_path": "/tmp/easily_session/shots/shot_0012_full.png", "task": {"intent": "cliquer sur « 25003451 »", "target_text": "25003451", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003451 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0371, "y_pct": 0.3831, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0013_2393_1215", "screenshot_path": "/tmp/easily_session/shots/shot_0013_full.png", "task": {"intent": "cliquer sur « Oui »", "target_text": "Oui", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Oui » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9348, "y_pct": 0.7594, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0014_910_378", "screenshot_path": "/tmp/easily_session/shots/shot_0014_full.png", "task": {"intent": "cliquer sur « Synthèse »", "target_text": "Synthèse", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Synthèse » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3555, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0015_638_381", "screenshot_path": "/tmp/easily_session/shots/shot_0015_full.png", "task": {"intent": "cliquer sur « Notes »", "target_text": "Notes", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Notes » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2492, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0020_695_379", "screenshot_path": "/tmp/easily_session/shots/shot_0020_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2715, "y_pct": 0.2369, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0021_127_395", "screenshot_path": "/tmp/easily_session/shots/shot_0021_full.png", "task": {"intent": "cliquer sur « d'admission »", "target_text": "d'admission", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « d'admission » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0496, "y_pct": 0.2469, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0022_73_304", "screenshot_path": "/tmp/easily_session/shots/shot_0022_full.png", "task": {"intent": "cliquer sur « IPP: »", "target_text": "IPP:", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP: » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": false, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0023_73_304", "screenshot_path": "/tmp/easily_session/shots/shot_0023_full.png", "task": {"intent": "cliquer sur « IPP »", "target_text": "IPP", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": false, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0025_67_790", "screenshot_path": "/tmp/easily_session/shots/shot_0025_full.png", "task": {"intent": "cliquer sur « 25012257 »", "target_text": "25012257", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25012257 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0262, "y_pct": 0.4938, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0028_770_385", "screenshot_path": "/tmp/easily_session/shots/shot_0028_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3008, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": true, "ocr_occurrences": 1}}
|
||||
{"case_id": "easily_shot_0037_2028_1290", "screenshot_path": "/tmp/easily_session/shots/shot_0037_full.png", "task": {"intent": "cliquer sur « terminé »", "target_text": "terminé", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « terminé » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7922, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "contained_in_line": false, "ocr_occurrences": 1}}
|
||||
@@ -0,0 +1,41 @@
|
||||
{"case_id": "easily_rec_shot_0001_72_538", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0001_full.png", "task": {"intent": "cliquer sur « 25003362 »", "target_text": "25003362", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003362 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.3362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003362", "ocr_dist": 0.0067, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0002_380_919", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0002_full.png", "task": {"intent": "cliquer sur « iméicamentset-substancs »", "target_text": "iméicamentset-substancs", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « iméicamentset-substancs » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1484, "y_pct": 0.5744, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "iméicamentset-substancs", "ocr_dist": 0.0107, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0003_388_380", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0003_full.png", "task": {"intent": "cliquer sur « cliniques »", "target_text": "cliniques", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « cliniques » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.1516, "y_pct": 0.2375, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "cliniques", "ocr_dist": 0.0075, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0004_552_381", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0004_full.png", "task": {"intent": "cliquer sur « Imagerie »", "target_text": "Imagerie", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Imagerie » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2156, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Imagerie", "ocr_dist": 0.009, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0005_685_385", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0005_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2676, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0127, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0006_2547_962", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0006_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9949, "y_pct": 0.6012, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0007_947_381", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0007_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3699, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0057, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0008_903_552", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0008_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3527, "y_pct": 0.345, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0009_903_552", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0009_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3527, "y_pct": 0.345, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0010_2546_1042", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0010_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9945, "y_pct": 0.6512, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0011_72_288", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0011_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003362 — LAFFONT Alice — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0281, "y_pct": 0.18, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0109, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0012_95_613", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0012_full.png", "task": {"intent": "cliquer sur « 25003451 »", "target_text": "25003451", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25003451 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0371, "y_pct": 0.3831, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25003451", "ocr_dist": 0.0111, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0013_2393_1215", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0013_full.png", "task": {"intent": "cliquer sur « Oui »", "target_text": "Oui", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Oui » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9348, "y_pct": 0.7594, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Oui", "ocr_dist": 0.0036, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0014_910_378", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0014_full.png", "task": {"intent": "cliquer sur « Synthèse »", "target_text": "Synthèse", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Synthèse » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3555, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Synthèse", "ocr_dist": 0.0172, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0015_638_381", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0015_full.png", "task": {"intent": "cliquer sur « Notes »", "target_text": "Notes", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Notes » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2492, "y_pct": 0.2381, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Notes", "ocr_dist": 0.0051, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0016_970_393", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0016_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3789, "y_pct": 0.2456, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.008, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0017_2506_1205", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0017_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9789, "y_pct": 0.7531, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0018_2549_1203", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0018_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9957, "y_pct": 0.7519, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0019_2557_244", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0019_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9988, "y_pct": 0.1525, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0020_695_379", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0020_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2715, "y_pct": 0.2369, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0057, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0021_127_395", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0021_full.png", "task": {"intent": "cliquer sur « d'admission »", "target_text": "d'admission", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « d'admission » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0496, "y_pct": 0.2469, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "d'admission", "ocr_dist": 0.0089, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0022_73_304", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0022_full.png", "task": {"intent": "cliquer sur « IPP: »", "target_text": "IPP:", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP: » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "IPP:", "ocr_dist": 0.0147, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0023_73_304", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0023_full.png", "task": {"intent": "cliquer sur « IPP »", "target_text": "IPP", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « IPP » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0285, "y_pct": 0.19, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "IPP", "ocr_dist": 0.0163, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0024_84_269", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0024_full.png", "task": {"intent": "cliquer sur « Patients »", "target_text": "Patients", "current_window": "Dossier 25003451 — ROUX Lou — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Patients » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0328, "y_pct": 0.1681, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Patients", "ocr_dist": 0.0018, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0025_67_790", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0025_full.png", "task": {"intent": "cliquer sur « 25012257 »", "target_text": "25012257", "current_window": "Dossier 25003284 — MOREL Catherine — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 25012257 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.0262, "y_pct": 0.4938, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "25012257", "ocr_dist": 0.0053, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0026_2545_1356", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0026_full.png", "task": {"intent": "cliquer sur « 95 »", "target_text": "95", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « 95 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9941, "y_pct": 0.8475, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "95", "ocr_dist": 0.0291, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0027_2541_284", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0027_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9926, "y_pct": 0.1775, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0028_770_385", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0028_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3008, "y_pct": 0.2406, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0166, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0029_766_378", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0029_full.png", "task": {"intent": "cliquer sur « médicales »", "target_text": "médicales", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « médicales » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.2992, "y_pct": 0.2362, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "médicales", "ocr_dist": 0.0196, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0030_2546_595", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0030_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9945, "y_pct": 0.3719, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0031_2558_1415", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0031_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9992, "y_pct": 0.8844, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0032_954_375", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0032_full.png", "task": {"intent": "cliquer sur « Urgences »", "target_text": "Urgences", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « Urgences » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.3727, "y_pct": 0.2344, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "Urgences", "ocr_dist": 0.0095, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0033_2544_704", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0033_full.png", "task": {"intent": "cliquer sur la cible", "target_text": "", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "Clique sur l'élément ciblé."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.9938, "y_pct": 0.44, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "", "ocr_dist": null, "needs_human_check": true}}
|
||||
{"case_id": "easily_rec_shot_0034_2188_1570", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0034_full.png", "task": {"intent": "cliquer sur « a »", "target_text": "a", "current_window": "Dossier 25012257 — BRUNEL Henri — DPI (maquette POC) - Google Chrome", "expected_next_window": "", "question": "L'élément « a » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8547, "y_pct": 0.9812, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "a", "ocr_dist": 0.0391, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0035_2166_1296", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0035_full.png", "task": {"intent": "cliquer sur « 0 »", "target_text": "0", "current_window": "Fenêtre de dépassement de capacité de la barre d’état système.", "expected_next_window": "", "question": "L'élément « 0 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8461, "y_pct": 0.81, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "0", "ocr_dist": 0.0133, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0036_2196_1285", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0036_full.png", "task": {"intent": "cliquer sur « - »", "target_text": "-", "current_window": "Fenêtre de dépassement de capacité de la barre d’état système.", "expected_next_window": "", "question": "L'élément « - » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8578, "y_pct": 0.8031, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "-", "ocr_dist": 0.0239, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0037_2028_1290", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0037_full.png", "task": {"intent": "cliquer sur « terminé »", "target_text": "terminé", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « terminé » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7922, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "terminé", "ocr_dist": 0.0395, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0038_2031_1283", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0038_full.png", "task": {"intent": "cliquer sur « 0 »", "target_text": "0", "current_window": "Fenêtre de dépassement de capacité de la barre d’état système.", "expected_next_window": "", "question": "L'élément « 0 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7934, "y_pct": 0.8019, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "0", "ocr_dist": 0.0407, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0039_2192_1298", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0039_full.png", "task": {"intent": "cliquer sur « 0 »", "target_text": "0", "current_window": "Fenêtre de dépassement de capacité de la barre d’état système.", "expected_next_window": "", "question": "L'élément « 0 » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8562, "y_pct": 0.8113, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "0", "ocr_dist": 0.0235, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0040_2131_1290", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0040_full.png", "task": {"intent": "cliquer sur « 9 0 - »", "target_text": "9 0 -", "current_window": "Fenêtre de dépassement de capacité de la barre d’état système.", "expected_next_window": "", "question": "L'élément « 9 0 - » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.8324, "y_pct": 0.8063, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "9 0 -", "ocr_dist": 0.0125, "needs_human_check": false}}
|
||||
{"case_id": "easily_rec_shot_0041_2010_1013", "screenshot_path": "../../../../tmp/easily_session/shots/shot_0041_full.png", "task": {"intent": "cliquer sur « mémorisées »", "target_text": "mémorisées", "current_window": "unknown_window", "expected_next_window": "", "question": "L'élément « mémorisées » est-il visible ? Clique uniquement dessus."}, "expectation": {"decision": "click", "click_region": {"x_pct": 0.7852, "y_pct": 0.6331, "radius_pct": 0.05}, "accepted_reasons": ["human_click_groundtruth"]}, "metadata": {"source": "easily_record", "session": "easily_session", "click_type": "mouse_click", "ocr_target": "mémorisées", "ocr_dist": 0.0454, "needs_human_check": false}}
|
||||
@@ -247,3 +247,33 @@ def map_roles(
|
||||
data = parse_vlm_json(raw)
|
||||
vlm_fields = data.get("champs", []) if isinstance(data, dict) else []
|
||||
return reconstruct_fields(tokens, vlm_fields)
|
||||
|
||||
|
||||
def extract_dossier_from_image(
|
||||
image_path: str,
|
||||
vlm_client: VlmClient,
|
||||
roles: Optional[Sequence[str]] = None,
|
||||
ocr_fn: Optional[Callable[[str], Sequence[Sequence[dict]]]] = None,
|
||||
min_confidence: float = 0.6,
|
||||
required_roles: Optional[Sequence[str]] = None,
|
||||
) -> dict:
|
||||
"""Orchestre l'extraction d'un dossier depuis une capture : OCR → rôles → qualité.
|
||||
|
||||
Enchaîne `ocr_fn` (grille OCR) → `tokens_from_grid` → `map_roles` (VLM, ancrage
|
||||
strict) → `assess_quality`. C'est la brique que le handler runtime
|
||||
`_handle_extract_dossier_action` appellera, avec le vrai OCR et le vrai client
|
||||
vLLM. `ocr_fn` et `vlm_client` sont INJECTABLES (testable hors-ligne).
|
||||
|
||||
`ocr_fn` par défaut = `core.llm.ocr_extractor.extract_grid_from_image` (import
|
||||
LAZY : le module reste pur quand l'OCR est injecté en test).
|
||||
|
||||
Returns:
|
||||
{fields: List[MappedField], status: str, n_tokens: int}
|
||||
"""
|
||||
if ocr_fn is None:
|
||||
from core.llm.ocr_extractor import extract_grid_from_image as ocr_fn
|
||||
grid = ocr_fn(image_path)
|
||||
tokens = tokens_from_grid(grid)
|
||||
fields = map_roles(image_path, tokens, vlm_client, roles)
|
||||
status = assess_quality(fields, required_roles=required_roles, min_confidence=min_confidence)
|
||||
return {"fields": fields, "status": status, "n_tokens": len(tokens)}
|
||||
|
||||
86
core/extraction/vlm_client.py
Normal file
86
core/extraction/vlm_client.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Client vLLM serveur : (image_path, prompt) -> texte de réponse.
|
||||
|
||||
Petit client réutilisable pour la lecture d'écran (extraction de dossier). Le
|
||||
grounder (`resolve_engine`) fait déjà un POST vers vLLM:8001 mais en INLINE, non
|
||||
exposé ; on factorise ici un client propre, configurable et testable.
|
||||
|
||||
- Image downscalée (largeur max) avant envoi : la fenêtre vLLM est limitée
|
||||
(`max_model_len`), un écran plein déborde sinon (vu 30/06 : 6193+2000 > 8192).
|
||||
- `thinking` désactivé (vérifié : think=on -> sortie vide/lente sur ce modèle).
|
||||
- `post_fn` injectable -> testable sans vLLM réel.
|
||||
|
||||
Branche feat/push-log-dgx.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from io import BytesIO
|
||||
from typing import Callable, Optional
|
||||
|
||||
VlmClient = Callable[[str, str], str]
|
||||
|
||||
_DEFAULT_PORT = os.environ.get("VLLM_PORT", "8001")
|
||||
DEFAULT_URL = f"http://localhost:{_DEFAULT_PORT}/v1/chat/completions"
|
||||
DEFAULT_MODEL = os.environ.get("VLLM_MODEL", "Qwen/Qwen3-VL-4B-Instruct")
|
||||
|
||||
|
||||
def img_data_url(image_path: str, max_w: int = 1280) -> str:
|
||||
"""Encode l'image en data-URL PNG base64, downscalée à `max_w` si plus large."""
|
||||
from PIL import Image
|
||||
img = Image.open(image_path).convert("RGB")
|
||||
if img.width > max_w:
|
||||
h = int(img.height * max_w / img.width)
|
||||
img = img.resize((max_w, h), Image.LANCZOS)
|
||||
buf = BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
||||
|
||||
|
||||
def build_chat_body(
|
||||
image_path: str,
|
||||
prompt: str,
|
||||
model: str = DEFAULT_MODEL,
|
||||
max_tokens: int = 1500,
|
||||
max_w: int = 1280,
|
||||
) -> dict:
|
||||
"""Construit le body chat/completions (image + prompt, thinking off)."""
|
||||
return {
|
||||
"model": model,
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "image_url", "image_url": {"url": img_data_url(image_path, max_w)}},
|
||||
{"type": "text", "text": prompt},
|
||||
],
|
||||
}],
|
||||
"temperature": 0.0,
|
||||
"max_tokens": max_tokens,
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
}
|
||||
|
||||
|
||||
def make_vllm_client(
|
||||
url: str = DEFAULT_URL,
|
||||
model: str = DEFAULT_MODEL,
|
||||
max_tokens: int = 1500,
|
||||
max_w: int = 1280,
|
||||
timeout: float = 120,
|
||||
post_fn: Optional[Callable] = None,
|
||||
) -> VlmClient:
|
||||
"""Construit un client `(image_path, prompt) -> texte`, branché sur vLLM.
|
||||
|
||||
`post_fn` (signature `requests.post`) est injectable pour les tests.
|
||||
Lève `RuntimeError` si le serveur ne répond pas 200 (message technique, sans PII).
|
||||
"""
|
||||
def client(image_path: str, prompt: str) -> str:
|
||||
body = build_chat_body(image_path, prompt, model=model, max_tokens=max_tokens, max_w=max_w)
|
||||
poster = post_fn
|
||||
if poster is None:
|
||||
import requests
|
||||
poster = requests.post
|
||||
r = poster(url, json=body, headers={}, timeout=timeout)
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError(f"vLLM {r.status_code}: {str(getattr(r, 'text', ''))[:300]}")
|
||||
return r.json()["choices"][0]["message"]["content"]
|
||||
return client
|
||||
119
core/navigation/__init__.py
Normal file
119
core/navigation/__init__.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Navigation brique — login visuel, recherche dossiers, vérification écran.
|
||||
|
||||
Modules :
|
||||
- visual_verifier : verify_before / verify_after chaque action (vision = validateur, OCR-ancré)
|
||||
- grounding : résolution visuelle d'éléments UI (OCR-anchor first, VLM fallback, coords cache)
|
||||
- visual_login : login form resolution + verification (DPI urgences default config)
|
||||
- action_resolver : pont navigation → runtime (coords normalisés, OCR/VLM adapters)
|
||||
|
||||
Pattern d'injection : VlmClient + OcrClient + OcrDetailedClient injectables
|
||||
"""
|
||||
|
||||
from .visual_verifier import verify_screen_match, ScreenMatchResult
|
||||
from .action_resolver import navigate_login, NavigateResult
|
||||
|
||||
__all__ = [
|
||||
"verify_screen_match",
|
||||
"ScreenMatchResult",
|
||||
"navigate_login",
|
||||
"NavigateResult",
|
||||
"_handle_navigate_action",
|
||||
]
|
||||
|
||||
# Handler pour replay_engine — importé par api_stream.py
|
||||
def _handle_navigate_action(
|
||||
action: dict,
|
||||
replay_state: dict,
|
||||
session_id: str,
|
||||
) -> bool:
|
||||
"""Handler serveur pour action navigate (branchement replay_engine).
|
||||
|
||||
Thin wrapper : résout coords du login form et les stocke dans
|
||||
replay_state["variables"] pour les actions type/click suivantes.
|
||||
|
||||
N'échoue jamais le replay — toute erreur → log + needs_review.
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger("navigation._handle_navigate_action")
|
||||
|
||||
params = action.get("parameters") or {}
|
||||
navigate_action = params.get("action", "login")
|
||||
|
||||
# Noms des variables output (configurable)
|
||||
login_var = (params.get("login_coords_var") or "navigate_login_coords").strip()
|
||||
password_var = (params.get("password_coords_var") or "navigate_password_coords").strip()
|
||||
submit_var = (params.get("submit_coords_var") or "navigate_submit_coords").strip()
|
||||
|
||||
variables = replay_state.setdefault("variables", {})
|
||||
|
||||
try:
|
||||
screenshot_path = ""
|
||||
# Résoudre screenshot depuis replay_state
|
||||
if "last_screenshot_path" in replay_state:
|
||||
screenshot_path = replay_state["last_screenshot_path"]
|
||||
elif "last_heartbeat" in replay_state:
|
||||
hb = replay_state["last_heartbeat"]
|
||||
screenshot_path = hb.get("screenshot_path", "") if isinstance(hb, dict) else ""
|
||||
|
||||
if not screenshot_path:
|
||||
logger.warning("navigate: no screenshot for session %s", session_id)
|
||||
variables[login_var] = {"error": "no_screenshot"}
|
||||
return False
|
||||
|
||||
# Dimensions écran (fallback 1920×1080)
|
||||
screen_width = replay_state.get("screen_width", 1920)
|
||||
screen_height = replay_state.get("screen_height", 1080)
|
||||
|
||||
# OCR/VLM clients — lazy import pour éviter circular dependency
|
||||
from core.llm import extract_grid_from_image
|
||||
from core.extraction.vlm_client import make_vllm_client
|
||||
from core.navigation.action_resolver import make_ocr_detailed_from_grid
|
||||
|
||||
ocr_detailed = make_ocr_detailed_from_grid(extract_grid_from_image)
|
||||
vlm_client = make_vllm_client()
|
||||
|
||||
# Config login
|
||||
from core.navigation.visual_login import LoginFormConfig, dpi_urgences_login_config
|
||||
config = dpi_urgences_login_config()
|
||||
if "login_field" in params:
|
||||
config = LoginFormConfig(
|
||||
login_field=params.get("login_field", config.login_field),
|
||||
password_field=params.get("password_field", config.password_field),
|
||||
submit_button=params.get("submit_button", config.submit_button),
|
||||
success_elements=params.get("success_elements", config.success_elements),
|
||||
context=params.get("context", config.context),
|
||||
)
|
||||
|
||||
# Orchestration navigate
|
||||
from core.navigation.action_resolver import navigate_login
|
||||
result = navigate_login(
|
||||
screenshot_path, config=config,
|
||||
ocr_client=ocr_detailed, vlm_client=vlm_client,
|
||||
screen_width=screen_width, screen_height=screen_height,
|
||||
)
|
||||
|
||||
# Stocker coords dans variables (format dict pour substitution)
|
||||
if result.login_coords:
|
||||
variables[login_var] = result.login_coords.to_dict()
|
||||
if result.password_coords:
|
||||
variables[password_var] = result.password_coords.to_dict()
|
||||
if result.submit_coords:
|
||||
variables[submit_var] = result.submit_coords.to_dict()
|
||||
|
||||
variables["navigate_result"] = {
|
||||
"all_resolved": result.all_resolved,
|
||||
"method": result.login_coords.method if result.login_coords else "",
|
||||
"error": result.error,
|
||||
}
|
||||
|
||||
if not result.all_resolved:
|
||||
logger.warning("navigate: incomplete — %s", result.error)
|
||||
return False
|
||||
|
||||
logger.info("navigate: login form resolved OK (method=%s)", result.login_coords.method if result.login_coords else "?")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("navigate: exception (%s) — needs_review", e)
|
||||
variables["navigate_result"] = {"all_resolved": False, "error": str(e)}
|
||||
return False
|
||||
205
core/navigation/action_resolver.py
Normal file
205
core/navigation/action_resolver.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Action resolver — pont entre modules navigation et runtime replay.
|
||||
|
||||
Orchestre verify → ground → store coords pour le handler replay_engine.
|
||||
Convertit coords pixels → normalisé (x_pct/y_pct) pour le client Agent V1.
|
||||
|
||||
Architecture :
|
||||
- handler replay_engine = thin wrapper (appelle action_resolver)
|
||||
- action_resolver = bridge (adapte OCR/VLM runtime → interfaces navigation)
|
||||
- modules navigation = pure functions (ne connaissent pas le runtime)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from core.navigation.grounding import (
|
||||
BBox,
|
||||
CoordsCache,
|
||||
GroundedElement,
|
||||
OcrDetailedClient,
|
||||
OcrTokenInfo,
|
||||
ground_element,
|
||||
)
|
||||
from core.navigation.visual_login import (
|
||||
LoginFormConfig,
|
||||
LoginResolution,
|
||||
dpi_urgences_login_config,
|
||||
resolve_login_form,
|
||||
verify_login_visible,
|
||||
verify_login_success,
|
||||
)
|
||||
from core.navigation.visual_verifier import (
|
||||
OcrClient,
|
||||
ScreenMatchResult,
|
||||
VlmClient,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Dataclasses ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class NavigateCoords:
|
||||
"""Normalized coords for a grounded element — format Agent V1 client."""
|
||||
|
||||
x_pct: float # center x normalized [0-1]
|
||||
y_pct: float # center y normalized [0-1]
|
||||
bbox_pct: Optional[Tuple[float, float, float, float]] = None # (x1, y1, x2, y2) normalized
|
||||
method: str = "" # grounding method used
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d = {"x_pct": self.x_pct, "y_pct": self.y_pct, "method": self.method}
|
||||
if self.bbox_pct:
|
||||
d["bbox_pct"] = list(self.bbox_pct)
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class NavigateResult:
|
||||
"""Result of a navigate action — coords for each resolved field."""
|
||||
|
||||
login_coords: Optional[NavigateCoords] = None
|
||||
password_coords: Optional[NavigateCoords] = None
|
||||
submit_coords: Optional[NavigateCoords] = None
|
||||
all_resolved: bool = False
|
||||
pre_verify: Optional[ScreenMatchResult] = None
|
||||
post_verify: Optional[ScreenMatchResult] = None # set later by verify_after
|
||||
error: str = ""
|
||||
|
||||
|
||||
# ── Coordinate conversion ────────────────────────────────────────────
|
||||
|
||||
|
||||
def grounded_to_coords(
|
||||
element: GroundedElement,
|
||||
screen_width: int,
|
||||
screen_height: int,
|
||||
) -> NavigateCoords:
|
||||
"""Convert GroundedElement (pixels) to NavigateCoords (normalized pct)."""
|
||||
x_pct = element.center[0] / screen_width if screen_width else 0
|
||||
y_pct = element.center[1] / screen_height if screen_height else 0
|
||||
x1_pct = element.bbox[0] / screen_width if screen_width else 0
|
||||
y1_pct = element.bbox[1] / screen_height if screen_height else 0
|
||||
x2_pct = element.bbox[2] / screen_width if screen_width else 0
|
||||
y2_pct = element.bbox[3] / screen_height if screen_height else 0
|
||||
return NavigateCoords(
|
||||
x_pct=x_pct,
|
||||
y_pct=y_pct,
|
||||
bbox_pct=(x1_pct, y1_pct, x2_pct, y2_pct),
|
||||
method=element.method,
|
||||
)
|
||||
|
||||
|
||||
# ── OCR adapter ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def make_ocr_detailed_from_grid(
|
||||
grid_fn: Callable[[str], List[List[Dict[str, Any]]]],
|
||||
) -> OcrDetailedClient:
|
||||
"""Adapt extract_grid_from_image → OcrDetailedClient (List[OcrTokenInfo]).
|
||||
|
||||
Converts the grid format (list of rows of cells with bbox) into
|
||||
flat OcrTokenInfo list with normalized LTRB bbox.
|
||||
"""
|
||||
from core.extraction.role_mapper import tokens_from_grid
|
||||
|
||||
def client(image_path: str) -> List[OcrTokenInfo]:
|
||||
grid = grid_fn(image_path)
|
||||
ocr_tokens = tokens_from_grid(grid)
|
||||
return [
|
||||
OcrTokenInfo(
|
||||
text=t.text,
|
||||
bbox=t.bbox,
|
||||
confidence=t.confidence,
|
||||
)
|
||||
for t in ocr_tokens
|
||||
]
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def make_ocr_simple_from_detailed(
|
||||
ocr_detailed: OcrDetailedClient,
|
||||
) -> OcrClient:
|
||||
"""Derive text-only OcrClient from OcrDetailedClient."""
|
||||
def client(image_path: str) -> List[str]:
|
||||
return [t.text for t in ocr_detailed(image_path)]
|
||||
return client
|
||||
|
||||
|
||||
# ── Navigate login orchestration ─────────────────────────────────────
|
||||
|
||||
|
||||
def navigate_login(
|
||||
screenshot_path: str,
|
||||
config: Optional[LoginFormConfig] = None,
|
||||
ocr_client: Optional[OcrDetailedClient] = None,
|
||||
vlm_client: Optional[VlmClient] = None,
|
||||
screen_width: int = 1920,
|
||||
screen_height: int = 1080,
|
||||
coords_cache: Optional[CoordsCache] = None,
|
||||
skip_pre_verify: bool = False,
|
||||
) -> NavigateResult:
|
||||
"""Orchestrate login navigation: verify → ground → convert coords.
|
||||
|
||||
Returns NavigateResult with normalized coords for each field.
|
||||
The handler stores these in replay_state variables for subsequent
|
||||
type/click actions.
|
||||
"""
|
||||
if config is None:
|
||||
config = dpi_urgences_login_config()
|
||||
|
||||
if ocr_client is None or vlm_client is None:
|
||||
return NavigateResult(
|
||||
all_resolved=False,
|
||||
error="ocr_client and vlm_client required",
|
||||
)
|
||||
|
||||
ocr_simple = make_ocr_simple_from_detailed(ocr_client)
|
||||
|
||||
# Step 1: Pre-verification (optional)
|
||||
pre_verify = None
|
||||
if not skip_pre_verify:
|
||||
pre_verify = verify_login_visible(
|
||||
screenshot_path, config, ocr_simple, vlm_client,
|
||||
)
|
||||
if not pre_verify.match:
|
||||
logger.warning("navigate_login: pre-verify failed — %s", pre_verify.describe())
|
||||
return NavigateResult(
|
||||
all_resolved=False,
|
||||
pre_verify=pre_verify,
|
||||
error=f"pre-verify failed: {pre_verify.describe()}",
|
||||
)
|
||||
|
||||
# Step 2: Ground all fields
|
||||
resolution = resolve_login_form(
|
||||
screenshot_path, config, ocr_client, vlm_client,
|
||||
screen_width=screen_width, screen_height=screen_height,
|
||||
coords_cache=coords_cache,
|
||||
)
|
||||
|
||||
if not resolution.all_resolved:
|
||||
logger.warning("navigate_login: incomplete resolution — %s", resolution.describe())
|
||||
return NavigateResult(
|
||||
all_resolved=False,
|
||||
pre_verify=pre_verify,
|
||||
error=f"incomplete resolution: {resolution.describe()}",
|
||||
)
|
||||
|
||||
# Step 3: Convert to normalized coords
|
||||
login_coords = grounded_to_coords(resolution.login_field, screen_width, screen_height) if resolution.login_field else None
|
||||
password_coords = grounded_to_coords(resolution.password_field, screen_width, screen_height) if resolution.password_field else None
|
||||
submit_coords = grounded_to_coords(resolution.submit_button, screen_width, screen_height) if resolution.submit_button else None
|
||||
|
||||
return NavigateResult(
|
||||
login_coords=login_coords,
|
||||
password_coords=password_coords,
|
||||
submit_coords=submit_coords,
|
||||
all_resolved=True,
|
||||
pre_verify=pre_verify,
|
||||
)
|
||||
375
core/navigation/grounding.py
Normal file
375
core/navigation/grounding.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""Grounding — résolution visuelle d'éléments UI → coords (bbox + center).
|
||||
|
||||
Architecture OCR-ancrée (alignée avec visual_verifier) :
|
||||
- STRATÉGIE 1 : OCR-anchor — si le texte cible est trouvé par OCR,
|
||||
utiliser le bbox du token OCR (déterministe, zero hallucination).
|
||||
- STRATÉGIE 2 : VLM grounder — si OCR ne trouve pas le texte,
|
||||
le VLM localise l'élément visuellement (fallback, risque contrôlé).
|
||||
- CACHE coords : mémorise les coords résolues, validées par vision avant usage.
|
||||
Si cached coords fail → re-résolution visuelle.
|
||||
|
||||
Coords = cache local validé par vue (Dom/Claude recadrage 01/07).
|
||||
Vision = source de vérité, coords = shortcut validé.
|
||||
|
||||
BBox format interne : LTRB (x1, y1, x2, y2) pixels absolus —
|
||||
cohérent avec SomElement, OcrToken, DetectedUIElement.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from core.navigation.visual_verifier import (
|
||||
fuzzy_match,
|
||||
normalize_text,
|
||||
OcrClient,
|
||||
VlmClient,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# BBox format: LTRB pixels (x1, y1, x2, y2)
|
||||
BBox = Tuple[int, int, int, int]
|
||||
|
||||
|
||||
# ── Dataclasses ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class OcrTokenInfo:
|
||||
"""OCR token with bounding box — for grounding (richer than text-only)."""
|
||||
|
||||
text: str
|
||||
bbox: Optional[BBox] = None # (x1, y1, x2, y2) LTRB pixels
|
||||
confidence: float = 1.0
|
||||
|
||||
|
||||
# Type alias — injectable OCR client returning tokens with bbox
|
||||
# More detailed than visual_verifier's OcrClient (which returns List[str])
|
||||
OcrDetailedClient = Callable[[str], List[OcrTokenInfo]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundedElement:
|
||||
"""A UI element grounded on screen with coordinates."""
|
||||
|
||||
role: str
|
||||
text: str
|
||||
bbox: BBox # (x1, y1, x2, y2) LTRB pixels
|
||||
center: Tuple[int, int] # (cx, cy) — click target
|
||||
confidence: float
|
||||
method: str # "ocr_anchor" or "vlm_grounder" or "cache"
|
||||
source_ocr_text: str = "" # actual OCR text that matched (for fuzzy)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordsCacheEntry:
|
||||
"""Cached coordinates for a UI element."""
|
||||
|
||||
element_key: str # "role:text"
|
||||
bbox: BBox
|
||||
center: Tuple[int, int]
|
||||
method: str # how it was originally resolved
|
||||
validation_count: int = 0
|
||||
|
||||
|
||||
class CoordsCache:
|
||||
"""In-memory cache of grounded coordinates.
|
||||
|
||||
Entries are validated by vision before use (verify_after).
|
||||
If cached coords fail verification → invalidate + re-resolve.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._entries: Dict[str, CoordsCacheEntry] = {}
|
||||
|
||||
def get(self, element_key: str) -> Optional[CoordsCacheEntry]:
|
||||
return self._entries.get(element_key)
|
||||
|
||||
def put(
|
||||
self,
|
||||
element_key: str,
|
||||
bbox: BBox,
|
||||
center: Tuple[int, int],
|
||||
method: str,
|
||||
) -> None:
|
||||
entry = self._entries.get(element_key)
|
||||
if entry:
|
||||
entry.bbox = bbox
|
||||
entry.center = center
|
||||
entry.method = method
|
||||
entry.validation_count += 1
|
||||
else:
|
||||
self._entries[element_key] = CoordsCacheEntry(
|
||||
element_key=element_key,
|
||||
bbox=bbox,
|
||||
center=center,
|
||||
method=method,
|
||||
validation_count=1,
|
||||
)
|
||||
|
||||
def invalidate(self, element_key: str) -> None:
|
||||
self._entries.pop(element_key, None)
|
||||
|
||||
def clear(self) -> None:
|
||||
self._entries.clear()
|
||||
|
||||
def keys(self) -> List[str]:
|
||||
return list(self._entries.keys())
|
||||
|
||||
|
||||
# ── Helper functions ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def bbox_center(bbox: BBox) -> Tuple[int, int]:
|
||||
"""Compute center point from LTRB bbox."""
|
||||
x1, y1, x2, y2 = bbox
|
||||
return ((x1 + x2) // 2, (y1 + y2) // 2)
|
||||
|
||||
|
||||
def make_element_key(role: str, text: str) -> str:
|
||||
"""Create a stable cache key from role + text."""
|
||||
return f"{role}:{normalize_text(text)}"
|
||||
|
||||
|
||||
# ── OCR-anchored grounding (deterministic) ───────────────────────────
|
||||
|
||||
|
||||
def ocr_anchor_ground(
|
||||
ocr_tokens: List[OcrTokenInfo],
|
||||
target: Dict[str, Any],
|
||||
fuzzy_threshold: float = 0.8,
|
||||
) -> Optional[GroundedElement]:
|
||||
"""Ground an element using OCR tokens with bbox (deterministic).
|
||||
|
||||
Finds the target text in OCR tokens via fuzzy match.
|
||||
Returns GroundedElement with bbox from the matching OCR token.
|
||||
"""
|
||||
target_text = target.get("text", "")
|
||||
target_role = target.get("role", "?")
|
||||
|
||||
if not target_text:
|
||||
return None
|
||||
|
||||
for token in ocr_tokens:
|
||||
if fuzzy_match(target_text, token.text, threshold=fuzzy_threshold):
|
||||
if token.bbox is None:
|
||||
continue # token found but no bbox → can't ground
|
||||
|
||||
return GroundedElement(
|
||||
role=target_role,
|
||||
text=target_text,
|
||||
bbox=token.bbox,
|
||||
center=bbox_center(token.bbox),
|
||||
confidence=token.confidence,
|
||||
method="ocr_anchor",
|
||||
source_ocr_text=token.text,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ── VLM grounder (fallback) ─────────────────────────────────────────
|
||||
|
||||
|
||||
def build_grounder_prompt(
|
||||
target: Dict[str, Any],
|
||||
context: str = "",
|
||||
) -> str:
|
||||
"""Build VLM prompt for locating a UI element on screen.
|
||||
|
||||
Asks for bounding box in normalized coordinates [0-1].
|
||||
"""
|
||||
role = target.get("role", "?")
|
||||
text = target.get("text", "")
|
||||
extra = target.get("extra", "")
|
||||
|
||||
prompt = (
|
||||
"You are a UI element locator. Find the specified element on this "
|
||||
"screenshot and return its bounding box.\n"
|
||||
)
|
||||
if context:
|
||||
prompt += f"Context: {context}\n"
|
||||
prompt += f"Target element: {role} with text \"{text}\""
|
||||
if extra:
|
||||
prompt += f" ({extra})"
|
||||
prompt += (
|
||||
"\n\nRespond in JSON format:\n"
|
||||
"{\"found\": true/false, "
|
||||
"\"bbox\": [x1_norm, y1_norm, x2_norm, y2_norm], "
|
||||
"\"confidence\": 0.0-1.0, "
|
||||
"\"description\": \"...\"}\n"
|
||||
"bbox coordinates are normalized [0.0-1.0] relative to image dimensions "
|
||||
"(x1=left, y1=top, x2=right, y2=bottom). "
|
||||
"Only return found=true if you can clearly locate the element."
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def parse_grounder_response(
|
||||
vlm_text: str,
|
||||
screen_width: int,
|
||||
screen_height: int,
|
||||
target: Dict[str, Any],
|
||||
) -> Optional[GroundedElement]:
|
||||
"""Parse VLM grounder response into GroundedElement.
|
||||
|
||||
Converts normalized bbox [0-1] to absolute pixels.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(vlm_text)
|
||||
except json.JSONDecodeError:
|
||||
json_match = re.search(r"\{[\s\S]*\}", vlm_text)
|
||||
if json_match:
|
||||
try:
|
||||
data = json.loads(json_match.group())
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("grounding: VLM response not parseable as JSON")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
if not data.get("found", False):
|
||||
return None
|
||||
|
||||
bbox_norm = data.get("bbox", [])
|
||||
if not isinstance(bbox_norm, list) or len(bbox_norm) != 4:
|
||||
logger.warning("grounding: invalid bbox format from VLM")
|
||||
return None
|
||||
|
||||
# Convert normalized [0-1] to absolute pixels
|
||||
try:
|
||||
x1 = int(float(bbox_norm[0]) * screen_width)
|
||||
y1 = int(float(bbox_norm[1]) * screen_height)
|
||||
x2 = int(float(bbox_norm[2]) * screen_width)
|
||||
y2 = int(float(bbox_norm[3]) * screen_height)
|
||||
except (ValueError, TypeError):
|
||||
logger.warning("grounding: bbox values not numeric")
|
||||
return None
|
||||
|
||||
# Clamp to screen bounds
|
||||
x1 = max(0, min(x1, screen_width))
|
||||
y1 = max(0, min(y1, screen_height))
|
||||
x2 = max(x1, min(x2, screen_width))
|
||||
y2 = max(y1, min(y2, screen_height))
|
||||
|
||||
confidence = data.get("confidence", 0.5)
|
||||
if isinstance(confidence, str):
|
||||
try:
|
||||
confidence = float(confidence)
|
||||
except ValueError:
|
||||
confidence = 0.5
|
||||
|
||||
bbox_abs: BBox = (x1, y1, x2, y2)
|
||||
|
||||
return GroundedElement(
|
||||
role=target.get("role", "?"),
|
||||
text=target.get("text", ""),
|
||||
bbox=bbox_abs,
|
||||
center=bbox_center(bbox_abs),
|
||||
confidence=confidence,
|
||||
method="vlm_grounder",
|
||||
)
|
||||
|
||||
|
||||
# ── Core grounding function (composition) ───────────────────────────
|
||||
|
||||
|
||||
def ground_element(
|
||||
screenshot_path: str,
|
||||
target: Dict[str, Any],
|
||||
ocr_client: OcrDetailedClient,
|
||||
vlm_client: VlmClient,
|
||||
screen_width: int = 1920,
|
||||
screen_height: int = 1080,
|
||||
coords_cache: Optional[CoordsCache] = None,
|
||||
context: str = "",
|
||||
fuzzy_threshold: float = 0.8,
|
||||
) -> Optional[GroundedElement]:
|
||||
"""Ground a UI element on screen — OCR-anchor first, VLM fallback.
|
||||
|
||||
Resolution strategy:
|
||||
1. Cache: if cached coords exist → return cached (validated separately)
|
||||
2. OCR-anchor: deterministic, zero hallucination
|
||||
3. VLM grounder: fallback when OCR can't find the text
|
||||
|
||||
Args:
|
||||
screenshot_path: path to screenshot image
|
||||
target: {"role": "bouton", "text": "Connexion"} — element to find
|
||||
ocr_client: injectable OCR client returning List[OcrTokenInfo]
|
||||
vlm_client: injectable VLM client (image_path, prompt) -> text
|
||||
screen_width/height: screen dimensions for pixel conversion
|
||||
coords_cache: optional CoordsCache for memoization
|
||||
context: optional context (e.g. "page login DPI")
|
||||
fuzzy_threshold: fuzzy match threshold for OCR anchoring
|
||||
|
||||
Returns:
|
||||
GroundedElement with bbox + center, or None if not found
|
||||
"""
|
||||
target_text = target.get("text", "")
|
||||
target_role = target.get("role", "?")
|
||||
element_key = make_element_key(target_role, target_text)
|
||||
|
||||
# Step 0: Check cache
|
||||
if coords_cache:
|
||||
cached = coords_cache.get(element_key)
|
||||
if cached:
|
||||
cached.validation_count += 1
|
||||
logger.info("grounding: using cached coords for %s", element_key)
|
||||
return GroundedElement(
|
||||
role=target_role,
|
||||
text=target_text,
|
||||
bbox=cached.bbox,
|
||||
center=cached.center,
|
||||
confidence=1.0, # cached = previously validated
|
||||
method="cache",
|
||||
)
|
||||
|
||||
# Step 1: OCR-anchor (deterministic)
|
||||
try:
|
||||
ocr_tokens = ocr_client(screenshot_path)
|
||||
except Exception as e:
|
||||
logger.warning("grounding: OCR call failed (%s)", e)
|
||||
ocr_tokens = []
|
||||
|
||||
ocr_result = ocr_anchor_ground(ocr_tokens, target, fuzzy_threshold)
|
||||
|
||||
if ocr_result:
|
||||
if coords_cache:
|
||||
coords_cache.put(element_key, ocr_result.bbox, ocr_result.center, "ocr_anchor")
|
||||
logger.info(
|
||||
"grounding: OCR-anchor found '%s' (matched OCR='%s', conf=%.2f)",
|
||||
target_text, ocr_result.source_ocr_text, ocr_result.confidence,
|
||||
)
|
||||
return ocr_result
|
||||
|
||||
# Step 2: VLM grounder (fallback)
|
||||
if not target_text:
|
||||
logger.warning("grounding: no text for target, VLM grounder needs text")
|
||||
return None
|
||||
|
||||
prompt = build_grounder_prompt(target, context)
|
||||
|
||||
try:
|
||||
vlm_text = vlm_client(screenshot_path, prompt)
|
||||
except Exception as e:
|
||||
logger.warning("grounding: VLM grounder call failed (%s)", e)
|
||||
return None
|
||||
|
||||
vlm_result = parse_grounder_response(vlm_text, screen_width, screen_height, target)
|
||||
|
||||
if vlm_result:
|
||||
if coords_cache:
|
||||
coords_cache.put(element_key, vlm_result.bbox, vlm_result.center, "vlm_grounder")
|
||||
logger.info(
|
||||
"grounding: VLM grounder found '%s' (conf=%.2f)",
|
||||
target_text, vlm_result.confidence,
|
||||
)
|
||||
return vlm_result
|
||||
|
||||
logger.warning("grounding: element '%s' not found by OCR or VLM", target_text)
|
||||
return None
|
||||
227
core/navigation/visual_login.py
Normal file
227
core/navigation/visual_login.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""Visual login — résolution + vérification du formulaire de login par grounding.
|
||||
|
||||
Architecture (alignée visual_verifier + grounding) :
|
||||
- verify_before : formulaire login visible (champs + bouton présents)
|
||||
- resolve_login_form : ground chaque champ (login, password, bouton) → coords
|
||||
- verify_after : dashboard/accueil visible (post-login)
|
||||
- Chaque étape encadrée par vision (DETTE-023 couvert)
|
||||
|
||||
Coords = cache local validé par vue (Dom/Claude recadrage).
|
||||
Le runtime exécute les actions (type/click) — ce module résout + valide.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from core.navigation.grounding import (
|
||||
BBox,
|
||||
CoordsCache,
|
||||
GroundedElement,
|
||||
OcrDetailedClient,
|
||||
OcrTokenInfo,
|
||||
ground_element,
|
||||
)
|
||||
from core.navigation.visual_verifier import (
|
||||
OcrClient,
|
||||
ScreenMatchResult,
|
||||
VlmClient,
|
||||
verify_before,
|
||||
verify_after,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Dataclasses ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoginFormConfig:
|
||||
"""Configuration for a login form — what to look for."""
|
||||
|
||||
login_field: Dict[str, Any] # {"role": "champ", "text": "Login"}
|
||||
password_field: Dict[str, Any] # {"role": "champ", "text": "Mot de passe"}
|
||||
submit_button: Dict[str, Any] # {"role": "bouton", "text": "Connexion"}
|
||||
success_elements: List[Dict[str, Any]] = field(default_factory=list)
|
||||
context: str = "" # e.g. "DPI urgences"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoginResolution:
|
||||
"""Result of login form resolution — grounded coords for each field."""
|
||||
|
||||
login_field: Optional[GroundedElement] = None
|
||||
password_field: Optional[GroundedElement] = None
|
||||
submit_button: Optional[GroundedElement] = None
|
||||
all_resolved: bool = False
|
||||
method: str = "" # "ocr_anchor", "vlm_grounder", "mixed", "cache"
|
||||
|
||||
def describe(self) -> str:
|
||||
parts = []
|
||||
if self.login_field:
|
||||
parts.append(f"login@{self.login_field.center} ({self.login_field.method})")
|
||||
else:
|
||||
parts.append("login: NOT FOUND")
|
||||
if self.password_field:
|
||||
parts.append(f"password@{self.password_field.center} ({self.password_field.method})")
|
||||
else:
|
||||
parts.append("password: NOT FOUND")
|
||||
if self.submit_button:
|
||||
parts.append(f"button@{self.submit_button.center} ({self.submit_button.method})")
|
||||
else:
|
||||
parts.append("button: NOT FOUND")
|
||||
status = "OK" if self.all_resolved else "INCOMPLETE"
|
||||
return f"Login resolution [{status}]: " + ", ".join(parts)
|
||||
|
||||
|
||||
# ── Default configs ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def dpi_urgences_login_config() -> LoginFormConfig:
|
||||
"""Default config for DPI urgences login form."""
|
||||
return LoginFormConfig(
|
||||
login_field={"role": "champ", "text": "Login", "extra": "champ identifiant"},
|
||||
password_field={"role": "champ", "text": "Mot de passe", "extra": "champ password"},
|
||||
submit_button={"role": "bouton", "text": "Connexion", "extra": "bouton submit"},
|
||||
success_elements=[
|
||||
{"role": "page", "text": "Accueil"},
|
||||
{"role": "page", "text": "Dashboard"},
|
||||
],
|
||||
context="DPI urgences — page login",
|
||||
)
|
||||
|
||||
|
||||
# ── Helper ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _ocr_detailed_to_simple(ocr_detailed: OcrDetailedClient) -> OcrClient:
|
||||
"""Convert OcrDetailedClient (text+bbox) to OcrClient (text-only) for verification."""
|
||||
def client(image_path: str) -> List[str]:
|
||||
return [t.text for t in ocr_detailed(image_path)]
|
||||
return client
|
||||
|
||||
|
||||
# ── Core functions ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def verify_login_visible(
|
||||
screenshot_path: str,
|
||||
config: LoginFormConfig,
|
||||
ocr_client: OcrClient,
|
||||
vlm_client: VlmClient,
|
||||
) -> ScreenMatchResult:
|
||||
"""Verify login form is visible on screen (pre-condition).
|
||||
|
||||
Checks that login field, password field, and submit button are present.
|
||||
Uses OCR-anchored verification (deterministic presence, VLM role).
|
||||
"""
|
||||
expected = [
|
||||
config.login_field,
|
||||
config.password_field,
|
||||
config.submit_button,
|
||||
]
|
||||
return verify_before(
|
||||
screenshot_path, expected, ocr_client, vlm_client,
|
||||
context=config.context,
|
||||
)
|
||||
|
||||
|
||||
def verify_login_success(
|
||||
screenshot_path: str,
|
||||
config: LoginFormConfig,
|
||||
ocr_client: OcrClient,
|
||||
vlm_client: VlmClient,
|
||||
) -> ScreenMatchResult:
|
||||
"""Verify dashboard/accueil visible after login (post-condition).
|
||||
|
||||
Higher threshold (verify_after = 0.8) — false positive = Léa proceeds wrong.
|
||||
"""
|
||||
if not config.success_elements:
|
||||
# No success criteria defined → can't verify
|
||||
return ScreenMatchResult(
|
||||
match=False,
|
||||
confidence=0.0,
|
||||
reason="no success_elements defined in config",
|
||||
)
|
||||
return verify_after(
|
||||
screenshot_path, config.success_elements, ocr_client, vlm_client,
|
||||
context=f"POST-LOGIN: {config.context}",
|
||||
)
|
||||
|
||||
|
||||
def resolve_login_form(
|
||||
screenshot_path: str,
|
||||
config: LoginFormConfig,
|
||||
ocr_client: OcrDetailedClient,
|
||||
vlm_client: VlmClient,
|
||||
screen_width: int = 1920,
|
||||
screen_height: int = 1080,
|
||||
coords_cache: Optional[CoordsCache] = None,
|
||||
) -> LoginResolution:
|
||||
"""Ground all login form elements → coords for runtime action.
|
||||
|
||||
Resolution strategy per element:
|
||||
1. Cache hit → return cached coords (validated separately)
|
||||
2. OCR-anchor → deterministic bbox from OCR token
|
||||
3. VLM grounder → fallback visual grounding
|
||||
|
||||
Returns LoginResolution with grounded coords for each field.
|
||||
Runtime uses these coords to type/click.
|
||||
"""
|
||||
login_el = ground_element(
|
||||
screenshot_path, config.login_field,
|
||||
ocr_client=ocr_client, vlm_client=vlm_client,
|
||||
screen_width=screen_width, screen_height=screen_height,
|
||||
coords_cache=coords_cache, context=config.context,
|
||||
)
|
||||
|
||||
password_el = ground_element(
|
||||
screenshot_path, config.password_field,
|
||||
ocr_client=ocr_client, vlm_client=vlm_client,
|
||||
screen_width=screen_width, screen_height=screen_height,
|
||||
coords_cache=coords_cache, context=config.context,
|
||||
)
|
||||
|
||||
button_el = ground_element(
|
||||
screenshot_path, config.submit_button,
|
||||
ocr_client=ocr_client, vlm_client=vlm_client,
|
||||
screen_width=screen_width, screen_height=screen_height,
|
||||
coords_cache=coords_cache, context=config.context,
|
||||
)
|
||||
|
||||
all_resolved = login_el is not None and password_el is not None and button_el is not None
|
||||
|
||||
# Determine overall method
|
||||
methods = []
|
||||
if login_el:
|
||||
methods.append(login_el.method)
|
||||
if password_el:
|
||||
methods.append(password_el.method)
|
||||
if button_el:
|
||||
methods.append(button_el.method)
|
||||
|
||||
unique_methods = set(methods)
|
||||
if len(unique_methods) == 1:
|
||||
method = unique_methods.pop()
|
||||
elif len(unique_methods) > 1:
|
||||
method = "mixed"
|
||||
else:
|
||||
method = ""
|
||||
|
||||
resolution = LoginResolution(
|
||||
login_field=login_el,
|
||||
password_field=password_el,
|
||||
submit_button=button_el,
|
||||
all_resolved=all_resolved,
|
||||
method=method,
|
||||
)
|
||||
|
||||
if all_resolved:
|
||||
logger.info("resolve_login_form: %s", resolution.describe())
|
||||
else:
|
||||
logger.warning("resolve_login_form: incomplete — %s", resolution.describe())
|
||||
|
||||
return resolution
|
||||
408
core/navigation/visual_verifier.py
Normal file
408
core/navigation/visual_verifier.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""Visual verifier — verify_before / verify_after avec ancrage OCR.
|
||||
|
||||
Architecture OCR-ancrée (challenge Claude 01/07, gate-vert 30/06) :
|
||||
- PRESENCE = tokens OCR (déterministe, pas d'hallucination possible)
|
||||
- RÔLE = VLM confirmation (semantic, ancré sur tokens OCR trouvés)
|
||||
- VLM ne décide JAMAIS de la présence d'un élément
|
||||
- Faux positif impossible par construction ; faux négatif = retry acceptable
|
||||
|
||||
Pattern d'injection : OcrClient + VlmClient injectables (tests sans réseau).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass, field
|
||||
from difflib import SequenceMatcher
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Type aliases — injectable callables for offline testing
|
||||
VlmClient = Callable[[str, str], str] # (image_path, prompt) -> text
|
||||
OcrClient = Callable[[str], List[str]] # (image_path) -> list of OCR text strings
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenMatchResult:
|
||||
"""Result of a screen verification check."""
|
||||
|
||||
match: bool
|
||||
confidence: float = 0.0
|
||||
reason: str = ""
|
||||
observed_elements: List[Dict[str, Any]] = field(default_factory=list)
|
||||
expected_elements: List[Dict[str, Any]] = field(default_factory=list)
|
||||
mismatches: List[str] = field(default_factory=list)
|
||||
|
||||
def describe(self) -> str:
|
||||
if self.match:
|
||||
return f"Screen match OK (conf={self.confidence:.2f})"
|
||||
parts = [f"Screen mismatch (conf={self.confidence:.2f})"]
|
||||
if self.mismatches:
|
||||
parts.append("missing: " + ", ".join(self.mismatches))
|
||||
if self.reason:
|
||||
parts.append(self.reason)
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
# ── Text normalization (pure functions) ────────────────────────────────
|
||||
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
"""Normalize text for fuzzy matching: lowercase, strip accents, collapse whitespace."""
|
||||
text = text.lower().strip()
|
||||
# Strip accents: é→e, è→e, ê→e, à→a, etc.
|
||||
text = unicodedata.normalize("NFKD", text)
|
||||
text = "".join(c for c in text if not unicodedata.combining(c))
|
||||
# Collapse whitespace
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
return text
|
||||
|
||||
|
||||
def fuzzy_match(expected: str, observed: str, threshold: float = 0.8) -> bool:
|
||||
"""Check if observed text fuzzy-matches expected text.
|
||||
|
||||
Three strategies (any wins):
|
||||
1. Exact match after normalization
|
||||
2. Substring containment (either direction)
|
||||
3. SequenceMatcher ratio >= threshold
|
||||
"""
|
||||
norm_expected = normalize_text(expected)
|
||||
norm_observed = normalize_text(observed)
|
||||
|
||||
if norm_expected == norm_observed:
|
||||
return True
|
||||
|
||||
if norm_expected in norm_observed or norm_observed in norm_expected:
|
||||
return True
|
||||
|
||||
ratio = SequenceMatcher(None, norm_expected, norm_observed).ratio()
|
||||
return ratio >= threshold
|
||||
|
||||
|
||||
# ── OCR presence check (deterministic, no VLM) ──────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class OcrPresenceResult:
|
||||
"""Result of OCR-based presence check."""
|
||||
|
||||
found_texts: Dict[str, str] = field(default_factory=dict)
|
||||
missing: List[str] = field(default_factory=list)
|
||||
all_found: bool = False
|
||||
|
||||
@property
|
||||
def presence_ratio(self) -> float:
|
||||
if not self.found_texts:
|
||||
return 1.0
|
||||
found_count = sum(1 for v in self.found_texts.values() if v != "")
|
||||
return found_count / len(self.found_texts)
|
||||
|
||||
|
||||
def ocr_presence_check(
|
||||
ocr_tokens: List[str],
|
||||
expected_elements: List[Dict[str, Any]],
|
||||
fuzzy_threshold: float = 0.8,
|
||||
) -> OcrPresenceResult:
|
||||
"""Check presence of expected texts against OCR tokens (deterministic).
|
||||
|
||||
Pure function — no VLM call, zero hallucination risk.
|
||||
"""
|
||||
found_texts: Dict[str, str] = {}
|
||||
missing: List[str] = []
|
||||
|
||||
for el in expected_elements:
|
||||
expected_text = el.get("text", "")
|
||||
if not expected_text:
|
||||
found_texts[""] = ""
|
||||
continue
|
||||
|
||||
matched_ocr = ""
|
||||
for token in ocr_tokens:
|
||||
if fuzzy_match(expected_text, token, threshold=fuzzy_threshold):
|
||||
matched_ocr = token
|
||||
break
|
||||
|
||||
if matched_ocr:
|
||||
found_texts[expected_text] = matched_ocr
|
||||
else:
|
||||
found_texts[expected_text] = ""
|
||||
missing.append(f"{el.get('role', '?')}: {expected_text}")
|
||||
|
||||
all_found = len(missing) == 0
|
||||
return OcrPresenceResult(
|
||||
found_texts=found_texts,
|
||||
missing=missing,
|
||||
all_found=all_found,
|
||||
)
|
||||
|
||||
|
||||
# ── VLM role confirmation (semantic, anchored on found OCR texts) ────
|
||||
|
||||
|
||||
def build_role_confirm_prompt(
|
||||
found_elements: List[Dict[str, Any]],
|
||||
expected_elements: List[Dict[str, Any]],
|
||||
context: str = "",
|
||||
) -> str:
|
||||
"""Build VLM prompt for role confirmation of OCR-found elements.
|
||||
|
||||
VLM receives found texts and confirms their ROLE only — never presence.
|
||||
"""
|
||||
found_lines = []
|
||||
for i, el in enumerate(found_elements):
|
||||
matched_ocr = el.get("matched_ocr", "")
|
||||
expected_role = el.get("expected_role", "?")
|
||||
line = f"{i+1}. Text \"{matched_ocr}\" — expected role: {expected_role}"
|
||||
found_lines.append(line)
|
||||
|
||||
found_block = "\n".join(found_lines)
|
||||
|
||||
prompt = (
|
||||
"You are a screen role validator. OCR has confirmed these texts are "
|
||||
"present on the screen. Your job is ONLY to confirm their ROLE — "
|
||||
"do NOT re-declare whether they are present.\n"
|
||||
)
|
||||
if context:
|
||||
prompt += f"Context: {context}\n"
|
||||
prompt += (
|
||||
f"Found texts with expected roles:\n{found_block}\n\n"
|
||||
"Respond in JSON format:\n"
|
||||
"{\"confirmed\": [{\"index\": 1, \"role_confirmed\": true/false, "
|
||||
"\"actual_role\": \"...\", \"confidence\": 0.0-1.0}], "
|
||||
"\"overall_confidence\": 0.0-1.0}\n"
|
||||
"Only confirm role_confirmed=true if the text clearly plays the "
|
||||
"expected role (e.g., a button, not just a label with the same text)."
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def parse_role_confirm_response(vlm_text: str) -> Dict[str, Any]:
|
||||
"""Parse VLM role confirmation JSON response."""
|
||||
try:
|
||||
data = json.loads(vlm_text)
|
||||
except json.JSONDecodeError:
|
||||
json_match = re.search(r"\{[\s\S]*\}", vlm_text)
|
||||
if json_match:
|
||||
try:
|
||||
data = json.loads(json_match.group())
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("role_confirm: VLM response not parseable as JSON")
|
||||
return {"confirmed": [], "overall_confidence": 0.0}
|
||||
else:
|
||||
return {"confirmed": [], "overall_confidence": 0.0}
|
||||
|
||||
confirmed = data.get("confirmed", [])
|
||||
overall_conf = data.get("overall_confidence", 0.0)
|
||||
if isinstance(overall_conf, str):
|
||||
try:
|
||||
overall_conf = float(overall_conf)
|
||||
except ValueError:
|
||||
overall_conf = 0.0
|
||||
|
||||
return {
|
||||
"confirmed": confirmed,
|
||||
"overall_confidence": float(overall_conf),
|
||||
}
|
||||
|
||||
|
||||
# ── Core verification (OCR-anchored composition) ────────────────────
|
||||
|
||||
|
||||
def verify_screen_match(
|
||||
screenshot_path: str,
|
||||
expected_elements: List[Dict[str, Any]],
|
||||
ocr_client: OcrClient,
|
||||
vlm_client: VlmClient,
|
||||
context: str = "",
|
||||
min_confidence: float = 0.7,
|
||||
) -> ScreenMatchResult:
|
||||
"""Verify screen state with OCR-anchored presence + VLM role confirmation.
|
||||
|
||||
Step 1: OCR screenshot → tokens → deterministic presence check
|
||||
Step 2: VLM confirms role of found elements (not presence!)
|
||||
|
||||
Eliminates VLM self-report hallucination for presence checks.
|
||||
"""
|
||||
if not expected_elements:
|
||||
return ScreenMatchResult(
|
||||
match=True,
|
||||
confidence=1.0,
|
||||
reason="no expected elements to verify",
|
||||
)
|
||||
|
||||
# Step 1: OCR presence check (deterministic)
|
||||
try:
|
||||
ocr_tokens = ocr_client(screenshot_path)
|
||||
except Exception as e:
|
||||
logger.warning("verify_screen_match: OCR call failed (%s)", e)
|
||||
return ScreenMatchResult(
|
||||
match=False,
|
||||
confidence=0.0,
|
||||
reason=f"OCR error: {e}",
|
||||
expected_elements=expected_elements,
|
||||
)
|
||||
|
||||
presence = ocr_presence_check(ocr_tokens, expected_elements)
|
||||
|
||||
if not presence.all_found:
|
||||
observed = []
|
||||
for el in expected_elements:
|
||||
text = el.get("text", "")
|
||||
matched = presence.found_texts.get(text, "")
|
||||
observed.append({
|
||||
"role": el.get("role", "?"),
|
||||
"expected_text": text,
|
||||
"matched_ocr": matched,
|
||||
"found": matched != "",
|
||||
})
|
||||
return ScreenMatchResult(
|
||||
match=False,
|
||||
confidence=presence.presence_ratio,
|
||||
reason="OCR presence check: some texts not found",
|
||||
observed_elements=observed,
|
||||
expected_elements=expected_elements,
|
||||
mismatches=presence.missing,
|
||||
)
|
||||
|
||||
# Step 2: VLM role confirmation (only for found elements)
|
||||
found_elements = []
|
||||
for el in expected_elements:
|
||||
text = el.get("text", "")
|
||||
matched_ocr = presence.found_texts.get(text, "")
|
||||
if text and matched_ocr:
|
||||
found_elements.append({
|
||||
"text": text,
|
||||
"expected_role": el.get("role", "?"),
|
||||
"matched_ocr": matched_ocr,
|
||||
})
|
||||
|
||||
if not found_elements:
|
||||
# All elements had no text → presence trivially OK
|
||||
return ScreenMatchResult(
|
||||
match=True,
|
||||
confidence=1.0,
|
||||
reason="no text-based elements to verify",
|
||||
expected_elements=expected_elements,
|
||||
)
|
||||
|
||||
prompt = build_role_confirm_prompt(found_elements, expected_elements, context)
|
||||
|
||||
try:
|
||||
vlm_text = vlm_client(screenshot_path, prompt)
|
||||
except Exception as e:
|
||||
logger.warning("verify_screen_match: VLM role confirm failed (%s)", e)
|
||||
observed = []
|
||||
for el in expected_elements:
|
||||
text = el.get("text", "")
|
||||
observed.append({
|
||||
"role": el.get("role", "?"),
|
||||
"expected_text": text,
|
||||
"matched_ocr": presence.found_texts.get(text, ""),
|
||||
"found": True,
|
||||
"role_confirmed": False,
|
||||
"role_confidence": 0.0,
|
||||
})
|
||||
return ScreenMatchResult(
|
||||
match=True,
|
||||
confidence=0.5,
|
||||
reason=f"OCR presence OK, VLM role confirm failed: {e}",
|
||||
observed_elements=observed,
|
||||
expected_elements=expected_elements,
|
||||
)
|
||||
|
||||
parsed = parse_role_confirm_response(vlm_text)
|
||||
overall_conf = parsed.get("overall_confidence", 0.0)
|
||||
confirmed = parsed.get("confirmed", [])
|
||||
|
||||
observed = []
|
||||
role_mismatches = []
|
||||
for i, el in enumerate(expected_elements):
|
||||
text = el.get("text", "")
|
||||
expected_role = el.get("role", "?")
|
||||
matched_ocr = presence.found_texts.get(text, "")
|
||||
|
||||
role_entry = None
|
||||
for c in confirmed:
|
||||
if c.get("index") == i + 1:
|
||||
role_entry = c
|
||||
break
|
||||
|
||||
role_confirmed = False
|
||||
actual_role = ""
|
||||
role_confidence = 0.0
|
||||
|
||||
if role_entry:
|
||||
role_confirmed = role_entry.get("role_confirmed", False)
|
||||
actual_role = role_entry.get("actual_role", "")
|
||||
role_confidence = role_entry.get("confidence", 0.0)
|
||||
if isinstance(role_confidence, str):
|
||||
try:
|
||||
role_confidence = float(role_confidence)
|
||||
except ValueError:
|
||||
role_confidence = 0.0
|
||||
|
||||
observed.append({
|
||||
"role": expected_role,
|
||||
"expected_text": text,
|
||||
"matched_ocr": matched_ocr,
|
||||
"found": True,
|
||||
"role_confirmed": role_confirmed,
|
||||
"actual_role": actual_role,
|
||||
"role_confidence": role_confidence,
|
||||
})
|
||||
|
||||
if not role_confirmed or role_confidence < min_confidence:
|
||||
role_mismatches.append(
|
||||
f"{expected_role}: {text} (actual={actual_role}, conf={role_confidence:.2f})"
|
||||
)
|
||||
|
||||
is_match = len(role_mismatches) == 0 and overall_conf >= min_confidence
|
||||
|
||||
return ScreenMatchResult(
|
||||
match=is_match,
|
||||
confidence=overall_conf,
|
||||
reason=f"OCR presence: {presence.presence_ratio:.0%}, VLM role: {overall_conf:.2f}",
|
||||
observed_elements=observed,
|
||||
expected_elements=expected_elements,
|
||||
mismatches=presence.missing + role_mismatches,
|
||||
)
|
||||
|
||||
|
||||
def verify_before(
|
||||
screenshot_path: str,
|
||||
expected_elements: List[Dict[str, Any]],
|
||||
ocr_client: OcrClient,
|
||||
vlm_client: VlmClient,
|
||||
context: str = "",
|
||||
) -> ScreenMatchResult:
|
||||
"""Verify screen state BEFORE an action (OCR-anchored).
|
||||
|
||||
Checks pre-conditions: expected texts present + roles correct.
|
||||
min_confidence=0.7 — some tolerance for pre-action verification.
|
||||
"""
|
||||
return verify_screen_match(
|
||||
screenshot_path, expected_elements, ocr_client, vlm_client,
|
||||
context=f"PRE-ACTION: {context}", min_confidence=0.7,
|
||||
)
|
||||
|
||||
|
||||
def verify_after(
|
||||
screenshot_path: str,
|
||||
expected_elements: List[Dict[str, Any]],
|
||||
ocr_client: OcrClient,
|
||||
vlm_client: VlmClient,
|
||||
context: str = "",
|
||||
) -> ScreenMatchResult:
|
||||
"""Verify screen state AFTER an action (OCR-anchored).
|
||||
|
||||
Checks post-conditions with higher threshold (0.8).
|
||||
False positive = Léa proceeds on wrong assumption → stricter gate.
|
||||
"""
|
||||
return verify_screen_match(
|
||||
screenshot_path, expected_elements, ocr_client, vlm_client,
|
||||
context=f"POST-ACTION: {context}", min_confidence=0.8,
|
||||
)
|
||||
@@ -208,7 +208,11 @@ REQUIRED=(
|
||||
"Lea/python-embed/Lib/site-packages/mss"
|
||||
"Lea/python-embed/Lib/site-packages/win32"
|
||||
"Lea/python-embed/Lib/site-packages/socketio"
|
||||
)
|
||||
"Lea/python-embed/Lib/site-packages/httpx"
|
||||
"Lea/python-embed/Lib/site-packages/httpcore"
|
||||
"Lea/python-embed/Lib/site-packages/h11"
|
||||
"Lea/python-embed/Lib/site-packages/anyio"
|
||||
"Lea/python-embed/Lib/site-packages/typing_extensions.py"
|
||||
MISSING=()
|
||||
for f in "${REQUIRED[@]}"; do
|
||||
[[ -e "$ASSEMBLY_DIR/$f" ]] || MISSING+=("$f")
|
||||
|
||||
60
deploy/dgx/vm_launch.sh
Executable file
60
deploy/dgx/vm_launch.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Persistent VM launch — starts swtpm first, waits for socket, then QEMU
|
||||
# VNC only, no SPICE (POC configuration)
|
||||
VMROOT=/home/aivanov/quickemu-win11-arm-lea
|
||||
SWTPM_SOCK="$VMROOT/windows-11-arm-lea.swtpm-sock"
|
||||
|
||||
/usr/bin/swtpm socket \
|
||||
--ctrl type=unixio,path="$SWTPM_SOCK" \
|
||||
--terminate \
|
||||
--tpmstate dir="$VMROOT" \
|
||||
--tpm2 &
|
||||
|
||||
# Wait for swtpm socket (up to 10s)
|
||||
for _i in $(seq 1 100); do
|
||||
if [ -S "$SWTPM_SOCK" ]; then break; fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ ! -S "$SWTPM_SOCK" ]; then
|
||||
echo "ERROR: swtpm socket not ready after 10s"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec /usr/bin/qemu-system-aarch64 \
|
||||
-name windows-11-arm-lea \
|
||||
-machine virt,highmem=on,pflash0=rom,pflash1=efivars,accel=kvm \
|
||||
-global kvm-pit.lost_tick_policy=discard \
|
||||
-cpu host \
|
||||
-smp cores=8,threads=1,sockets=1 \
|
||||
-m 8G \
|
||||
-device virtio-balloon \
|
||||
-pidfile "$VMROOT/windows-11-arm-lea.pid" \
|
||||
-rtc base=utc,clock=host \
|
||||
-device ramfb \
|
||||
-vga none \
|
||||
-device virtio-gpu-pci,id=video0,xres=1280,yres=800 \
|
||||
-display none \
|
||||
-vnc 127.0.0.1:2,password=on \
|
||||
-device virtio-serial-pci \
|
||||
-chardev socket,id=agent0,path="$VMROOT/windows-11-arm-lea-agent.sock",server=on,wait=off \
|
||||
-device virtserialport,chardev=agent0,name=org.qemu.guest_agent.0 \
|
||||
-device virtio-rng-pci,rng=rng0 \
|
||||
-object rng-random,id=rng0,filename=/dev/urandom \
|
||||
-device qemu-xhci,id=input \
|
||||
-device usb-kbd,bus=input.0 \
|
||||
-k fr \
|
||||
-device usb-tablet,bus=input.0 \
|
||||
-device virtio-net-pci,netdev=nic \
|
||||
-netdev user,hostname=windows-11-arm-lea,hostfwd=tcp::22220-:22,id=nic \
|
||||
-blockdev node-name=rom,driver=file,filename=/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd,read-only=true \
|
||||
-blockdev node-name=efivars,driver=file,filename="$VMROOT/OVMF_VARS.fd" \
|
||||
-device virtio-scsi-pci,id=scsi0 \
|
||||
-device scsi-hd,drive=SystemDisk,bus=scsi0.0,bootindex=2 \
|
||||
-drive id=SystemDisk,if=none,format=qcow2,file="$VMROOT/disk.qcow2",discard=unmap,detect-zeroes=unmap,cache=writeback,aio=threads \
|
||||
-chardev socket,id=chrtpm,path="$SWTPM_SOCK" \
|
||||
-tpmdev emulator,id=tpm0,chardev=chrtpm \
|
||||
-device tpm-tis-device,tpmdev=tpm0 \
|
||||
-monitor unix:"$VMROOT/windows-11-arm-lea-monitor.socket",server,nowait \
|
||||
-serial unix:"$VMROOT/windows-11-arm-lea-serial.socket",server,nowait \
|
||||
2>"$VMROOT/qemu.log"
|
||||
50
deploy/dgx/vm_stop.sh
Executable file
50
deploy/dgx/vm_stop.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Persistent VM stop — ACPI poweroff via QEMU monitor, then SIGTERM, then SIGKILL
|
||||
# Kill swtpm after QEMU exits. Cleanup PID/sockets.
|
||||
|
||||
VMROOT=/home/aivanov/quickemu-win11-arm-lea
|
||||
MONITOR_SOCKET="$VMROOT/windows-11-arm-lea-monitor.socket"
|
||||
PIDFILE="$VMROOT/windows-11-arm-lea.pid"
|
||||
|
||||
# Step 1: Send ACPI poweroff via QEMU monitor
|
||||
if [ -S "$MONITOR_SOCKET" ]; then
|
||||
echo "system_powerdown" | socat - UNIX-CONNECT:"$MONITOR_SOCKET" - > /dev/null 2>&1
|
||||
echo "ACPI poweroff sent, waiting 30s..."
|
||||
for i in $(seq 1 30); do
|
||||
if [ ! -f "$PIDFILE" ] || ! ps -p "$(cat "$PIDFILE" 2>/dev/null)" > /dev/null 2>&1; then
|
||||
echo "QEMU exited gracefully"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 2: SIGTERM (10s more)
|
||||
QEMU_PID=$(cat "$PIDFILE" 2>/dev/null)
|
||||
if [ -n "$QEMU_PID" ] && ps -p "$QEMU_PID" > /dev/null 2>&1; then
|
||||
echo "Still running, sending SIGTERM..."
|
||||
kill "$QEMU_PID" 2>/dev/null
|
||||
for i in $(seq 1 10); do
|
||||
if ! ps -p "$QEMU_PID" > /dev/null 2>&1; then
|
||||
echo "QEMU exited after SIGTERM"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 3: SIGKILL
|
||||
QEMU_PID=$(cat "$PIDFILE" 2>/dev/null)
|
||||
if [ -n "$QEMU_PID" ] && ps -p "$QEMU_PID" > /dev/null 2>&1; then
|
||||
echo "Still running, SIGKILL..."
|
||||
kill -9 "$QEMU_PID" 2>/dev/null
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Step 4: Kill swtpm
|
||||
pkill -f "swtpm.*windows-11-arm-lea" 2>/dev/null
|
||||
sleep 2
|
||||
|
||||
# Step 5: Cleanup
|
||||
rm -f "$PIDFILE" "$VMROOT"/*.sock "$VMROOT"/*.socket 2>/dev/null
|
||||
echo "VM stop complete"
|
||||
@@ -23,7 +23,7 @@
|
||||
; ============================================================
|
||||
|
||||
#define MyAppName "Lea"
|
||||
#define MyAppVersion "1.0.1"
|
||||
#define MyAppVersion "1.0.2"
|
||||
#define MyAppPublisher "AIVANOV"
|
||||
#define MyAppURL "https://lea.labs.laurinebazin.design"
|
||||
#define MyAppExeName "Lea.bat"
|
||||
@@ -182,6 +182,7 @@ var
|
||||
TokenPage: TInputQueryWizardPage;
|
||||
MachineIdValue: string;
|
||||
ConfigFilePath: string;
|
||||
ExistingMachineId: string;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Helper : ajoute des guillemets autour d'une chaine
|
||||
@@ -267,6 +268,72 @@ end;
|
||||
// --------------------------------------------------------------------
|
||||
procedure LoadConfigFromCommandLine(); forward;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// UPGRADE — trouve le dossier d'une install Lea existante (config.txt present)
|
||||
// --------------------------------------------------------------------
|
||||
function FindExistingInstallDir(): string;
|
||||
var
|
||||
Candidates: array[0..1] of string;
|
||||
I: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
Candidates[0] := ExpandConstant('{localappdata}\Programs\Lea');
|
||||
Candidates[1] := ExpandConstant('{autopf}\Lea');
|
||||
for I := 0 to 1 do
|
||||
begin
|
||||
if FileExists(Candidates[I] + '\config.txt') then
|
||||
begin
|
||||
Result := Candidates[I];
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// UPGRADE — lit le config.txt existant : pre-remplit le wizard avec la
|
||||
// VRAIE conf du poste (serveur/token/user) et MEMORISE le machine_id pour
|
||||
// le PRESERVER (ne pas regenerer une nouvelle identite fleet).
|
||||
// --------------------------------------------------------------------
|
||||
procedure LoadExistingConfig();
|
||||
var
|
||||
Dir, ConfPath: string;
|
||||
Lines: TArrayOfString;
|
||||
I, EqPos: Integer;
|
||||
Line, Key, Value: string;
|
||||
begin
|
||||
ExistingMachineId := '';
|
||||
Dir := FindExistingInstallDir();
|
||||
if Dir = '' then Exit; // install neuve -> comportement par defaut
|
||||
|
||||
ConfPath := Dir + '\config.txt';
|
||||
if LoadStringsFromFile(ConfPath, Lines) then
|
||||
begin
|
||||
for I := 0 to GetArrayLength(Lines) - 1 do
|
||||
begin
|
||||
Line := Trim(Lines[I]);
|
||||
if (Length(Line) = 0) or (Line[1] = '#') then Continue;
|
||||
EqPos := Pos('=', Line);
|
||||
if EqPos = 0 then Continue;
|
||||
Key := Trim(Copy(Line, 1, EqPos - 1));
|
||||
Value := Trim(Copy(Line, EqPos + 1, Length(Line)));
|
||||
|
||||
if Key = 'RPA_SERVER_URL' then TokenPage.Values[0] := Value
|
||||
else if Key = 'RPA_API_TOKEN' then TokenPage.Values[1] := Value
|
||||
else if Key = 'RPA_USER_NAME' then EnrollmentPage.Values[0] := Value
|
||||
else if Key = 'RPA_USER_EMAIL' then EnrollmentPage.Values[1] := Value
|
||||
else if Key = 'RPA_USER_ID' then EnrollmentPage.Values[2] := Value
|
||||
else if Key = 'RPA_MACHINE_ID' then ExistingMachineId := Value;
|
||||
end;
|
||||
end;
|
||||
|
||||
// Fallback : machine_id.txt si absent du config.txt
|
||||
if (ExistingMachineId = '') and FileExists(Dir + '\machine_id.txt') then
|
||||
begin
|
||||
if LoadStringsFromFile(Dir + '\machine_id.txt', Lines) and (GetArrayLength(Lines) > 0) then
|
||||
ExistingMachineId := Trim(Lines[0]);
|
||||
end;
|
||||
end;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Initialisation : cree les pages custom d'enrollment
|
||||
// --------------------------------------------------------------------
|
||||
@@ -301,7 +368,11 @@ begin
|
||||
TokenPage.Values[0] := SERVER_URL_DEFAULT;
|
||||
TokenPage.Values[1] := DEFAULT_TOKEN;
|
||||
|
||||
// Si un fichier /CONFIG= est passe en ligne de commande, pre-remplir
|
||||
// UPGRADE : si une install existe, pre-remplir avec SA config (pas les
|
||||
// defauts) et memoriser son machine_id pour le preserver.
|
||||
LoadExistingConfig();
|
||||
|
||||
// Si un fichier /CONFIG= est passe en ligne de commande, pre-remplir (prioritaire)
|
||||
LoadConfigFromCommandLine();
|
||||
end;
|
||||
|
||||
@@ -508,6 +579,54 @@ begin
|
||||
DeleteFile(PsFile);
|
||||
end;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// UPGRADE — AVANT la copie des fichiers : tuer une Lea en cours (via le
|
||||
// PID du lock) pour liberer les DLL de python-embed. Evite une install
|
||||
// partielle / "reboot required". Ne tue QUE le PID du lock (jamais tous
|
||||
// les pythonw du poste).
|
||||
// --------------------------------------------------------------------
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
AppDir, LockPath, BackupDir, SessionsDir: string;
|
||||
Lines: TArrayOfString;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
AppDir := ExpandConstant('{app}');
|
||||
|
||||
// 1) Tuer une Lea en cours (via le PID du lock) pour liberer les DLL
|
||||
// python-embed. Ne tue QUE ce PID, jamais tous les pythonw du poste.
|
||||
LockPath := AppDir + '\lea_agent.lock';
|
||||
if FileExists(LockPath) then
|
||||
begin
|
||||
if LoadStringsFromFile(LockPath, Lines) and (GetArrayLength(Lines) > 0) then
|
||||
Exec('taskkill.exe', '/F /PID ' + Trim(Lines[0]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
DeleteFile(LockPath);
|
||||
Sleep(1500);
|
||||
end;
|
||||
|
||||
// UPGRADE uniquement (install existante detectee via config.txt).
|
||||
if FileExists(AppDir + '\config.txt') then
|
||||
begin
|
||||
// 2) BACKUP (rollback) : copie code+config vers <app>_backup, HORS
|
||||
// python-embed / sessions / logs (leger, rapide). Filet si la nouvelle
|
||||
// version deconne : Julien restaure ce dossier.
|
||||
BackupDir := AppDir + '_backup';
|
||||
Exec(ExpandConstant('{cmd}'),
|
||||
'/c rmdir /s /q "' + BackupDir + '" 2>nul & robocopy "' + AppDir + '" "' + BackupDir +
|
||||
'" /E /XD python-embed sessions logs __pycache__ /XF *.pyc /R:1 /W:1 /NFL /NDL /NJH /NJS /NP >nul 2>&1',
|
||||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
|
||||
// 3) PURGE des captures accumulees (donnees d'apprentissage internes, non
|
||||
// exploitables cote clinique) : libere le disque. Le fix capture JPEG
|
||||
// evite que la saturation reprenne. Les logs (compliance 180j) restent.
|
||||
SessionsDir := AppDir + '\agent_v1\sessions';
|
||||
if DirExists(SessionsDir) then
|
||||
Exec(ExpandConstant('{cmd}'),
|
||||
'/c rmdir /s /q "' + SessionsDir + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
end;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Hook : actions apres copie des fichiers (ssPostInstall)
|
||||
// --------------------------------------------------------------------
|
||||
@@ -515,8 +634,11 @@ procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if CurStep = ssInstall then
|
||||
begin
|
||||
// Genere le machine_id AVANT la copie des fichiers
|
||||
MachineIdValue := GenerateMachineId();
|
||||
// UPGRADE : preserver l'identite existante ; sinon en generer une neuve.
|
||||
if ExistingMachineId <> '' then
|
||||
MachineIdValue := ExistingMachineId
|
||||
else
|
||||
MachineIdValue := GenerateMachineId();
|
||||
end;
|
||||
|
||||
if CurStep = ssPostInstall then
|
||||
|
||||
@@ -81,16 +81,29 @@ cd deploy/installer
|
||||
wget https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip
|
||||
mkdir python-3.12-embed
|
||||
unzip python-3.12.8-embed-amd64.zip -d python-3.12-embed/
|
||||
|
||||
# IMPORTANT : l'embed doit contenir TOUTES les dependances HORS LIGNE.
|
||||
# Le runtime client ne fait AUCUN pip/reseau (POC clinique). On installe donc
|
||||
# les dependances une fois dans l'embed, puis on le commit/reutilise tel quel :
|
||||
python312._pth # decommenter 'import site'
|
||||
python -m pip install --target python-3.12-embed/Lib/site-packages \
|
||||
-r ../lea_package/requirements_agent.txt
|
||||
# => doit inclure httpx (+ httpcore, h11) pour l'orchestrateur Lea (POST /api/learn/start).
|
||||
```
|
||||
|
||||
Le staging copie automatiquement ce dossier si present. Le composant
|
||||
"pythonembed" devient alors selectionnable dans l'installeur.
|
||||
|
||||
Le script `configure_embed.ps1` :
|
||||
Le script `configure_embed.ps1` (execute a l'installation, sur le poste) :
|
||||
1. Patche `python312._pth` pour activer `import site`
|
||||
2. Installe `pip` via `get-pip.py`
|
||||
3. Installe `requirements_agent.txt`
|
||||
4. Reecrit `Lea.bat` pour pointer sur `python-embed\pythonw.exe`
|
||||
2. VERIFIE que les dependances sont deja embarquees (offline, aucun pip/reseau) —
|
||||
`socketio, tkinter, mss, pynput, pystray, plyer, requests, httpx, PIL, win32api` ;
|
||||
si une dependance manque, l'installation echoue explicitement.
|
||||
3. Reecrit `Lea.bat` pour pointer sur `python-embed\pythonw.exe`
|
||||
|
||||
> Note : `build_installer.sh` et `build_package_full.sh` valident aussi la presence
|
||||
> des paquets (dont `httpx`, `httpcore`, `h11`) dans `Lib/site-packages/` avant de
|
||||
> produire le paquet — un embed incomplet interrompt le build cote Linux.
|
||||
|
||||
## Installation silencieuse (deploiement de masse)
|
||||
|
||||
|
||||
@@ -154,6 +154,8 @@ REQUIRED_EMBED=(
|
||||
"Lib/site-packages/pystray" "Lib/site-packages/plyer"
|
||||
"Lib/site-packages/requests" "Lib/site-packages/PIL"
|
||||
"Lib/site-packages/win32"
|
||||
"Lib/site-packages/httpx" "Lib/site-packages/httpcore" "Lib/site-packages/h11"
|
||||
"Lib/site-packages/anyio" "Lib/site-packages/typing_extensions.py"
|
||||
)
|
||||
MISSING_EMBED=()
|
||||
for f in "${REQUIRED_EMBED[@]}"; do
|
||||
|
||||
@@ -25,3 +25,5 @@ USER_ID=
|
||||
# Connexion serveur (remplacer les valeurs CONFIGURE_ME avant utilisation)
|
||||
SERVER_URL=CONFIGURE_ME
|
||||
API_TOKEN=CONFIGURE_ME
|
||||
|
||||
AGENT_VERSION=1.0.2
|
||||
|
||||
@@ -44,7 +44,7 @@ if ($PthFile) {
|
||||
# L'embed DOIT contenir toutes les dependances runtime.
|
||||
# AUCUN pip, AUCUN reseau : si une dependance manque -> echec explicite.
|
||||
# ---------------------------------------------------------------
|
||||
$RequiredModules = @('socketio','tkinter','mss','pynput','pystray','plyer','requests','PIL','win32api')
|
||||
$RequiredModules = @('socketio','tkinter','mss','pynput','pystray','plyer','requests','httpx','PIL','win32api')
|
||||
$Missing = @()
|
||||
foreach ($m in $RequiredModules) {
|
||||
& $PythonExe -c "import $m" 2>$null
|
||||
@@ -76,6 +76,29 @@ if exist "lea_agent.lock" (
|
||||
timeout /t 2 >nul
|
||||
)
|
||||
|
||||
:: MAJ SILENCIEUSE — swap atomique + rollback (renames uniquement)
|
||||
if exist "PENDING_BOOT" (
|
||||
echo [MAJ] Boot precedent non confirme : retour a la version precedente.
|
||||
if exist "agent_v1_prev" (
|
||||
if exist "agent_v1_echec" rmdir /s /q "agent_v1_echec" >nul 2>&1
|
||||
if exist "agent_v1" move "agent_v1" "agent_v1_echec" >nul 2>&1
|
||||
move "agent_v1_prev" "agent_v1" >nul 2>&1
|
||||
)
|
||||
del /f /q "PENDING_BOOT" >nul 2>&1
|
||||
) else if exist "UPDATE_READY" (
|
||||
if exist "agent_v1_new" (
|
||||
echo [MAJ] Application de la mise a jour...
|
||||
if exist "agent_v1" (
|
||||
if exist "agent_v1_prev" rmdir /s /q "agent_v1_prev" >nul 2>&1
|
||||
move "agent_v1" "agent_v1_prev" >nul 2>&1
|
||||
)
|
||||
move "agent_v1_new" "agent_v1" >nul 2>&1
|
||||
move "UPDATE_READY" "PENDING_BOOT" >nul 2>&1
|
||||
) else (
|
||||
del /f /q "UPDATE_READY" >nul 2>&1
|
||||
)
|
||||
)
|
||||
|
||||
if exist "config.txt" (
|
||||
for /f "usebackq eol=# tokens=1,* delims==" %%a in ("config.txt") do (
|
||||
if not "%%a"=="" if not "%%b"=="" set "%%a=%%b"
|
||||
|
||||
@@ -20,6 +20,35 @@ if exist "lea_agent.lock" (
|
||||
timeout /t 2 >nul
|
||||
)
|
||||
|
||||
:: ---------------------------------------------------------------
|
||||
:: MAJ SILENCIEUSE — swap atomique + rollback (hors-process)
|
||||
:: L'ancienne instance est fermee ci-dessus : agent_v1\ est libre.
|
||||
:: Renames uniquement (quasi-atomiques), jamais d'ecrasement fichier par fichier.
|
||||
:: ---------------------------------------------------------------
|
||||
if exist "PENDING_BOOT" (
|
||||
:: Le boot precedent n'a JAMAIS confirme (crash) -> ROLLBACK version precedente
|
||||
echo [MAJ] Boot precedent non confirme : retour a la version precedente.
|
||||
if exist "agent_v1_prev" (
|
||||
if exist "agent_v1_echec" rmdir /s /q "agent_v1_echec" >nul 2>&1
|
||||
if exist "agent_v1" move "agent_v1" "agent_v1_echec" >nul 2>&1
|
||||
move "agent_v1_prev" "agent_v1" >nul 2>&1
|
||||
)
|
||||
del /f /q "PENDING_BOOT" >nul 2>&1
|
||||
) else if exist "UPDATE_READY" (
|
||||
:: Une MAJ est armee (agent_v1_new pret) -> SWAP
|
||||
if exist "agent_v1_new" (
|
||||
echo [MAJ] Application de la mise a jour...
|
||||
if exist "agent_v1" (
|
||||
if exist "agent_v1_prev" rmdir /s /q "agent_v1_prev" >nul 2>&1
|
||||
move "agent_v1" "agent_v1_prev" >nul 2>&1
|
||||
)
|
||||
move "agent_v1_new" "agent_v1" >nul 2>&1
|
||||
move "UPDATE_READY" "PENDING_BOOT" >nul 2>&1
|
||||
) else (
|
||||
del /f /q "UPDATE_READY" >nul 2>&1
|
||||
)
|
||||
)
|
||||
|
||||
:: ---------------------------------------------------------------
|
||||
:: Verifier que l'installation a ete faite
|
||||
:: ---------------------------------------------------------------
|
||||
|
||||
@@ -36,5 +36,15 @@ RPA_MACHINE_ID=CONFIGURE_ME
|
||||
RPA_USER_LABEL=CONFIGURE_ME
|
||||
|
||||
# --- Parametres avances (ne pas modifier sauf indication) ---
|
||||
RPA_AGENT_VERSION=1.0.2
|
||||
RPA_BLUR_SENSITIVE=false
|
||||
RPA_LOG_RETENTION_DAYS=180
|
||||
|
||||
# --- MAJ silencieuse (DETTE-022 v2) — DESACTIVEE par defaut ---
|
||||
# Deploiement CANARY : on active d'ABORD ce flag sur le SEUL poste pilote
|
||||
# (Emilie), on verifie, puis on elargit. Le poste interroge le serveur et
|
||||
# telecharge la MAJ en staging ; le remplacement reel des fichiers reste manuel
|
||||
# / supervise (reserve revision humaine). Decommenter pour activer ce poste :
|
||||
# RPA_AUTO_UPDATE_ENABLED=true
|
||||
# Intervalle d'interrogation serveur en secondes (defaut 3600 = 1h) :
|
||||
# RPA_AUTO_UPDATE_INTERVAL_S=3600
|
||||
|
||||
@@ -5,6 +5,7 @@ mss>=9.0.1 # Capture d'ecran haute performance
|
||||
pynput>=1.7.7 # Clavier/Souris
|
||||
Pillow>=10.0.0 # Traitement image (crops, compression)
|
||||
requests>=2.31.0 # Communication serveur
|
||||
httpx>=0.27 # Client HTTP orchestrateur Lea (POST /api/learn/start) - brique conversationnelle
|
||||
psutil>=5.9.0 # Monitoring CPU/RAM
|
||||
pystray>=0.19.5 # Icone systray
|
||||
plyer>=2.1.0 # Notifications toast natives
|
||||
|
||||
44
deploy/windows-rdp-launcher/Connexion-VM-Lea.cmd
Normal file
44
deploy/windows-rdp-launcher/Connexion-VM-Lea.cmd
Normal file
@@ -0,0 +1,44 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title Connexion VM Lea (via DGX)
|
||||
|
||||
REM ============================================================
|
||||
REM Connexion Bureau a distance a la VM Windows (Lea) du DGX.
|
||||
REM Ouvre un tunnel SSH, lance le RDP (presse-papier actif),
|
||||
REM puis referme le tunnel quand la session RDP est fermee.
|
||||
REM ============================================================
|
||||
|
||||
REM --- Parametres (ajuste si besoin) ---
|
||||
set "DGX_USER=aivanov"
|
||||
set "DGX_HOST=192.168.1.45"
|
||||
REM En deplacement (WireGuard, plus tard) : mettre DGX_HOST=10.10.0.1
|
||||
set "LOCAL_PORT=13389"
|
||||
set "RDP_FILE=%~dp0VM-Lea.rdp"
|
||||
|
||||
echo.
|
||||
echo [1/3] Ouverture du tunnel SSH vers %DGX_USER%@%DGX_HOST% ...
|
||||
echo (si un mot de passe est demande, saisis-le dans la fenetre "Tunnel")
|
||||
start "Tunnel-DGX-VMLea" ssh -o StrictHostKeyChecking=accept-new -o ExitOnForwardFailure=yes -N -L %LOCAL_PORT%:127.0.0.1:3390 %DGX_USER%@%DGX_HOST%
|
||||
|
||||
echo [2/3] Attente de l'etablissement du tunnel (max ~30s)...
|
||||
set /a tries=0
|
||||
:wait
|
||||
timeout /t 1 /nobreak >nul
|
||||
powershell -NoProfile -Command "try{(New-Object Net.Sockets.TcpClient).Connect('127.0.0.1',%LOCAL_PORT%);exit 0}catch{exit 1}" >nul 2>&1
|
||||
if not errorlevel 1 goto ready
|
||||
set /a tries+=1
|
||||
if %tries% lss 30 goto wait
|
||||
echo ! Tunnel non etabli. Verifie l'acces SSH au DGX (mot de passe / reseau).
|
||||
pause
|
||||
goto cleanup
|
||||
|
||||
:ready
|
||||
echo [3/3] Connexion Bureau a distance (localhost:%LOCAL_PORT%) ...
|
||||
mstsc "%RDP_FILE%"
|
||||
|
||||
:cleanup
|
||||
echo.
|
||||
echo Fermeture du tunnel SSH...
|
||||
taskkill /FI "WINDOWTITLE eq Tunnel-DGX-VMLea*" /T /F >nul 2>&1
|
||||
echo Termine.
|
||||
timeout /t 2 /nobreak >nul
|
||||
35
deploy/windows-rdp-launcher/LISEZMOI.txt
Normal file
35
deploy/windows-rdp-launcher/LISEZMOI.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
CONNEXION BUREAU A DISTANCE - VM Lea (DGX)
|
||||
==========================================
|
||||
|
||||
CONTENU
|
||||
- Connexion-VM-Lea.cmd : le lanceur (double-clic)
|
||||
- VM-Lea.rdp : le profil de connexion RDP (presse-papier active)
|
||||
|
||||
INSTALLATION (sur ton laptop Windows)
|
||||
1. Copie les DEUX fichiers dans le MEME dossier (ex: le Bureau).
|
||||
2. (Optionnel) clic droit sur Connexion-VM-Lea.cmd > Envoyer vers > Bureau
|
||||
(creer un raccourci), pour un acces rapide.
|
||||
|
||||
UTILISATION
|
||||
- Double-clic sur "Connexion-VM-Lea.cmd".
|
||||
- Une fenetre "Tunnel" s'ouvre : si un mot de passe SSH est demande,
|
||||
saisis le mot de passe du compte aivanov du DGX.
|
||||
- Le Bureau a distance s'ouvre ensuite : saisis ton identifiant + mot de
|
||||
passe WINDOWS de la VM.
|
||||
- Copier-coller (texte ET fichiers) fonctionne dans les deux sens.
|
||||
- Ferme la fenetre RDP pour finir : le tunnel se referme automatiquement.
|
||||
|
||||
PRE-REQUIS
|
||||
- Etre sur le reseau du labo (meme WiFi) pour joindre 192.168.1.45.
|
||||
- OpenSSH client (inclus dans Windows 10/11).
|
||||
- Le Bureau a distance doit etre active dans la VM (deja fait).
|
||||
|
||||
EN DEPLACEMENT (plus tard)
|
||||
- Quand WireGuard sera en place, edite Connexion-VM-Lea.cmd et remplace
|
||||
DGX_HOST=192.168.1.45 par DGX_HOST=10.10.0.1
|
||||
- Tout le reste est identique. L'adresse RDP reste localhost:13389.
|
||||
|
||||
CONFORT (optionnel, recommande)
|
||||
- Pour ne plus saisir le mot de passe SSH a chaque fois : on signe la cle
|
||||
SSH de ton laptop avec la CA (acces par certificat). Demande-le moi et
|
||||
envoie-moi la cle publique de ton laptop.
|
||||
18
deploy/windows-rdp-launcher/VM-Lea.rdp
Normal file
18
deploy/windows-rdp-launcher/VM-Lea.rdp
Normal file
@@ -0,0 +1,18 @@
|
||||
full address:s:localhost:13389
|
||||
prompt for credentials:i:1
|
||||
redirectclipboard:i:1
|
||||
redirectdrives:i:1
|
||||
drivestoredirect:s:*
|
||||
redirectprinters:i:0
|
||||
redirectsmartcards:i:0
|
||||
audiomode:i:2
|
||||
authentication level:i:0
|
||||
negotiate security layer:i:1
|
||||
enablecredsspsupport:i:1
|
||||
screen mode id:i:2
|
||||
dynamic resolution:i:1
|
||||
desktopwidth:i:1280
|
||||
desktopheight:i:800
|
||||
session bpp:i:32
|
||||
compression:i:1
|
||||
username:s:
|
||||
32
deploy/windows-rdp-launcher/connexion-vm-lea.sh
Executable file
32
deploy/windows-rdp-launcher/connexion-vm-lea.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# RDP vers la VM Windows (Lea) du DGX, depuis ce serveur Linux (.40).
|
||||
# Ouvre un tunnel SSH (par certificat) puis lance xfreerdp.
|
||||
# Presse-papier + dossier $HOME partage. Tunnel ferme a la sortie.
|
||||
#
|
||||
# Usage:
|
||||
# ./connexion-vm-lea.sh # labo (DGX = 192.168.1.45)
|
||||
# ./connexion-vm-lea.sh 10.10.0.1 # en deplacement (via WireGuard)
|
||||
# ./connexion-vm-lea.sh 192.168.1.45 /u:MonUserWindows
|
||||
set -euo pipefail
|
||||
|
||||
DGX_HOST="${1:-192.168.1.45}"
|
||||
[ $# -gt 0 ] && shift || true
|
||||
LOCAL_PORT=13389
|
||||
CTL="$(mktemp -u /tmp/rdp-vmlea-ctl.XXXXXX)"
|
||||
|
||||
cleanup(){ ssh -S "$CTL" -O exit "aivanov@${DGX_HOST}" >/dev/null 2>&1 || true; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
echo "[1/3] Tunnel SSH (cert) vers aivanov@${DGX_HOST} ..."
|
||||
ssh -o ExitOnForwardFailure=yes -fN -M -S "$CTL" -L "${LOCAL_PORT}:127.0.0.1:3390" "aivanov@${DGX_HOST}"
|
||||
|
||||
echo "[2/3] Attente du tunnel ..."
|
||||
for _i in $(seq 1 40); do
|
||||
ss -tlnp 2>/dev/null | grep -q "127.0.0.1:${LOCAL_PORT} " && break
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
echo "[3/3] Connexion RDP (localhost:${LOCAL_PORT}) — presse-papier + dossier $HOME ..."
|
||||
xfreerdp /v:localhost:${LOCAL_PORT} /cert:ignore /clipboard /dynamic-resolution /drive:home,"$HOME" "$@" || true
|
||||
|
||||
echo "Session RDP terminee, fermeture du tunnel."
|
||||
59
deploy/windows-rdp-launcher/unblock_nomachine.ps1
Normal file
59
deploy/windows-rdp-launcher/unblock_nomachine.ps1
Normal file
@@ -0,0 +1,59 @@
|
||||
# Unblock NoMachine on Windows 11 — run as Administrator
|
||||
# Adds firewall rules for port 4000 (TCP+UDP) and verifies NoMachine service
|
||||
|
||||
Write-Host "=== Unblock NoMachine ===" -ForegroundColor Cyan
|
||||
|
||||
# 1. Add firewall inbound rules for NoMachine (port 4000 TCP + UDP)
|
||||
$ruleName = "NoMachine Server (Port 4000)"
|
||||
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
Write-Host "Firewall rule '$ruleName' already exists — enabling it" -ForegroundColor Yellow
|
||||
Enable-NetFirewallRule -DisplayName $ruleName
|
||||
} else {
|
||||
Write-Host "Creating firewall rule '$ruleName' for port 4000 TCP+UDP" -ForegroundColor Green
|
||||
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Protocol TCP -LocalPort 4000 -Action Allow -Profile Any -Enabled True -Description "Allow NoMachine remote desktop connections"
|
||||
New-NetFirewallRule -DisplayName "$ruleName (UDP)" -Direction Inbound -Protocol UDP -LocalPort 4000 -Action Allow -Profile Any -Enabled True -Description "Allow NoMachine UDP discovery"
|
||||
}
|
||||
|
||||
# 2. Check NoMachine service is running
|
||||
$svc = Get-Service -Name "nxsrv" -ErrorAction SilentlyContinue
|
||||
if (-not $svc) {
|
||||
$svc = Get-Service -Name "NoMachine Server" -ErrorAction SilentlyContinue
|
||||
if (-not $svc) {
|
||||
$svc = Get-Service | Where-Object { $_.DisplayName -like "*NoMachine*" -and $_.DisplayName -like "*Server*" } | Select-Object -First 1
|
||||
}
|
||||
}
|
||||
|
||||
if ($svc) {
|
||||
Write-Host "NoMachine service: $($svc.Name) — Status: $($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') {'Green'} else {'Red'})
|
||||
if ($svc.Status -ne 'Running') {
|
||||
Write-Host "Starting NoMachine service..." -ForegroundColor Yellow
|
||||
Start-Service -Name $svc.Name -ErrorAction SilentlyContinue
|
||||
$svc = Get-Service -Name $svc.Name
|
||||
Write-Host "After start: $($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') {'Green'} else {'Red'})
|
||||
}
|
||||
} else {
|
||||
Write-Host "WARNING: NoMachine server service not found!" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 3. Verify port 4000 is listening
|
||||
Write-Host ""
|
||||
Write-Host "Checking port 4000..." -ForegroundColor Cyan
|
||||
$port4000 = Get-NetTCPConnection -LocalPort 4000 -ErrorAction SilentlyContinue
|
||||
if ($port4000) {
|
||||
Write-Host "Port 4000 is LISTENING on $($port4000.LocalAddress):$($port4000.LocalPort) — State: $($port4000.State)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "WARNING: Port 4000 NOT listening — NoMachine server may not be active" -ForegroundColor Red
|
||||
Write-Host "Try: restart NoMachine from the Start Menu or Services app" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 4. Show this machine's IP for remote connection
|
||||
$ip = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notlike '*Loopback*' -and $_.IPAddress -notlike '127.*' -and $_.IPAddress -match '192\.168' } | Select-Object -First 1).IPAddress
|
||||
if ($ip) {
|
||||
Write-Host ""
|
||||
Write-Host "Laptop IP on LAN: $ip" -ForegroundColor Green
|
||||
Write-Host "From workstation: connect NoMachine to $ip" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Done ===" -ForegroundColor Cyan
|
||||
106
docs/AUDIT_CODE_MORT_2026-07-02.md
Normal file
106
docs/AUDIT_CODE_MORT_2026-07-02.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Audit Code Mort — Classification A/B/C — 2026-07-02
|
||||
|
||||
**Auteur**: Qwen (vérifié par grep/glob/commandes réelles)
|
||||
**Date**: 2026-07-02
|
||||
**Méthode**: Parallel agent exploration + grep verification + graphify cross-check
|
||||
|
||||
---
|
||||
|
||||
## Méthodologie
|
||||
|
||||
- **A (WIRED/ACTIF)** : Code importé et appelé dans le runtime de production
|
||||
- **B (ORPHAN/PROJECTION)** : Code avec lazy import ou projection future, pas appelé actuellement mais structuré pour activation
|
||||
- **C (MORT/CONFIRMÉ)** : Code zero imports, zero callers, zero runtime activation — candidat suppression
|
||||
|
||||
**Règle**: C-MORT nécessite GO Dom avant suppression. B-ORPHELIN conserve. A-WIRED documenté.
|
||||
|
||||
---
|
||||
|
||||
## C-MORT Confirmé (8 items, ~843 lignes)
|
||||
|
||||
| # | Fichier/Zone | Lignes | Preuve C-MORT | Risque suppression |
|
||||
|---|-------------|--------|---------------|-------------------|
|
||||
| C1 | `agent_v0/deploy_windows.py` | ~244 | Comment "OBSOLETE avril 2026" + zero imports | LOW — standalone script |
|
||||
| C2 | `core/config.py`: 7 deprecated config classes | ~160 | Zero prod imports, mirrorent SystemConfig | LOW — mais vérifier .env references |
|
||||
| C3 | `core/detection/owl_detector.py`: 4 methods | ~90 | Zero callers dans prod | LOW — vérifier examples/ |
|
||||
| C4 | `core/detection/ollama_client.py`: 5 old methods | ~150 | Remplacés par classify_element_complete() | LOW — vérifier examples/ |
|
||||
| C5 | `ollama_client.py:check_ollama_available()` standalone | ~15 | 8/9 callers in examples/, 1 in VWB (duplicat D2) | LOW — VWB a sa propre copie |
|
||||
| C6 | `agent_chat/app.py`: 2 Flask 410 endpoints | ~14 | Endpoints déprecated, retour 410 Gone | LOW — API contract check |
|
||||
| C7 | `core/grounding/smart_resize.py` (77 lines) | 77 | Zero prod callers, DETTE-007 triple impl | LOW — 2 autres impls existent |
|
||||
| C8 | PP-OCRv5 (paddleocr+paddlepaddle venv) | ~deps | 0 .py imports across entire project | LOW — venv deps uninstall |
|
||||
|
||||
**Total C-MORT**: ~843 lignes code + venv deps
|
||||
|
||||
---
|
||||
|
||||
## B-ORPHELIN (5 items, ~537 lignes)
|
||||
|
||||
| # | Fichier/Zone | Lignes | Preuve B | Action |
|
||||
|---|-------------|--------|----------|--------|
|
||||
| B1 | VWB ui_detection_service OmniParser path | ~70 | HARD-DISABILÉ `_omniparser_available = False # DÉSACTIVÉ` | Conserver, documenter activation condition |
|
||||
| B2 | `fusion_engine.py:_fuse_concat_projection()` | ~15 | Stub, prévu pour future fusion modes | Conserver, marque PROJECTION |
|
||||
| B3 | `omniparser_adapter.py` | ~429 | BRANCHABLE DORMANT, try/except import | Conserver, documenter activation condition |
|
||||
| B4 | `CorrectionStatus.DEPRECATED` enum value | ~3 | Enum value, pas supprimable sans break | Conserver, marque DEPRECATED |
|
||||
| B5 | `catalog_routes_v2_vlm.py:check_ollama_available()` | ~20 | Duplicat de ollama_client.py (D2) | DÉCISION Dom : unifier ou garder 2 impls |
|
||||
|
||||
---
|
||||
|
||||
## Duplicats Identifiés (4)
|
||||
|
||||
| # | Item | Impl 1 | Impl 2 | Statut |
|
||||
|---|------|--------|--------|--------|
|
||||
| D1 | smart_resize | smart_resize.py (C7) | ui_detection_service.py resize | C-MORT vs WIRED |
|
||||
| D2 | check_ollama_available | ollama_client.py (C5) | catalog_routes_v2_vlm.py (B5) | C-MORT vs B-ORPHELIN |
|
||||
| D3 | ground_element | seeclick_adapter.py (B→provenance?) | ollama_client.py old method | B vs C4 |
|
||||
| D4 | 7 deprecated config classes | core/config.py (C2) | SystemConfig (WIRED) | C-MORT vs A-WIRED |
|
||||
|
||||
---
|
||||
|
||||
## Classification Updates (C→A upgrades confirmés)
|
||||
|
||||
| Item | Prior Status | Current Status | Preuve upgrade |
|
||||
|------|-------------|---------------|---------------|
|
||||
| autonomous_planner.py | C | **A** | Migrated to agent_chat/, wired by app.py |
|
||||
| seeclick_adapter.py | C | **B** | Lazy re-export, `_seeclick_available` never consulted mais impl ground_element indépendante |
|
||||
| grounding/server.py | C | **A** | HTTP service port 8200, standalone Flask |
|
||||
| get_grounding_profile() | C | **A** | Wired via ollama_client.py:303-304 lazy import |
|
||||
|
||||
---
|
||||
|
||||
## OmniParser — Classification 7 Zones
|
||||
|
||||
| # | Zone | Statut | Activation | Fallback |
|
||||
|---|------|---------|-----------|----------|
|
||||
| 1 | SoM engine (som_engine.py) | **A-WIRED** | YOLO weights direct | docTR OCR |
|
||||
| 2 | resolve_engine (_get_omniparser) | **B-DORMANT** | Lazy Optional[bool] | None → skipped |
|
||||
| 3 | phase25_analyzer (_OmniParserSafeWrapper) | **B-DORMANT** | Lazy import + healthcheck | docTR-only |
|
||||
| 4 | api_stream healthcheck | **A-WIRED** | Always 200 omniparser_available:bool | degraded:true |
|
||||
| 5 | omniparser_adapter.py | **B-DORMANT** | Import phase25 & resolve | empty list |
|
||||
| 6 | VWB ui_detection_service.py | **B-HARD-DISABILÉ** | `_omniparser_available = False # DÉSACTIVÉ` | ui-detr-1 only |
|
||||
| 7 | VWB catalog_routes_v2_vlm.py | **B-DORMANT** | try/except, flips True si installé | VLM fallback |
|
||||
|
||||
---
|
||||
|
||||
## QG-Gated Lots (proposé, nécessite GO Dom)
|
||||
|
||||
### Lot 1 — C-MORT Low Risk (suppression directe après GO Dom)
|
||||
- C1 deploy_windows.py
|
||||
- C7 smart_resize.py
|
||||
- C6 agent_chat 410 endpoints
|
||||
- C8 PP-OCRv5 venv deps uninstall
|
||||
|
||||
### Lot 2 — C-MORT Medium Risk (vérification examples/ avant suppression)
|
||||
- C2 7 deprecated config classes (vérifier .env)
|
||||
- C3 owl_detector 4 methods (vérifier examples/)
|
||||
- C4 ollama_client 5 old methods (vérifier examples/)
|
||||
- C5 check_ollama_available standalone (vérifier VWB duplicat)
|
||||
|
||||
### Lot 3 — Duplicats Unification (décision Dom)
|
||||
- D1 smart_resize: unifier ou garder 2 impls
|
||||
- D2 check_ollama_available: unifier VWB vs core
|
||||
- D3 ground_element: unifier seeclick vs ollama
|
||||
- D4 config classes: supprimer deprecated vs garder compat
|
||||
|
||||
---
|
||||
|
||||
**Prochaine étape**: Dom review → GO/NOGO par lot → exécution séquentielle avec tests verification après chaque lot.
|
||||
174
docs/AUDIT_GAPS_APPLI_100PCT_2026-06-10.md
Normal file
174
docs/AUDIT_GAPS_APPLI_100PCT_2026-06-10.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Audit — Ce qui manque pour une appli 100% fonctionnelle
|
||||
|
||||
- **Date** : 2026-06-10
|
||||
- **Demandeur** : Dom
|
||||
- **Auteur** : Claude (audit read-only par 4 sous-agents d'exploration + contre-vérifications manuelles)
|
||||
- **Périmètre** : agent client Windows (Léa), chaîne d'apprentissage, capacité de replay, maturité produit/fleet
|
||||
- **Statut findings** : les fichier:ligne proviennent d'agents d'exploration. Les 3 contradictions majeures ont été re-vérifiées à la main (voir annexe). **Avant d'engager du code sur un item, revalider le point au cas par cas** (méthode habituelle).
|
||||
- **Avis croisé Qwen** : reçu 2026-06-10 23:30 (`inbox_claude/2026-06-10_2330_qwen-to-claude_AVIS-GAPS-APPLI-100PCT.md`) — intégré en **Addendum (§7)**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Diagnostic central
|
||||
|
||||
L'appli aujourd'hui, honnêtement qualifiée : **un record-and-replay supervisé robuste, avec une couche sémantique réelle au grounding, mais dont la boucle d'apprentissage n'est pas fermée et dont les filets de sécurité sont écrits mais débranchés.**
|
||||
|
||||
Trois promesses produit non tenues dans le code qui tourne :
|
||||
|
||||
1. **La boucle d'apprentissage est ouverte** — Shadow observe et construit des workflows, mais ils n'arrivent jamais dans VWB (import jamais déclenché).
|
||||
2. **L'exécution ne se vérifie pas elle-même** — verify, healing, recovery : tout existe, tout est désactivé ou non branché.
|
||||
3. **Pas de généralisation** — un workflow appris ne survit pas à un changement de poste/résolution ; FAISS est construit au training mais jamais consulté au replay.
|
||||
|
||||
Point structurel transverse : **deux chemins d'exécution aux capacités différentes** :
|
||||
- `visual_workflow_builder/backend/api_v3/execute.py` (exécution locale VWB, Legacy + ORA)
|
||||
- `agent_v0/server_v1/replay_engine.py` → agent Windows Léa (chemin POC)
|
||||
|
||||
Certains manques n'existent que sur l'un des deux. `t2a_decision` et le templating profond `{{var.field.sub}}` sont **implémentés sur le chemin Léa** (replay_engine.py:1922 et :2017) mais absents du chemin local. Cette asymétrie a piégé jusqu'aux agents d'audit eux-mêmes — c'est un coût de maintenance et un risque d'erreur permanent.
|
||||
|
||||
---
|
||||
|
||||
## 2. Axe agent client Windows (Léa) — ~80% fonctionnel, 1 bug critique
|
||||
|
||||
### Ce qui marche (vérifié wired)
|
||||
- Capture complète : clics/double-clics, clavier+buffer texte, scroll, multi-écrans, DPI awareness, floutage sensible, dédup pHash.
|
||||
- Résilience réseau : buffer SQLite persistant, retry 3×, backoff 1→30s, heartbeat 5s.
|
||||
- Purge captures après ACK serveur (`PURGE_AFTER_ACK=1` défaut).
|
||||
- Enroll Bearer token + machine_id ; détection dialogues système UAC/CredUI/SmartScreen **fail-closed** (pause supervisée).
|
||||
- Rétention logs 180 j (conforme Règlement IA art. 12).
|
||||
|
||||
### Gaps
|
||||
|
||||
| # | Gap | Sévérité | Preuve | Type |
|
||||
|---|-----|----------|--------|------|
|
||||
| A1 | **Timeout HTTP client 5s** : étape serveur > 5s (extract_text, t2a) → client coupe, action déjà sortie de la queue → **perdue silencieusement**. Incident documenté 8 mai (4 actions perdues, pause step 18). | 🔴 BLOQUANT POC | `agent_v1/core/executor.py:1786` ; `docs/REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md` | Bug |
|
||||
| A2 | **Watchdog `_retry_pending` absent** côté serveur : actions perdues jamais republiées. | 🔴 BLOQUANT POC | `network/streamer.py:99-105` (constat) | Non implémenté |
|
||||
| A3 | Écran verrouillé non détecté : agent capture noir, tape dans le vide. | 🟠 Important | — | Non implémenté |
|
||||
| A4 | RecoveryEngine client : code complet, jamais appelé. | 🟡 Nice-to-have | `agent_v1/core/recovery.py` | Écrit non branché |
|
||||
| A5 | Long-polling HTTP fragile par construction (vs SSE/WebSocket). | 🟡 Post-POC | `main.py:287-366` | Architecture |
|
||||
|
||||
Note : la suspicion « appel Ollama de commentaire d'action orphelin côté client » **ne se confirme pas** côté agent_v1 (`OLLAMA_HOST` défini dans config mais aucun appel client). Le commentaire d'action est côté serveur.
|
||||
|
||||
---
|
||||
|
||||
## 3. Axe chaîne d'apprentissage — marche jusqu'au dernier mètre, puis s'arrête
|
||||
|
||||
### Ce qui marche (vérifié wired)
|
||||
- Chaîne Shadow complète : streaming → LiveSessionManager → `_worker_queue.txt` → `run_worker.py` → ScreenAnalyzer → CLIP → FAISS indexation → GraphBuilder DBSCAN → workflow JSON dans `data/training/workflows/{machine_id}/`.
|
||||
- Apprentissage post-replay : ReplayLearner (JSONL `data/learning/replay_results/`) + TargetMemoryStore (SQLite `data/learning/target_memory.db`), consultés avant résolution, alimentés après succès/échec.
|
||||
|
||||
### Gaps
|
||||
|
||||
| # | Gap | Sévérité | Preuve | Type |
|
||||
|---|-----|----------|--------|------|
|
||||
| L1 | **Workflows Shadow jamais importés dans VWB** : `import_core_workflow()` existe (`visual_workflow_builder/backend/api/workflows.py:622`) mais aucun appel automatique post-finalize. La boucle d'apprentissage produit des fichiers que personne ne voit. | 🔴 BLOQUANT promesse produit | grep `import_core_workflow` depuis server_v1 = 0 hit | Écrit non branché |
|
||||
| L2 | ShadowLearningHook jamais importé (état avril 2026 inchangé). | 🟠 Important | `core/grounding/shadow_learning_hook.py` | Écrit non branché |
|
||||
| L3 | **FAISS construit mais jamais interrogé au replay** — la mémoire sémantique ne sert pas à la résolution. | 🟠 Important | `core/embedding/faiss_manager.py` | Écrit non branché |
|
||||
| L4 | Pas de généralisation cross-résolution/cross-poste : workflows cloisonnés par machine_id, ancres dépendantes du poste source. | 🟠 Important | `core/models/workflow_graph.py` | Non implémenté |
|
||||
| L5 | **Copilot : inexistant** (aucun moteur de suggestion). **Autonomous : AutonomousPlanner isolé du replay engine.** Le cycle Shadow→Copilot→Autonomous est aujourd'hui Shadow→(rien)→exécution déclenchée manuellement. | 🔴 BLOQUANT promesse produit | `agent_chat/autonomous_planner.py`, `agent_v0/server_v1/execution_plan_runner.py` | Squelettes |
|
||||
| L6 | Recapture anchor VWB : pas de revalidation/régénération PNG post-modification (bug connu mai 2026). | 🟠 Important | `visual_workflow_builder/backend/services/anchor_image_service.py` | Non implémenté |
|
||||
|
||||
---
|
||||
|
||||
## 4. Axe replay/exécution — ça clique bien, mais ça ne sait pas si ça a marché
|
||||
|
||||
### Ce qui marche (vérifié wired)
|
||||
- Cascade de résolution active : template matching → CLIP → OCR/UI-TARS → VLM.
|
||||
- DialogHandler branché (détection popups pré-step).
|
||||
- Pause supervisée avec choix utilisateur (skip/static/coords, timeout 120s).
|
||||
- Chemin Léa : `t2a_decision` (replay_engine.py:1922, handlers :2045+), templating profond `{{var.field.sub}}` (`path.split('.')` :2017), extract_text.
|
||||
|
||||
### Gaps
|
||||
|
||||
| # | Gap | Sévérité | Preuve | Type |
|
||||
|---|-----|----------|--------|------|
|
||||
| R1 | **`verify_level='none'` en dur** : aucune vérification post-action que le clic a produit l'effet attendu (seul pHash d'attente d'écran). Contraire au principe « vérif avant + après chaque clic ». | 🟠 Important (🔴 avant clinique) | `execute.py:1545` | Branché désactivé |
|
||||
| R2 | **VLM pre-check `if False:` en dur** : pas de validation que l'élément trouvé = l'élément attendu. | 🟠 Important | `core/execution/observe_reason_act.py:1707` | Branché désactivé |
|
||||
| R3 | Healing engine implémenté, jamais appelé. | 🟠 Important | `core/healing/healing_engine.py:21-150` | Écrit non branché |
|
||||
| R4 | **Aucune reprise après crash** : crash au step N → redémarrage à 0, pas de checkpoint. | 🔴 BLOQUANT clinique | `execute.py:1732` (thread daemon sans checkpoint) | Non implémenté |
|
||||
| R5 | **OCR-DIRECT « centre de ligne »** : substring d'une ligne docTR → coordonnées du centre de la ligne entière. Sur une barre d'onglets, Imagerie/Notes/Synthèse ≈ mêmes coords. Latent et sournois. | 🟠 Important | `agent_v0/server_v1/resolve_engine.py:1447-1527` | Bug |
|
||||
| R6 | Timeout VWB→serveur 30s vs étapes longues (t2a/Ollama lent) → 504. | 🟠 Important | `server_client.py:207` | Bug config |
|
||||
| R7 | Reporting d'exécution pauvre : méthode de grounding utilisée et raison d'échec non tracées. | 🟠 Important | ExecutionStep DB | Incomplet |
|
||||
| R8 | Popup détecté mais gestion échouée → continue (log seul), pas de pause. | 🟡 Nice-to-have | `execute.py:283` | Incomplet |
|
||||
| R9 | 3 systèmes de grounding morts (code zombie) : `fast_detector`, `smart_matcher`, `template_matcher` standalone. | 🟡 Ménage | `core/grounding/` | Poids mort |
|
||||
| R10 | TitleVerifier aveugle en VM (crop EasyOCR 45px illisible). | 🟡 Connu | `core/grounding/title_verifier.py:34-67` | Limitation |
|
||||
|
||||
---
|
||||
|
||||
## 5. Axe maturité produit / fleet — OK pour 5 TIM supervisés, pas au-delà
|
||||
|
||||
### Ce qui marche (vérifié)
|
||||
- Fleet : enroll/uninstall/revoke SQLite (`agent_registry.py`), `_guard_agent_registry_access`, last_seen heartbeat.
|
||||
- Sessions concurrentes thread-safe ; uploads images rate-limités sans sérialisation.
|
||||
- Healthcheck multi-composants + timer systemd (API, dashboard, worker heartbeat, disque).
|
||||
- Export ZIP workflows ; dashboard HTTP Basic fail-closed.
|
||||
|
||||
### Gaps
|
||||
|
||||
| # | Gap | Sévérité | Preuve | Type |
|
||||
|---|-----|----------|--------|------|
|
||||
| P1 | **DETTE-006/010 — grounding Qwen3-VL instable** (`smart_resize` non déterministe, config checkpoint factor 28 vs 32). LE risque technique du calendrier POC. | 🔴 BLOQUANT POC | `docs/MIGRATION_VLM_PLAN_2026-05-09.md`, DETTE_TECHNIQUE.md | En cours |
|
||||
| P2 | **1 seul replay simultané** (verrou global `_replay_lock`). Acceptable POC séquentiel, bloquant au-delà. | 🟠 Important post-POC | `api_stream.py` | Limitation |
|
||||
| P3 | Opérabilité non-dev : pas d'onglet « état systèmes », pas de monitoring GPU/Ollama, erreurs JSON brut. Acceptable seulement avec Dom en SSH derrière. | 🟠 Important | `web_dashboard/app.py` | Incomplet |
|
||||
| P4 | Export ZIP sans **restore** en masse ; backup exporte les JSON, pas la DB (DETTE-015 : symlink tient pour le POC). | 🟠 Important | `core/system/backup_exporter.py:58-160` | Incomplet |
|
||||
| P5 | Multi-users/RBAC : 1 user statique. Accepté POC (lié DETTE-016). | 🟡 Post-POC | `web_dashboard/app.py:67` | Accepté |
|
||||
| P6 | Pas de rotation des logs serveurs (`logs/*.log`) — artifact_retention ne couvre que les données. | 🟡 Nice-to-have | `core/system/artifact_retention.py` | Incomplet |
|
||||
| P7 | DETTE-013 : tests unit non exécutables sans `RPA_API_TOKEN` (sys.exit à l'import). | 🟠 Important dev | `agent_v0/server_v1/api_stream.py:135` | Bug |
|
||||
| P8 | Ménage pré-POC (~9-10 j-h, `MENAGE_PRE_POC_2026-05-29.md`) non engagé ; ~21 modules core/ orphelins documentés. | 🟡 Planifié | docs/POC/ | Dette |
|
||||
|
||||
---
|
||||
|
||||
## 6. Priorisation proposée
|
||||
|
||||
### Horizon 1 — avant le M2 live (jours) : fiabiliser la chaîne qui existe
|
||||
1. **A1** Timeout client 5s → 30s (1 constante) + **A2** watchdog `_retry_pending` serveur — le duo qui a tué la session du 8 mai.
|
||||
2. **P1** Trancher DETTE-006/010 (calibration grounding Qwen3-VL sur DGX) avant le bench J+6.
|
||||
3. **A3** Détection écran verrouillé.
|
||||
|
||||
### Horizon 2 — avant la clinique (semaines) : les filets de sécurité
|
||||
4. **R1/R2** Réactiver verify (au moins post-condition légère) + pre-check.
|
||||
5. **R5** Fix OCR centre-de-ligne (span du substring, pas centre de ligne).
|
||||
6. **R4** Reprise sur crash (checkpoint step) + **R7** tracer la méthode de résolution.
|
||||
7. **R6** Timeout VWB 30s → adapté aux étapes longues (ou polling asynchrone).
|
||||
|
||||
### Horizon 3 — la promesse produit (post-POC) : fermer la boucle
|
||||
8. **L1** Pont auto Shadow→VWB (`import_core_workflow` post-finalize) — LA pièce qui transforme l'outil en produit apprenant.
|
||||
9. **L3** FAISS consulté au replay + **L4** début de généralisation cross-poste.
|
||||
10. **L5** Copilot (moteur de suggestion) puis branchement AutonomousPlanner.
|
||||
11. Unifier les deux chemins d'exécution (execute.py local vs replay_engine.py Léa).
|
||||
12. **P2** Replays parallèles, **P3** opérabilité TIM, **P4** restore, RBAC.
|
||||
|
||||
---
|
||||
|
||||
## 7. Addendum — Avis croisé Qwen (historien/QG, 2026-06-10 23:30)
|
||||
|
||||
### Convergences avec l'audit code
|
||||
- **DETTE-006/010 = les deux vrais risques démo** (« si le grounding dérive, les clics ratent. Démo morte. ») — aligné avec P1/Horizon 1.
|
||||
- **Monitoring/alerting productif absent** (P3) : « si un worker crashe à 3h du matin sur un TIM, personne ne le saura ».
|
||||
- Écarts doc vs réalité confirmés par son registre : ContinuousLearner/Shadow hook orphelins (L2), cascade YOLO et OmniParser neutralisées (DETTE-004), ~1900 lignes de code mort jamais câblé (autonomous_planner, seeclick…) — cohérent avec L5/R9.
|
||||
|
||||
### Apports nouveaux de Qwen (absents de l'audit code)
|
||||
1. **Multi-TIM jamais testé > 1 agent simultané** : le Fleet existe, mais routage session, isolation mémoire et contention GPU sous charge réelle sont **inconnus**. Mon audit avait noté le verrou replay global (P2) ; Qwen élargit : c'est toute la concurrence multi-agents qui n'a aucune preuve d'exécution.
|
||||
2. **Le pipeline complet record → replay → compétence n'a jamais tourné en conditions réelles** : M2 live n'a pas encore eu lieu. « Le premier vrai test sera devant le client » si on ne fait pas M2 avant.
|
||||
3. **Incidents récurrents de son registre** : worker zombie 5 jours (résolu par watchdog N3), tunnel Ollama instable (stabilisé systemd), UI-TARS 500 non détecté (toujours 0 test dédié), OOM VRAM GB10 (fixé).
|
||||
4. **DETTE-015 jugée fragile** : le symlink a déjà cassé une fois (P0-1) ; peut resurgir si cwd change.
|
||||
|
||||
### Point de tension à arbitrer en M2 (pas tranché)
|
||||
Qwen affirme : « si le serveur redémarre, les agents Windows tombent — pas de reconnexion automatique ». Mon audit client a trouvé buffer SQLite persistant + retry + backoff + health-check 30s (`streamer.py`). Les deux peuvent être vrais : le **transport** se reconnecte, mais la **session/replay en cours** ne reprend probablement pas après un restart serveur. À vérifier explicitement pendant M2 (test : restart serveur en cours de session).
|
||||
|
||||
### Verdict Qwen
|
||||
- **1 TIM en démo contrôlée : prêt** (sous réserve DETTE-006/010).
|
||||
- **5 TIM réels en clinique : pas prêt** — le gap n'est pas dans le code métier (OCR, VLM, grounding) mais dans l'**infra multi-utilisateur** : sessions, isolation, monitoring, résilience.
|
||||
|
||||
Ce verdict est compatible avec la priorisation §6 et la renforce : l'Horizon 2 doit inclure un **test de charge multi-agents** (2-3 agents simultanés minimum) avant la clinique, en plus des filets de sécurité.
|
||||
|
||||
---
|
||||
|
||||
## Annexe — Contradictions inter-agents résolues (contre-vérifiées à la main)
|
||||
|
||||
| Affirmation agent d'audit | Verdict après vérification |
|
||||
|---|---|
|
||||
| « t2a_decision non implémenté » | **FAUX sur le chemin Léa** : implémenté `agent_v0/server_v1/replay_engine.py:1922` + handlers :2045+. Vrai uniquement pour le chemin local execute.py. |
|
||||
| « `{{var.field.sub}}` ne marche pas » | **FAUX sur le chemin Léa** : `path.split('.')` replay_engine.py:2017. Vrai uniquement chemin local. |
|
||||
| « Le chemin replay vers Léa est démis / Léa n'existe plus » | **FAUX** : pont `learned_workflow_bridge.py` côté VWB + polling `/replay/next` côté client actifs. Les deux chemins coexistent — c'est l'asymétrie connue. |
|
||||
|
||||
Leçon : tout audit ou modification doit d'abord identifier **sur quel chemin d'exécution** il porte.
|
||||
170
docs/BENCH_OCR_PPOCRV5_2026-07-02.md
Normal file
170
docs/BENCH_OCR_PPOCRV5_2026-07-02.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Benchmark OCR PP-OCRv5 CPU — 02/07/2026
|
||||
|
||||
> **Label**: baseline CPU, non verdict GPU
|
||||
> **Machine**: Ryzen 9 9950X 32 threads, 123GB RAM, RTX 5070 12GB VRAM, CUDA driver 580.159.03/13.0
|
||||
> **Image**: `shot_0172_full.png` (2560×1600, 721K, RGB) — capture écran Windows Léa
|
||||
> **PaddleOCR**: 3.4.0, paddlepaddle 3.3.1 CPU-only (non compilé CUDA)
|
||||
|
||||
---
|
||||
|
||||
## 1. Résultats synthèse
|
||||
|
||||
| Engine | Cold (s) | Warm (s) | Detections | Mem init (MB) | Mem peak (MB) | Statut |
|
||||
|--------|----------|----------|------------|---------------|---------------|--------|
|
||||
| **docTR CPU** | 0.776 | 0.717 | 139 | 263.2 | 263.2 | ✅ OK |
|
||||
| **EasyOCR CPU** | 4.878 | 4.856 | 54 | 0.6 | 156.9 | ✅ OK |
|
||||
| **PP-OCRv5 CPU** | — | — | — | — | — | ❌ BLOCKED |
|
||||
|
||||
---
|
||||
|
||||
## 2. PP-OCRv5 CPU — VERDICT: BLOCKED
|
||||
|
||||
### Crash récurrent
|
||||
|
||||
Toute inference PaddleOCR sur paddlepaddle 3.3.1 CPU-only crash systématiquement :
|
||||
|
||||
```
|
||||
(Unimplemented) ConvertPirAttribute2RuntimeAttribute not support
|
||||
[pir::ArrayAttribute<pir::DoubleAttribute>]
|
||||
(at /paddle/paddle/fluid/framework/new_executor/instruction/onednn/onednn_instruction.cc:116)
|
||||
```
|
||||
|
||||
### Root cause
|
||||
|
||||
Bug dans le **PIR new executor** de paddlepaddle 3.3.1 CPU-only : l'instruction OneDNN
|
||||
tente de convertir un `ArrayAttribute<DoubleAttribute>` en runtime attribute, opération
|
||||
non implémentée. Ce bug est :
|
||||
|
||||
- **NON model-spécifique** : v3_mobile_det, v4_mobile_det, v5_mobile_det crashent tous
|
||||
- **NON version-spécifique** : PP-OCRv3, v4 (fr absent), v5 crashent tous
|
||||
- **NON API-spécifique** : `ocr()` (deprecated) et `predict()` crashent identiquement
|
||||
- **NON contournable** par flags : `FLAGS_use_mkldnn=0`, `FLAGS_use_pir_api=0` n'ont aucun effet
|
||||
|
||||
### 7 approches testées — TOUTES FAILED
|
||||
|
||||
| # | Approche | Résultat |
|
||||
|---|----------|----------|
|
||||
| 1 | `FLAGS_use_mkldnn=0` via `os.environ` | Same crash |
|
||||
| 2 | `det='PP-OCRv5_mobile_det'` param | ValueError "Unknown argument: det" (PaddleOCR 3.4.0 rejette ce param) |
|
||||
| 3 | `FLAGS_use_mkldnn=0` shell-level avant Python | Same crash |
|
||||
| 4 | `text_detection_model_name='PP-OCRv5_mobile_det'` | mobile_det DL OK → inference crash (same OneDNN) |
|
||||
| 5 | `ocr_version='PP-OCRv4', lang='fr'` | ValueError "No models available for language 'fr' and PP-OCRv4" |
|
||||
| 6 | PP-OCRv3 + `ocr(img, cls=True)` legacy | DeprecationWarning → TypeError sur `cls` kwarg → predict() → same crash |
|
||||
| 7 | `FLAGS_use_pir_api=0` shell + os level | Same crash |
|
||||
|
||||
### PaddleOCR 3.4.0 __init__ params inspectés
|
||||
|
||||
28 paramètres au total. **Pas** de `enable_mkldnn`, `use_pir`, ou `det`. Param de détection
|
||||
remplacé par `text_detection_model_name`. API v3.4.0 : `use_angle_cls` deprecated
|
||||
→ `use_textline_orientation=True`, `show_log` supprimé (ValueError si utilisé).
|
||||
|
||||
### Incompatibilité downgrade
|
||||
|
||||
paddlepaddle 2.6.2 existe mais **incompatible** avec PaddleOCR 3.4.0 (requires ≥3.x).
|
||||
PaddleOCR 2.x serait compatible avec paddlepaddle 2.6.2 mais API/outils complètement
|
||||
différents — non évalué dans ce bench.
|
||||
|
||||
### Conclusion
|
||||
|
||||
**PP-OCRv5 CPU = BLOCKED**. Bug upstream dans paddlepaddle CPU-only binary, aucune
|
||||
workaround applicative possible. Seules alternatives :
|
||||
|
||||
1. **paddlepaddle GPU binary** (RTX 5070 + CUDA 13.0 compatible) → bench GPU séparé
|
||||
2. **Fix upstream** paddlepaddle (PR PIR executor OneDNN)
|
||||
3. **Downgrade PaddleOCR 2.x + paddlepaddle 2.6.2** (API legacy, non testé)
|
||||
|
||||
---
|
||||
|
||||
## 3. docTR CPU — Résultats détaillés
|
||||
|
||||
- **Cold latency**: 0.776s (incl. model loading)
|
||||
- **Warm latency**: 0.717s
|
||||
- **Detections**: 139 (mot-level, agressif — fragmente "Dites", "Sortie", "de", "veille")
|
||||
- **Mémoire**: 263.2MB stable (init = peak)
|
||||
- **Qualité**: haute sur mots courts, fragmente les phrases longues
|
||||
- **Confiance**: variable (0.26→0.99), nombreux tokens <0.7
|
||||
|
||||
### Observations docTR
|
||||
|
||||
- Word-level detection = 139 items → beaucoup de fragments 1-2 lettres
|
||||
- Bonne qualité sur labels UI ("Mode", "veille", "RPA", "VWB", "Python", "proxmox")
|
||||
- Fragmente les phrases ("Sortie de veille de l'accès vocal ou appuyez..." → 12 mots isolés)
|
||||
- IP correctement détecté : "192.168.1.40:3002" (conf 0.90)
|
||||
- Faux positifs : "0", "E03", "E", "€" isolés avec conf <0.4
|
||||
|
||||
---
|
||||
|
||||
## 4. EasyOCR CPU — Résultats détaillés
|
||||
|
||||
- **Cold latency**: 4.878s (heavy model loading)
|
||||
- **Warm latency**: 4.856s
|
||||
- **Detections**: 54 (line-level, plus conservatif)
|
||||
- **Mémoire**: 0.6MB init → 156.9MB peak
|
||||
- **Qualité**: bonne sur lignes complètes, plus robuste sur phrases
|
||||
|
||||
### Observations EasyOCR
|
||||
|
||||
- Line-level detection = 54 items → phrases plus cohérentes
|
||||
- Cold start très lent (5x docTR) mais warm identique
|
||||
- Meilleur sur textes longs, moins de fragmentation
|
||||
- Peak mémoire plus élevé que docTR (156.9 vs 263.2 MB init docTR)
|
||||
|
||||
---
|
||||
|
||||
## 5. Comparaison avec baselines Mai 2026
|
||||
|
||||
> Bench Mai 2026 — image `landing_wide.png`, critère 11 items de référence
|
||||
|
||||
| Engine | Score Mai (11 ref) | Score Juillet (detections) | Latency warm | Commentaire |
|
||||
|--------|-------------------|---------------------------|--------------|-------------|
|
||||
| Tesseract | **11/11** | — (non re-benché) | — | Référence May, non retesté |
|
||||
| EasyOCR brut | 8/11 | 54 det (shot_0172) | 4.856s | Fragmente moins, score < Tesseract |
|
||||
| EasyOCR preproc | 9/11 | — | — | +1 vs brut May |
|
||||
| docTR CPU | 10/11 | 139 det (shot_0172) | 0.717s | **Meilleur rapport qualité/latence** |
|
||||
| PP-OCRv5 CPU | non testé May | BLOCKED | — | Bug PIR/OneDNN, 0 inference possible |
|
||||
|
||||
### Hierarchie CPU confirmée
|
||||
|
||||
```
|
||||
docTR CPU (0.7s, 10/11) > EasyOCR preproc (4.9s, 9/11) > EasyOCR brut (4.9s, 8/11) > PP-OCRv5 CPU (BLOCKED)
|
||||
```
|
||||
|
||||
docTR reste le **meilleur moteur OCR CPU** pour Léa en termes de latence + qualité.
|
||||
Tesseract reste le plus précis (11/11) mais sans bounding boxes exploitables.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommandations
|
||||
|
||||
1. **docTR = moteur OCR CPU de production** — latence <1s, qualité 10/11, word-level bboxes
|
||||
2. **PP-OCRv5 GPU bench = action séparée** — requiere paddlepaddle GPU binary sur RTX 5070
|
||||
3. **PaddleOCR 3.4.0 = ORPHAN** — 0 imports dans le projet, pas dans requirements.txt,
|
||||
CPU-only install sans CUDA → retirer du venv si cleanup D2 (C-MORT)
|
||||
4. **Ne pas dépendre de PaddleOCR** pour POC T1 — docTR suffisant
|
||||
5. **Bug report upstream** — paddlepaddle PIR executor OneDNN, repro: any model + CPU binary
|
||||
|
||||
---
|
||||
|
||||
## 7. Annexes
|
||||
|
||||
### A. Script bench
|
||||
|
||||
`scripts/bench_ppocrv5_cpu.py` — compare PP-OCRv5, docTR, EasyOCR sur shot_0172_full.png.
|
||||
PP-OCRv5 crash → résultats JSON avec error field.
|
||||
|
||||
### B. Résultats JSON
|
||||
|
||||
`scripts/bench_ppocrv5_results.json` — 4522 lignes, contient tous texts + bboxes pour
|
||||
docTR (139 items) et EasyOCR (54 items). PP-OCRv5 = error only.
|
||||
|
||||
### C. Machine specs
|
||||
|
||||
- CPU: Ryzen 9 9950X, 32 threads
|
||||
- RAM: 123 GB
|
||||
- GPU: RTX 5070 12GB VRAM (non utilisé — bench CPU)
|
||||
- CUDA driver: 580.159.03 / runtime 13.0
|
||||
- OS: Linux (Ubuntu)
|
||||
- paddlepaddle: 3.3.1 CPU-only (pip install)
|
||||
- PaddleOCR: 3.4.0
|
||||
- docTR: (version installée dans venv)
|
||||
- EasyOCR: (version installée dans venv)
|
||||
104
docs/CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16.md
Normal file
104
docs/CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Cartographie — Chaîne d'apprentissage & mise en commun des connaissances (2026-06-16)
|
||||
|
||||
> Question Dom : *« Comment le savoir appris sur chaque poste est-il mutualisé dans un fonds commun unique partagé par tous les postes, et exploité vers l'autonomie ? »*
|
||||
> Méthode : graphify (graphe code 58k nodes) + 3 agents Explore + vérif code directe. Cross-checks Codex (pipeline server_v1) & Qwen (fédération/FAISS) **en cours** — ce doc sera enrichi.
|
||||
> ⚠️ Verdicts = état runtime constaté ce jour (`poc-dgx` @ `2b1743c20`), pas la doc d'intention.
|
||||
|
||||
## TL;DR (réponse directe)
|
||||
|
||||
Le « fonds commun → autonomie » est **partiellement construit, mais les maillons clés sont soit silotés par machine, soit dormants** :
|
||||
|
||||
1. ✅ **Ce qui EST déjà commun** : les **compétences YAML** (`core/competences/`) et les **embeddings** (CLIP→FAISS) — partagés serveur, tous postes.
|
||||
2. ❌ **Ce qui est SILOTÉ par machine (codé en dur)** : le **stockage** workflows/sessions (`{machine_id}/`) et surtout le **cross-session learning** qui **refuse de matcher entre machines** (`if workflow_machine != machine_id: continue`). C'est l'anti-pattern direct vs la vision.
|
||||
3. 🌙 **Ce qui est DORMANT** : la **fédération** (`core/federation` : LearningPack, GlobalFAISSIndex) — le vrai mécanisme de fonds commun — est **bien conçue, globale et anonymisée** (Qwen confirmé : zéro machine_id, clé `pack_source_hash`), mais **doublement inerte au runtime** : (a) alimentée **seulement** par l'endpoint import **manuel** (jamais auto-déclenché) ; (b) son **`search()` n'est JAMAIS appelé** (`faiss_global.py:199` défini, zéro consommateur actif) → **index write-only, jamais consulté** → contribue **zéro** au comportement/à l'autonomie aujourd'hui.
|
||||
4. 🚧 **Ce qui manque pour l'autonomie** : la **couche graphe** (WorkflowGraph) **EST construite en live** (`finalize_session` → `GraphBuilder`, import lazy `stream_processor.py:3017-3022`, DBSCAN) — **correction d'un faux "orphelin"** — **mais le graphe est siloté par machine** (persisté sous `workflows/{machine_id}/`) et le merge cross-session est machine-filtré. La progression **Shadow→Copilot→Autonomous** reste **du design, pas du runtime** (Shadow observe+log).
|
||||
|
||||
> ⚠️ **Caveat méthodo** : ce code utilise massivement des **imports lazy dans les handlers/méthodes**. Les verdicts "orphelin" basés sur grep d'imports top-level sont **non fiables** (federation ET GraphBuilder étaient ainsi faussement classés orphelins, puis confirmés WIRED). Tout "orphelin" ci-dessous non vérifié par lazy-import est à recontrôler.
|
||||
|
||||
→ Aujourd'hui, par défaut, **chaque poste est un silo cognitif** : il n'apprend pas des autres, sauf pour les compétences YAML et les embeddings centralisés.
|
||||
|
||||
## Tableau de synthèse (WIRED ? · COMMUN/SILOTÉ)
|
||||
|
||||
### A. Capture → construction → stockage (agent Explore #1)
|
||||
| Composant | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|
||||
|---|---|---|---|
|
||||
| TraceStreamer (tag machine_id) | `agent_v1/network/streamer.py:91` | ✅ (main.py:227) | tague machine_id sur chaque POST |
|
||||
| register/stream/finalize | `server_v1/api_stream.py:1748/1801/2336` | ✅ endpoints | session taguée machine_id |
|
||||
| `_persist_workflow` | `server_v1/stream_processor.py:4417` (appelé 3066) | ✅ | **SILOTÉ** : écrit `data/training/workflows/{machine_id}/` + tag `_machine_id` |
|
||||
| Store disque sessions | `data/training/live_sessions/{machine_id}/` | ✅ | **SILOTÉ** (arbo 1:1 par machine) |
|
||||
| `_run_cross_session_learning` / `_find_best_cross_session_match` | `stream_processor.py:3149 / 3273` | ✅ (via finalize) | **SILOTÉ CODÉ** : `if workflow_machine != machine_id: continue` (L3284-3286) → un poste n'apprend jamais d'un autre |
|
||||
| Listing `list_workflows` | `api_stream.py:2799` + `stream_processor.py:4518` | ✅ | **BIMODAL** : `machine_id=None` → tous ; sinon filtré |
|
||||
| Client `list_workflows()` | `lea_ui/server_client.py:228` | ✅ (smart_tray:802) | **COMMUN** : n'envoie PAS machine_id → reçoit tous |
|
||||
| Dashboard list_sessions | `web_dashboard/app.py:2289` | ✅ | filtre disque par machine_id (optionnel) |
|
||||
| Replay ciblage | `api_stream.py:3064` + `replay_engine.py:1559` | ✅ | machine_id = **ROUTE** l'exécution vers le bon poste (légitime) |
|
||||
|
||||
### B. Apprentissage / cognition / compétences (agent Explore #2)
|
||||
| Module | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|
||||
|---|---|---|---|
|
||||
| `target_memory_store` (mémoire cibles, phase 1) | `core/learning/target_memory_store.py:77` | ✅ hot-path `resolve_engine.py:1869` | **SILOTÉ par machine** (`data/learning/target_memory.db` local, sauf `RPA_LEARNING_DIR` partagé) |
|
||||
| `continuous_learner` | `core/learning/continuous_learner.py` | ✅ (`stream_processor.py:3145`) | **SILOTÉ par session** |
|
||||
| `replay_learner` | `server_v1/replay_learner.py:90` | ✅ (`api_stream.py:2436`) | **SILOTÉ par session** |
|
||||
| `learning_manager` (états workflow VWB) | `core/learning/learning_manager.py:37` | ✅ singleton VWB | **COMMUN** |
|
||||
| **Compétences YAML** (catalog/replay/persist/verdicts/promotions) | `core/competences/*` | ✅ endpoints `api_stream` + dashboard | ✅ **COMMUN** (tous lisent `data/competences/`) — **c'est le vrai fonds commun qui marche** |
|
||||
| `observe_reason_act` (ORALoop) | `core/execution/observe_reason_act.py:145` | ✅ (VWB `api_v3/execute.py`) | siloté par exécution |
|
||||
| `feedback_processor`, `versioned_store`, `core/cognition/*`, `core/knowledge`, `core/coaching`, `core/healing`, `core/supervision` | — | ⚠️ **ORPHELINS** (tests seuls) | — |
|
||||
|
||||
### C. Graphe / embeddings / autonomie (agent Explore #3)
|
||||
| Composant | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|
||||
|---|---|---|---|
|
||||
| ScreenState (perception) | `core/pipeline/workflow_pipeline.py` | ✅ serveur | COMMUN |
|
||||
| StateEmbedding + Builder (fusion 512d) | `core/models/state_embedding.py:44` / `core/embedding/state_embedding_builder.py:25` | ✅ `stream_processor` startup | **COMMUN** (vecteurs `data/training/embeddings/*.npy` centralisés) |
|
||||
| CLIP (OpenCLIP ViT-B-32) | `core/embedding/clip_embedder.py` | ✅ | COMMUN |
|
||||
| FAISSManager (similarité) | `core/embedding/faiss_manager.py:40` | ✅ `stream_processor` | **COMMUN serveur** mais **index per-session en mémoire** (pas un index global persistant) |
|
||||
| **WorkflowGraph builder (couche 4)** | `core/graph/graph_builder.py:148` | ✅ **WIRED** (import lazy `stream_processor.py:3017`, instancié `:3022` dans `finalize_session`, DBSCAN) — *corrigé : Agent #3 l'avait dit orphelin à tort* | **SILOTÉ** : graphe construit par session, persisté `workflows/{machine_id}/` ; merge cross-session machine-filtré |
|
||||
| WorkflowNode/Edge (modèles couche 4) | `core/models/workflow_graph.py:384` | ✅ utilisés par GraphBuilder | siloté (idem) |
|
||||
| **Shadow** observer | `core/workflow/shadow_observer.py:25` | ⚠️ partiel (`api_stream:2700` observe + LOG seulement) | pas d'apprentissage collectif |
|
||||
| **Copilot / Autonomous** | `core/learning/learning_engine.py` | ❌ **design papier**, pas runtime | — |
|
||||
| `audit_trail.execution_mode` (shadow/assisted/autonomous) | `server_v1/audit_trail.py:50` | ✅ enregistré | mais **pas exploité** pour décider |
|
||||
|
||||
### D. Fédération = LE fonds commun par design (vérif directe + Qwen en cours)
|
||||
| Composant | fichier:ligne | WIRED | COMMUN/SILOTÉ |
|
||||
|---|---|---|---|
|
||||
| `LearningPackExporter` | `core/federation/learning_pack.py` | 🌙 **endpoint manuel** `GET /api/v1/traces/stream/learning-pack/export` (`api_stream.py:6278`, import lazy L6292) | conçu commun |
|
||||
| `LearningPack` + `GlobalFAISSIndex` | `core/federation/learning_pack.py:294` / `faiss_global.py:51` | 🌙 **endpoint manuel** `POST .../learning-pack/import` (`api_stream.py:6323`, L6334-6353 `GlobalFAISSIndex()` ré-instancié) | conçu commun |
|
||||
| Déclenchement automatique | — | ❌ **AUCUN** : rien dans le flux capture→learn n'appelle export/import | → fonds commun **dormant** |
|
||||
|
||||
**Verdict fédération (Claude + Qwen)** :
|
||||
- ✅ **Bien architecturé** (Qwen) : `LearningPack` anonymise (machine_id blacklisté `_SENSITIVE_METADATA_KEYS:64`, `source_hash` SHA-256), `GlobalFAISSIndex` est **global** (clé `pack_source_hash`+`workflow_skeleton_id`+`node_name`+`app_name`, **aucun machine_id**), persistant (`.faiss`+`.meta.json`, `save/load` L245/277). Export prend **tous** les workflows (`processor._workflows.values()` L6305) sans filtre machine.
|
||||
- ❌ **Mais doublement inerte** : (a) **pas d'auto-déclenchement** (rien dans capture→learn n'appelle export/import) ; (b) **`search()` jamais appelé** — `_global_faiss_index` n'est référencé QU'aux lignes `api_stream.py:6351-6372` (instanciation + `add_pack` à l'import). Aucun chemin de résolution/apprentissage/replay ne **lit** l'index global. → **write-only, jamais consulté** : contribue 0 au runtime.
|
||||
|
||||
Conclusion : le fonds commun fédéré est **codé correctement mais débranché du runtime** — c'est exactement le maillon à activer pour la vision.
|
||||
|
||||
## Où `machine_id` cloisonne vs route
|
||||
- **ROUTE (légitime)** : ciblage d'un replay/d'une session vers le bon poste (`replay_engine.py:1559`, `start_replay`).
|
||||
- **CLOISONNE (à corriger vs vision)** : (1) dossiers de stockage `{machine_id}/` ; (2) **filtre dur du cross-session learning** (`stream_processor.py:3284`) ; (3) `target_memory.db` local par machine. + machine_id **instable** (nouvel ID à chaque relance) qui fragmente même au sein d'un poste.
|
||||
|
||||
## Gap vs vision « fonds commun → autonomie » (constats, pas décisions)
|
||||
Pour réaliser la vision, il manque le câblage de :
|
||||
1. **Dé-siloter le savoir workflows/sessions** : retirer le filtre machine_id du cross-session learning + stockage commun (ou index commun), en gardant machine_id pour le seul routing.
|
||||
2. **Activer la fédération en continu** (auto-export/import ou store partagé) au lieu du manuel dormant — c'est l'endroit conçu pour ça.
|
||||
3. **Câbler la couche graphe (4)** en live (aujourd'hui orpheline) pour un knowledge graph commun.
|
||||
4. **Implémenter Shadow→Copilot→Autonomous** (aujourd'hui observe+log / design) consommant ce fonds commun.
|
||||
5. **Stabiliser machine_id** (persisté) pour ne pas fragmenter.
|
||||
|
||||
## Ce qui marche DÉJÀ comme fonds commun (à capitaliser)
|
||||
- **Compétences YAML** (`core/competences/`) : micro-workflows réutilisables, états supervisés, **lus par tous les postes**. C'est le modèle commun qui fonctionne → piste à étendre.
|
||||
- **Embeddings centralisés** (`data/training/embeddings/`) : matière première commune déjà là.
|
||||
|
||||
## E. Pipeline server_v1 (cross-check Codex — intégré)
|
||||
- Pipeline **WIRED** : capture → `_worker_queue.txt` → `run_worker.py` → `StreamProcessor.reprocess_session()` → workflow JSON → replay → apprentissage. `api_stream.py` importe+instancie `ReplayLearner`/`StreamProcessor`/`StreamWorker` (`:32/40/41`, `:562-563`, startup `:1626-1629`).
|
||||
- **`ReplayLearner` = commun mais faiblement effectif** : `ActionOutcome` sans `machine_id`, stockage global `data/learning/replay_results/` ; MAIS `query_similar()` ne lit que le cache mémoire `_recent` (`:273-304`) et `build_replay_from_raw_events()` crée une **nouvelle instance** (`stream_processor.py:2379-2382`) → **l'historique JSONL global n'est pas exploité** après restart/hors instance globale.
|
||||
- **Risque ambiguïté** : la queue worker ne porte que `session_id`, pas `machine_id` (`api_stream.py:734-760`) → résolution disque ambiguë si 2 machines ont le même `session_id`.
|
||||
- `machine_id` **route** légitimement : `/replay`, `/replay/next` refusent les actions d'une autre machine (`:3978-3982`, `:4033-4054`), `_find_active_agent_session` filtre `machine_id`/`bg_<machine_id>` (`replay_engine.py:1559-1588`).
|
||||
|
||||
## Pistes de correction CONVERGENTES (constats Codex+Claude — NON décidées, mapping seulement)
|
||||
1. **Cantonner `machine_id` au routing/fleet/replay-target/audit** — pas au stockage ni au matching du savoir.
|
||||
2. **Dé-siloter les workflows appris** : sortir du stockage logique `workflows/{machine_id}/` (ou neutraliser ce marqueur en lecture/matching).
|
||||
3. **Retirer/rendre optionnel le filtre machine** dans `_run_cross_session_learning()` / `_find_best_cross_session_match()` (`stream_processor.py:3193-3203`, `3283-3286`) → apprendre sur tous les workflows compatibles.
|
||||
4. **Brancher le fonds commun fédéré** : (a) alimenter le `GlobalFAISSIndex` en continu (auto export/import ou store partagé) ; (b) **appeler son `search()`** dans le hot-path résolution/apprentissage (aujourd'hui jamais lu).
|
||||
5. **Rendre `ReplayLearner` durable** : charger/interroger l'historique JSONL global, réutiliser l'instance globale (pas une neuve par session).
|
||||
6. **Stabiliser `machine_id`** (persisté) pour ne pas fragmenter intra-poste.
|
||||
7. **Étendre le modèle "compétences communes"** (`core/competences/`, déjà commun + supervisé) comme colonne vertébrale du fonds commun.
|
||||
|
||||
---
|
||||
*Sources : graphify-out (58k nodes) ; agents Explore #1/#2/#3 (⚠ verdicts "orphelin" sur imports top-level corrigés par lazy-import) ; cross-checks **Codex** (server_v1, msg 16:43) & **Qwen** (federation/FAISS, msg 16:50) intégrés ; vérifs directes `api_stream.py:6271-6372`, `faiss_global.py:199`, `stream_processor.py:3017-3022`.*
|
||||
186
docs/CARTO_CODE_NON_BRANCHE_2026-07-02.md
Normal file
186
docs/CARTO_CODE_NON_BRANCHE_2026-07-02.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# CARTO CODE NON BRANCHÉ — carte de référence wiring (2026-07-02)
|
||||
|
||||
> **But** : carte « existing-first » de référence. AVANT tout chantier/bench/proposition,
|
||||
> consulter ce doc pour savoir si une brique existe et si elle est **réellement branchée au
|
||||
> runtime**. Recadrage Dom 02/07 : « vérifier ce qui existe et non branché, c'est le BABA ».
|
||||
>
|
||||
> **Méthode** : verdict prouvé par chaîne d'imports depuis un point d'entrée actif
|
||||
> (fichier:ligne), imports lazy inclus, gates de config citées. Jamais de conclusion sur un
|
||||
> grep seul. Sources fusionnées : agent Claude « intelligence » + carto Qwen (volet 1
|
||||
> détection) + `AUDIT_CODE_MORT_2026-07-02.md` (Qwen) + vérifs ponctuelles Claude.
|
||||
>
|
||||
> **Légende** : **WIRED** (chaîne prouvée) · **GATED** (branché mais derrière flag, défaut
|
||||
> cité) · **ORPHELIN** (0 appelant runtime, recherche exhaustive) · **INCERTAIN** (non tranché,
|
||||
> raison donnée).
|
||||
>
|
||||
> **Points d'entrée actifs runtime** : `api_stream.py` (streaming 5005, `rpa-streaming`) ·
|
||||
> `run_worker.py` (worker VLM 5099) · VWB `app.py` (5002) · `web_dashboard/app.py` (5001) ·
|
||||
> `agent_chat/app.py` (5004) · `server/api_upload.py` (8000).
|
||||
|
||||
---
|
||||
|
||||
## 0. Résumé exécutif — les découvertes qui changent une décision
|
||||
|
||||
1. **Self-healing = façade morte, malgré doc « wired ».** Chaîne d'import réelle (VWB →
|
||||
`core/healing`), routes REST répondent, MAIS déclenchement **impossible** : le code teste
|
||||
`hasattr(healing_integration, 'enable_healing')` et cette méthode **n'existe nulle part**
|
||||
(`execution_integration.py:421`). `handle_execution_failure` = 0 appelant d'exécution.
|
||||
Preuve d'inertie : `logs/healing/recovery.log` = **0 octet, mtime déc. 2025**. Le pont
|
||||
manquant tient à **une méthode**, pas un module. → `PLAN_MENAGE_CODE_MORT` le classait
|
||||
« wired — NE PAS TOUCHER » : **doc fausse**.
|
||||
|
||||
2. **`core/navigation` (commit du matin `f9a053132`) = write-only.** Le handler résout le
|
||||
login et écrit `navigate_login_coords` dans `replay_state["variables"]`, mais **aucun
|
||||
consommateur** : le compilateur `_edge_to_normalized_actions` n'a pas de branche `navigate`
|
||||
et produit des coords littérales, jamais de templates `{{navigate_login_coords.x_pct}}`.
|
||||
Détail : `docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md`. Décision D1 en attente Dom.
|
||||
|
||||
3. **AutonomousPlanner : coût sans usage.** Instancié au boot d'`agent_chat` (charge LLM +
|
||||
OWL detector via `autonomous_planner.py:36`), mais **aucune route n'appelle une méthode de
|
||||
planification** — seuls des setters. Type même du « code écrit jamais invoqué ».
|
||||
|
||||
4. **PaddleOCR installé, jamais importé.** `paddleocr 3.4.0` + `paddlepaddle 3.3.1` (CPU)
|
||||
présents dans `.venv`, **0 `import paddle` dans le code**, 0 requirements, 0 deploy. Piste
|
||||
bench en cours (Qwen), pas un composant actif.
|
||||
|
||||
5. **YOLO cascade de résolution = mort.** `_resolve_by_yolo` défini
|
||||
(`resolve_engine.py:458`) + importé (`api_stream.py:6114`) mais **jamais appelé** ; aucune
|
||||
branche `yolo` dans la cascade compilée. ⚠ À NE PAS confondre avec le YOLO de `som_engine`
|
||||
(OmniParser SoM), lui **WIRED**.
|
||||
|
||||
6. **`server/api_core.py`** : blueprint Flask complet (capture/detect/embed/faiss) **jamais
|
||||
enregistré** — orphelin absent du plan ménage.
|
||||
|
||||
7. **Nos propres cartos avaient 4 erreurs** (cf. §4). Re-prouver était justifié.
|
||||
|
||||
---
|
||||
|
||||
## 1. Chaîne détection / grounding / résolution
|
||||
|
||||
| Module | Verdict | Preuve (fichier:ligne) | Remarque |
|
||||
|--------|---------|------------------------|----------|
|
||||
| `core/detection/som_engine.py` | **WIRED** | resolve_engine.py:1192 (replay) · stream_processor.py:643 (recording) · api_stream.py:1958 (temps réel) | 3 chemins indépendants, singleton thread-safe. Tire YOLO weights direct. |
|
||||
| `core/detection/omniparser_adapter.py` | **B-DORMANT** (branché lazy, fallback vide) | phase25_analyzer.py:388 · resolve_engine.py:437 · désactivé côté VWB (`_omniparser_available=False`) | Import lazy try/except, singleton. 7 zones cartographiées (§ audit Qwen). |
|
||||
| `core/detection/owl_detector.py` | **WIRED (via AutonomousPlanner) — mais planner inerte** | autonomous_planner.py:36 | Chargé au boot agent_chat pour rien (cf. §0.3). 4 méthodes internes C-MORT. |
|
||||
| `core/detection/ollama_client.py` | **WIRED partiel** | `classify_element_complete()` actif ; 5 vieilles méthodes + `check_ollama_available()` standalone = C-MORT | Duplicat VWB (D2). |
|
||||
| `_resolve_by_yolo` (resolve_engine.py:458) | **ORPHELIN** | importé api_stream.py:6114, **0 appel réel**, 0 branche cascade | ≠ YOLO de som_engine (wired). |
|
||||
| `core/grounding/bbox_parser.py` | **WIRED** | resolve_engine.py:29 | |
|
||||
| `core/grounding/smart_resize.py` | **ORPHELIN (C-MORT)** | 0 appelant prod, DETTE-007 triple impl (2 autres existent) | |
|
||||
| `core/grounding/server.py` | **WIRED** | service HTTP Flask port 8200 standalone | Upgrade C→A (Qwen). |
|
||||
| `visual_workflow_builder/.../api/ui_detection.py` | **WIRED** | VWB app.py:310 (blueprint) · fast_detector.py:117 | UI-DETR-1 du recording, modèle rfdetr RFDETRMedium, 5 endpoints `/api/ui-detection`. |
|
||||
| `core/semantic/phase25_analyzer.py` | **WIRED** | api_stream.py:7690 (route `lea_competence_persist:7435`) | |
|
||||
| `core/extraction/{field_extractor,vlm_client,role_mapper}` | **WIRED-transitif** | field_extractor ← input_handler.py:121/504/722 (lazy) · vlm_client+role_mapper ← core/navigation/__init__.py:69, action_resolver.py:109 | Le plan ménage 23/06 (« 4/5 morts ») précède navigation. |
|
||||
| `core/llm/` (ocr_extractor, extract_grid) | **WIRED** | api_stream.py:1766 · replay_engine.py:2115-2403 · resolve_engine.py:2597 | |
|
||||
| `core/navigation/` | **WIRED (boot) / write-only (fonctionnel)** | api_stream.py:440 top-level NON gardé · handler résout mais 0 consommateur coords | cf. §0.2. ⚠ import non gardé → si casse, 5005 ne boote pas (garde-fou test_navigate_wiring.py). |
|
||||
| PaddleOCR (venv) | **ORPHELIN** | 0 import, 0 requirements, 0 deploy | cf. §0.4. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Modules « intelligence »
|
||||
|
||||
| Module | Verdict | Preuve (fichier:ligne) | Remarque |
|
||||
|--------|---------|------------------------|----------|
|
||||
| `core/healing/` | **ORPHELIN de fait** (importé, indéclenchable) | chaîne VWB app.py:217 → api/self_healing.py → services/self_healing_integration.py, MAIS `enable_healing` inexistant (execution_integration.py:421) ; `handle_execution_failure` 0 appelant | cf. §0.1. `logs/healing/recovery.log` vide depuis déc. 2025. |
|
||||
| `core/coaching/` | **WIRED** | VWB app.py:284-285 (blueprint) → api/coaching_sessions.py:17,22 · exec : execution_integration.py:869 · front WebSocket | REST blueprint peut-être non consommé par l'UI (front = socket.io). |
|
||||
| `core/cognition/working_memory` | **WIRED-transitif** | observe_reason_act.py:30,506 · ORALoop ← VWB execute.py:1542,2075 | Les 4 autres sous-modules cognition = MORTS (tests only). |
|
||||
| `core/learning/` (4/5) | **WIRED** | target_memory_store: resolve_engine.py:1865 + api_stream.py:5132 · continuous_learner: stream_processor.py:3147 · learning_manager: VWB learning_integration.py:36, api/workflows.py:696 · feedback_processor: execution_loop.py:317 | `versioned_store` ORPHELIN. `record_observation` = **0 appelant** (learning_manager.py:54). |
|
||||
| `core/execution/` | **WIRED massif** | observe_reason_act ← execute.py:1542 · input_handler ← execute.py:69 · dag_executor+llm_actions ← dag_execute.py:33,40 · action_executor/target_resolver/error_handler/execution_loop ← agent_chat app.py:328-340 · +transitifs | Morts : spatial_index, target_memory, workflow_runner (⚠ encore exporté par `__init__.py:10`). |
|
||||
| `core/auth/` | **GATED — défaut OFF** | api_stream.py:278-286 : import lazy SSI `RPA_AUTH_VAULT_PATH` **et** `RPA_AUTH_VAULT_PASSWORD` définis (absents par défaut). Seul lieu qui les définit : CI `.gitea/workflows/tests.yml:35` | Vault inactif en prod. TOTP dans la même chaîne gated. |
|
||||
| `core/federation/` | **WIRED manuel, write-only** | routes actives non gated : GET learning-pack/export api_stream.py:6431 · POST import :6476 | `GlobalFAISSIndex.search()` = **0 appelant**. Aucun auto-déclenchement. |
|
||||
| `core/gpu/` (2/6) | **WIRED** | device_policy ← resolve_engine.py:1750 (hot-path) · gpu_resource_manager ← agent_chat app.py:53,266 | clip_manager, ollama_manager, vram_monitor, preflight = morts. |
|
||||
| `core/embedding/` | **WIRED (lazy)** | construction CLIP/FAISS ← stream_processor.py:2560 `_ensure_initialized` (appelé process_screenshot:2804 + finalize_session:2969) · lecture web_dashboard app.py:309+ | Se déclenche au 1er screenshot / finalisation, pas au boot. |
|
||||
| `agent_chat/autonomous_planner` | **INSTANCIÉ mais INERTE** | import app.py:48, instancié :358, mais seuls appels = setters :362,367 ; 0 route de planification | cf. §0.3. Tire owl_detector pour rien. |
|
||||
| `agent_chat/urgences_orchestrator` | **WIRED** | import lazy app.py:2740, routes `/api/urgences/*` | |
|
||||
| `agent_chat/gesture_catalog` | **WIRED ×2** | agent_chat app.py:377,955 · **api_stream.py:269,3598** (hot-path replay `optimize_replay_actions`) | Pas seulement le chat. |
|
||||
| `core/validation/` | **GATED — défaut OFF** | flag `RPA_VALIDATOR_V2_ENABLED` défaut OFF (api_stream.py:91), consommé report_action_result:4924 | |
|
||||
|
||||
**WIRED confirmés (survol)** : capture, models, competences, corrections, data, graph,
|
||||
knowledge, monitoring, persistence, pipeline, system, workflow, visual, config.py,
|
||||
anonymisation (PII), matching/training (transitifs).
|
||||
**ORPHELINS confirmés** : variants, precision, supervision, interfaces (0 importeur non-test) ·
|
||||
`core/evaluation/` (consommé seulement par `tools/lea_bench*.py`, outillage CLI) ·
|
||||
`server/api_core.py` (blueprint jamais enregistré).
|
||||
|
||||
---
|
||||
|
||||
## 3. Zones GATED (flags + défaut) — activation supervisée
|
||||
|
||||
| Flag | Défaut | Effet si ON | Preuve |
|
||||
|------|--------|-------------|--------|
|
||||
| `RPA_AUTH_VAULT_PATH` + `RPA_AUTH_VAULT_PASSWORD` | absents | active `core/auth` (vault Fernet + TOTP) | api_stream.py:278-286 |
|
||||
| `RPA_VALIDATOR_V2_ENABLED` | OFF | active validation V2 (report_action_result) | api_stream.py:91 |
|
||||
| `RPA_R1_AUTO_IMPORT` | OFF | active import auto core→DB VWB (R1) | api_stream.py:~4480 (revue en cours) |
|
||||
| `RPA_AUTO_UPDATE_ENABLED` | OFF | MAJ silencieuse client (DETTE-022) | agent_v1/config.py:103 |
|
||||
| `RPA_GROUNDING_ENGINE=qwen3vl_vllm` | legacy Qwen2.5-VL | grounder Qwen3-VL (override DGX runtime) | resolve_engine.py:1001-1007 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Divergences corrigées avec les docs existants
|
||||
|
||||
1. **`core/healing` : doc `PLAN_MENAGE` = « wired, NE PAS TOUCHER » → FAUX.** Indéclenchable
|
||||
(`enable_healing` fantôme, log vide déc. 2025). Le pont est à une méthode près.
|
||||
2. **`feedback_processor` : CARTO 16/06 = ORPHELIN → FAUX.** Instancié à chaque ExecutionLoop
|
||||
(execution_loop.py:317).
|
||||
3. **`core/cognition` : CARTO 16/06 = tout orphelin → FAUX pour working_memory** (vivant au
|
||||
runtime VWB via observe_reason_act).
|
||||
4. **`core/extraction` : plan ménage « 4/5 morts » → périmé.** vlm_client + role_mapper
|
||||
branchés via `core/navigation` (postérieur au doc).
|
||||
|
||||
Upgrades C→A/B confirmés par Qwen : autonomous_planner (C→A, mais inerte cf. §0.3),
|
||||
seeclick_adapter (C→B), grounding/server.py (C→A), get_grounding_profile (C→A).
|
||||
|
||||
---
|
||||
|
||||
## 5. Code mort candidat suppression → voir `AUDIT_CODE_MORT_2026-07-02.md`
|
||||
|
||||
Résumé : **8 C-MORT** (~843 lignes, ex. deploy_windows.py, smart_resize.py, 7 config classes
|
||||
dépréciées, agent_chat 410 endpoints) · **5 B-ORPHELIN** (à conserver, projections) · **4
|
||||
duplicats** (décision Dom). Suppression = GO Dom par lot, worktree isolé + tests après chaque
|
||||
lot. ⚠ Prudence renforcée vu les 4 erreurs de doc du §4 : re-prouver chaque item avant
|
||||
suppression.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cascade de résolution UI (`resolve_engine.py`) — ordre RÉEL prouvé
|
||||
|
||||
Point d'entrée unique au replay : le client Léa (`executor.py:2847`, **`strict_mode=True` hardcodé**
|
||||
:2870) → route `resolve_target` (`api_stream.py:6131`) → `_resolve_target_sync`
|
||||
(`resolve_engine.py:1804`). `replay_engine.py` ne résout pas (il construit le target_spec).
|
||||
|
||||
**Ordre réel au replay (mode strict VLM-first, `resolve_engine.py:1957`)** :
|
||||
```
|
||||
0. Mémoire persistante (replay_memory.memory_lookup:1869) — hit → skip toute la cascade
|
||||
0c. dialog_button → OCR seul (1920-1952)
|
||||
── strict VLM-first (1957) ──
|
||||
S0a. Grounding VLM (_resolve_by_grounding:2019) si by_text_source ∈ {ocr, vlm}
|
||||
S0b. Template matching icônes (2057) sinon
|
||||
S0.5 OCR direct (_resolve_by_ocr_text:2105) si by_text
|
||||
S1. VLM Quick Find (_vlm_quick_find:2158)
|
||||
S1.5 SoM + VLM (_resolve_by_som:2207)
|
||||
S2. Template matching fallback (2238)
|
||||
S3. STOP replay resolved=False (2283)
|
||||
```
|
||||
Note : grounding VLM (S0a) et VLM Quick Find (S1) sont **deux appels VLM distincts**.
|
||||
|
||||
**Statut resolvers** : `_resolve_by_grounding`, `_resolve_by_template_matching`,
|
||||
`_resolve_by_ocr_text`, `_vlm_quick_find`, `_resolve_by_som`, `replay_memory` = **WIRED**
|
||||
(preuves lignes ci-dessus). Grounder Qwen3-VL : bascule dans `_resolve_by_grounding:1006`
|
||||
(flag `RPA_GROUNDING_ENGINE=qwen3vl_vllm`, sinon legacy Qwen2.5-VL) — change modèle/endpoint/
|
||||
prompt/parser, pas le flux.
|
||||
|
||||
**3 branches MORTES dans la cascade** :
|
||||
- `_resolve_by_yolo` (:458) — importé api_stream.py:6114, **0 appel réel**. ORPHELIN.
|
||||
- **Vérification CLIP** (:1972-2008) — **dead gate** : lit `target_spec["clip_embedding"]`
|
||||
qui n'est **jamais peuplé** dans tout `agent_v0/` → branche jamais exécutée.
|
||||
- **V4 pré-compilé** (`_resolve_with_precompiled_order:1601`, ordre figé `["ocr","template",
|
||||
"vlm"]`) — **WIRED mais dormant en replay normal** : alimenté uniquement par l'endpoint
|
||||
`/replay/plan` (`execution_plan_runner.py:173`), jamais par le flux VWB→Léa.
|
||||
|
||||
**Verdict README « OCR→template→YOLO→VLM » = FAUX** : (1) YOLO mort, (2) l'ordre est
|
||||
VLM-first, (3) la séquence `ocr,template,vlm` n'existe que dans le V4 dormant.
|
||||
|
||||
## 7. Zones restantes non re-vérifiées (honnêteté)
|
||||
|
||||
- `core/analytics/` : ~13 sous-modules orphelins non re-vérifiés un par un (conforme doc).
|
||||
- Reste couvert : chaîne détection/grounding/résolution + intelligence = prouvés. **Carto
|
||||
considérée complète sur le périmètre runtime actif.**
|
||||
145
docs/CHECKLIST_DGX_PRE_CLINIQUE.md
Normal file
145
docs/CHECKLIST_DGX_PRE_CLINIQUE.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# CHECKLIST DGX — Contrôle avant installation clinique
|
||||
|
||||
- `Auteur`: Qwen
|
||||
- `Date`: 2026-06-19
|
||||
- `Version`: v1 — à vérifier point par point avant déploiement site clinique
|
||||
|
||||
---
|
||||
|
||||
## 1. SERVICES — Tous démarrent au reboot
|
||||
|
||||
| # | Service | Port | Statut attendu | Check |
|
||||
|---|---|---|---|---|
|
||||
| 1.1 | rpa-streaming | 5005 | `health=200` | `curl http://127.0.0.1:5005/health` |
|
||||
| 1.2 | rpa-vision-v3-dashboard | 5001 | `401 sans creds, 200 avec creds` | `curl -u lea:<password> http://127.0.0.1:5001/api/system/status` |
|
||||
| 1.3 | rpa-vision-v3-vwb-backend | 5002 | `401 LAN, 200 loopback` | `curl http://127.0.0.1:5002/health` puis `curl http://192.168.x.x:5002/health` |
|
||||
| 1.4 | rpa-agent-chat | 5004 | `200` | `curl http://127.0.0.1:5004/api/status` |
|
||||
| 1.5 | rpa-vision-v3-api | 8000 | `fermé LAN` | `curl http://192.168.x.x:8000` → timeout/refused |
|
||||
| 1.6 | rpa-vision-v3-vwb-frontend | 3002 | `200` | `curl http://127.0.0.1:3002` |
|
||||
| 1.7 | rpa-vision-v3-stream-worker | 5099 | `running` | `systemctl status rpa-vision-v3-stream-worker` |
|
||||
| 1.8 | rpa-vision-v3-worker | — | `running` | `systemctl status rpa-vision-v3-worker` |
|
||||
| 1.9 | rpa-firewall | — | `active (exited)` | `systemctl status rpa-firewall` |
|
||||
| 1.10 | Dashboard systemd | 5001 | **service system ACTIF** (pas fallback user) | ✅ **VALIDÉ reboot 20/06** — system service active, fallback user masked |
|
||||
|
||||
**Check reboot** : `systemctl list-units --type=service | grep rpa` → tous `active running` ou `active exited`
|
||||
|
||||
---
|
||||
|
||||
## 2. RÉSEAU — Ports sensibles fermés LAN
|
||||
|
||||
| # | Port | Risque | Statut attendu | Check |
|
||||
|---|---|---|---|---|
|
||||
| 2.1 | 5900 (VNC GNOME) | Remote desktop | **LAN fermé, loopback OK** | `nmap 192.168.x.x -p 5900` → filtered/closed |
|
||||
| 2.2 | 5902 (VNC VM Windows) | Remote desktop VM | **LAN fermé, tunnel SSH only** | `nmap 192.168.x.x -p 5902` → filtered/closed |
|
||||
| 2.3 | 3389 (RDP/xrdp) | Remote desktop | **LAN fermé** | `nmap 192.168.x.x -p 3389` → filtered/closed |
|
||||
| 2.4 | 22220 (SSH VM Windows) | Shell VM | **LAN fermé** | `nmap 192.168.x.x -p 22220` → filtered/closed |
|
||||
| 2.5 | 8000 (API upload) | API non protégé | **LAN fermé** | `nmap 192.168.x.x -p 8000` → filtered/closed |
|
||||
| 2.6 | 11434 (Ollama) | Modèles IA | **LAN fermé** | `nmap 192.168.x.x -p 11434` → filtered/closed |
|
||||
| 2.7 | 5002 (VWB backend) | Données workflows | **LAN : auth requise (401)** | `curl http://192.168.x.x:5002/api/workflows/` → 401 |
|
||||
| 2.8 | 5004 (Agent chat) | Chat interface | **À arbitrer** — ouvert ou fermé ? | Décision Dom |
|
||||
| 2.9 | 3002 (VWB frontend) | Interface web | **À arbitre** — ouvert ou fermé ? | Décision Dom |
|
||||
|
||||
---
|
||||
|
||||
## 3. SÉCURITÉ — Authentification + accès
|
||||
|
||||
| # | Item | Statut attendu | Check |
|
||||
|---|---|---|---|
|
||||
| 3.1 | Dashboard Basic Auth | `401 sans creds` | `curl http://192.168.x.x:5001/api/system/status` → 401 |
|
||||
| 3.2 | VWB Basic Auth | `401 LAN, 200 loopback` | Vérifié ✅ (commit cf81ce4c7) |
|
||||
| 3.3 | Streaming Bearer Auth | `401 sans token` | `curl http://127.0.0.1:5005/api/v1/...` → 401 |
|
||||
| 3.4 | SSH clé uniquement | Pas de password login | `grep PasswordAuthentication /etc/ssh/sshd_config` → no |
|
||||
| 3.5 | Firewall persistant reboot | Ports fermés après reboot | ✅ **VALIDÉ reboot 20/06** — ports sensibles filtrés, services ouverts OK |
|
||||
| 3.6 | RPA_SIGNING_KEY défini | FAISS metadata valide | ⚠️ **À FIXER** — HMAC mismatch, Option A en attente |
|
||||
|
||||
---
|
||||
|
||||
## 4. VM WINDOWS — Autostart + stabilité
|
||||
|
||||
| # | Item | Statut attendu | Check |
|
||||
|---|---|---|---|
|
||||
| 4.1 | VM boot auto au reboot DGX | Service systemd user `aivanov` | ✅ **VALIDÉ reboot 20/06** — `win11-arm-lea.service` auto-démarre, linger=yes |
|
||||
| 4.2 | VM accessible VNC | Tunnel SSH `localhost:5902` | Vérifié ✅ |
|
||||
| 4.3 | VM ne pas libvirt en parallèle | Pas de conflit disk.qcow2 owner | ⚠️ **À DOCUMENTER** — ne pas lancer libvirt VM |
|
||||
| 4.4 | disk.qcow2 owner = aivanov | Pas libvirt-qemu | `ls -la disk.qcow2` → aivanov:aivanov |
|
||||
| 4.5 | swtpm lancé par script | Pas manuel | Script standalone gère swtpm ✅ |
|
||||
| 4.6 | Léa config.txt pointe DGX | Pas cloud URL | `cat config.txt` → DGX IP |
|
||||
|
||||
---
|
||||
|
||||
## 5. DONNÉES — Persistence + integrity
|
||||
|
||||
| # | Item | Risque | Statut attendu | Check |
|
||||
|---|---|---|---|---|
|
||||
| 5.1 | workflows.db | 24 workflows live | `curl -u lea:<pw> http://127.0.0.1:5001/api/workflows | jq '.total'` → 24 |
|
||||
| 5.2 | FAISS index | 13666 vectors | `curl ... /api/knowledge-base/stats | jq '.vectors_indexed'` → 13666 |
|
||||
| 5.3 | FAISS metadata HMAC | Test endpoint 200 | ⚠️ **À FIXER** — Option A (resigner) |
|
||||
| 5.4 | Sessions training | Non trackées git → safe au reset | `ls data/training/sessions/` |
|
||||
| 5.5 | Git aligné | HEAD = dernier commit P0 | `git log -1` → cf81ce4c7 |
|
||||
| 5.6 | workflows.db préservé au git reset | Backup avant reset | ⚠️ **Procédure à respecter** |
|
||||
|
||||
---
|
||||
|
||||
## 6. STABILITÉ — Test reboot (✅ exécuté en réel le 2026-06-20)
|
||||
|
||||
| # | Item | Check | Résultat | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| 6.1 | Reboot DGX | Coupure secteur 02:07 | 9 services reviennent | ✅ PASS |
|
||||
| 6.2 | VM Windows auto-start | `win11-arm-lea.service` | VM auto-démarre | ✅ PASS |
|
||||
| 6.3 | Firewall persisté | Ports après reboot | Sensibles filtrés, services ouverts | ✅ PASS |
|
||||
| 6.4 | Dashboard systemd | Après reboot | System service actif, user fallback masked | ✅ PASS |
|
||||
| 6.5 | Worker healthy | Après reboot | PID 2267 actif, last_cycle continu | ✅ PASS |
|
||||
| 6.6 | **IP DHCP dérive** | `.45` → `.46` | IP statique `.45` appliquée (Dom) | ⚠️ **G1 — IP statique obligatoire clinique** |
|
||||
| 6.7 | **OVMF corruption VM** | Coupure brutale | OVMF corrompu, récupération manuelle (Codex) | ⚠️ **G2 — auto-réparation OVMF à implémenter** |
|
||||
| 6.8 | **Léa guest reconnecte** | config.txt | CONFIGURE_ME, pas DGX | ⚠️ **G4 — config.txt à renseigner** |
|
||||
|
||||
---
|
||||
|
||||
## 7. PRÉ-REQUIS DSI (envoyés à Nicolas PORQUET)
|
||||
|
||||
| # | Item | Statut | Check |
|
||||
|---|---|---|---|
|
||||
| 7.1 | Proxy HTTPS | À installer clinique | Architecture validée |
|
||||
| 7.2 | Docker | À installer | — |
|
||||
| 7.3 | VLAN isolation | À configurer | — |
|
||||
| 7.4 | SSH clé uniquement | ✅ Configuré DGX | `PasswordAuthentication no` |
|
||||
| 7.5 | 100% on-premise | ✅ Aucune cloud call | Vérifier config Léa |
|
||||
| 7.6 | Pas de secrets exposés | ✅ .env.local permissions | `ls -la .env.local` → 600 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ITEMS À FIXER AVANT CLINIQUE
|
||||
|
||||
1. **Dashboard fallback user** → ✅ **FIXÉ 20/06** (mask persistant, system service actif)
|
||||
2. **Auto-start VM** → ✅ **VALIDÉ 20/06** (reboot réel prouvé)
|
||||
3. **FAISS Option A** → ✅ **FIXÉ 19/06** (metadata resigné, 13666 vectors, test success=true)
|
||||
4. **Git DGX aligné** : DGX sur ec1fb81, cible cf81ce4c7 → aligner avec backup workflows.db
|
||||
5. **Test reboot** → ✅ **exécuté en réel 20/06** (5 PASS, 3 gaps identifiés)
|
||||
6. **G1 Dérive IP DHCP** : IP statique labo `.45` OK ; clinique = Ethernet `.178` obligatoire
|
||||
7. **G2 Auto-réparation OVMF** : snapshot sain au boot + restauration auto si TianoCode loop → **À IMPLÉMENTER**
|
||||
8. **G4 Léa reprise auto** : config.txt persistant DGX + token + auto-login → **À RENSEIGNER**
|
||||
|
||||
---
|
||||
|
||||
## Commandes smoke rapide (à lancer sur DGX)
|
||||
|
||||
```bash
|
||||
# Services
|
||||
systemctl list-units --type=service | grep rpa
|
||||
|
||||
# Health endpoints
|
||||
curl -s http://127.0.0.1:5002/health
|
||||
curl -s http://127.0.0.1:5005/health
|
||||
curl -s -u lea:v_zhmqOpGYcR-t7xJFKZyW-LjpvBuOOKss0ZleyH4jQ http://127.0.0.1:5001/api/system/status | jq '{workflows_count,status}'
|
||||
curl -s -H "Authorization: Bearer o3_LHqV_7_Gc6OVPHndhsBbvG6HJ5PCgl8yIBhGUIz8" http://127.0.0.1:5005/api/v1/traces/stream/processing/status | jq '{status,processing_ready}'
|
||||
|
||||
# Firewall LAN
|
||||
nmap 192.168.1.45 -p 5900,5902,3389,22220,8000,11434
|
||||
|
||||
# VM
|
||||
virsh -c qemu:///system list # doit être VIDE (standalone, pas libvirt)
|
||||
ps aux | grep qemu-system-aarch64 | grep win11
|
||||
|
||||
# Git
|
||||
cd ~/ai/rpa_vision_v3 && git log -1 --oneline
|
||||
```
|
||||
87
docs/DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md
Normal file
87
docs/DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Décisions produit en attente — à trancher à tête reposée
|
||||
|
||||
- `Date`: 2026-06-23
|
||||
- `Auteur`: Claude (questions) / Dom (réponses)
|
||||
- `Usage`: Dom remplit la ligne **Réponse Dom** quand il a la tête au calme. Chaque décision débloque un ou plusieurs chantiers du plan `PLAN_ACTION_SUITE_2026-06-23.md`. Segmentation par **fonctionnalité (F#)**.
|
||||
|
||||
> Contexte : H3 (cœur produit) traité **en premier**, décisions d'abord puis exécution séquencée (validé Dom 23/06). On ne touche pas l'archi rejeu pendant la livraison clinique du jour.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Déjà tranché (23/06)
|
||||
|
||||
| Réf | Décision | Réponse Dom |
|
||||
|---|---|---|
|
||||
| **PROC-1** | « H3 en premier » = trancher les décisions produit d'abord, exécution séquencée ensuite | ✅ OK |
|
||||
| **F6-1** | Niveau de mutualisation du fonds appris | ✅ **Cross-clinique ET intra-clinique** → la fédération anonymisée (cross) **est dans le périmètre**, + lever le silo `machine_id` entre postes d'une même clinique |
|
||||
| **F11-1** | Accès distant | ✅ **Multi-VPN par site** (WireGuard=nôtre, Stormshield=clinique, autres à venir) ; SSH cert + RDP = transport commun |
|
||||
| **F9-1** | Source de vérité workflows | ✅ **DB SQLAlchemy = vérité, JSON = échange** ; métrique produit = rejouables validés ; migration JSON→DB séquencée post-clinique (23/06) |
|
||||
| **F1-1** | Critère de fusion workflows | ✅ **Signature de trajectoire** (hash de la séquence d'actions/écrans) → débloque U-A create-or-update (23/06) |
|
||||
| **F2-1 / F14-1** | Principe rejeu intelligent | ✅ **Oui, prérequis** — le rejeu consulte le fonds appris (TargetMemory + FAISS anchors), pas de coords figées ; cohérent F6 cross-clinique (23/06) |
|
||||
|
||||
---
|
||||
|
||||
## ⏳ À trancher
|
||||
|
||||
> ✅ **Tranchés 23/06** (voir §Déjà tranché) : Q-F9-1 (DB), Q-F1-1 (signature trajectoire), Q-F2-1 + Q-F14-1 (rejeu intelligent = prérequis). **Restent** : Q-F2-2 (point d'entrée Léa — Claude trace en read-only), et les items **parkés** : Q-F8-* (natif/sécu mis de côté momentanément), Q-F11-2 (VPN).
|
||||
|
||||
### F2 — Rejeu intelligent (le plus urgent)
|
||||
|
||||
**Q-F2-1 — Principe « rejeu intelligent ».** Valides-tu que le rejeu **doit consulter le fonds appris** (TargetMemoryStore + FAISS anchors) au lieu de rejouer des coordonnées figées ? *(Vérif runtime 23/06 : aujourd'hui le rejeu est « brut », il ne consulte rien.)* C'est la décision qui change l'archi du replay direct (chantiers R2/R3).
|
||||
> ⚠️ **Quasi-entraînée par ta décision F6 = cross-clinique** : un pack fédéré anonymisé n'a ni coords ni templates → la re-résolution visuelle (anchors/FAISS) devient le seul moyen de rejouer ailleurs. Voir Q-F14-1.
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
**Q-F2-2 — Provider Léa au runtime.** Quel modèle/route sert la **résolution** pendant le rejeu ? (impacte l'import auto R1 + la cascade de résolution). Options connues : Qwen3-VL-4B/vLLM (grounder prod), gemma4 (cerveau/lecteur), autre.
|
||||
> **Tracé Claude 23/06** (read-only) : point d'entrée runtime = **agent_chat (5004)** → `SemanticMatcher.find_workflow()` (`agent_chat/app.py:906`) sur `/api/chat`. Sélection sur **fichiers JSON** (`data/workflows/`, `data/training/workflows/`, `…/live_sessions/workflows/`), **pas la DB** → ⚠️ **gap avec Q-F9-1 (DB=vérité)** : la sélection runtime devra migrer sur la DB. **Pas de filtre machine_id** à la sélection. `api_stream` (5005) = présent mais pas le chemin chat actif. Modèle de résolution = Qwen3-VL-4B grounder + gemma4 (bench 13/06).
|
||||
|
||||
### F1 / F9 — Apprentissage & workflows
|
||||
|
||||
**Q-F1-1 — Critère de fusion des workflows** *(tu as dit « on verra à tête reposée »)*. Quand deux sessions apprises sont-elles « le même » workflow (→ create-or-update plutôt que doublon) ? Par signature d'écran de départ ? nom ? séquence d'actions ? application cible ?
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
**Q-F9-1 — Source de vérité workflows + métrique produit.** → **Ma reco ci-dessous (§Reco F9-1)**, à valider ou amender.
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
### F14 — Unification Léa ↔ VWB (anchors)
|
||||
|
||||
**Q-F14-1 — Re-exécutabilité des packs fédérés (décision induite).** Un pack cross-clinique anonymisé n'emporte **ni coordonnées ni templates** (PII) → re-résolution **visuelle** obligatoire au rejeu (anchors/FAISS). ⇒ acte-t-on que **le « rejeu intelligent » (Q-F2-1) devient un prérequis** (pas une option) dès lors que F6 = cross-clinique ?
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
*(Le sous-chantier U-B « propagation des anchors aux substeps compound + ré-import » ne demande aucune décision produit — c'est un fix prêt, à exécuter post-stabilisation sous GO. Idem U-D « asymétrie grounding UI-DETR-1 vs cascade » = à trancher plus tard, sujet ouvert post-démo.)*
|
||||
|
||||
### F8 — Exécution native agentique (zéro-shot)
|
||||
|
||||
*Constat 23/06 : briques présentes et wirées (boucle ORA `/execute/instruction`, planner NL→plan gemma4), mais mode « free » peu mûr, **sans sandbox ni validation humaine**, exécution directe sur l'host.*
|
||||
|
||||
**Q-F8-1 — Périmètre du mode natif.** Desktop/navigateur **généraliste** (« ouvre YouTube ») ou **borné aux apps métier** (Easily…) ? (impacte le risque et le choix moteur)
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
**Q-F8-2 — Surface d'exécution / sandbox (sécurité).** Acte-t-on que le mode free ne tourne **QUE dans un sandbox** (VM/Xvfb + kill-switch + pause humaine), **jamais l'host** — prérequis avant tout élargissement ? (= la décision CUA P1)
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
**Q-F8-3 — Moteur agentique.** Garder ORA + gemma4/Qwen3-VL (100 % local) ou évaluer un agent computer-use dédié (UI-TARS mode agent, Qwen3-VL agent…) ? → un mini-état-de-l'art web peut éclairer (les modèles évoluent vite).
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
**Q-F8-4 — Niveau d'autonomie.** Validation humaine step-by-step au début (Copilot) puis desserrage progressif (cohérent F7 Shadow→Copilot→Autonomous + safety) ?
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
### F11 — Accès distant multi-VPN
|
||||
|
||||
**Q-F11-2 — Abstraction « fiche site → accès ».** Quels **types de site/VPN** anticiper au-delà de Stormshield + WireGuard ? Faut-il coder dès maintenant une abstraction générique « fiche site → méthode d'accès », ou gérer les 2 cas connus d'abord et généraliser plus tard ?
|
||||
> **Réponse Dom :** _____
|
||||
|
||||
---
|
||||
|
||||
## 💡 Reco Claude — Q-F9-1 (source de vérité workflows)
|
||||
|
||||
**Recommandation : la DB (SQLAlchemy/SQLite `workflows.db`) = source de vérité unique ; le JSON = format d'échange/export uniquement (packs fédération, portabilité, import VWB), PAS un 2ᵉ magasin.**
|
||||
|
||||
**Pourquoi la DB :**
|
||||
- Les workflows sont **relationnels** (workflow ↔ steps ↔ substeps ↔ anchors ↔ session/machine) : intégrité référentielle, requêtes, versionnement.
|
||||
- Le **create-or-update à la fusion** (Q-F1-1) = un upsert atomique trivial en SQL, pénible et risqué sur des fichiers JSON.
|
||||
- **Concurrence** : 5 Léa qui apprennent/s'enrôlent en parallèle (test de charge) → JSON + chemins relatifs au cwd = races + fragilité du symlink (DETTE-015 déjà constatée).
|
||||
- **F6 fédération** a justement besoin de DB-comme-vérité + JSON-comme-échange : un `LearningPack` = sérialisation d'une requête DB → pack JSON ; l'import = désérialisation → DB. JSON-comme-vérité **entre en conflit** avec ça.
|
||||
|
||||
**Nuance de timing :** la **migration** JSON→DB est un refactor persistant → **séquencée post-clinique** (après stabilisation + merge), comme le prévoit déjà `PLAN_MIGRATION_WORKFLOWS_STORE`. D'ici là le symlink reste, dette connue, **on n'y touche pas pendant la livraison**.
|
||||
|
||||
**Métrique produit (24/79/37) :** la seule défendable face à un client/DSI = **les workflows rejouables validés** (≈ les **24** VWB aujourd'hui, ce que Léa exécute vraiment de bout en bout). Les **79** (catalogue agent-chat) et **37** (KB/FAISS) = compteurs **internes**, à ne pas exposer comme « N workflows ». Une fois la DB source de vérité : métrique = `count(workflows WHERE statut = rejouable_validé)`.
|
||||
44
docs/DESIGN_ANONYMISATION_TOKENS_TYPES_2026-06-28.md
Normal file
44
docs/DESIGN_ANONYMISATION_TOKENS_TYPES_2026-06-28.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Design — Anonymisation par tokens typés (= apprentissage des variables)
|
||||
|
||||
- `Date`: 2026-06-28
|
||||
- `Auteur`: Claude (idée Dom)
|
||||
- `Statut`: design actif
|
||||
- `Origine`: PII patient en clair confirmée en production clinique (titres de fenêtre + **contenu médical des `text_input`**). Idée Dom : remplacer la PII par des **tokens typés** plutôt que flouter.
|
||||
|
||||
## Principe directeur (Dom 28/06)
|
||||
|
||||
**Léa apprend l'INTERFACE, pas la DONNÉE.** Ce qui compte pour l'apprentissage, c'est **où sont les champs, leur type, l'enchaînement** — **pas leur contenu**. Après apprentissage, Léa devra :
|
||||
- **saisir des données** dans les bons champs (contenu fourni au runtime par un agent / une extraction),
|
||||
- **lire les écrans pour extraire des données** que l'« agent » traitera.
|
||||
|
||||
→ Le contenu capturé (texte saisi, valeurs OCR, nom patient dans un titre) est une **variable**, pas une constante à mémoriser. On le remplace par un **token typé** : on **anonymise** ET on **apprend la carte des variables** d'un seul geste. Flouter détruit l'info ; tokeniser la préserve **structurellement** sans la valeur.
|
||||
|
||||
Conforme au principe d'anonymisation déjà acté (`feedback_anonymisation_stricte` : remplacer uniquement les identités, ne jamais réécrire le clinique) — ici on va plus loin : **le contenu d'un champ saisi n'est même pas nécessaire**, on garde le champ, pas la valeur.
|
||||
|
||||
## Modèle d'anonymisation par type de capture
|
||||
|
||||
| Capture | Ce qu'on garde (interface) | Ce qu'on retire (donnée) | Comment |
|
||||
|---|---|---|---|
|
||||
| **`text_input`** (texte tapé) | le **fait** qu'un champ texte a été saisi (+ éventuellement nb de caractères) | **tout le contenu** (diagnostics, notes médicales = données de santé) | **`[SAISIE]`** (option **b**, décision Dom). Pas de NER nécessaire — on ne stocke simplement pas le contenu. **Résout la fuite la plus grave.** |
|
||||
| **`active_window_title`** | l'**app/écran** (« GXD5 Pacs », « Expert Santé », « Firefox ») = contexte d'apprentissage | l'**identité patient** (nom, IPP, âge) | tokens typés `[NOM_1]`/`[IPP_1]`/`[AGE_1]` via **couche 1 (regex+structurel)** + **couche 2 (NER)** pour les noms libres |
|
||||
| **OCR / lecture d'écran** | les **zones/champs** d'où extraire | les **valeurs** extraites (PII) | token typé de la zone (« extraire un `[NOM]` ici »), valeur réinjectée au runtime, non persistée |
|
||||
|
||||
## Architecture (2 couches + intégration)
|
||||
|
||||
- **Couche 1 — regex + structurel (FAIT, `agent_v0/server_v1/pii_sanitizer.py`)** : tokens typés cohérents, **sans modèle**, déployable partout. Capte IPP/NIR/TEL/EMAIL/AGE + noms format clinique (`NOM (NAISSANCE) Prénom`, `[Nom Prénom]` PACS) + blacklist logiciels. Couvre 5/7 patients du jour, **tous les IPP**. 5 tests verts.
|
||||
- **Couche 2 — NER CamemBERT-bio (à vendorer)** : moteur `CamembertNerManager` du projet `~/ai/anonymisation` (ONNX **CPU**, ~9-12 ms/titre, labels typés PER/IPP/AGE/DATE/HOPITAL…). Pour les **noms libres** que la couche 1 rate (« Prénom NOM — Firefox »). Modèle **421 Mo → côté DGX** (postes cliniques trop légers — contrainte Dom). Lazy, optionnel.
|
||||
- **Intégration** : une fonction `sanitize_event(event, mapping)` au **point de persistance serveur** : `text_input` → `[SAISIE]` ; titres → `anonymize_text` (couche 1 + 2) ; cohérence par **mapping de session** (même entité → même token).
|
||||
|
||||
## Placement & déploiement
|
||||
|
||||
- **Côté serveur (DGX)** : les events y remontent déjà ; le client reste léger. (Cible privacy-by-design = supprimer le contenu **au plus près du poste** avant stream — évolution, quand le client pourra être modifié.)
|
||||
- **Déploiement GATED** : ne pas redémarrer le serveur DGX pendant des sessions live.
|
||||
- **Ne casse pas l'apprentissage** : `workflow_trajectory_signature` tokenise déjà la PII pour le discriminant ; les tokens typés **renforcent** la carte des variables.
|
||||
|
||||
## Décisions Dom
|
||||
- ✅ **Option (b)** pour `text_input` : placeholder `[SAISIE]`, on ne garde pas le contenu (28/06).
|
||||
- ⏳ **Donnée déjà capturée** (9 patients, 6 IPP, contenu médical) : assainir a posteriori vs **purger** — avec Amina (reco Claude : purger les sessions du jour une fois le fix en place).
|
||||
|
||||
## Connexe
|
||||
- **Config-remontée** des specs postes (CPU/RAM/GPU/OS) pour cibler + fournir des prérequis (`screen_metadata` en remonte déjà une partie).
|
||||
- Réutilise : `~/ai/anonymisation` (`camembert_ner_manager.py`, gazetteers INSEE, blacklists, regex, PLACEHOLDERS).
|
||||
193
docs/DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md
Normal file
193
docs/DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# DESIGN — MAJ silencieuse du client Léa + déploiement CANARY (DETTE-022 v2)
|
||||
|
||||
Date : 2026-07-01
|
||||
Branche : `feat/push-log-dgx`
|
||||
Statut : **premier draft fonctionnel — GATED OFF partout, aucun swap réel, revue supervisée Dom requise avant toute activation**
|
||||
|
||||
> ⚠️ RIEN N'A ÉTÉ DÉPLOYÉ. Aucun SSH poste, aucune action fleet. Ce document +
|
||||
> le code de la branche sont un livrable de conception/implémentation pour revue.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problème
|
||||
|
||||
Pousser des correctifs au client Léa sur ~19 postes cliniques live (Wallerstein)
|
||||
**sans** patch manuel DSI et **sans** déranger les TIM en plein travail. Contrainte
|
||||
absolue : une MAJ ratée peut **briquer toute la flotte**. Le mécanisme doit donc
|
||||
être **conservateur** : canary lent + rollback béton plutôt que rapide et risqué.
|
||||
|
||||
## 2. État de départ (stub commit `813b33b47`) — ce qui existait déjà
|
||||
|
||||
Le noyau était plus avancé qu'un simple squelette. Déjà présent et **testé (vert)** :
|
||||
|
||||
| Brique | Fichier | Rôle |
|
||||
|---|---|---|
|
||||
| Décision serveur PURE | `agent_v0/server_v1/update_check.py` | `parse_version`/`is_newer` (semver correct : `1.0.2 < 1.0.10`), `decide_update()`, `build_download_url()` |
|
||||
| Endpoint serveur gated | `agent_v0/server_v1/api_stream.py:7843+` | `GET /api/v1/agents/update/check` — **503 si `RPA_AUTO_UPDATE_SERVER_ENABLED` OFF**, Bearer requis |
|
||||
| Noyau client PUR | `agent_v0/agent_v1/network/updater.py` | `auto_update_enabled()` (flag `RPA_AUTO_UPDATE_ENABLED`, défaut OFF), `should_update()` (double garde anti-downgrade), `download_update()` (staging + SHA256, ne touche jamais les fichiers vivants) |
|
||||
| **Stubs dangereux (no-op)** | `updater.py:246+` | `apply_update()` / `write_boot_ok_marker()` — **réservés révision humaine** (swap fichiers, édition `Lea.bat`, restart) |
|
||||
| Version agent | `agent_v0/agent_v1/config.py:30` | `AGENT_VERSION = os.environ.get("RPA_AGENT_VERSION", "1.0.1")` (amorcé `105ade959`) |
|
||||
| Tests | `tests/unit/test_update_check_server.py`, `tests/unit/test_agent_v1_updater.py`, `tests/integration/test_update_check_endpoint.py` | R2/R3 verts |
|
||||
|
||||
### Ce qui MANQUAIT (comblé par ce draft)
|
||||
|
||||
1. **Aucune logique canary** : `decide_update` recevait `machine_id` mais l'ignorait pour choisir la version. La version cible était une seule var globale `RPA_AGENT_LATEST_VERSION` → une MAJ partait sur **toute** la flotte d'un coup. **C'est le trou de sécurité n°1.**
|
||||
2. **Le noyau client n'était pas wiré** : `updater.py` n'était appelé nulle part. `main.py` ne l'importait pas. Aucun caller HTTP de `/agents/update/check`.
|
||||
3. **Pas d'orchestrateur** reliant check → décide → download (staging) côté client.
|
||||
|
||||
## 3. Fleet / versioning existant (réutilisé, pas réinventé)
|
||||
|
||||
- Registre SQLite `enrolled_agents` (`agent_v0/server_v1/agent_registry.py:105`) : colonne `version` + `last_seen_at` par `machine_id`. Le dashboard Fleet (`web_dashboard/templates/index.html:2247`) affiche déjà la version par poste.
|
||||
- **Limite connue** : `version` n'est écrite qu'à l'`enroll` (installateur), pas rafraîchie par le heartbeat runtime. Le serveur connaît donc la version *installée*, pas forcément la *version vive*. → **inventaire de version = amélioration future** (voir §8), non bloquante pour le canary (le canary est piloté par une allow-list de `machine_id`, pas par l'inventaire).
|
||||
|
||||
## 4. Design retenu (et pourquoi)
|
||||
|
||||
Aligné sur l'état de l'art self-update desktop 2025 (canary / blue-green / A-B swap + watchdog rollback + intégrité + version) — sources en fin de doc.
|
||||
|
||||
### 4.1 CANARY côté serveur — la keystone de sécurité (IMPLÉMENTÉ)
|
||||
|
||||
Nouveau module PUR `agent_v0/server_v1/update_policy.py`. Il résout la version cible
|
||||
**PAR MACHINE** :
|
||||
|
||||
- poste dans l'allow-list canary → `canary_version` (la nouvelle) ;
|
||||
- tous les autres postes → `stable_version` (le floor, inchangé).
|
||||
|
||||
Piloté 100 % par **variables d'environnement serveur** (aucun rebuild, aucune
|
||||
DSI) :
|
||||
|
||||
```
|
||||
RPA_AGENT_STABLE_VERSION # version servie à TOUTE la flotte (défaut 1.0.1)
|
||||
RPA_AGENT_CANARY_VERSION # version servie AUX SEULS postes canary (optionnel)
|
||||
RPA_AGENT_CANARY_MACHINES # allow-list CSV des machine_id canary
|
||||
```
|
||||
|
||||
Garde-fous du résolveur (tous prudents par défaut) :
|
||||
- machine_id absent / liste vide / pas de `canary_version` → **stable** ;
|
||||
- `canary_version` doit être **strictement plus récente** que `stable` (sinon on sert stable — jamais de recul) ;
|
||||
- ne lève jamais ; version illisible → retombe sur stable via le comparateur semver tolérant.
|
||||
|
||||
Wiring : `_latest_agent_version(machine_id)` dans `api_stream.py` appelle
|
||||
`resolve_target_version_from_env(machine_id)`. **Rétrocompat** : si l'ancienne
|
||||
`RPA_AGENT_LATEST_VERSION` est positionnée, elle prime (pas de régression d'un
|
||||
déploiement existant).
|
||||
|
||||
**Effet** : la 1.0.2 ne peut PAS fuiter hors de la liste canary. Blast radius =
|
||||
la liste. On démarre la liste = `lea-4zbgwxty` (Émilie) seul.
|
||||
|
||||
**Promotion** = quand le canary est validé : `RPA_AGENT_STABLE_VERSION=<canary>`
|
||||
+ vider `RPA_AGENT_CANARY_MACHINES` → toute la flotte suit.
|
||||
**Rollback canary** = vider `RPA_AGENT_CANARY_MACHINES` / remettre l'ancienne
|
||||
`RPA_AGENT_CANARY_VERSION` → le prochain check ne propose plus rien.
|
||||
|
||||
### 4.2 Orchestrateur client (IMPLÉMENTÉ, GATED, sans swap)
|
||||
|
||||
`updater.run_update_cycle(local_version, machine_id, staging_dir, checker?, downloader?)` :
|
||||
|
||||
1. **GATE** `auto_update_enabled()` (`RPA_AUTO_UPDATE_ENABLED`, défaut OFF) — si OFF, ne fait **strictement rien**, aucun appel réseau ;
|
||||
2. `checker(...)` → réponse serveur (défaut = `_default_update_checker` : GET vers l'endpoint gated, Bearer, 503→None, jamais d'exception) ;
|
||||
3. `should_update(...)` → plan (double garde semver anti-downgrade) ;
|
||||
4. `download_update(...)` → ZIP en **staging** + vérif **SHA256** (fichiers vivants jamais touchés) ;
|
||||
5. `apply_update(staged)` = **stub no-op** → résultat `applied: False`. **Le swap réel n'est PAS fait par du code d'agent.**
|
||||
|
||||
Statuts retournés (diagnostic/log) : `disabled | check_failed | up_to_date | download_failed | staged`. Best-effort total : aucune exception ne remonte (ne casse jamais Léa).
|
||||
|
||||
### 4.3 Wiring runtime (IMPLÉMENTÉ, GATED)
|
||||
|
||||
`main.py` : thread daemon `_auto_update_loop`, démarré **uniquement si**
|
||||
`AUTO_UPDATE_ENABLED`, à côté des boucles permanentes existantes (même pattern
|
||||
que le log shipper). Sécurité « **au bon moment** » : on ne stage PAS pendant un
|
||||
enregistrement (`self.session_id`) ou un replay actif (`self._replay_active`) —
|
||||
pas de perturbation du travail TIM. Intervalle `RPA_AUTO_UPDATE_INTERVAL_S`
|
||||
(défaut **3600 s / 1 h** : une MAJ n'est jamais urgente).
|
||||
|
||||
### 4.4 Intégrité + version
|
||||
|
||||
- **Intégrité** : SHA256 vérifié dans `download_update` (déjà présent) ; mismatch → rejet + staging propre.
|
||||
- **Version** : `AGENT_VERSION` envoyée à chaque check (`current_version`) ; le serveur choisit la cible par machine.
|
||||
- **Signature (à ajouter, §8)** : SHA256 seul protège de la corruption, pas de l'usurpation. Recommandation : signer le manifeste (le SHA256 vient d'un canal authentifié — l'endpoint Bearer — donc chaîne acceptable pour le POC ; signature détachée = durcissement futur).
|
||||
|
||||
### 4.5 Swap atomique + rollback (SPEC — réservé révision humaine, PAS codé par agent)
|
||||
|
||||
Le swap réel reste dans les stubs `apply_update` / `write_boot_ok_marker` et
|
||||
dans `Lea.bat`. **Un agent ne doit pas écrire de code qui écrase des binaires
|
||||
vivants ni relance un process.** Spec cible pour la revue humaine :
|
||||
|
||||
- **A-B / staging** : le ZIP est extrait dans `Lea_next\`. Au **prochain démarrage**, `Lea.bat` (hors-process) : backup `Lea\`→`Lea_prev\`, swap `Lea_next\`→`Lea\`, lance la nouvelle version.
|
||||
- **Watchdog rollback** : la nouvelle version doit écrire un marker `boot_ok_<version>` **après** ~60 s de heartbeat DGX sain + session OK. Si `Lea.bat` ne trouve pas le marker au démarrage suivant (crash au boot), il restaure `Lea_prev\` automatiquement. Cible « rollback latency » < 90 s (état de l'art).
|
||||
- **Cas edge** (documenté dans les stubs) : DGX down ≠ Léa N+1 buguée — le health-check doit distinguer les deux pour éviter un faux rollback.
|
||||
|
||||
## 5. Fichiers touchés (cette branche)
|
||||
|
||||
**Ajouts**
|
||||
- `agent_v0/server_v1/update_policy.py` — canary PUR (résolveur par machine + lecture env).
|
||||
- `tests/unit/test_update_policy_canary.py` — TDD canary (résolveur + env).
|
||||
|
||||
**Modifs**
|
||||
- `agent_v0/server_v1/api_stream.py` — `_latest_agent_version(machine_id)` canary-aware (rétrocompat legacy) + docstring endpoint.
|
||||
- `agent_v0/agent_v1/network/updater.py` — `_default_update_checker()` + `run_update_cycle()` (orchestrateur gated, sans swap).
|
||||
- `agent_v0/agent_v1/config.py` — `AUTO_UPDATE_INTERVAL_S`, `AUTO_UPDATE_STAGING_DIR`.
|
||||
- `agent_v0/agent_v1/main.py` — thread `_auto_update_loop` gated + import config.
|
||||
- `tests/unit/test_agent_v1_updater.py` — TDD `run_update_cycle` (gate off, up-to-date, staged, sha mismatch, checker raise).
|
||||
- `tests/integration/test_update_check_endpoint.py` — TDD canary HTTP (poste canary vs hors-canary).
|
||||
- `deploy/lea_package/config.txt` — flags client MAJ documentés (commentés, OFF).
|
||||
|
||||
**Intacts (réservés révision humaine)** : `updater.apply_update`, `updater.write_boot_ok_marker`, `Lea.bat`.
|
||||
|
||||
## 6. Matrice des flags (tout OFF par défaut)
|
||||
|
||||
| Flag | Côté | Défaut | Effet |
|
||||
|---|---|---|---|
|
||||
| `RPA_AUTO_UPDATE_SERVER_ENABLED` | serveur | OFF (503) | active l'endpoint de décision |
|
||||
| `RPA_AGENT_STABLE_VERSION` | serveur | `1.0.1` | version floor de toute la flotte |
|
||||
| `RPA_AGENT_CANARY_VERSION` | serveur | — | nouvelle version, postes canary seulement |
|
||||
| `RPA_AGENT_CANARY_MACHINES` | serveur | — | allow-list CSV canary |
|
||||
| `RPA_AGENT_LATEST_VERSION` (legacy) | serveur | — | si set, prime sur le canary (rétrocompat) |
|
||||
| `RPA_AUTO_UPDATE_ENABLED` | client | OFF | active la boucle de check + staging |
|
||||
| `RPA_AUTO_UPDATE_INTERVAL_S` | client | `3600` | intervalle de check |
|
||||
|
||||
## 7. Plan de déploiement CANARY (étapes + critères GO / ROLLBACK)
|
||||
|
||||
> Prérequis avant TOUTE étape : la mécanique de **swap réel** (§4.5) doit avoir
|
||||
> été implémentée et revue par un humain. Tant qu'elle est en stub, ce plan ne
|
||||
> fait que **stager** un ZIP (aucun poste ne change réellement de version) — ce
|
||||
> qui est déjà utile pour valider la chaîne check/download/intégrité à vide.
|
||||
|
||||
**Étape 0 — Serveur seul (aucun poste touché)**
|
||||
- Action : `RPA_AUTO_UPDATE_SERVER_ENABLED=true`, `RPA_AGENT_STABLE_VERSION=1.0.1`, PAS de canary encore.
|
||||
- GO si : `GET /agents/update/check` répond 200 pour un `machine_id` quelconque avec `update_available:false`. Aucun poste n'a la MAJ activée côté client.
|
||||
- ROLLBACK : repasser le flag serveur OFF.
|
||||
|
||||
**Étape 1 — Canary Émilie, staging seul**
|
||||
- Action serveur : `RPA_AGENT_CANARY_VERSION=<nouvelle>`, `RPA_AGENT_CANARY_MACHINES=lea-4zbgwxty`.
|
||||
- Action poste Émilie (config.txt) : `RPA_AUTO_UPDATE_ENABLED=true`.
|
||||
- GO si : dans les logs d'Émilie (remontés par le push-log DGX), `[UPDATE] MAJ <v> téléchargée en staging (SHA256=True)`, ZIP présent dans le staging, `applied:False`, Léa continue de tourner normalement (session/replay non perturbés). Vérifier qu'AUCUN autre poste ne reçoit `update_available:true`.
|
||||
- ROLLBACK : vider `RPA_AGENT_CANARY_MACHINES` (le check ne propose plus rien). Aucun impact : rien n'avait été appliqué.
|
||||
|
||||
**Étape 2 — Canary Émilie, swap réel (après implémentation humaine du §4.5)**
|
||||
- GO si : après redémarrage, Émilie tourne la nouvelle version (`AGENT_VERSION` remontée), marker `boot_ok` écrit, heartbeat DGX sain > 24 h, zéro régression fonctionnelle (enregistrement + replay OK).
|
||||
- ROLLBACK : automatique par watchdog `Lea.bat` si pas de `boot_ok` au boot ; manuel = restaurer `Lea_prev\` + vider la liste canary.
|
||||
|
||||
**Étape 3 — Élargissement progressif (rings)**
|
||||
- Ajouter 2-3 postes à `RPA_AGENT_CANARY_MACHINES`, attendre 48 h par palier.
|
||||
- GO/ROLLBACK : mêmes critères qu'étape 2, par palier.
|
||||
|
||||
**Étape 4 — Promotion générale**
|
||||
- `RPA_AGENT_STABLE_VERSION=<nouvelle>` + vider `RPA_AGENT_CANARY_MACHINES`.
|
||||
- Toute la flotte converge au rythme de son intervalle de check.
|
||||
- ROLLBACK flotte : remettre `RPA_AGENT_STABLE_VERSION` à l'ancienne (les postes ne redescendent pas seuls — le swap-down reste une opération supervisée ; les nouveaux checks ne proposeront plus la MAJ).
|
||||
|
||||
## 8. Améliorations futures (hors périmètre de ce draft)
|
||||
|
||||
1. **Swap réel + watchdog rollback** (§4.5) — la brique manquante n°1, révision humaine.
|
||||
2. **Inventaire de version vive** : rafraîchir `enrolled_agents.version` au heartbeat (le serveur saurait exactement quelle version tourne où — utile pour piloter le canary depuis le dashboard).
|
||||
3. **Signature détachée** du manifeste (durcissement au-delà du SHA256 sur canal Bearer).
|
||||
4. **Endpoint de download versionné** : aujourd'hui `/api/fleet/download/<machine_id>` (dashboard) sert l'installateur complet et **ignore `?type=&version=`** ; il faudra qu'il serve le vrai payload `code-only` incrémental attendu par le contrat d'URL.
|
||||
5. **Auto-report du résultat de swap** (succès/rollback) au serveur pour un tableau de bord canary.
|
||||
|
||||
## 9. Sources (état de l'art self-update desktop / canary 2025)
|
||||
|
||||
- [Rollback Strategies for Enterprise: 2025 Best Practices — sparkco.ai](https://sparkco.ai/blog/rollback-strategies-for-enterprise-2025-best-practices)
|
||||
- [Canary Deployment with Auto-Rollback for AI Agents — antigravitylab.net](https://antigravitylab.net/en/articles/agents/antigravity-ai-agent-canary-deployment-burn-rate-slo)
|
||||
- [awesome-agentic-patterns — canary rollout & automatic rollback](https://github.com/nibzard/awesome-agentic-patterns/blob/main/patterns/canary-rollout-and-automatic-rollback-for-agent-policy-changes.md)
|
||||
- [What is Canary Testing — aqua-cloud.io](https://aqua-cloud.io/canary-testing/)
|
||||
- [Rollback Automation Best Practices for CI/CD — hokstadconsulting.com](https://hokstadconsulting.com/blog/rollback-automation-best-practices-for-ci-cd)
|
||||
218
docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md
Normal file
218
docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Design Note — NavigateCoords Consumption Gap (Write-Only)
|
||||
|
||||
**Auteur**: Qwen
|
||||
**Date**: 2026-07-02
|
||||
**Statut**: DESIGN NOTE — pas de câblage sans GO Dom
|
||||
**Référence**: `tests/unit/test_coords_consumption_gap.py` (10 tests PASSING documenting the gap)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Le module navigation (`core/navigation`) produit des coords normalisés (`NavigateCoords`) via OCR/VLM, les stocke dans `replay_state["variables"]`, mais **aucun consommateur** dans le runtime n'utilise ces coords. Le résultat est un pattern **write-only** : coords générés mais jamais consommés par les actions suivantes (click/type).
|
||||
|
||||
Trois gaps structurels confirmés par code lecture :
|
||||
|
||||
---
|
||||
|
||||
## Gap A — Compiler Produces Literals, Not Templates
|
||||
|
||||
**Localisation**: `replay_engine.py:1832-1846` (`_edge_to_normalized_actions`)
|
||||
|
||||
**Problème**: Pour `mouse_click`, le compiler bake `x_pct` et `y_pct` comme **floats littéraux** depuis `by_position` :
|
||||
|
||||
```python
|
||||
# replay_engine.py:1843-1846
|
||||
normalized["type"] = "click"
|
||||
normalized["x_pct"] = x_pct # float littéral (ex: 0.15)
|
||||
normalized["y_pct"] = y_pct # float littéral (ex: 0.07)
|
||||
```
|
||||
|
||||
Ces floats sont **hardcodés** dans le step definition. Il n'existe pas de mécanisme pour référencer les coords navigate via templates comme `{{navigate_login_coords.x_pct}}`.
|
||||
|
||||
**La substitution existante ne couvre pas ce cas** :
|
||||
- `_substitute_variables()` → `${var}` → appliqué uniquement à `text_input.text`
|
||||
- `_RUNTIME_VAR_PATTERN` → `{{var.field}}` → compilé regex, **jamais appliqué à `x_pct/y_pct`**
|
||||
|
||||
**Conséquence**: Un navigate step qui résolve coords login à (0.15, 0.07) ne peut PAS injecter ces coords dans un click step suivant, car le click step a ses propres `x_pct/y_pct` hardcodés.
|
||||
|
||||
---
|
||||
|
||||
## Gap B — Zero Consumers in Runtime
|
||||
|
||||
**Localisation**: `core/navigation/__init__.py:43-113` (`_handle_navigate_action`)
|
||||
|
||||
**Problème**: `_handle_navigate_action` stocke coords dans `replay_state["variables"]` :
|
||||
|
||||
```python
|
||||
# core/navigation/__init__.py:100-105
|
||||
if result.login_coords:
|
||||
variables[login_var] = result.login_coords.to_dict()
|
||||
# → {"x_pct": 0.15, "y_pct": 0.07, "method": "ocr_anchor"}
|
||||
```
|
||||
|
||||
**Zéro consommateur** : aucun action handler (click, type, double_click, right_click) lit `variables["navigate_login_coords"]` pour résoudre ses propres coords. Chaque action utilise exclusivement `by_position` depuis son edge definition.
|
||||
|
||||
**Preuve par grep** : `navigate_login_coords|navigate_password_coords|navigate_submit_coords` apparaît uniquement dans :
|
||||
- `core/navigation/__init__.py` (write)
|
||||
- `tests/unit/test_*.py` (test verification)
|
||||
- **0 occurrences** dans `replay_engine.py` action dispatch ou `api_stream.py` action handlers
|
||||
|
||||
---
|
||||
|
||||
## Gap C — Navigate Edge → Empty Actions List
|
||||
|
||||
**Localisation**: `replay_engine.py:1806-1955` (`_edge_to_normalized_actions`)
|
||||
|
||||
**Problème**: Le type `navigate` est dans `_ALLOWED_ACTION_TYPES` (ligne 44) et possède un handler câblé dans `api_stream.py` (ligne 4459-4463 via `_handle_navigate_action`). Mais `_edge_to_normalized_actions` **n'a pas de branche** pour `navigate` :
|
||||
|
||||
```python
|
||||
# replay_engine.py:1954-1955 (else branch)
|
||||
else:
|
||||
logger.warning(f"Type d'action inconnu : {action_type}")
|
||||
return []
|
||||
```
|
||||
|
||||
**Conséquence** : Quand le BFS traverse un edge navigate, `_edge_to_normalized_actions(edge, params)` retourne `[]`. L'action navigate est **skippée** dans le path. Le handler existe dans `api_stream.py` mais est **inaccessible** car le normalized action dict n'est jamais produit.
|
||||
|
||||
**Paradoxe** : Le navigate handler est câblé et fonctionnel, mais le pipeline edge→action le bloque à l'entrée.
|
||||
|
||||
---
|
||||
|
||||
## Options de Résolution
|
||||
|
||||
### Option 1 — Compiler Injection (modifier `_edge_to_normalized_actions`)
|
||||
|
||||
**Approche**: Ajouter une branche `navigate` dans `_edge_to_normalized_actions` qui produit un normalized action dict. Modifier les actions click/type pour permettre des template refs `{{navigate_login_coords.x_pct}}` dans `x_pct/y_pct`, avec résolution runtime.
|
||||
|
||||
```python
|
||||
# Option 1 — Branch navigate dans _edge_to_normalized_actions
|
||||
elif action_type == "navigate":
|
||||
normalized["type"] = "navigate"
|
||||
normalized["parameters"] = {
|
||||
"action": action_params.get("action", "login"),
|
||||
"login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"),
|
||||
"password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"),
|
||||
"submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"),
|
||||
}
|
||||
return [normalized]
|
||||
```
|
||||
|
||||
**+ Avantages** :
|
||||
- Minimal change — 1 branche ajoutée + template resolution dans click/type
|
||||
- Compatible avec handler existant (`_handle_navigate_action`)
|
||||
- BFS path inclut navigate → handler appelé → coords stockés → consommés
|
||||
|
||||
**– Risques** :
|
||||
- Template resolution dans `x_pct/y_pct` nécessite modification de click/type dispatch
|
||||
- Float vs string : `{{navigate_login_coords.x_pct}}` résout en `"0.15"` (string), pas `0.15` (float) — nécessite conversion
|
||||
- Ordonnancement : navigate doit s'exécuter AVANT les actions click/type qui consomment ses coords — scheduling implication
|
||||
|
||||
### Option 2 — Declarative YAML Templates (step definitions avec coords_template)
|
||||
|
||||
**Approche**: Ajouter un champ `coords_template` dans les step YAML definitions. Au runtime, le template est résolu par substitution des variables navigate.
|
||||
|
||||
```yaml
|
||||
# Option 2 — YAML step definition avec coords_template
|
||||
steps:
|
||||
- action: navigate
|
||||
parameters:
|
||||
action: login
|
||||
login_coords_var: navigate_login_coords
|
||||
- action: mouse_click
|
||||
coords_template: "{{navigate_login_coords}}"
|
||||
# Au runtime : x_pct/y_pct résolus depuis navigate_login_coords dict
|
||||
```
|
||||
|
||||
**+ Avantages** :
|
||||
- Déclaratif — coords templates dans YAML, pas hardcoded
|
||||
- Séparation compiler/runtime : compiler produit templates, runtime résout
|
||||
- Extensible à autres types de coords (search, dossier)
|
||||
|
||||
**– Risques** :
|
||||
- Plus de changement : schema YAML + template resolver + compiler modifications
|
||||
- Retro-compatibilité : workflows existants sans coords_template doivent continuer à fonctionner (fallback by_position)
|
||||
- Validation : templates malformés → runtime errors subtiles
|
||||
|
||||
---
|
||||
|
||||
## Table Comparative
|
||||
|
||||
| Critère | Option 1 (Compiler Injection) | Option 2 (YAML Templates) |
|
||||
|---------|-------------------------------|---------------------------|
|
||||
| Changement code | Small — 1 branch + template resolve | Medium — schema + resolver + compiler |
|
||||
| Retro-compat | Full — by_position fallback intact | Full — fallback by_position si pas de template |
|
||||
| Ordonnancement | Navigate avant click (BFS order) | Navigate avant click (step order) |
|
||||
| Extensibilité | Navigate-specific | General — coords_template applicable à tout |
|
||||
| Risque runtime | Float/string conversion | Template validation errors |
|
||||
| Tests impact | 1-3 nouveaux tests | 5-8 nouveaux tests (schema + resolver) |
|
||||
| GO Dom needed | YES | YES |
|
||||
| Timeline | ~2h implementation | ~4h implementation + schema design |
|
||||
|
||||
---
|
||||
|
||||
## Test Rouge Proposal
|
||||
|
||||
**Objectif**: Démontrer Gap C avec 1 test unitaire qui montre qu'un edge navigate produit une empty action list.
|
||||
|
||||
```python
|
||||
# tests/unit/test_coords_consumption_gap.py — ajout proposé
|
||||
|
||||
def test_gap_c_navigate_edge_produces_empty_actions():
|
||||
"""Gap C: _edge_to_normalized_actions returns [] for navigate edge.
|
||||
|
||||
Prove: navigate is in _ALLOWED_ACTION_TYPES but has no branch
|
||||
in _edge_to_normalized_actions → falls into else → empty list.
|
||||
"""
|
||||
from agent_v0.server_v1.replay_engine import _edge_to_normalized_actions
|
||||
|
||||
# Minimal mock edge with navigate action type
|
||||
edge = MockEdge(
|
||||
edge_id="e1",
|
||||
from_node="start",
|
||||
to_node="login",
|
||||
action=MockAction(
|
||||
type="navigate",
|
||||
target=None,
|
||||
parameters={"action": "login"},
|
||||
),
|
||||
)
|
||||
result = _edge_to_normalized_actions(edge, {})
|
||||
|
||||
# GAP: navigate edge produces zero actions
|
||||
assert result == [], f"Expected empty list, got {result}"
|
||||
# This proves the handler in api_stream.py is unreachable
|
||||
```
|
||||
|
||||
**Note**: Ce test est un **red flag** — il doit FAIL quand le gap est résolu (navigate branch ajoutée → result ≠ []). Il sert de guardrail : si quelqu'un câble navigate sans résoudre les gaps A+B, le test rouge continue à signaler le problème.
|
||||
|
||||
---
|
||||
|
||||
## Decision Required from Dom
|
||||
|
||||
**⚠️ PAS DE CÂBLAGE SANS GO DOM**
|
||||
|
||||
Ce design note documente les gaps et propose des options. La décision appartient à Dom :
|
||||
|
||||
1. **Option préférée** : 1 (compiler injection) ou 2 (YAML templates) ?
|
||||
2. **Timeline** : implémenter maintenant (POC phase) ou post-POC ?
|
||||
3. **Scope** : navigate login only, ou general coords template system ?
|
||||
4. **Test rouge** : ajouter le test gap C maintenant (documentation) ou attendre GO ?
|
||||
|
||||
---
|
||||
|
||||
## Appendix — Code References
|
||||
|
||||
| Fichier | Lignes | Rôle |
|
||||
|---------|--------|------|
|
||||
| `replay_engine.py:44` | `_ALLOWED_ACTION_TYPES` includes "navigate" | Allowlist |
|
||||
| `replay_engine.py:1806-1955` | `_edge_to_normalized_actions` — no navigate branch | Gap C |
|
||||
| `replay_engine.py:1843-1846` | mouse_click bakes literal x_pct/y_pct | Gap A |
|
||||
| `core/navigation/__init__.py:43-113` | `_handle_navigate_action` — writes coords to variables | Gap B (write) |
|
||||
| `core/navigation/action_resolver.py:47-62` | `NavigateCoords` dataclass definition | Data model |
|
||||
| `api_stream.py:4459-4463` | navigate handler dispatch | Wired but unreachable |
|
||||
| `tests/unit/test_coords_consumption_gap.py` | 10 tests documenting write-only gap | Evidence |
|
||||
|
||||
---
|
||||
|
||||
*Qwen — design note, pas wiring. GO Dom required.*
|
||||
59
docs/DESIGN_OVMF_AUTOREPAIR_VM_2026-06-20.md
Normal file
59
docs/DESIGN_OVMF_AUTOREPAIR_VM_2026-06-20.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Design — Auto-réparation OVMF de la VM Léa (gap G2, reprise non-assistée)
|
||||
|
||||
- `Auteur`: Claude (infra)
|
||||
- `Date`: 2026-06-20 ~03:15 CEST
|
||||
- `Statut`: **PROPOSITION / design read-only** — à appliquer après revue Dom (garde-fou : aucun changement service prod sans Dom).
|
||||
- `Référence`: post-mortem `docs/POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20.md` (gap G2).
|
||||
|
||||
## 1. Problème
|
||||
|
||||
Une coupure brutale corrompt `OVMF_VARS.fd` (NVRAM UEFI) → la VM boucle dans TianoCore/Windows Boot Manager. Le 2026-06-20, blocage 02:07→02:18 jusqu'à intervention manuelle de Codex (restore OVMF connu-bon + TPM frais). En clinique sans technicien, ce blocage serait **permanent**.
|
||||
|
||||
## 2. Pourquoi le service ne s'auto-répare pas aujourd'hui
|
||||
|
||||
`~/.config/systemd/user/win11-arm-lea.service` :
|
||||
- `Restart=on-failure` — **inopérant** : en boucle TianoCore, QEMU **ne sort pas** (process « running » à 99 % CPU). Aucun échec → aucun restart.
|
||||
- `ExecStartPre` efface `tpm2-00.permall` (TPM frais à chaque boot) **mais pas `OVMF_VARS.fd`** → un OVMF corrompu **survit aux restarts** → boucle permanente.
|
||||
|
||||
## 3. Détecteur fiable
|
||||
|
||||
Le **guest agent QEMU** (`windows-11-arm-lea-agent.sock`) ne répond **que** si Windows a réellement booté. En boucle firmware, il ne répond jamais. Signature d'échec = *pas de réponse guest-agent après N min* **+** *CPU QEMU élevé*. (Plus robuste qu'une analyse framebuffer ; v1 suffisante.)
|
||||
|
||||
## 4. Design proposé (2 briques + garde-fous)
|
||||
|
||||
### Brique A — Snapshot « known-good » après boot sain
|
||||
Watchdog compagnon (lancé en `ExecStartPost=... &` ou service apparié `vm-health-watchdog.service`) :
|
||||
1. Fenêtre de boot (0→~6 min), poll guest-agent toutes les 30 s (`guest-ping` via socket agent).
|
||||
2. **Guest-agent répond** → boot sain : copie atomique `OVMF_VARS.fd` → `OVMF_VARS.fd.known-good`, écrit sentinel `boot-ok`, log horodaté. C'est le point de restauration.
|
||||
|
||||
### Brique B — Détection boucle + restauration auto
|
||||
Si à T+6 min le guest-agent **ne répond toujours pas** ET CPU QEMU > 80 % (signature boucle) :
|
||||
1. Écrit sentinel `boot-failed`.
|
||||
2. Archive l'OVMF suspect : `OVMF_VARS.fd` → `OVMF_VARS.fd.failed-<ts>` (convention déjà utilisée par Codex).
|
||||
3. Restaure `OVMF_VARS.fd.known-good` → `OVMF_VARS.fd`.
|
||||
4. Arrête le QEMU en boucle firmware. **Sûr** : aucun OS n'a booté (guest-agent jamais monté) → pas de risque de corruption Windows (≠ règle « jamais kill un Windows booté », qui ne s'applique pas ici).
|
||||
5. systemd relance (`Restart=on-failure` se déclenche enfin) avec le bon OVMF.
|
||||
|
||||
### Garde-fous (anti-mauvais comportement)
|
||||
- **Pas de known-good** (1er boot, jamais eu de boot sain) → ne PAS restaurer ; log + alerte, comportement actuel conservé.
|
||||
- **Compteur d'essais** : max 2 auto-restaurations consécutives (sentinel compteur). Au-delà → stop + alerte (évite la boucle restore→échec→restore si le known-good est lui aussi mauvais).
|
||||
- **Faux positifs** : Windows peut booter lentement → fenêtre 6–8 min + double critère (guest-agent ET CPU). Réglable.
|
||||
- **TPM** : on garde l'effacement `tpm2-00.permall` existant (évite le hang TPM) ; l'auto-réparation OVMF est complémentaire.
|
||||
- **Idempotence** : nettoyage des sentinels en début de cycle.
|
||||
|
||||
## 5. Points d'intégration (à valider Dom avant écriture)
|
||||
|
||||
- `win11-arm-lea.service` : ajouter `ExecStartPre` de garde (si `boot-failed` + known-good → restaurer avant lancement) et `ExecStartPost` qui lance le watchdog.
|
||||
- Nouveau script `vm-health-watchdog.sh` (briques A+B) dans `~/quickemu-win11-arm-lea/`.
|
||||
- Optionnel : `vm-health-watchdog.service` (PartOf=win11-arm-lea.service) plutôt qu'un `&`, pour un cycle de vie propre.
|
||||
|
||||
## 6. Plan de test (sans risque, sur la VM labo)
|
||||
|
||||
1. Boot sain → vérifier création `OVMF_VARS.fd.known-good` + sentinel `boot-ok`.
|
||||
2. Simuler corruption (copier l'`OVMF_VARS.fd.failed-powercut-20260620-021854` archivé sur le live) → vérifier détection à T+6 min, archivage, restauration, restart, boot sain.
|
||||
3. Vérifier le compteur d'essais (corrompre aussi le known-good) → stop + alerte, pas de boucle infinie.
|
||||
4. Mesurer le temps total de reprise auto (cible < 10 min sans intervention).
|
||||
|
||||
## 7. Décision attendue (Dom)
|
||||
|
||||
GO/NO-GO sur l'écriture + le réglage de la fenêtre (6 vs 8 min) et du mécanisme d'alerte (log seul ? message coordination ? mail ?). Application supervisée, un changement / un test, après ton réveil.
|
||||
@@ -38,6 +38,8 @@ P0 / P1 / P2 / P3 (alignées sur convention handoffs)
|
||||
| DETTE-020 | 2026-06-25 | 2026-07-09 | P1 | OPEN | **Incidents silencieux — aucune détection/alerte des composants critiques d'inférence.** Un composant critique peut tomber sans alerte : `rpa-vllm-grounder.service` (grounder Qwen3-VL/vLLM) trouvé en **crash-loop (auto-restart, restart counter ×3960)** → le runtime a basculé **silencieusement** sur le fallback `qwen2.5vl:7b-rpa` (Ollama, ~×7 plus lent), avec une latence/contention accrue mais **aucune remontée visible** (ni dashboard, ni log d'alerte). Découvert uniquement par vérif manuelle au runtime (session 2026-06-25). La cause de CE crash (SSL HuggingFace au boot vs cache local — manque `HF_HUB_OFFLINE`) se corrige à part ; la dette ici = **le mode dégradé est silencieux**. Cible : health-check + supervision des composants critiques (grounder vLLM, Ollama, services `rpa-*`) avec **remontée VISIBLE** (dashboard 5001 / log d'alerte / notification) → une bascule en mode dégradé ne doit jamais passer inaperçue. ⚠️ Vérifier d'abord l'existant (module monitoring `:5003`) avant de construire. | session vérif runtime DGX clinique 2026-06-25 |
|
||||
| DETTE-021 | 2026-06-25 | 2026-07-09 | P1 | OPEN | **Journalisation client Léa non effective.** `LOG_FILE` (`agent_v0/agent_v1/config.py:88` → `<install>/logs/agent_v1.log`) est défini mais **jamais branché** : aucun `FileHandler`/`addHandler` dans tout le client. Seul logging actif = `basicConfig` (`main.py:46`) → **stderr**, perdu car Léa tourne en `pythonw.exe` (sans console). Dossier `logs/` vide. Conséquences : (1) **diagnostic terrain aveugle** — impossible de tracer pourquoi Léa « disparaît » côté poste ; (2) **non-conformité Règlement IA Art. 12** (journalisation + conservation 180 j — citée dans le code mais non effective ; `LOG_RETENTION_DAYS` ne couvre que les *sessions*). Cible : brancher un `RotatingFileHandler`/`TimedRotating` vers `LOG_FILE` (rotation + purge 180 j, niveau INFO). ⚠️ modif client → **redéploiement** (cf. DETTE-022). Pendant client du DETTE-020 (observabilité serveur). | session diagnostic « disparition » Léa poste Émilie 2026-06-25 |
|
||||
| DETTE-022 | 2026-06-25 | 2026-07-09 | P1 | OPEN | **Pas de mise à jour automatique du client Léa.** Toute modif du client (`agent_v0/agent_v1/**`) impose un **redéploiement manuel poste par poste** (Léa « gelée »). En clinique (5 postes, croissant), intervenir sur chaque poste à chaque correctif (ex. fix logging DETTE-021) **dérange les TIM et décourage l'adoption** (constat Dom). Cible : mécanisme de **MAJ auto / en tâche de fond** (auto-update silencieux, versionné, piloté serveur/dashboard, avec rollback), **zéro intervention sur le poste**. ⚠️ Vérifier d'abord l'existant côté enrôlement Fleet (dashboard build ZIP + token) avant de construire. | décision Dom 2026-06-25 (« on ne peut pas intervenir constamment sur les postes, on va décourager ») |
|
||||
| DETTE-023 | 2026-06-30 | 2026-07-14 | P1 | OPEN | **Validation post-action systématique non câblée au replay live.** `core/execution/action_executor.py` expose `verify_postconditions=True` (+ re-vérif/retry, l.187-242) mais le runtime live `replay_engine.py` **n'importe pas `ActionExecutor`** (seulement `LLMActionHandler`, l.2497) → la vérif de post-condition après CHAQUE action est **écrite-non-wired**. Le replay live ne valide qu'à **gros grain** : `precheck` de similarité d'écran avant action (≥ 0.85, replay_engine.py:2844) + `verify_screen` **entre GROUPES** d'actions (l.39), pas après chaque clic. Lié à DETTE-008 (pre-check VLM par-clic désactivé `if False:`, observe_reason_act.py:1704) et DETTE-001 (pré-check OCR spatialement aveugle). **Enjeu produit** (décision Dom 2026-06-30 : « vision = validateur des actions ET de l'apprentissage », pour ZÉRO erreur en récupération de dossiers et scaling multi-VM/postes) : densifier la validation visuelle aux points critiques (login, ouverture dossier, lecture écran→JSON) **ou** rebrancher la vérif post-condition au replay live. ⚠️ Vérifier d'abord l'existant (`verify_screen`, `ActionExecutor`, ORALoop) avant de construire. | session 30/06 trace runtime (replay_engine n'utilise pas ActionExecutor) + décision Dom VM/vision 2026-06-30 |
|
||||
| DETTE-024 | 2026-06-30 | 2026-07-14 | P1 | OPEN | **Le dashboard fleet `/api/fleet/download/<machine_id>` sert un ZIP NON autoportant.** Test 30/06 : le download a renvoyé un ZIP de **210 Ko** (sans `python-3.12-embed`) au lieu du `Lea_full_v1.0.1.zip` (33 Mo) pourtant déposé dans `deploy/build/` → le dashboard lit le **fallback** `deploy/Lea_v1.0.0.zip` (ou un chemin relatif au cwd, cf. DETTE-015) et NON le full. Conséquence : un poste enrôlé via le dashboard recevrait un exe **non installable** (pas de Python embarqué). Contourné manuellement pour Émilie (ZIP full local + `config.txt` du download + flag). Cible : le download doit servir le **full autoportant à jour** (chemin absolu, pas de fallback silencieux). ⚠️ Bloquant pour s'appuyer sur le dashboard au déploiement GPO/multi-postes. | session livraison exe Émilie 2026-06-30 (web_dashboard/app.py:2379) |
|
||||
|
||||
## Convention de référencement
|
||||
|
||||
|
||||
540
docs/INSTALLATION_MULTI_SITE.md
Normal file
540
docs/INSTALLATION_MULTI_SITE.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# Guide d'installation Lea - POC, MVP, production et multi-site
|
||||
|
||||
- Date: 2026-06-19
|
||||
- Statut: version initiale exploitable, a durcir avant production
|
||||
- Scope: installations Lea/RPA Vision V3 sur DGX + postes ou VM Windows
|
||||
- Source: etat POC DGX, runbooks coordination, installeur Windows, systemd, dashboard/VWB/worker
|
||||
|
||||
## Objectif
|
||||
|
||||
Ce document sert de base d'installation reproductible pour plusieurs phases:
|
||||
|
||||
1. **POC**: installation controlee sur un site pilote, avec assistance technique forte.
|
||||
2. **MVP**: installation repetable avec artefacts figes, checklist, rollback et preuves.
|
||||
3. **Production**: installation industrialisee, secrets par site, supervision, sauvegardes, signature de l'installeur, support.
|
||||
4. **Multi-etablissement**: meme produit, mais variables reseau, comptes, tokens, politiques de securite et donnees separees par etablissement.
|
||||
|
||||
Le principe directeur: une installation Lea ne doit pas dependre de la memoire d'un agent ou d'une session de debug. Elle doit etre executable par une checklist, testable, rollbackable et auditable.
|
||||
|
||||
## Architecture cible d'une installation
|
||||
|
||||
### Composants cote serveur
|
||||
|
||||
| Composant | Role | Port POC | Exposition attendue |
|
||||
|---|---|---:|---|
|
||||
| Dashboard Flask | supervision, verite produit, fleet, workflow status | 5001 | LAN/VPN autorise avec auth |
|
||||
| VWB backend | workflows, anchors, base SQLite VWB | 5002 | local ou LAN controle selon packaging |
|
||||
| Agent Chat | chat Lea, bulles, feedback temps reel | 5004 | accessible depuis poste/VM Windows |
|
||||
| Streaming/Fleet | ingestion agent Windows, sessions, API agent | 5005 | accessible depuis poste/VM Windows |
|
||||
| Upload/API core | API interne upload/traitement | 8000 | loopback par defaut |
|
||||
| Worker | compilation/apprentissage/replay | n/a | service systemd |
|
||||
| Ollama/VLM | inference locale | 11434 | local DGX, pas expose sauf decision explicite |
|
||||
| VM/VNC Windows | console Windows DGX si utilisee | 5902 | tunnel SSH/VPN uniquement |
|
||||
|
||||
### Composants cote Windows
|
||||
|
||||
| Composant | Role | Artefact |
|
||||
|---|---|---|
|
||||
| Lea agent | capture, tray, chat, apprentissage, streaming | `deploy/lea_package` ou installeur Inno |
|
||||
| `config.txt` | URL serveur, token API, machine id, label utilisateur | genere par dashboard ou installeur |
|
||||
| Installeur Inno | installation MVP/prod, Python embedded, silent install | `deploy/installer/Lea.iss` |
|
||||
| VM Windows | poste de test ou execution clinique | VMware, Hyper-V, QEMU standalone ou poste physique |
|
||||
|
||||
## Niveaux d'installation
|
||||
|
||||
| Niveau | Usage | Acceptable | Interdit |
|
||||
|---|---|---|---|
|
||||
| POC labo | tests rapides, validation technique | scripts manuels documentes, tunnel VNC, debug coordonne | secrets reutilises sans trace, actions non consignees |
|
||||
| POC site | installation pilote chez client | artefact fige, runbook, rollback, preuves | activer reseau/site sans fenetre et rollback |
|
||||
| MVP | repetition sur plusieurs postes | installeur signe ou controle, config par poste, smoke automatique | ZIP manuel non versionne comme seul chemin |
|
||||
| Production | support multi-etablissement | CI release, signature, supervision, backups, rotation secrets | `DASHBOARD_AUTH_DISABLED`, tokens partages, ports remote ouverts |
|
||||
|
||||
## Fiche site obligatoire
|
||||
|
||||
Chaque etablissement doit avoir une fiche separee avant installation.
|
||||
|
||||
```text
|
||||
SITE_ID=
|
||||
Nom etablissement=
|
||||
Contact technique=
|
||||
Contact metier=
|
||||
Fenetre installation=
|
||||
Fenetre rollback=
|
||||
|
||||
DGX_HOSTNAME=
|
||||
DGX_IP_LAB=
|
||||
DGX_IP_SITE=
|
||||
DGX_PREFIX=
|
||||
DGX_GATEWAY=
|
||||
DGX_DNS_1=
|
||||
DGX_DNS_2=
|
||||
IPv6=off/on
|
||||
VLAN=non/oui + id
|
||||
Interface Ethernet cible=
|
||||
|
||||
Dashboard URL=
|
||||
Streaming URL agent=
|
||||
Agent Chat URL=
|
||||
VWB URL=
|
||||
|
||||
Windows cible=
|
||||
Type Windows=poste physique / VMware / Hyper-V / QEMU DGX
|
||||
Nom machine Windows=
|
||||
Utilisateur Windows=
|
||||
Mode acces distant=VNC / RDP / console hyperviseur / aucun
|
||||
|
||||
RPA_API_TOKEN=
|
||||
DASHBOARD_USER=
|
||||
DASHBOARD_PASSWORD=
|
||||
ENCRYPTION_PASSWORD=
|
||||
SECRET_KEY=
|
||||
|
||||
Politique retention=
|
||||
Chemin backup=
|
||||
Responsable validation GO=
|
||||
```
|
||||
|
||||
Regle: aucun token ou mot de passe d'un site ne doit etre reutilise sur un autre site.
|
||||
|
||||
## Pre-requis avant installation
|
||||
|
||||
### Pre-requis DGX ou serveur Linux
|
||||
|
||||
- OS Linux valide pour le deploiement.
|
||||
- Acces admin local ou fenetre avec administrateur.
|
||||
- Python 3.10 a 3.12.
|
||||
- GPU NVIDIA si inference VLM locale attendue.
|
||||
- Ollama installe si le mode VLM local l'utilise.
|
||||
- Repo disponible sur la branche cible, par exemple `poc-dgx` pour le POC.
|
||||
- Ports internes/externes arbitres avant installation.
|
||||
- Disque libre suffisant pour sessions, captures, backups et modeles.
|
||||
- Politique backup validee avant toute migration ou reset.
|
||||
|
||||
### Pre-requis Windows
|
||||
|
||||
- Windows 10/11.
|
||||
- Droits suffisants pour installer Lea.
|
||||
- Acces reseau au DGX sur les ports agent requis.
|
||||
- Pour MVP/prod, privilegier l'installeur Inno avec Python embedded.
|
||||
- Pour POC, le ZIP `deploy/Lea_v<version>.zip` reste acceptable si documente.
|
||||
|
||||
### Pre-requis securite
|
||||
|
||||
- `DASHBOARD_AUTH_DISABLED` interdit hors dev local.
|
||||
- `RPA_AUTH_DISABLED` interdit hors dev local.
|
||||
- `RPA_API_TOKEN` obligatoire.
|
||||
- `DASHBOARD_PASSWORD` obligatoire.
|
||||
- Remote desktop/VNC/RDP ouverts uniquement en tunnel/VPN, jamais en LAN large par defaut.
|
||||
- Les secrets sont stockes hors git.
|
||||
- Le poste Windows doit afficher clairement l'etat d'enregistrement/apprentissage.
|
||||
|
||||
## Procedure POC actuelle
|
||||
|
||||
Cette section decrit l'etat connu du POC DGX. Elle n'est pas encore le chemin production final.
|
||||
|
||||
### 1. DGX - recuperer le code
|
||||
|
||||
```bash
|
||||
git clone <repo> rpa_vision_v3
|
||||
cd rpa_vision_v3
|
||||
git checkout poc-dgx
|
||||
```
|
||||
|
||||
Si le DGX contient deja des donnees runtime, ne jamais faire de reset destructif sans backup des chemins suivants:
|
||||
|
||||
```text
|
||||
visual_workflow_builder/backend/instance/workflows.db
|
||||
visual_workflow_builder/backend/data/
|
||||
data/training/
|
||||
data/runtime/
|
||||
data/workflows
|
||||
graphify-out/ si l'historique graphe doit etre preserve
|
||||
```
|
||||
|
||||
### 2. DGX - environnement Python
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. DGX - configuration runtime
|
||||
|
||||
Creer `.env.local` depuis le modele:
|
||||
|
||||
```bash
|
||||
cp deploy/systemd/rpa_vision_v3.env.example .env.local
|
||||
```
|
||||
|
||||
Valeurs obligatoires a remplacer:
|
||||
|
||||
```text
|
||||
ENCRYPTION_PASSWORD=CHANGE_ME
|
||||
SECRET_KEY=CHANGE_ME
|
||||
RPA_API_TOKEN=CHANGE_ME
|
||||
DASHBOARD_USER=lea
|
||||
DASHBOARD_PASSWORD=CHANGE_ME
|
||||
ENVIRONMENT=production
|
||||
RPA_PROCESSING_WORKER=external
|
||||
RPA_API_HOST=127.0.0.1
|
||||
RPA_DASHBOARD_HOST=0.0.0.0
|
||||
RPA_VLM_MODEL=gemma4:26b
|
||||
RPA_GROUNDING_ENGINE=qwen3vl_vllm
|
||||
VLLM_MODEL=Qwen/Qwen3-VL-4B-Instruct
|
||||
```
|
||||
|
||||
Pour un site, documenter explicitement quels services restent en loopback et quels services sont accessibles depuis la VM/poste Windows. En POC DGX, le dashboard est expose au LAN sur `0.0.0.0:5001` avec auth. En clinique durcie, preferer un reverse proxy HTTPS ou un bind loopback derriere proxy/VPN.
|
||||
|
||||
### 4. DGX - services
|
||||
|
||||
Services systemd attendus ou equivalents:
|
||||
|
||||
```text
|
||||
rpa-agent-chat.service
|
||||
rpa-firewall.service
|
||||
rpa-streaming.service
|
||||
rpa-vision-v3-api.service
|
||||
rpa-vision-v3-dashboard.service ou fallback rpa-vision-v3-dashboard-user
|
||||
rpa-vision-v3-worker.service
|
||||
rpa-vision-v3-stream-worker.service
|
||||
rpa-vision-v3-vwb-backend.service
|
||||
rpa-vision-v3-vwb-frontend.service
|
||||
rpa-vllm-grounder.service si active
|
||||
rpa-vision.target
|
||||
```
|
||||
|
||||
Commandes de controle:
|
||||
|
||||
```bash
|
||||
systemctl status rpa-agent-chat.service
|
||||
systemctl status rpa-firewall.service
|
||||
systemctl status rpa-streaming.service
|
||||
systemctl status rpa-vision-v3-dashboard.service
|
||||
systemctl status rpa-vision-v3-worker.service
|
||||
systemctl status rpa-vision-v3-stream-worker.service
|
||||
systemctl status rpa-vision-v3-vwb-backend.service
|
||||
systemctl status rpa-vision-v3-vwb-frontend.service
|
||||
systemctl list-units 'rpa*'
|
||||
```
|
||||
|
||||
En POC dev, `svc.sh` reste utilisable:
|
||||
|
||||
```bash
|
||||
./svc.sh status
|
||||
./svc.sh start
|
||||
./svc.sh restart api
|
||||
./svc.sh stop
|
||||
```
|
||||
|
||||
### 5. DGX - modele local
|
||||
|
||||
Verifier Ollama et les modeles attendus. Le POC DGX distingue:
|
||||
|
||||
- cerveau/lecteur Ollama: par exemple `gemma4:26b` selon `.env.local`;
|
||||
- grounder VLM via vLLM: `Qwen/Qwen3-VL-4B-Instruct` avec `think=false`.
|
||||
|
||||
```bash
|
||||
curl http://localhost:11434/api/tags
|
||||
ollama list
|
||||
# Si le modele site manque:
|
||||
ollama pull <modele_ollama_site>
|
||||
```
|
||||
|
||||
Ne pas exposer `11434` au LAN sans decision explicite.
|
||||
|
||||
### 6. DGX - donnees VWB/apprentissage
|
||||
|
||||
Verifier:
|
||||
|
||||
```bash
|
||||
test -f visual_workflow_builder/backend/instance/workflows.db
|
||||
find visual_workflow_builder/backend/data -maxdepth 3 -type f | head
|
||||
find data/training -maxdepth 3 -type f | head
|
||||
```
|
||||
|
||||
Endpoints utiles:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:5002/health
|
||||
curl -s http://127.0.0.1:5005/health
|
||||
curl -s http://127.0.0.1:5001/api/system/status
|
||||
```
|
||||
|
||||
Selon l'auth dashboard, `5001` peut repondre `401`; c'est attendu si l'auth est active.
|
||||
|
||||
## Installation Windows
|
||||
|
||||
### Chemin POC ZIP
|
||||
|
||||
Construire le package:
|
||||
|
||||
```bash
|
||||
./deploy/build_package.sh --clean
|
||||
./deploy/build_package.sh
|
||||
```
|
||||
|
||||
Copier `deploy/Lea_v<version>.zip` sur la machine Windows, puis:
|
||||
|
||||
1. Dezipper.
|
||||
2. Modifier `config.txt`.
|
||||
3. Lancer `install.bat`.
|
||||
4. Lancer `Lea.bat`.
|
||||
|
||||
`config.txt` minimum:
|
||||
|
||||
```text
|
||||
RPA_SERVER_URL=http://<DGX_IP>:5005/api/v1
|
||||
RPA_API_TOKEN=<token_site_ou_poste>
|
||||
RPA_MACHINE_ID=<site-machine-unique>
|
||||
RPA_USER_LABEL=<nom_utilisateur_ou_poste>
|
||||
RPA_BLUR_SENSITIVE=false
|
||||
RPA_LOG_RETENTION_DAYS=180
|
||||
```
|
||||
|
||||
### Chemin MVP/prod installeur
|
||||
|
||||
Le chemin cible est l'installeur Inno Setup:
|
||||
|
||||
```bash
|
||||
./deploy/installer/build_installer.sh
|
||||
```
|
||||
|
||||
Sortie attendue:
|
||||
|
||||
```text
|
||||
deploy/releases/Lea-Setup-v1.0.1.exe
|
||||
```
|
||||
|
||||
Installation silencieuse type:
|
||||
|
||||
```cmd
|
||||
Lea-Setup-v1.0.1.exe /VERYSILENT /CONFIG=C:\temp\enroll.txt /DIR="C:\Lea" /LOG="C:\temp\lea-install.log"
|
||||
```
|
||||
|
||||
`enroll.txt`:
|
||||
|
||||
```text
|
||||
USER_NAME=Prenom Nom
|
||||
USER_EMAIL=prenom.nom@example.local
|
||||
USER_ID=EMP-00123
|
||||
SERVER_URL=http://<DGX_IP>:5005/api/v1
|
||||
API_TOKEN=<token>
|
||||
```
|
||||
|
||||
Production: l'installeur doit etre signe pour eviter les alertes SmartScreen et faciliter le deploiement IT.
|
||||
|
||||
## VM Windows DGX - etat POC et regles
|
||||
|
||||
Etat confirme le 2026-06-19:
|
||||
|
||||
- Windows 11 ARM DGX fonctionne via QEMU standalone.
|
||||
- Acces utilisateur via VNC tunnel `localhost:5902`.
|
||||
- Le runtime actif utilise `disk.qcow2` cote utilisateur `aivanov`.
|
||||
- La definition libvirt `win11-arm-lea` peut apparaitre arretee pendant que le standalone tourne.
|
||||
|
||||
Regles:
|
||||
|
||||
- Ne pas demarrer la VM libvirt `win11-arm-lea` pendant que QEMU standalone utilise deja `disk.qcow2`.
|
||||
- Ne pas lancer deux VM sur le meme disque.
|
||||
- En standalone, `disk.qcow2` doit etre accessible a l'utilisateur qui lance QEMU, actuellement `aivanov`.
|
||||
- Si retour libvirt, prevoir de remettre l'ownership attendu par libvirt, par exemple `libvirt-qemu:libvirt-qemu`, apres arret complet du standalone.
|
||||
- Le VNC doit rester en loopback/tunnel, par exemple `127.0.0.1:5902`, pas en exposition LAN large.
|
||||
|
||||
Loose ends a traiter avant MVP:
|
||||
|
||||
1. Persister proprement le VNC loopback dans le script original ou dans un wrapper.
|
||||
2. Decider si le VNC a un password pose automatiquement via monitor, ou pas de password car tunnel obligatoire.
|
||||
3. Choisir un seul runtime officiel pour la VM DGX: standalone documente ou libvirt corrige.
|
||||
4. Documenter le rollback disk owner standalone <-> libvirt.
|
||||
5. Decider et implementer l'auto-start au reboot DGX si la VM Windows doit etre disponible apres redemarrage:
|
||||
- script persiste, pas `/tmp/vmvnc.sh`;
|
||||
- service systemd `User=aivanov` ou user service avec `loginctl enable-linger`;
|
||||
- `After=network-online.target`;
|
||||
- garde-fou contre un demarrage libvirt parallele;
|
||||
- choix VNC: sans password car loopback+tunnel, ou wrapper qui pose le password via monitor.
|
||||
|
||||
## Reseau site
|
||||
|
||||
Chaque site doit avoir un plan reseau valide avant installation.
|
||||
|
||||
Exemple clinique prepare:
|
||||
|
||||
```text
|
||||
DGX_IP_SITE=192.168.1.178
|
||||
PREFIX=/24
|
||||
GATEWAY=192.168.1.243
|
||||
DNS_1=192.168.1.9
|
||||
DNS_2=192.168.1.8
|
||||
IPv6=off
|
||||
VLAN=non
|
||||
Interface=Ethernet uniquement
|
||||
```
|
||||
|
||||
Regles:
|
||||
|
||||
- Ne pas activer le profil Ethernet site pendant les tests labo sans GO.
|
||||
- Prevoir un acces local ou une console avant toute bascule reseau.
|
||||
- Noter le rollback exact avant modification IP.
|
||||
- Valider depuis Windows: dashboard, chat, streaming.
|
||||
|
||||
## Smoke tests d'acceptation
|
||||
|
||||
### Serveur
|
||||
|
||||
| Test | Attendu |
|
||||
|---|---|
|
||||
| `systemctl status rpa-streaming` | actif |
|
||||
| `systemctl status rpa-vision-v3-dashboard` | actif ou fallback documente |
|
||||
| `curl :5005/health` | 200/healthy |
|
||||
| dashboard `:5001` | login/401 ou UI, pas 500 |
|
||||
| `/api/system/status` | coherent, pas de faux vert |
|
||||
| `/api/workflows` | workflows VWB visibles |
|
||||
| worker status | `healthy` ou `idle` non degrade si aucun job |
|
||||
| ports remote VNC/RDP | fermes au LAN, tunnel only |
|
||||
|
||||
### Windows
|
||||
|
||||
| Test | Attendu |
|
||||
|---|---|
|
||||
| `config.txt` | valeurs site/poste remplacees, aucun `CONFIGURE_ME` |
|
||||
| lancement Lea | tray visible |
|
||||
| chat Lea | connexion au DGX |
|
||||
| capture/apprentissage | demarre avec consentement utilisateur |
|
||||
| stop apprentissage | session ecrite cote DGX |
|
||||
| dashboard fleet | machine visible |
|
||||
| streaming | session recue sur `5005` |
|
||||
|
||||
### VWB/apprentissage
|
||||
|
||||
| Test | Attendu |
|
||||
|---|---|
|
||||
| `workflows.db` | present, backup effectue |
|
||||
| workflows dashboard | liste chargee |
|
||||
| anchors | images visibles |
|
||||
| replay supervise | action ou demande de confirmation, pas de clic non controle |
|
||||
| worker | session traitee puis retour sain |
|
||||
|
||||
## Criteres GO / NO-GO
|
||||
|
||||
GO installation site si:
|
||||
|
||||
- artefacts versionnes et identifies;
|
||||
- fiche site complete;
|
||||
- secrets generes pour le site;
|
||||
- DGX accessible et services verts;
|
||||
- Windows connecte au DGX;
|
||||
- dashboard/VWB/worker coherents;
|
||||
- ports remote non exposes hors tunnel/VPN;
|
||||
- rollback documente;
|
||||
- preuves archivees.
|
||||
|
||||
NO-GO si:
|
||||
|
||||
- un secret `CHANGE_ME` ou `CONFIGURE_ME` reste en place;
|
||||
- dashboard auth desactive hors dev;
|
||||
- `RPA_AUTH_DISABLED=true` hors dev;
|
||||
- VWB/workflows absents sans decision;
|
||||
- Windows ne rejoint pas le streaming;
|
||||
- VM ou poste controle le mauvais DGX;
|
||||
- deux runtimes VM utilisent le meme disque;
|
||||
- ports VNC/RDP exposes au LAN sans validation;
|
||||
- pas de backup `workflows.db` avant reset/deploy.
|
||||
|
||||
## Sauvegarde et rollback
|
||||
|
||||
Avant chaque installation ou upgrade:
|
||||
|
||||
```bash
|
||||
STAMP=$(date +%Y%m%dT%H%M%S)
|
||||
mkdir -p .codex_backups/install-$STAMP
|
||||
cp -a visual_workflow_builder/backend/instance/workflows.db .codex_backups/install-$STAMP/ 2>/dev/null || true
|
||||
cp -a visual_workflow_builder/backend/data .codex_backups/install-$STAMP/vwb-data 2>/dev/null || true
|
||||
cp -a data/training .codex_backups/install-$STAMP/training 2>/dev/null || true
|
||||
cp -a .env.local .codex_backups/install-$STAMP/env.local 2>/dev/null || true
|
||||
```
|
||||
|
||||
Rollback code:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout <commit_valide>
|
||||
systemctl restart rpa-streaming rpa-vision-v3-dashboard rpa-vision-v3-worker rpa-agent-chat
|
||||
```
|
||||
|
||||
Rollback donnees: restaurer uniquement les chemins sauvegardes, jamais faire `git clean -xfd` sur le DGX POC avec donnees runtime non trackees.
|
||||
|
||||
## Journal de preuve d'installation
|
||||
|
||||
Pour chaque installation, creer un dossier de preuve:
|
||||
|
||||
```text
|
||||
installations/<SITE_ID>/<YYYY-MM-DD>/
|
||||
site-sheet.md
|
||||
versions.txt
|
||||
services.txt
|
||||
ports.txt
|
||||
dashboard-smoke.txt
|
||||
windows-agent-smoke.txt
|
||||
vwb-smoke.txt
|
||||
backups.txt
|
||||
incidents.md
|
||||
verdict.md
|
||||
```
|
||||
|
||||
`versions.txt` doit contenir:
|
||||
|
||||
```bash
|
||||
git rev-parse HEAD
|
||||
git status -sb
|
||||
python --version
|
||||
pip freeze | sort
|
||||
systemctl --version
|
||||
ollama list
|
||||
```
|
||||
|
||||
## Industrialisation requise avant production
|
||||
|
||||
| Sujet | Etat POC | Attendu MVP/prod |
|
||||
|---|---|---|
|
||||
| Installeur Windows | Inno disponible | release signee, tests install/desinstall |
|
||||
| Enrollment | config/token manuel ou dashboard | token par poste, expiration/revocation |
|
||||
| Secrets | `.env.local` manuel | coffre ou procedure secrete auditee |
|
||||
| Services | systemd partiel selon DGX | target unique, healthcheck, recover |
|
||||
| Supervision | dashboard + logs | alerting, retention, export incident |
|
||||
| Backups | manuels | job planifie, test de restauration |
|
||||
| VM Windows DGX | standalone VNC manuel fonctionnel | runtime officiel choisi, persiste et auto-start arbitre |
|
||||
| Multi-site | variables en coordination | fiche site versionnee, aucun secret partage |
|
||||
| Documentation utilisateur | `LISEZMOI.txt` | guide utilisateur + admin par site |
|
||||
| Support | agents war-room | procedure support N1/N2/N3 |
|
||||
|
||||
## Check-list courte jour d'installation
|
||||
|
||||
1. Valider fiche site et fenetre rollback.
|
||||
2. Verifier artefact serveur et installeur Windows.
|
||||
3. Generer secrets site/poste.
|
||||
4. Sauvegarder donnees DGX existantes.
|
||||
5. Installer ou mettre a jour serveur.
|
||||
6. Configurer reseau sans perdre l'acces de rollback.
|
||||
7. Demarrer services.
|
||||
8. Installer Lea sur Windows.
|
||||
9. Lancer smoke serveur.
|
||||
10. Lancer smoke Windows.
|
||||
11. Tester dashboard, VWB, chat, streaming, worker.
|
||||
12. Archiver preuves.
|
||||
13. Donner verdict GO/NO-GO.
|
||||
14. Si GO, remettre les acces temporaires en mode securise.
|
||||
|
||||
## Documents sources
|
||||
|
||||
- `README.md`
|
||||
- `deploy/systemd/rpa_vision_v3.env.example`
|
||||
- `deploy/lea_package/LISEZMOI.txt`
|
||||
- `deploy/lea_package/config.txt`
|
||||
- `deploy/build_package.sh`
|
||||
- `deploy/installer/README.md`
|
||||
- `deploy/installer/Lea.iss`
|
||||
- `docs/coordination/RUNBOOK-DGX-POST-REBOOT-CHECK.md`
|
||||
- `docs/coordination/RUNBOOK-LEA-LIVE-DRAFT.md`
|
||||
- `docs/coordination/active/2026-06-19_1418_postaction-windows-dgx-fonctionne.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-19_1420_claude-to-codex_ACK-STOP-DIAG-VM-LOOSE-ENDS.md`
|
||||
34
docs/MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23.md
Normal file
34
docs/MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Mémo Jour J — Livraison définitive du DGX à la clinique (2026-06-23)
|
||||
|
||||
> Contexte : le DGX part **définitivement** à la clinique. Après, travail **100% à distance** depuis le labo. Tout doit marcher avant de débrancher.
|
||||
|
||||
## ✅ Validé la veille (2026-06-22) — rien à refaire
|
||||
- **Installateur Léa 1-clic autoportant** (python-embed, **sans Python système**, raccourci Bureau + démarrage auto) — testé sur VM Win11, enrôlement confirmé (Qwen).
|
||||
- **Apprentissage RPA** : OK.
|
||||
- **Reboot complet du DGX** : 11 services + grounder (modèle ~60 s) + VM reviennent **seuls**.
|
||||
- **Auto-réparation OVMF de la VM** (watchdog user `vm-ovmf-watchdog.service`) **LIVE** : après une coupure secteur, la VM se **répare et reboote seule** (~3-5 min). Testé (détection boucle CPU 99% → restauration known-good → relance).
|
||||
- **IP DGX clinique `192.168.1.178`** : réservation DHCP côté clinique pour la MAC Ethernet `10:b6:76:f0:2f:f4`. Tous les services `enabled` (auto-start au boot).
|
||||
- **Dashboard** bascule faite : `RPA_PUBLIC_URL=http://192.168.1.178:5005` → les Léa générées pointent direct `.178`.
|
||||
|
||||
## 🏥 Sur site (jour J)
|
||||
1. Brancher le DGX sur l'**Ethernet clinique** + allumer. **Attendre ~3-5 min** (boot + grounder + VM).
|
||||
2. Vérifier `.178` : depuis un poste clinique, ouvrir **http://192.168.1.178:5001** (login `lea`). Si le dashboard répond → DGX up + bonne IP.
|
||||
3. Grounder vision : prêt ~3 min après le boot — **ne pas tester la vision de Léa avant**.
|
||||
4. **Installer Léa sur les postes TIM** : sur chaque poste → `http://192.168.1.178:5001` → Fleet → enrôler → télécharger le ZIP → **clic droit Extraire** → double-clic **`Installer-Lea.bat`** → vérifier : pas d'erreur Python, Léa dans le systray, apparaît dans la fleet. **Tester l'apprentissage sur 1 poste avant de généraliser.**
|
||||
5. 🔴 **AVANT DE PARTIR (point de non-retour)** : depuis ton laptop via le **VPN Stormshield**, ouvrir **http://192.168.1.178:5001**. Si le dashboard s'affiche → **ton accès distant est bon, tu peux partir**.
|
||||
|
||||
## 🏠 Après le départ — travail à distance (labo)
|
||||
- Accès DGX : **VPN Stormshield → `http://192.168.1.178:5001`** (dashboard) + `ssh aivanov@192.168.1.178` (cert).
|
||||
- ⚠️ Déploiement de code/correctifs : **scp/rsync par-dessus le VPN** — PAS `git pull` (la Gitea maison n'est pas exposée sur internet).
|
||||
- Coupure secteur clinique → la VM s'auto-répare (watchdog OVMF). Accès VM : RDP (presse-papier) ou VNC.
|
||||
|
||||
## Identifiants
|
||||
- Voir ta note `DGX SSH aivanov Dom31.txt` (Bureau VM / clé USB) et `~/ai/rpa_vision_v3/.env.local` sur le DGX. (Non recopiés ici : ce fichier est dans le dépôt.)
|
||||
- VM Windows : compte `aivanov`.
|
||||
|
||||
## Reste / dette (post-installation — « on a encore du boulot »)
|
||||
- **Compte VM `aivanov`** : définir un **mot de passe** (pas juste un PIN) pour que le RDP + presse-papier marchent.
|
||||
- Code dev↔DGX : aligné (diff fait — seuls des tests + `config.py`/`index.html` mineurs diffèrent, non bloquant).
|
||||
- Mot de passe VNC perdu à chaque reboot VM (cosmétique — RDP = voie normale). Persistance VNC à câbler si besoin.
|
||||
- `hostname` Léa remonte `N/A` (python-embed) — cosmétique.
|
||||
- Profil **Stormshield laptop** (2ᵉ accès) à confirmer avec PORQUET (atteint `.178` ?).
|
||||
40
docs/NOTE_DSI_DEPLOIEMENT_GPO_LEA_2026-06-28.md
Normal file
40
docs/NOTE_DSI_DEPLOIEMENT_GPO_LEA_2026-06-28.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Note DSI — Déploiement / mise à jour de Léa par GPO (AD clinique)
|
||||
|
||||
- Date : 2026-06-28
|
||||
- Pour : Nicolas PORQUET (DSI Hôpital privé Wallerstein)
|
||||
- De : équipe Léa (Dom)
|
||||
- Objet : utiliser l'AD/GPO de la clinique comme **canal de déploiement** des mises à jour de l'agent Léa sur les postes pilotes, en remplacement d'un updater applicatif maison.
|
||||
|
||||
## 1. Contexte technique de Léa (côté poste)
|
||||
- Léa s'installe **par utilisateur** (`%LOCALAPPDATA%\Lea\`), **sans droits administrateur ni UAC**, sans Python système (Python embarqué).
|
||||
- Le programme d'installation est un **EXE (Inno Setup)** supportant le mode **silencieux** (`/SILENT` / `/VERYSILENT`). **Pas de package MSI** à ce jour.
|
||||
- L'app tourne en contexte utilisateur (`pythonw.exe`), démarrage par raccourci/observation.
|
||||
|
||||
## 2. Besoin
|
||||
Pousser une **mise à jour** (la prochaine, et idéalement les suivantes) sur **un sous-ensemble de postes** (les postes pilotes Léa), **sans intervention manuelle poste par poste**.
|
||||
|
||||
## 3. Options GPO envisagées (à valider avec vous)
|
||||
| Option | Mécanisme | Adapté ? |
|
||||
|---|---|---|
|
||||
| **Logon script (utilisateur)** | script au login qui lance l'installeur en silencieux ou copie les fichiers à jour dans `%LOCALAPPDATA%\Lea\` | ✅ **fit naturel** (per-user, sans admin) |
|
||||
| **GPP — Files / Scheduled Task** | déploiement de fichiers ou tâche planifiée de mise à jour | ✅ alternative |
|
||||
| GPO **Software Installation** | déploiement **MSI** assigné machine/utilisateur | ❌ nécessite un **MSI** (non disponible) |
|
||||
|
||||
- **Idempotence** : le script vérifiera un **marqueur de version** pour ne PAS réinstaller à chaque ouverture de session.
|
||||
- **Payload** : les fichiers de mise à jour seront déposés sur un **partage atteignable** par les postes (SYSVOL ou share réseau dédié) — à définir avec vous.
|
||||
- **Rollback** : en cas de version défaillante, repush de la version précédente par la même voie.
|
||||
|
||||
## 4. Questions / ce que nous vous demandons
|
||||
1. **Topologie exacte des postes pilotes** : PC physiques joints à l'AD ? hôtes **RDP** ? applications **Citrix** publiées ? (cela décide GPO **machine** vs **utilisateur**).
|
||||
2. Pouvez-vous créer une **OU dédiée** ou un **groupe de sécurité** ciblant les postes/utilisateurs Léa ?
|
||||
3. Un **partage réseau** (ou SYSVOL) pour héberger le payload de mise à jour ?
|
||||
4. Vos **contraintes de sécurité** : les **logon/startup scripts** sont-ils autorisés par votre politique ? Y a-t-il **AppLocker / SRP / WDAC** ou une exigence de **signature de code** sur les exécutables ? (le cas échéant on fournit l'empreinte SHA256 / on discute signature).
|
||||
5. Validez-vous le principe **rollback = repush version précédente** ?
|
||||
|
||||
## 5. Ce que nous fournissons
|
||||
- L'**installeur silencieux** (EXE) + son empreinte SHA256.
|
||||
- Un **script de logon** type (lancement silencieux + contrôle de version/marqueur).
|
||||
- La **liste des postes** pilotes concernés.
|
||||
|
||||
## 6. Bénéfice
|
||||
Canal de déploiement **standard, piloté par la DSI, traçable**, sans updater applicatif maison (donc sans risque de « briquer » les postes par un mécanisme de mise à jour custom). Compatible mises à jour ultérieures.
|
||||
138
docs/PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20.md
Normal file
138
docs/PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# PLAN — Accès distant DGX consolidé par SSH-certificat (labo + déplacement)
|
||||
|
||||
- `Auteur`: Claude (infra), co-construit avec Codex (réseau/box) + Qwen (client Windows)
|
||||
- `Date`: 2026-06-20
|
||||
- `Statut`: **PLAN / proposition** — décisions + application supervisées par Dom (rien de modifiant sans GO).
|
||||
- `Réf`: [[project_remote_access_consolidation_20260620]], `reference_dgx_static_ip`.
|
||||
|
||||
## 1. Objectif
|
||||
|
||||
Accès distant **robuste et unifié** au DGX, authentifié par **certificat SSH (CA OpenSSH)**, dans 2 contextes — **labo** (LAN/WiFi) et **déplacement depuis le PC Windows de Dom** (internet) — vers 3 cibles :
|
||||
1. **Terminal DGX** (SSH) ;
|
||||
2. **VM Windows** (VNC `5902` over SSH) ;
|
||||
3. **Bureau Ubuntu du DGX** (GNOME, via VNC — « éventuel »).
|
||||
|
||||
Contrainte forte : **on-premise / no-cloud** (pas de Tailscale-cloud ni Cloudflare).
|
||||
|
||||
## 2. État actuel (relevé read-only)
|
||||
|
||||
| Élément | Constat | Conséquence |
|
||||
|---|---|---|
|
||||
| SSH DGX | Aucune CA (`trustedusercakeys none`) ; auth par clé OK | terrain vierge propre pour une CA |
|
||||
| sshd | ⚠️ `PasswordAuthentication yes` (la checklist DSI annonce "no") | à corriger dans le durcissement |
|
||||
| VPN/overlay | Aucun installé | à mettre en place |
|
||||
| Bureau Ubuntu | Pas de VNC `5900` actif (DGX headless) | cible 3 à créer |
|
||||
| IPv4 box | **`82.64.97.95` routable** (Free, pas CGNAT) | **port-forward UDP possible → WireGuard direct viable** |
|
||||
| Ingress | Box forwarde déjà 80/443 → NPM ; `lea.labs…` → `82.64.97.95` | DDNS de fait + infra réutilisable |
|
||||
| IPv6 | DGX a une IPv6 globale `2a01:e0a:28:ad60:…` | option SSH IPv6 direct (secondaire) |
|
||||
|
||||
## 3. Architecture cible — 2 couches découplées
|
||||
|
||||
**Couche AUTH (identique partout) = CA OpenSSH.** On ne gère plus des `authorized_keys` poste par poste : une CA signe les clés. Un seul point de confiance, révocation centralisée, plus d'avertissement host-key.
|
||||
|
||||
**Couche TRANSPORT (selon contexte) :**
|
||||
- Labo : SSH direct `192.168.1.45` (LAN).
|
||||
- Déplacement : **WireGuard self-hosted** → une fois le tunnel monté, on est « sur le LAN » et **tout passe par SSH/tunnels** (terminal + VNC VM + VNC bureau) — *exactement le même geste qu'au labo*.
|
||||
|
||||
Bénéfice : **un seul mécanisme d'accès (SSH + tunnels) pour les 3 cibles**, que l'on soit au labo ou en déplacement. Le transport ne change que la « route ».
|
||||
|
||||
## 4. Approches transport comparées
|
||||
|
||||
| | A. WireGuard self-hosted ✅ reco | B. Mesh self-hosted (Netbird/Headscale) | C. SSH-over-443 (NPM/wstunnel) |
|
||||
|---|---|---|---|
|
||||
| NAT/CGNAT | OK (IPv4 routable + fwd UDP dispo) | OK même CGNAT (relais) | OK (443 quasi toujours ouvert) |
|
||||
| No-cloud | ✅ total | ✅ si self-hosted (serveur ctrl à tenir) | ✅ (réutilise NPM) |
|
||||
| Complexité | Faible/moyenne | Plus élevée (mgmt+signal+relay) | Moyenne (wrap TLS) |
|
||||
| Réseaux hôtels restrictifs (UDP bloqué) | ⚠️ → fallback nécessaire | OK | ✅ idéal |
|
||||
| Pertinence ici | **Forte** (on a IPv4 routable) | Surdimensionné maintenant | **Bon comme fallback** |
|
||||
|
||||
**Recommandation : A (WireGuard) en primaire + C (WireGuard-over-wstunnel sur 443) en fallback** pour les réseaux qui bloquent l'UDP (hôtels, certains 4G). B (mesh) seulement si une clinique se révèle CGNAT/durcie.
|
||||
|
||||
## 5. Couche AUTH — détail CA OpenSSH
|
||||
|
||||
Bonnes pratiques (Red Hat / Teleport / OpenSSH cookbook) : **2 CA séparées** (privilege separation), clés CA **hors DGX**.
|
||||
|
||||
1. **Générer les CA** (sur le poste Dom, stockage sûr, JAMAIS sur le DGX) :
|
||||
- `ssh-keygen -t ed25519 -f user_ca -C "rpa-user-ca"`
|
||||
- `ssh-keygen -t ed25519 -f host_ca -C "rpa-host-ca"`
|
||||
2. **Certifier l'hôte DGX** (supprime l'avertissement host-key, survit aux rotations) :
|
||||
- `ssh-keygen -s host_ca -I dgx-zgx2ff4 -h -n 192.168.1.45,dgx.lab,<ipv6> -V +52w /etc/ssh/ssh_host_ed25519_key.pub`
|
||||
- sshd : `HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub`
|
||||
- client `known_hosts` : `@cert-authority 192.168.1.45,*.lab <contenu host_ca.pub>`
|
||||
3. **Certifier l'utilisateur Dom** (laptop + poste labo) :
|
||||
- `ssh-keygen -s user_ca -I dom-laptop -n aivanov -V +52w id_ed25519.pub` (principal = compte cible)
|
||||
- sshd : `TrustedUserCAKeys /etc/ssh/user_ca.pub` (+ option `AuthorizedPrincipalsFile`)
|
||||
4. **Durcir sshd** : `PasswordAuthentication no`, `KbdInteractiveAuthentication no`, `PermitRootLogin prohibit-password`.
|
||||
5. **Révocation** : KRL (`ssh-keygen -k`) + `RevokedKeys` dans sshd — révoquer un poste perdu sans toucher les autres.
|
||||
|
||||
Validité : **certs user 1 an** (simple, + KRL) ou **courts** (plus sûr, nécessite un re-signing régulier). Reco équipe réduite : **1 an + KRL**.
|
||||
|
||||
## 6. Couche TRANSPORT — détail
|
||||
|
||||
**WireGuard (primaire)** :
|
||||
- Serveur `wg0` **sur le DGX** (miroir du futur déploiement clinique) : ex. `10.10.0.1/24`, `ListenPort 51820`.
|
||||
- Box : **port-forward UDP 51820 → DGX**. Nom dédié `vpn.<…>.laurinebazin.design` → `82.64.97.95` (DDNS).
|
||||
- Laptop = peer (`10.10.0.2/24`), `Endpoint vpn.…:51820`, `AllowedIPs` couvrant `10.10.0.0/24` + `192.168.1.45/32` (accès DGX + ses services).
|
||||
- Connecté → `ssh dom@10.10.0.1` (cert) ; `-L 5902` VM ; `-L 5900` bureau.
|
||||
|
||||
**Fallback 443 (réseaux UDP-bloqués)** : `wstunnel` encapsule WireGuard dans du WebSocket/`wss` sur **443** via le NPM (`wstunnel … wss://vpn.…:443`). Tout sort en 443 → passe les proxys/hôtels. Réf : Hetzner/eduVPN.
|
||||
|
||||
**IPv6 direct (secondaire/dépannage)** : quand le réseau client a l'IPv6, `ssh` direct vers l'IPv6 globale du DGX (épingler `mngtmpaddr`, firewall allow, cert). Pratique mais non garanti (réseaux IPv4-only).
|
||||
|
||||
## 7. Accès aux 3 cibles (même geste partout)
|
||||
|
||||
| Cible | Mécanisme | Commande type |
|
||||
|---|---|---|
|
||||
| Terminal DGX | SSH cert | `ssh dom@<dgx>` |
|
||||
| VM Windows | VNC over SSH | `ssh -N -L 5902:127.0.0.1:5902 dom@<dgx>` → VNC `localhost:5902` |
|
||||
| Bureau Ubuntu DGX | VNC over SSH | **DGX = X11/Xorg (confirmé Qwen)** → `x11vnc -localhost -rfbauth ~/.vnc/passwd` sur la session `:1` (ou TigerVNC), **bind loopback + password** → `ssh -N -L 5900:127.0.0.1:5900 …` → VNC `localhost:5900`. ⚠️ l'ancien x11vnc était en `nohup` non-persisté (mort à la panne) et sans password — à recréer en **service systemd user sécurisé** (P0). |
|
||||
|
||||
`<dgx>` = `192.168.1.45` au labo, `10.10.0.1` (wg) en déplacement. **La seule variable est l'hôte.**
|
||||
|
||||
**Presse-papier / copier-coller (demande Dom)** :
|
||||
- Bureau Ubuntu (x11vnc/TigerVNC) : **texte natif** ✅, rien à faire.
|
||||
- VM Windows (VNC QEMU) : **à activer**. QEMU 8.2.2 supporte le clipboard vdagent et le bus `virtio-serial` est déjà présent → ajouter `-chardev qemu-vdagent,id=vdagent,clipboard=on` + `-device virtserialport,chardev=vdagent,name=com.redhat.spice.0` + installer **SPICE guest tools (vdagent)** dans Windows = **copier-coller texte bidirectionnel** (garde le viewer VNC). Le tunnel SSH ne gêne pas (in-band).
|
||||
- Transfert **fichiers** (option) : SPICE (client virt-viewer) ou RDP natif (selon édition Windows — Home ARM = pas de serveur RDP).
|
||||
- ⚠️ Sous-chantier séparé : modif `vm_launch.sh` + install guest + **redémarrage VM gracieux** (jamais kill) → GO Dom.
|
||||
|
||||
## 8. Décisions attendues (Dom)
|
||||
|
||||
1. **Endpoint WireGuard** : sur le **DGX** (reco, mirroir clinique) ou sur le **poste Dom** (gateway always-on) ?
|
||||
2. **Fallback 443 (wstunnel)** : oui/non (utile si tu te connectes depuis hôtels/4G restrictifs) ?
|
||||
3. **Validité cert user** : **1 an + KRL** (reco) ou courts ?
|
||||
4. **Bureau Ubuntu DGX** : on le met en place maintenant ou « éventuel » plus tard ? (+ Wayland vs X11 — input Qwen).
|
||||
5. **Garde des clés CA** : sur le poste Dom (reco) — confirmer le lieu de stockage.
|
||||
|
||||
**Convergence équipe (Qwen + Claude)** sur les 5 options recommandées : WG sur DGX ✅, fallback 443 ✅, certs 1 an + KRL ✅, bureau Ubuntu maintenant (X11, P0 sécurité) ✅, clés CA hors DGX ✅. Codex (box/transport) à compléter.
|
||||
|
||||
**À confirmer par Dom (poste Windows — Qwen n'y a pas accès)** :
|
||||
- Version OpenSSH du laptop (`ssh -V`) — natif Win10 1803+/11, support cert OK normalement.
|
||||
- Client VNC Windows : **RealVNC Viewer** ou **TigerVNC viewer** (reco ; Remmina = Linux only).
|
||||
- A-t-il déjà un accès SSH depuis Windows vers le DGX (aujourd'hui : non testé) ?
|
||||
|
||||
**Note credentials** : seul `RPA_API_TOKEN` (config.txt Léa = `.env.local` DGX) est partagé entre Léa et le DGX ; le plan SSH-cert n'y touche pas → pas de conflit, mais si un renouvellement de token est fait par ailleurs, le répercuter aux 2 endroits.
|
||||
|
||||
## 9. Mise en œuvre — chirurgie itérative supervisée (un pas = un test)
|
||||
|
||||
| Phase | Contenu | Owner | Réversible |
|
||||
|---|---|---|---|
|
||||
| 0 | Backups (`sshd_config`, known state), inventaire exposition | Claude/Codex | — |
|
||||
| 1 | CA générées (off-DGX) + host cert + `@cert-authority` client → **test : plus d'avertissement host-key** | Claude | oui |
|
||||
| 2 | User cert + `TrustedUserCAKeys` → **test : login par cert** *en gardant clé/MDP en secours* | Claude | oui |
|
||||
| 3 | Durcissement (`PasswordAuthentication no`, KRL) → **test** ; retrait ancienne clé | Claude (GO Dom) | oui (backup) |
|
||||
| 4 | WireGuard DGX + fwd UDP box + peer laptop → **test reach déplacement (terminal+VM+bureau)** | Codex+Claude | oui |
|
||||
| 5 (opt) | Fallback wstunnel/443 | Codex | oui |
|
||||
| 6 (opt) | Bureau Ubuntu (TigerVNC/grd headless) | Qwen | oui |
|
||||
| 7 | Doc + **portage clinique** (Ethernet `.178`, même CA, même WG) | tous | — |
|
||||
|
||||
## 10. Sécurité & garde-fous
|
||||
|
||||
- Clé privée **CA hors DGX** (compromission DGX ≠ compromission CA).
|
||||
- `PasswordAuthentication no` corrige l'écart actuel (DSI annonçait "no").
|
||||
- WG = port unique UDP exposé (vs SSH brut exposé). SSH reste **non exposé en direct sur internet** (accès via WG), sauf option IPv6/fallback maîtrisée.
|
||||
- Révocation par KRL (poste volé) sans réémettre les autres certs.
|
||||
- Rollback : `sshd_config.bak` + redémarrage sshd ; WG = `wg-quick down`.
|
||||
|
||||
## 11. Portage clinique
|
||||
|
||||
Même CA + même WireGuard rejouables sur le DGX clinique (Ethernet statique `.178`). L'accès distant de Dom (support/maintenance à distance) devient un livrable DSI propre (un port UDP, auth cert, révocable) — argument sécurité pour Nicolas PORQUET.
|
||||
205
docs/PLAN_ACTION_SUITE_2026-06-23.md
Normal file
205
docs/PLAN_ACTION_SUITE_2026-06-23.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Plan d'action — la suite (post-livraison DGX clinique)
|
||||
|
||||
- `Date`: 2026-06-23
|
||||
- `Auteur`: Claude (mandat Dom)
|
||||
- `Statut`: vivant — à mettre à jour au fil des validations
|
||||
- `Portée`: **chapeaute** les plans existants (ne les remplace pas), dédoublonne et priorise les actions **encore ouvertes** après la livraison clinique du jour.
|
||||
|
||||
> Contexte : le DGX part **définitivement** en clinique aujourd'hui (`192.168.1.178`), puis travail **100 % à distance** (VPN Stormshield + scp). Le jour J lui-même est couvert par `MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23.md` — non répété ici.
|
||||
|
||||
---
|
||||
|
||||
## 0. Deux clarifications de cadrage (lire avant le reste)
|
||||
|
||||
1. **Les gaps « pré-clinique » sont clos.** L'audit (`AUDIT_GAPS_APPLI_100PCT_2026-06-10`) et le postmortem (`POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20`) listaient des gaps durs (OVMF G2, IP statique G1, reconnexion Léa G4, reboot 11 services). Le `MEMO_JOUR_J` + `TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21` confirment qu'ils sont **résolus** (watchdog OVMF LIVE testé, `.178` réservé DHCP, installateur autoportant). → **sortis de ce plan.**
|
||||
|
||||
2. **Accès distant = multi-VPN par site (correction Dom 23/06).** Pas « WireGuard caduc » : **WireGuard = notre VPN** (labo/éditeur, reste valide) ; **Stormshield = le VPN de la clinique** (côté client) ; d'autres clients auront **d'autres VPN**. ⇒ l'accès distant devient une **fonctionnalité paramétrée par fiche site** (`INSTALLATION_MULTI_SITE`), avec **SSH cert-only + RDP comme transport commun** au-dessus du VPN propre à chaque site. Le `PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20` (WireGuard) n'est pas suspendu — il devient *un* profil d'accès parmi d'autres.
|
||||
|
||||
---
|
||||
|
||||
## Tableau de bord — sous-projets (labo d'abord, WIP ≤ 3 actifs)
|
||||
|
||||
> Chaque feature F# = **un sous-projet** (objectif, branche, prérequis, statut, done). Backlog priorisé ; **on n'ouvre que 2-3 sous-projets actifs à la fois** (réactivité = focus). **Merge prod = supervisé Dom.** Codex orchestre par sous-projet à son retour (28/06).
|
||||
|
||||
**Prérequis socle (avant parallélisme)** : merger `fix/dashboard-complete-installer` (HEAD `d686c3ac2`) sur `poc-dgx` (`1d6efdb1b`) → base git propre ; ménage code mort (Qwen) cadré.
|
||||
|
||||
| SP | Feature | Statut | Prérequis bloquant |
|
||||
|----|---------|--------|--------------------|
|
||||
| **SP-0** | Socle git (merge branche + base propre) | ✅ **fait** (23/06 — FF `1d6efdb1b`→`d686c3ac2`, local) | — *(parallélisme débloqué)* |
|
||||
| **SP-1** | **F14/U-B Anchors** (compound) | ✅ **code FAIT, commit `2cabc6cb7` (br. `sp1/anchors-compound`), validé données réelles + persistance** | ré-import **en place** entravé par U-A (import = doublon) → SP-4 |
|
||||
| SP-2 | F2 Rejeu intelligent (R1→R7) | 🟢 **débloqué** (Q-F2-1 ✅ ; point d'entrée tracé = agent_chat 5004 / SemanticMatcher) | gros chantier R1→R7, à cadrer |
|
||||
| SP-3 | F8 Exécution native (durcir + sandbox) | 🔴 bloqué | Q-F8-1..4 + vérif sécurité `/execute/instruction` |
|
||||
| SP-4 | F1/U-A Consolidation fragmentation (T3) | 🟢 **débloqué (Q-F1-1 ✅ signature trajectoire, source=DB Q-F9-1 ✅)** | — (active quand on veut) |
|
||||
| SP-5 | F6/U-C Mutualisation/fédération | 🟠 décidé, à coder | dépend SP-1 (anchors) |
|
||||
| SP-Q | F13 Ménage code mort | 🔵 en cours (Qwen, read-only) | — |
|
||||
| — | F3 F4 F5 F7 F9 F10 F11 F12 | ⚪ backlog priorisé | — |
|
||||
|
||||
**Cadrage 3 sous-projets (23/06, 3 agents read-only) — interfaces communes & séquencement** :
|
||||
- **3 interfaces partagées à poser UNE fois** : (1) **signature de trajectoire** (SP-4 = propriétaire) ; (2) **index embeddings/FAISS partagé** (`core/embeddings/shared_index.py`, consommé par SP-2-R3 **et** la sélection skill↔tâche) ; (3) **guard `machine_id` centralisé** dans `stream_processor.py` (SP-4 ∩ SP-2-R6, ~L3197/3284).
|
||||
- **Phase 0** (fondation, série, petit) : signature de trajectoire + accès index partagé.
|
||||
- **Phase 1** (parallèle, WIP=2, faible collision) : **SP-4** (propriétaire `stream_processor.py`) ∥ **compétences** (propriétaire `core/competences/`).
|
||||
- **Phase 2** : **SP-2 rejeu** (le plus intriqué : touche `stream_processor` *et* FAISS) — après stabilisation Phase 0 + SP-4.
|
||||
- ⚠️ **Endpoints compétences `verdict`/`promote` EXISTENT déjà** (`api/lea_competences.py`, blueprint `app.py:277`, test `tests/unit/test_lea_competence_verdict_api.py`) → chantier compétences = **auto-déclenchement (hook) + sélection intelligente** seulement.
|
||||
- 1 **branche par sous-projet**, merge supervisé Dom.
|
||||
|
||||
**QG Qwen (23/06, 18:30) — GO avec 4 ajustements** (intégrés) :
|
||||
1. **Marqueurs de propriété dans `api_stream.py`** (commentaires par range d'endpoints SP-4 vs compétences) — seul point de contact Phase 1, éviter conflits silencieux. Pas de refactor.
|
||||
2. **Fallback R2/R3 obligatoire** (SP-2) : chaque nouveau chemin de résolution **retombe sur les coords figées** si la cascade intelligente échoue. Le rejeu *enrichit*, ne remplace pas. **Non-négociable démo.**
|
||||
3. **`machine_id` guard intouché en Phase 1** : le lifting du silo (`stream_processor` ~L3197/3284) est **entièrement Phase 2 / SP-2-R6**. → lève la collision SP-4 ↔ SP-2.
|
||||
4. **Dead import `ExternalDecisionClient`** (`api_stream.py:L7285`, module absent, inoffensif via try/except) → à nettoyer dans le **ménage code mort** (catégorie C), pas dans SP-4.
|
||||
|
||||
---
|
||||
|
||||
## Axe central — Rejeu intelligent des actions apprises 🎯
|
||||
|
||||
**C'est le cœur produit** (« Léa apprend, comprend, **rejoue en exploitant ce qu'elle a appris** » — pas du record-and-replay). Vérification **runtime du 2026-06-23** (à reconfirmer `fichier:ligne` avant toute modif — méthode projet) :
|
||||
|
||||
### État réel de la chaîne apprentissage → rejeu
|
||||
|
||||
| Maillon | État | Preuve (à reconfirmer) | Constat |
|
||||
|---|---|---|---|
|
||||
| Import auto Shadow → workflow rejouable | ❌ **débranché** | `finalize` `api_stream` ~2430-2466 : enqueue worker VLM, **pas** de conversion/import auto ; `ShadowLearningHook` jamais appelé | Ce qui est appris n'est **pas** rendu rejouable sans geste manuel |
|
||||
| Rejeu consulte le fonds appris (TargetMemoryStore) | ❌ **débranché** | `build_replay_from_raw_events` (~1841-2200) ne consulte rien ; `TargetResolver.lookup()` (~3263) jamais appelé par `replay-session` | Le rejeu rejoue des **coords/anchors figés**, pas la cible apprise |
|
||||
| Lecture FAISS / GlobalFAISSIndex au rejeu | ❌ **write-only** | `workflow_replay.py` accepte `faiss_manager` en param mais ne l'utilise pas ; aucun `.search()` au rejeu | Index **écrit, jamais lu** au rejeu (cohérent fédération dormante) |
|
||||
| verify post-condition (état UI après action) | ⚠️ **absent** | `safety_checks` + pause supervisée OK si mode supervisé (`api_stream` ~4299-4367) ; **pas** de `verify_screen` post-action | Pas de boucle de feedback succès/échec |
|
||||
| Templating `{{var.field.sub}}` au rejeu | ✅ **marche** | `_resolve_runtime_vars()` `replay_engine` ~2027-2041, appelé ~4293 | Données récupérées (T2A, extract) **réinjectées** en temps réel — **acquis, ne pas refaire** |
|
||||
| Filtre `machine_id` (cross-session) | ✅ actif = **silo** | `stream_processor` ~3197-3200 et ~3285 | Apprentissage **siloté par poste** ; rejeu direct non filtré |
|
||||
|
||||
**Verdict** : le rejeu est aujourd'hui **« brut » (events → coords), pas « intelligent »**. L'apprentissage tourne (ShadowLearning, TargetMemory, FAISS) mais **en silos jamais wirés au rejeu**. Le templating des données récupérées, lui, fonctionne déjà.
|
||||
|
||||
### Chaîne cible (ce vers quoi on va)
|
||||
|
||||
```
|
||||
Capture/Shadow → finalize → [R1] import auto en workflow rejouable
|
||||
→ au rejeu, chaque action résolue par : cascade UI (OCR/template/YOLO/VLM)
|
||||
+ [R2] fonds appris (TargetMemoryStore) + [R3] FAISS anchors (similarité)
|
||||
→ [✅] réinjection des données récupérées (templating)
|
||||
→ [R4] verify post-condition (échec = pause supervisée, pas stop = apprentissage)
|
||||
→ [R5] le résultat du rejeu réécrit le fonds (boucle)
|
||||
→ [R6] mutualisation : lever silo machine_id + brancher fédération
|
||||
```
|
||||
|
||||
### Chantiers rejeu (ordonnés, du plus structurant au plus fin)
|
||||
|
||||
| ID | Chantier | Dépend de |
|
||||
|---|---|---|
|
||||
| **R1** | Brancher l'**import auto** d'une session apprise en workflow rejouable post-finalize | décision « provider Léa runtime » |
|
||||
| **R2** | Faire **consulter le fonds appris au rejeu** : câbler `TargetResolver.lookup()` / `TargetMemoryStore` dans le chemin `replay-session` (résoudre par cible apprise, pas coords figées) | R1 |
|
||||
| **R3** | **Lire FAISS au rejeu** : utiliser le `faiss_manager` déjà passé à `workflow_replay.py` comme fallback de résolution par similarité d'anchor | R2 |
|
||||
| **R4** | **verify post-condition** : vérif état UI après action ; échec → pause supervisée (cf. *failure-is-learning*) | — |
|
||||
| **R5** | **Boucle d'apprentissage** : succès/échec/correction humaine du rejeu réécrivent le fonds (TargetMemory + FAISS) | R2, R4 |
|
||||
| **R6** | **Mutualisation** : lever le filtre `machine_id` + brancher la fédération (`GlobalFAISSIndex.search()` jamais appelé) | décisions produit (silo vs fédéré, PII) |
|
||||
| **R7** | Bugs rejeu résiduels : reprise sur crash (R4 audit), OCR span/centre-de-ligne (R5 audit) | — |
|
||||
|
||||
⚠️ **Tout cet axe touche le chemin runtime de la démo.** Méthode imposée : **chirurgie itérative supervisée** — un maillon = un test ≤ 2 min = GO Dom, jamais de batch, démo `Urgence_aiva_demo` intacte à chaque étape. Reconfirmer le wiring au runtime **avant** chaque modif (imports lazy).
|
||||
|
||||
---
|
||||
|
||||
## Feature — Unification Léa ↔ VWB (anchors) 🔗
|
||||
|
||||
**Manque corrigé (signalé Dom 23/06).** Chantier dédié : `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17.md`. Cœur = **les anchors**, le pont entre ce que VWB capture au recording et ce que Léa **récupère au rejeu**. Symptôme T3 « Léa ne trouve pas le bloc-notes » = **fragmentation de l'apprentissage**, pas silo.
|
||||
|
||||
Cycle de vie cible de l'anchor : capture (VWB recording / Shadow) → persistance (`visual_anchors`) → **propagation au workflow** → **récupération au rejeu** (résolution visuelle).
|
||||
|
||||
| ID | Sous-chantier | État | Risque |
|
||||
|---|---|---|---|
|
||||
| **U-B** | **Anchors — propager `anchor_image_base64` aux substeps *compound*** (`b8b963059` n'a fait que les actions simples ; les compound, majoritaires côté Léa, restent `anchor_id=NULL` → « Ancre requise » sans image) **+ ré-importer** les workflows. `learned_workflow_bridge.py` `_convert_compound_substep` ~L279 | **prêt — fix ciblé bloquant** | faible/additif |
|
||||
| **U-A** | **Consolidation fragmentation** : `workflow_id` = signature stable de trajectoire + create-or-update (fusion + agrégation d'observations) — débloque T3 | design décidé | moyen (touche build/persist démo) |
|
||||
| **U-C** | **Fonds commun** = **F6** (décidé cross + intra) : lever filtre `machine_id` (`stream_processor` ~L3197/L3284) + brancher fédération anonymisée (`GlobalFAISSIndex.search()` jamais appelé) | décision prise, à coder | moyen-élevé |
|
||||
| **U-D** | **Asymétrie grounding** : VWB recording = UI-DETR-1 ; replay Léa = cascade OCR/template/VLM → unifier le chemin de résolution | sujet ouvert post-démo | à trancher |
|
||||
|
||||
**Lien fort avec F2 (rejeu intelligent)** : la récupération des anchors au rejeu (U-B) est le **substrat de R2/R3** — sans anchors propagés/retrouvés, le rejeu ne peut pas résoudre par la vision et retombe sur des coords figées.
|
||||
|
||||
**⚠️ Implication de la décision F6 = cross-clinique** : un pack fédéré anonymisé **n'emporte ni coordonnées ni templates** (PII) → seule la **re-résolution visuelle par anchors/FAISS** permet de le rejouer ailleurs. Donc **F6 cross-clinique entraîne quasi-mécaniquement le principe « rejeu intelligent » (Q-F2-1)** : il devient un prérequis, pas une option. *(Décision induite Q-F14-1 au registre.)*
|
||||
|
||||
**Ordre interne** (du chantier) : confirmer provider Léa (Q-F2-2) → **U-B anchors** (gain visuel immédiat, faible risque) → U-A consolidation → U-C fédération.
|
||||
|
||||
**Prép SP-1 / U-B (vérif runtime 23/06, read-only)** :
|
||||
- **Gap confirmé** : `learned_workflow_bridge.py:_convert_compound_substep` (L279-321) ne pose **jamais** `_anchor_image_base64` ; la branche action simple (L226-233) le fait. → substeps compound = `anchor_id NULL`.
|
||||
- **Source dispo** : dans le JSON core, l'ancre du compound est à `target.context_hints.anchor_image_base64` (pas `target.anchor_image_base64`). `target` (parent) est **déjà passé** à `_convert_compound_substep`.
|
||||
- **Fix** (additif, ~1 endroit) : dans la boucle compound (L169-187), poser `_anchor_image_base64` (même fallback que simple) sur le **1er substep cliquable** uniquement.
|
||||
- **Impact DB** : **487/582 steps `anchor_id NULL`** (84 %) ; démo `Urgence_aiva_demo` = **8/18** manquants.
|
||||
- ⚠️ **Caveat ré-import** : le ré-import lit la source via `_load_core_workflow(workflow_id, machine_id)` dans `data/training/workflows/{machine_id}/` → **disponible par workflow** ; la source de la démo n'est pas trouvée par nom → **vérifier par core_workflow_id avant de compter sur le ré-import de la démo**.
|
||||
- **Test** : ré-importer un workflow compound → un substep cliquable doit afficher son image via `GET /api/v3/anchor/<id>/thumbnail` + `StepNode.tsx`. **Risque** : code = additif (faible) ; **ré-import = étape sensible** (backup `workflows.db` + par workflow + revérifier le replay).
|
||||
- **FAIT 23/06** : fix commité (`2cabc6cb7`, br. `sp1/anchors-compound`), TDD RED→GREEN, **validé sur données réelles** (source réelle : 2/2 clics compound désormais ancrés). Persistance confirmée : `import_learned_workflow` (L332) `pop("_anchor_image_base64")` → `save_anchor_image` → `VisualAnchor` + `step.anchor_id` (même chemin que les actions simples). Backup DB : `instance/workflows.db.bak-sp1-2026-06-23`.
|
||||
- ⚠️ **Découverte** : `import_learned_workflow` **crée un nouveau workflow** (`generate_id`, L301) — **pas de mise à jour en place**. Donc rafraîchir les anchors d'un workflow **existant** (ex. démo) par ré-import = **doublon** → dépend de **U-A** (create-or-update). Le fix U-B est correct **en avant** (tout nouvel import aura les anchors compound) ; le rafraîchissement des workflows déjà en base est porté par U-A (SP-4, décision Q-F1-1).
|
||||
|
||||
---
|
||||
|
||||
## Feature — Exécution native agentique (computer-use zéro-shot) 🤖
|
||||
|
||||
**Manque recadré (signalé Dom 23/06).** Donner un **objectif en langage naturel** (« ouvre un navigateur et va sur YouTube ») et l'exécuter **sans workflow appris**, par planification + grounding visuel. Complément du rejeu appris (F2).
|
||||
|
||||
**⚠️ Constat runtime (vérif 23/06) : ce n'est PAS absent — les briques existent et sont wirées.** Le manque réel = **durcissement + sandbox + validation humaine**.
|
||||
|
||||
| ID | Brique | État runtime | Manque |
|
||||
|---|---|---|---|
|
||||
| F8.1 | **Boucle ORA** observe→reason(VLM)→act→verify→retry (`core/execution/observe_reason_act.py:run_instruction`) | ✅ **wired** via endpoint `/execute/instruction` (`api_v3/execute.py:2033`) | durcissement |
|
||||
| F8.2 | **Planner NL→plan** (`agent_v0/server_v1/task_planner.py:understand()` gemma4, mode `_execute_free()`) | ⚠️ présent, **mode « free » peu mûr/peu testé** | maturation + tests |
|
||||
| F8.3 | **Grounding cascade** OCR→**UI-TARS**→VLM (= F3, partagé) | ✅ wired (`input_handler.py:_grounding_ui_tars`) | — |
|
||||
| F8.4 | **Sandbox Worker** (VM/Xvfb/VNC + kill-switch + **validation humaine**) | ❌ **absent** — exécution **directe sur l'host**, sans isolation ni pause | **= le vrai manque** (décision CUA P1) |
|
||||
| F8.5 | **Boucle vers l'apprentissage** : un run natif réussi → capturé comme workflow appris (alimente F1) | ❌ absent | à câbler |
|
||||
| F8.6 | **Replanification dynamique** si l'écran change radicalement (app crash…) | ❌ absent (ORA linéaire) | à ajouter |
|
||||
|
||||
**🔴 Sécurité — vérifié 23/06 (audit read-only)** : `POST /api/v3/execute/instruction` (VWB backend **5002**, `app.py:321` / `execute.py:2033`) lance la boucle ORA qui pilote **directement l'écran X11 de l'host** (pyautogui/xdotool, **pas de sandbox**, pas de pause humaine, pas de kill-switch) — cible de fait la VM Léa affichée. Auth = middleware Basic global (`DASHBOARD_PASSWORD`) **mais loopback exempté** ; sur le DGX clinique 5002 est **atteignable sur le LAN** (401 sans creds / 200 loopback), pas d'expo WAN. → Un acteur du LAN clinique avec les creds partagés (faibles, `Medecin2026!`) **ou tout process local (loopback)** peut déclencher une instruction agentique arbitraire sur l'host. **Mitigation à décider (prod)** : restreindre `/execute/instruction` à loopback / désactiver le mode « free » tant que F8.4 (sandbox) n'existe pas. Tant que F8.4 n'est pas en place, le mode « free » ne doit **PAS** être ouvert au-delà d'un environnement jetable — « JAMAIS l'hôte » + safety agent.
|
||||
|
||||
**Articulation avec le mode appris** : routage — pas de workflow appris pour le but → **mode natif** ; sinon **rejeu** (F2). Et le natif réussi **devient** appris (F8.5, la boucle se referme). Décisions → registre F8.
|
||||
|
||||
---
|
||||
|
||||
## H1 — Stabilisation clinique (jour J → quelques jours, à distance)
|
||||
|
||||
| # | Action | Source |
|
||||
|---|---|---|
|
||||
| 1 | **Aligner DGX↔local avant débranchement** : git 5 commits behind (`ec1fb81`→`d686c3ac2`) ; **backup `workflows.db` AVANT** reset | Qwen + tableau B′ |
|
||||
| 2 | Ajouter **`RPA_SIGNING_KEY`** dans `.env.local` DGX (HMAC métadonnées FAISS, absent) | KO-1 Qwen |
|
||||
| 3 | **Vérifier accès Stormshield** depuis laptop = point de non-retour avant départ | MEMO §5 |
|
||||
| 4 | **Mot de passe** (pas PIN) compte VM `aivanov` → RDP + presse-papier | handoff |
|
||||
| 5 | Confirmer **profil Stormshield laptop** avec PORQUET (atteint `.178` ?) | handoff |
|
||||
| 6 | **Apprentissage e2e sur 1 poste TIM réel** avant de généraliser aux 5 | MEMO + audit |
|
||||
|
||||
## H2 — Consolidation & dette (semaines suivantes, scp via VPN)
|
||||
|
||||
| # | Chantier | Pourquoi |
|
||||
|---|---|---|
|
||||
| 7 | **Merger les branches** : `fix/dashboard-complete-installer` non mergée sur `poc-dgx` ; clarifier topologie `poc-dgx`↔`main` | base git propre avant tout ménage |
|
||||
| 8 | **Ménage code mort** (mission Qwen en cours) → exécution **par lots + QG** | post-stabilisation |
|
||||
| 9 | **Test de charge multi-agents** (2-3 puis 5 Léa simultanées) | 1 TIM démo ≠ 5 TIM réels |
|
||||
| 10 | Bugs non-rejeu résiduels de l'audit (watchdog `_retry_pending` A2, écran verrouillé non détecté…) — **re-vérifier lesquels sont encore ouverts** | audit gaps |
|
||||
|
||||
## H3 — Produit & fond (après stabilisation, sur décisions Dom)
|
||||
|
||||
| # | Chantier | Bloqué par |
|
||||
|---|---|---|
|
||||
| 11 | **Chantier B anchors VWB** (propager `anchor_image_base64` aux substeps compound) — fix ciblé, risque faible | rien, prêt |
|
||||
| 12 | **CUA Sandbox Worker P1** (décision Dom 18/06 : VM/Xvfb/VNC + kill-switch, jamais l'hôte) | priorisation |
|
||||
| 13 | **Source vérité workflows** (migration JSON→SQLAlchemy, sortir DETTE-015) | post-POC |
|
||||
| 14 | **Shadow → Copilot → Autonomous** au runtime (aujourd'hui design) | R1-R6 + décisions |
|
||||
|
||||
---
|
||||
|
||||
## Décisions qui reviennent à Dom (registre dédié)
|
||||
|
||||
→ Suivi vivant dans **`DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md`** (Dom remplit à tête reposée).
|
||||
|
||||
**Déjà tranché (23/06)** :
|
||||
- ✅ **H3 en premier**, décisions avant exécution (séquencement préservé).
|
||||
- ✅ **F6 = mutualisation cross-clinique ET intra-clinique** → fédération anonymisée **dans le périmètre** + lever le silo `machine_id` entre postes.
|
||||
- ✅ **F11 = accès multi-VPN par site** (cf. §0.2).
|
||||
|
||||
**Encore ouvert** (bloque l'exécution des chantiers liés) :
|
||||
1. **Principe « rejeu intelligent »** (R2/R3) — le rejeu *doit* consulter le fonds appris ? Change l'archi du replay.
|
||||
2. **Provider Léa au runtime** (R1 + résolution).
|
||||
3. **Critère de fusion des workflows** (create-or-update).
|
||||
4. **Source de vérité workflows** (DB vs JSON) + **métrique produit** (24/79/37) — *reco Claude consignée : DB = vérité, JSON = échange ; métrique = rejouables validés*.
|
||||
|
||||
## Séquencement imposé
|
||||
|
||||
**Stabiliser (H1)** → **base git propre + merge (H2-7)** → **ménage code mort** → **axe rejeu R1→R7** (chirurgie supervisée) → **fond produit (H3)**.
|
||||
On ne dégraisse pas, et on ne recâble pas le rejeu, sur une base mouvante.
|
||||
|
||||
---
|
||||
|
||||
## Plans sources (référence, ne pas dupliquer)
|
||||
|
||||
`MEMO_JOUR_J_LIVRAISON_DGX_CLINIQUE_2026-06-23` · `TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21` · `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17` · `PLAN_ACCES_DISTANT_SSH_CERT_DGX_2026-06-20` (volet WG suspendu) · `PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09` · `AUDIT_GAPS_APPLI_100PCT_2026-06-10` · `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16` · `CHECKLIST_DGX_PRE_CLINIQUE` · `INSTALLATION_MULTI_SITE`.
|
||||
92
docs/PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17.md
Normal file
92
docs/PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Plan de chantier — Unification Léa + VWB (préparé le 2026-06-17 au soir)
|
||||
|
||||
> Préparé par analyse multi-agents (3 agents read-only) + graphify, après le check UI post-reboot DGX.
|
||||
> **Rien n'a été modifié.** Document de cadrage pour la reprise.
|
||||
> Méthode imposée : chirurgie itérative supervisée (1 modif = 1 test = validation Dom). Pas de batch.
|
||||
|
||||
## 0. Diagnostic corrigé (important — deux hypothèses du soir invalidées)
|
||||
|
||||
Le check UI a fait remonter 3 symptômes : **T3** (Léa « ne trouve pas » le bloc-notes), **T5/anchors** (images d'ancres absentes au VWB), et le sujet de fond **fonds commun**. L'analyse de code rectifie le diagnostic « à chaud » :
|
||||
|
||||
| Hypothèse du soir | Verdict après analyse code |
|
||||
|---|---|
|
||||
| T3 = silo machine_id (Léa-VM ne voit pas le savoir du .11) | **FAUX au niveau sélection.** Le `SemanticMatcher` ne filtre par aucune machine ; Léa tourne sur le DGX qui héberge les dossiers des deux postes → elle *voit* déjà tous les workflows. |
|
||||
| T3 = filtre `is_production_ready` | **FAUX.** Aucun composant runtime (matcher, exécuteur, chat) ne lit `is_production_ready`/`learning_state`. |
|
||||
| **Vraie cause T3** | **Fragmentation de l'apprentissage.** 100 répétitions → ~20 workflows distincts nommés d'après les apps vues (« Bloc-notes, Explorateur et Terminal (2)(3)… »), aucun nommé franchement « ouvrir le bloc-notes » → le matching sémantique se dilue sur 20 quasi-doublons. (Le silo existe, mais au niveau *renforcement cross-machine*, pas sélection.) |
|
||||
|
||||
---
|
||||
|
||||
## Chantier A — Consolidation de l'apprentissage (débloque T3) — PRIORITÉ HAUTE
|
||||
|
||||
### Cause racine
|
||||
Chaque session de streaming **crée** un workflow neuf, **sans jamais chercher ni fusionner** un existant.
|
||||
- Nommage + suffixe `(2)(3)` : `agent_v0/server_v1/stream_processor.py:4335-4344` (`_generate_workflow_name`) — collision de nom → variante numérotée au lieu de renforcement.
|
||||
- Persistance directe sans dédup : `stream_processor.py:4417-4445` (`_persist_workflow`), build `:2966-3112` (`_build_workflow_from_session`).
|
||||
- 1 observation / node : build **toujours séquentiel** (`graph_builder.py:345` `clusters={i:[i]}`), donc `observation_count = 1` (`graph_builder.py:909`). DBSCAN d'agrégation volontairement désactivé.
|
||||
- **Aucune fonction de merge/dédup de workflows** dans tout `core/`. Le `VariantManager` (`core/variants/variant_manager.py:266`, seul code qui fait `observation_count += 1`) **n'est jamais appelé**.
|
||||
- `is_production_ready` calculé dans `core/training/quality_validator.py:114,238-246` (seuil `min_observations_per_node=3`) — toujours False car 1 obs/node, **mais sans effet runtime** (label informatif uniquement).
|
||||
|
||||
### Leviers (effort / risque / impact)
|
||||
- **A. Découpler « exécutable » de « production_ready »** — faible / faible / moyen — quick-win sémantique.
|
||||
- **B. Fusion/dédup create-or-update à la persistance** — élevé / moyen / **fort** — cause racine.
|
||||
- **C. Rebrancher l'agrégation des observations** (`VariantManager` ou `_run_cross_session_learning` qui réécrit `observation_count`) — moyen / moyen / fort.
|
||||
- **D. Seuil `min_observations` configurable/contextuel** — faible / moyen / faible — cosmétique seul.
|
||||
- **E. `workflow_id` = signature stable de la trajectoire** (hash de la séquence d'actions) au lieu des apps vues — moyen / moyen / fort — supprime le `(2)(3)` à la racine, rend B trivial.
|
||||
|
||||
**Reco** : **E + B** (signature stable + create-or-update avec agrégation d'observations), D en complément, A en quick-win si on veut juste « rendre exécutable » vite.
|
||||
|
||||
### Composants
|
||||
`stream_processor.py` (L2966-3112, L4335-4344, L4417-4445, cross-session L3149-3268, list L4518) · `graph_builder.py` (L345, L909, L384-456) · `quality_validator.py` (L114, L238) · `semantic_matcher.py` (sélection) · `variant_manager.py` (L266, à rebrancher) · `replay_learner.py:358` (consolidate = hints seulement, leurre).
|
||||
|
||||
---
|
||||
|
||||
## Chantier B — Affichage anchors VWB (débloque T5) — FIX PRÉCIS, FAIBLE RISQUE
|
||||
|
||||
### Cause racine
|
||||
Le commit `b8b963059` n'a corrigé **que la moitié** : l'import lit `target.context_hints.anchor_image_base64` **uniquement pour les actions simples**.
|
||||
- `services/learned_workflow_bridge.py` : branche action simple `else` L226-233 (lit le base64, ajouté par le commit) ; **les actions *compound*** (majoritaires dans les workflows Léa) passent par `_convert_compound_substep` L279 qui **ne lit jamais le base64** → substeps `anchor_id=NULL` → frontend affiche « Ancre requise » sans image (`frontend_v4/.../StepNode.tsx:113-119`).
|
||||
- Aggravant état DGX : les workflows en base datent d'avril, **100% des steps `anchor_id=NULL`** → ré-import nécessaire après fix.
|
||||
|
||||
### Chaîne (pour mémoire)
|
||||
import `api_v3/learned_workflows.py:249` → convert `learned_workflow_bridge.py:72` → `save_anchor_image` → table `visual_anchors` (`db/models.py:163`) → API `GET /api/v3/anchor/<id>/thumbnail` (`api_v3/capture.py:356`) → React `StepNode.tsx` (`api.ts:136`). Pas de mismatch URL/chemin.
|
||||
|
||||
### Fix
|
||||
Propager `anchor_image_base64` aux substeps compound (passer `target` dans la boucle compound L169-187 / `_convert_compound_substep`, poser l'ancre sur le 1er substep cliquable — éviter de dupliquer sur N substeps). Risque faible/additif. **Puis ré-importer** les workflows cibles.
|
||||
|
||||
---
|
||||
|
||||
## Chantier C — Fonds commun / mutualisation cross-poste — STRATÉGIQUE, DÉCISION PRODUIT D'ABORD
|
||||
|
||||
### État
|
||||
- Fédération `core/federation/` **entièrement débranchée au runtime** : `GlobalFAISSIndex.search()` (`faiss_global.py:199`) **jamais appelé** ; endpoints export/import (`api_stream.py:6277-6372`) sans aucun déclencheur (ni cron, ni frontend).
|
||||
- **Anonymisation = le maillon le plus mûr et prêt** : `learning_pack.py` exclut machine_id/hostname/patient/nip/ipp (`_clean_metadata` L410, `_SENSITIVE_METADATA_KEYS` L61-66), hash SHA-256 (L388), export = embeddings 512d + signatures (pas de pixels/OCR brut).
|
||||
- Silo réel = `stream_processor._run_cross_session_learning` L3197/L3284 (`workflow_machine != machine_id → continue`) : bloque le renforcement cross-machine.
|
||||
- Identité : `machine_id` workflows = `hostname_os` (ex `DESKTOP-58D5CAC_windows`, `agent_v1/config.py:34-37`) ≠ token enrôlement `cbd8f9f0…` (`agent_registry.py:107-111`, sécurité parc). À ne pas confondre.
|
||||
|
||||
### Options
|
||||
- **Intra-clinique brut** (lever filtre cross-session L3197/L3284, charger toutes machines) — simple, **non anonymisé** → acceptable seulement intra-site.
|
||||
- **Cross-clinique anonymisé** (brancher `search()` global + déclencheur export/import + index global peuplé) — la vraie « fédération », effort moyen-élevé, **seul canal sûr PII**.
|
||||
|
||||
---
|
||||
|
||||
## Décisions produit à trancher AVANT de coder (pour Dom)
|
||||
|
||||
1. **Quel point d'entrée Léa au runtime ?** Deux providers concurrents : `agent_chat/app.py:678,906` (port 5004, SemanticMatcher sur `data/training/workflows/`) vs chat serveur `api_stream.py:6623` (`_list_available_workflows` qui liste des **sessions live**, pas les workflows entraînés). Les logs DGX du soir montrent le 5004 + SemanticMatcher → **probablement 5004**, à confirmer formellement avant de coder A.
|
||||
2. **Critère de « même parcours »** pour la fusion (levier E/B) : signature de trajectoire ? nom de base ? (workflows de 7 à 89 nodes pour « le même » parcours → alignement non trivial).
|
||||
3. **Source de vérité workflows** (DETTE-015) : DB VWB SQLite (5002) vs JSON `data/training/workflows/` — la route `/api/workflows` de Léa fusionne les deux avec une dédup fragile. Trancher la source canonique.
|
||||
4. **Niveau de mutualisation** : intra-clinique brut (rapide, non anonymisé) vs fonds commun cross-clinique anonymisé (fédération). Implications réglementaires opposées.
|
||||
5. **Re-exécutabilité des packs fédérés** : l'export n'emporte que des squelettes anonymisés (pas les templates/coordonnées) → un chemin de re-résolution visuelle est nécessaire (cohérent avec le contrat 100% vision).
|
||||
|
||||
---
|
||||
|
||||
## Ordre recommandé pour la reprise
|
||||
|
||||
1. **Confirmer le provider runtime de Léa** (Q1) — 10 min, read-only, débloque tout le reste.
|
||||
2. **Chantier B (anchors)** — fix ciblé compound + ré-import. Faible risque, gain visuel immédiat (T5).
|
||||
3. **Chantier A (consolidation)** — E + B, chirurgie itérative. Débloque T3 (le bloc-notes « connu »).
|
||||
4. **Chantier C (fonds commun)** — décision produit (Q4) puis implémentation anonymisée. Le plus stratégique pour la proposition de valeur, mais le moins urgent pour une démo.
|
||||
|
||||
## Garde-fous
|
||||
- Tout changement au build/persist (Chantier A) touche le chemin qui alimente la démo Urgence → chirurgie itérative, 1 test par modif.
|
||||
- Champ de mines `core/` : vérifier le wiring runtime réel avant de rebrancher (`VariantManager`), pas seulement la présence du code.
|
||||
- Mutualisation cross-site = uniquement via `LearningPack` anonymisé, jamais recopie de JSON bruts (PII : OCR, titres fenêtres patients).
|
||||
474
docs/PLAN_D1_NAVIGATE_COORDS_IMPLEMENTATION_2026-07-02.md
Normal file
474
docs/PLAN_D1_NAVIGATE_COORDS_IMPLEMENTATION_2026-07-02.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# D1 — NavigateCoords Implementation Plan
|
||||
|
||||
**Auteur**: Qwen
|
||||
**Date**: 2026-07-02
|
||||
**Statut**: EN ATTENTE GO Dom/Claude — Option 1 vs Option 2
|
||||
**Référence**: `docs/DESIGN_NAVIGATE_COORDS_CONSUMPTION_2026-07-02.md` (3 gaps documentés)
|
||||
|
||||
---
|
||||
|
||||
## Résumé des gaps à résoudre
|
||||
|
||||
| Gap | Description | Fichier:Ligne | Preuve |
|
||||
|-----|-------------|---------------|--------|
|
||||
| A | Compiler bake floats littéraux — aucun template pour coords | `replay_engine.py:1821-1833` | `x_pct = px` (literal float) |
|
||||
| B | Zéro consommateur de `navigate_*_coords` variables | `replay_engine.py` + `api_stream.py` | grep: 0 occurrences |
|
||||
| C | `_edge_to_normalized_actions` pas de branche `navigate` → `[]` | `replay_engine.py:1951-1953` | `else: return []` |
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure existante (non-modifiée)
|
||||
|
||||
### `_ALLOWED_ACTION_TYPES` (replay_engine.py:35-50)
|
||||
|
||||
`"navigate"` est **déjà présent** (ligne 44). La validation de sécurité l'accepte déjà.
|
||||
|
||||
### `_SERVER_SIDE_ACTION_TYPES` (replay_engine.py:55-64)
|
||||
|
||||
`"navigate"` est **déjà présent** (ligne 59). Le dispatch loop le traite comme serveur-side.
|
||||
|
||||
### `_handle_navigate_action` (core/navigation/__init__.py:24-113)
|
||||
|
||||
Handler **déjà câblé** dans api_stream.py (ligne 4459-4467). Résout screenshot, OCR/VLM, stocke coords dans `replay_state["variables"]`.
|
||||
|
||||
### `_resolve_runtime_vars` (replay_engine.py:2031-2045)
|
||||
|
||||
Resolver **existant** pour `{{var.field}}` — récursif sur dict/list/str. Retourne `str(value)` au niveau leaf → float→string conversion nécessaire pour coords.
|
||||
|
||||
---
|
||||
|
||||
## OPTION 1 — Compiler Injection (~2h)
|
||||
|
||||
### Principe
|
||||
|
||||
Ajouter une branche `navigate` dans `_edge_to_normalized_actions` + ajouter `coords_var` mechanism dans les branches `mouse_click`/`text_input` + runtime resolution + float conversion.
|
||||
|
||||
### Patch P1-A : Branche navigate dans `_edge_to_normalized_actions`
|
||||
|
||||
**Fichier**: `agent_v0/server_v1/replay_engine.py`
|
||||
**Position**: Après `elif action_type == "llm_generate":` (ligne 1949), avant `else:` (ligne 1951)
|
||||
|
||||
```python
|
||||
elif action_type == "navigate":
|
||||
normalized["type"] = "navigate"
|
||||
normalized["parameters"] = {
|
||||
"login_field": action_params.get("login_field", "login"),
|
||||
"password_field": action_params.get("password_field", "password"),
|
||||
"submit_button": action_params.get("submit_button", "submit"),
|
||||
"login_coords_var": action_params.get("login_coords_var", "navigate_login_coords"),
|
||||
"password_coords_var": action_params.get("password_coords_var", "navigate_password_coords"),
|
||||
"submit_coords_var": action_params.get("submit_coords_var", "navigate_submit_coords"),
|
||||
}
|
||||
return [normalized]
|
||||
```
|
||||
|
||||
**Justification**: Action serveur-side — pas besoin de `x_pct/y_pct` ni `target_spec`. Le handler `_handle_navigate_action` lit `parameters` pour config, résout coords au runtime.
|
||||
|
||||
**Impact**: Gap C résolu. Navigate edge → 1 normalized action au lieu de `[]`.
|
||||
|
||||
### Patch P1-B : coords_var dans branches mouse_click / text_input
|
||||
|
||||
**Fichier**: `agent_v0/server_v1/replay_engine.py`
|
||||
**Position**: Lignes 1844-1856 (branches click et type)
|
||||
|
||||
**mouse_click** (ligne 1844-1848) — AVANT :
|
||||
|
||||
```python
|
||||
if action_type == "mouse_click":
|
||||
normalized["type"] = "click"
|
||||
normalized["x_pct"] = x_pct
|
||||
normalized["y_pct"] = y_pct
|
||||
normalized["button"] = action_params.get("button", "left")
|
||||
```
|
||||
|
||||
**mouse_click** — APRES :
|
||||
|
||||
```python
|
||||
if action_type == "mouse_click":
|
||||
normalized["type"] = "click"
|
||||
coords_var = action_params.get("coords_var")
|
||||
if coords_var:
|
||||
normalized["x_pct"] = f"{{{{{coords_var}.x_pct}}}}"
|
||||
normalized["y_pct"] = f"{{{{{coords_var}.y_pct}}}}"
|
||||
normalized["coords_var"] = coords_var
|
||||
else:
|
||||
normalized["x_pct"] = x_pct
|
||||
normalized["y_pct"] = y_pct
|
||||
normalized["button"] = action_params.get("button", "left")
|
||||
```
|
||||
|
||||
**text_input** (ligne 1850-1856) — AVANT :
|
||||
|
||||
```python
|
||||
elif action_type == "text_input":
|
||||
normalized["type"] = "type"
|
||||
text = action_params.get("text", "")
|
||||
text = _substitute_variables(text, params, action_params.get("defaults", {}))
|
||||
normalized["text"] = text
|
||||
normalized["x_pct"] = x_pct
|
||||
normalized["y_pct"] = y_pct
|
||||
```
|
||||
|
||||
**text_input** — APRES :
|
||||
|
||||
```python
|
||||
elif action_type == "text_input":
|
||||
normalized["type"] = "type"
|
||||
text = action_params.get("text", "")
|
||||
text = _substitute_variables(text, params, action_params.get("defaults", {}))
|
||||
normalized["text"] = text
|
||||
coords_var = action_params.get("coords_var")
|
||||
if coords_var:
|
||||
normalized["x_pct"] = f"{{{{{coords_var}.y_pct}}}}"
|
||||
normalized["y_pct"] = f"{{{{{coords_var}.y_pct}}}}"
|
||||
normalized["coords_var"] = coords_var
|
||||
else:
|
||||
normalized["x_pct"] = x_pct
|
||||
normalized["y_pct"] = y_pct
|
||||
```
|
||||
|
||||
**⚠️ BUG dans le draft ci-dessus**: `x_pct` template pour text_input doit être `{{coords_var.x_pct}}` (pas `.y_pct` deux fois). Version corrigée :
|
||||
|
||||
```python
|
||||
elif action_type == "text_input":
|
||||
normalized["type"] = "type"
|
||||
text = action_params.get("text", "")
|
||||
text = _substitute_variables(text, params, action_params.get("defaults", {}))
|
||||
normalized["text"] = text
|
||||
coords_var = action_params.get("coords_var")
|
||||
if coords_var:
|
||||
normalized["x_pct"] = f"{{{{{coords_var}.x_pct}}}}"
|
||||
normalized["y_pct"] = f"{{{{{coords_var}.y_pct}}}}"
|
||||
normalized["coords_var"] = coords_var
|
||||
else:
|
||||
normalized["x_pct"] = x_pct
|
||||
normalized["y_pct"] = y_pct
|
||||
```
|
||||
|
||||
**Justification**: `coords_var` = mécanisme minimal pour déclarer "ces coords viennent de la variable navigate_login_coords". Template strings résolus au runtime par `_resolve_runtime_vars`.
|
||||
|
||||
**Impact**: Gap A résolu. Gap B partiellement — les actions click/type deviennent consommatrices via `coords_var`.
|
||||
|
||||
### Patch P1-C : Coercion helper après resolver existant
|
||||
|
||||
**⚠️ CORRECTION IMPORTANT (2026-07-02 14:45)** : Le plan original sur-dimensionnait P1-C en proposant un second resolver runtime. **Codex a correctement identifié** que `_resolve_runtime_vars` est **déjà appelé** dans la boucle dispatch à `api_stream.py:4331-4335` :
|
||||
|
||||
```python
|
||||
# L4331-4335 (EXISTANT, ne pas modifier)
|
||||
if owning_replay is not None:
|
||||
runtime_vars = owning_replay.get("variables") or {}
|
||||
if runtime_vars:
|
||||
action = _resolve_runtime_vars(action, runtime_vars)
|
||||
```
|
||||
|
||||
**Besoin réel = coercion helper uniquement** : `_resolve_runtime_vars` résout les templates `{{var.field}}` mais retourne `str(value)` au leaf → `{{navigate_login_coords.x_pct}}` devient `"0.15"` (string), pas `0.15` (float). Le client attend des floats pour x_pct/y_pct.
|
||||
|
||||
**Fichier**: `agent_v0/server_v1/api_stream.py`
|
||||
**Position**: Juste après la ligne 4335 (`action = _resolve_runtime_vars(action, runtime_vars)`)
|
||||
|
||||
**Politique coords_var non résolu** : Skip + pause supervisée (AGREED Qwen/Codex). Jamais fallback 0.0/0.0 — un clic sur coords (0,0) = top-left = potentiellement dangereux.
|
||||
|
||||
```python
|
||||
def _coerce_action_coords(action: dict) -> dict:
|
||||
"""Cast x_pct/y_pct en float après template resolution par _resolve_runtime_vars.
|
||||
|
||||
Politique : si string non convertible ou template encore present → skip + pause_for_human.
|
||||
Idempotent sur les actions qui ont déjà des floats (mouse_click existant).
|
||||
|
||||
Appelé APRÈS _resolve_runtime_vars dans la boucle dispatch (api_stream.py ~L4335).
|
||||
"""
|
||||
for key in ("x_pct", "y_pct"):
|
||||
val = action.get(key)
|
||||
if val is None:
|
||||
continue
|
||||
if isinstance(val, float):
|
||||
continue # déjà float, idempotent
|
||||
if isinstance(val, str):
|
||||
# Template encore présent = non résolu par _resolve_runtime_vars
|
||||
if val.startswith("{{") and val.endswith("}}"):
|
||||
action["_skip_reason"] = f"coords_var non résolu: {key}={val}"
|
||||
action["type"] = "pause_for_human"
|
||||
action["safety_level"] = "high"
|
||||
return action
|
||||
try:
|
||||
action[key] = float(val)
|
||||
except (ValueError, TypeError):
|
||||
action["_skip_reason"] = f"coords invalide: {key}={val}"
|
||||
action["type"] = "pause_for_human"
|
||||
action["safety_level"] = "high"
|
||||
return action
|
||||
return action
|
||||
```
|
||||
|
||||
**Appel dans la boucle dispatch** (à insérer après L4335) :
|
||||
|
||||
```python
|
||||
# L4335 existant: action = _resolve_runtime_vars(action, runtime_vars)
|
||||
# NOUVEAU — coercion coords après resolver existant
|
||||
action = _coerce_action_coords(action)
|
||||
```
|
||||
|
||||
**Justification**: `_resolve_runtime_vars` (existant à L4335) résout les templates → strings. `_coerce_action_coords` cast les strings en floats. Si template non résolu ou conversion impossible → pause_for_human (fail-safe), jamais fallback coords (0,0). Idempotent sur actions existantes (floats déjà présents).
|
||||
|
||||
**Risques additionnels identifiés** :
|
||||
1. **Résolution partielle** : si seul y_pct est résolu mais x_pct reste template → `_coerce_action_coords` convertit pause_for_human (safe stop, pas top-left click).
|
||||
2. **Idempotence** : si action existante a déjà x_pct=0.35 (float) → helper passe sans modification (isinstance(float) → continue).
|
||||
3. **Race condition** : variables dict partagé entre navigate handler et dispatch loop — mais BFS séquentiel garantit que navigate stocke AVANT click consomme.
|
||||
|
||||
**Impact**: Gap B résolu — les coords navigate sont consommées au runtime par click/type, avec coercion + fail-safe.
|
||||
|
||||
### Patch P1-D : VWB YAML schema — coords_var field
|
||||
|
||||
**Fichier**: Schema VWB (workflow YAML format) — documentation
|
||||
**Nature**: Ajout d'un champ `coords_var` dans `action.parameters` pour les steps `mouse_click` et `text_input`
|
||||
|
||||
Exemple de workflow YAML avec navigate + click consommateur :
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- id: s1
|
||||
action:
|
||||
type: navigate
|
||||
parameters:
|
||||
login_coords_var: navigate_login_coords
|
||||
password_coords_var: navigate_password_coords
|
||||
to_node: n2
|
||||
|
||||
- id: s2
|
||||
action:
|
||||
type: mouse_click
|
||||
parameters:
|
||||
coords_var: navigate_login_coords
|
||||
button: left
|
||||
to_node: n3
|
||||
|
||||
- id: s3
|
||||
action:
|
||||
type: text_input
|
||||
parameters:
|
||||
coords_var: navigate_password_coords
|
||||
text: "${password}"
|
||||
to_node: n4
|
||||
```
|
||||
|
||||
**Justification**: Le VWB builder doit savoir qu'un click peut référencer une variable coords au lieu de fournir des pixels littéraux. C'est un changement de schema minimal (1 champ optionnel).
|
||||
|
||||
---
|
||||
|
||||
## OPTION 2 — Declarative YAML Templates (~4h)
|
||||
|
||||
### Principe
|
||||
|
||||
Introduire un `coords_template` field dans les step definitions + un resolver typed qui extrait directement les floats du dict variables sans passage string→float.
|
||||
|
||||
### Patch P2-A : Même branche navigate (identique à P1-A)
|
||||
|
||||
Inchangé — Gap C résolu par la même branche.
|
||||
|
||||
### Patch P2-B : coords_template field + typed resolver
|
||||
|
||||
**Fichier**: `agent_v0/server_v1/replay_engine.py`
|
||||
|
||||
Nouvelle fonction `_resolve_coords_template` :
|
||||
|
||||
```python
|
||||
def _resolve_coords_template(
|
||||
coords_template: str,
|
||||
variables: Dict[str, Any],
|
||||
) -> Optional[Dict[str, float]]:
|
||||
"""Résoudre un coords_template en dict {x_pct, y_pct, bbox_pct} depuis variables.
|
||||
|
||||
Retourne None si la variable n'existe pas ou si les champs ne sont pas floats.
|
||||
Pas de conversion string→float : les valeurs doivent déjà être des floats.
|
||||
"""
|
||||
coords_dict = variables.get(coords_template)
|
||||
if not coords_dict or not isinstance(coords_dict, dict):
|
||||
return None
|
||||
|
||||
x_pct = coords_dict.get("x_pct")
|
||||
y_pct = coords_dict.get("y_pct")
|
||||
|
||||
if not isinstance(x_pct, (int, float)) or not isinstance(y_pct, (int, float)):
|
||||
logger.warning(
|
||||
f"coords_template {coords_template}: x_pct/y_pct not numeric "
|
||||
f"(x_pct={x_pct}, y_pct={y_pct})"
|
||||
)
|
||||
return None
|
||||
|
||||
result = {"x_pct": float(x_pct), "y_pct": float(y_pct)}
|
||||
|
||||
bbox_pct = coords_dict.get("bbox_pct")
|
||||
if bbox_pct:
|
||||
result["bbox_pct"] = bbox_pct # tuple, pas de conversion
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
### Patch P2-C : Branches mouse_click / text_input avec coords_template
|
||||
|
||||
```python
|
||||
if action_type == "mouse_click":
|
||||
normalized["type"] = "click"
|
||||
coords_template = action_params.get("coords_template")
|
||||
if coords_template:
|
||||
normalized["coords_template"] = coords_template
|
||||
# x_pct/y_pct résolus au runtime par _resolve_coords_template
|
||||
normalized["x_pct"] = None # placeholder → resolved at runtime
|
||||
normalized["y_pct"] = None
|
||||
else:
|
||||
normalized["x_pct"] = x_pct
|
||||
normalized["y_pct"] = y_pct
|
||||
normalized["button"] = action_params.get("button", "left")
|
||||
```
|
||||
|
||||
### Patch P2-D : Runtime resolution typed dans dispatch loop
|
||||
|
||||
```python
|
||||
# --- Résolution coords_template (typed, no string→float) ---
|
||||
if action.get("coords_template"):
|
||||
variables = owning_replay.replay_state.get("variables", {})
|
||||
from agent_v0.server_v1.replay_engine import _resolve_coords_template
|
||||
coords = _resolve_coords_template(action["coords_template"], variables)
|
||||
if coords:
|
||||
action["x_pct"] = coords["x_pct"]
|
||||
action["y_pct"] = coords["y_pct"]
|
||||
if coords.get("bbox_pct"):
|
||||
action["bbox_pct"] = coords["bbox_pct"]
|
||||
del action["coords_template"] # résolu, pas besoin de garder le ref
|
||||
else:
|
||||
logger.warning(
|
||||
f"coords_template {action['coords_template']} unresolved — skipping action"
|
||||
)
|
||||
# skip → next action
|
||||
```
|
||||
|
||||
**Avantage Option 2**: Pas de string→float conversion. Les coords restent des floats du navigate handler au click handler. Plus clean, plus safe.
|
||||
|
||||
**Inconvénient Option 2**: `_resolve_coords_template` est une nouvelle fonction + le `x_pct = None` placeholder nécessite que le client tolère les None temporairement (ou que la resolution se fasse AVANT transmission). Le schema VWB doit documenter `coords_template` comme champ alternatif à `by_position`.
|
||||
|
||||
---
|
||||
|
||||
## Comparative Table — Patches
|
||||
|
||||
| Aspect | Option 1 (Compiler Injection) | Option 2 (YAML Templates) |
|
||||
|--------|-------------------------------|---------------------------|
|
||||
| **Gap C fix** | Identique (branche navigate) | Identique (branche navigate) |
|
||||
| **Gap A fix** | Template strings `{{var.field}}` dans x_pct/y_pct | `x_pct = None` placeholder + typed resolver |
|
||||
| **Gap B fix** | `_resolve_runtime_vars` + float conversion | `_resolve_coords_template` typed (no conversion) |
|
||||
| **String→float** | Nécessaire (design smell) | Aucun (floats passent directement) |
|
||||
| **Nouvelles fonctions** | 0 (reuse `_resolve_runtime_vars`) | 1 (`_resolve_coords_template`) |
|
||||
| **Schema VWB** | 1 champ `coords_var` | 1 champ `coords_template` |
|
||||
| **Temps implémentation** | ~2h | ~4h |
|
||||
| **Extensibilité** | Limitée (coupling navigate→click) | Extensible (any coords source) |
|
||||
| **Risque POC** | Minimal | Moyen (placeholder None + typed resolver) |
|
||||
| **Migration post-POC** | Option 2 refactor needed | Already Option 2 |
|
||||
|
||||
---
|
||||
|
||||
## Test Rouge Proposal
|
||||
|
||||
### Test TR-1 : Prouve Gap C (navigate → [])
|
||||
|
||||
```python
|
||||
def test_edge_to_normalized_actionsnavigate_returns_empty():
|
||||
"""Gap C: _edge_to_normalized_actions retourne [] pour navigate type."""
|
||||
from agent_v0.server_v1.replay_engine import _edge_to_normalized_actions
|
||||
|
||||
edge = WorkflowEdge(
|
||||
edge_id="e1",
|
||||
from_node="n1",
|
||||
to_node="n2",
|
||||
action=ActionSpec(
|
||||
type="navigate",
|
||||
parameters={"login_coords_var": "navigate_login_coords"},
|
||||
),
|
||||
)
|
||||
result = _edge_to_normalized_actions(edge, {})
|
||||
# BEFORE fix: result == [] (Gap C)
|
||||
# AFTER fix: result == [{"type": "navigate", "parameters": {...}}]
|
||||
assert len(result) >= 1, "navigate must produce at least 1 normalized action"
|
||||
assert result[0]["type"] == "navigate"
|
||||
```
|
||||
|
||||
### Test TR-2 : Prouve coords_var resolution (Option 1)
|
||||
|
||||
```python
|
||||
def test_coords_var_runtime_resolution():
|
||||
"""Option 1: coords_var template resolved + float conversion."""
|
||||
from agent_v0.server_v1.replay_engine import _resolve_runtime_vars
|
||||
|
||||
variables = {
|
||||
"navigate_login_coords": {
|
||||
"x_pct": 0.15,
|
||||
"y_pct": 0.35,
|
||||
"method": "ocr+vlm",
|
||||
}
|
||||
}
|
||||
action = {
|
||||
"type": "click",
|
||||
"x_pct": "{{navigate_login_coords.x_pct}}",
|
||||
"y_pct": "{{navigate_login_coords.y_pct}}",
|
||||
"coords_var": "navigate_login_coords",
|
||||
}
|
||||
resolved = _resolve_runtime_vars(action, variables)
|
||||
# resolved["x_pct"] == "0.15" (string) → needs float conversion
|
||||
assert resolved["x_pct"] == "0.15" # string from resolver
|
||||
assert float(resolved["x_pct"]) == 0.15 # conversion works
|
||||
```
|
||||
|
||||
### Test TR-3 : Prouve coords_template typed resolution (Option 2)
|
||||
|
||||
```python
|
||||
def test_coords_template_typed_resolution():
|
||||
"""Option 2: coords_template returns floats directly, no conversion."""
|
||||
from agent_v0.server_v1.replay_engine import _resolve_coords_template
|
||||
|
||||
variables = {
|
||||
"navigate_login_coords": {
|
||||
"x_pct": 0.15,
|
||||
"y_pct": 0.35,
|
||||
"method": "ocr+vlm",
|
||||
}
|
||||
}
|
||||
coords = _resolve_coords_template("navigate_login_coords", variables)
|
||||
assert coords is not None
|
||||
assert isinstance(coords["x_pct"], float) # float, not string
|
||||
assert coords["x_pct"] == 0.15
|
||||
assert coords["y_pct"] == 0.35
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BFS Ordonnancement — Risque scheduling
|
||||
|
||||
Le dispatch loop (`api_stream.py:get_next_action`) traite les actions séquentiellement par path BFS. Navigate est serveur-side → traité en boucle interne avant transmission. Click/type consommant coords_var/template sont visuels → transmis au client.
|
||||
|
||||
**Flows correct**:
|
||||
1. BFS traverse edge navigate → normalized action `type=navigate`
|
||||
2. Loop interne: `_handle_navigate_action` → stocke coords dans variables
|
||||
3. BFS traverse edge click → normalized action avec `coords_var`
|
||||
4. Loop: resolution runtime → float conversion → transmission client
|
||||
|
||||
**Risque**: Si le BFS ordonnance le click AVANT le navigate (par ex. edges parallèles), coords_var sera unresolved → fallback 0.0/0.0.
|
||||
|
||||
**Mitigation**: VWB builder doit garantir que navigate edge précède click consommateur dans le path topologique. C'est une contrainte de schema, pas un bug runtime.
|
||||
|
||||
---
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Critère | Option 1 | Option 2 | Recommandation POC |
|
||||
|---------|----------|----------|--------------------|
|
||||
| Temps | 2h | 4h | **Option 1** |
|
||||
| Risque runtime | string→float edge | None placeholder | Option 1 (conversion simple) |
|
||||
| Extensibilité | Limitée | Extensible | Option 1 pour POC, migration Option 2 post-POC |
|
||||
| Code mort risk | 0 nouvelles fonctions | 1 nouvelle fonction | Option 1 |
|
||||
| Test coverage | TR-1 + TR-2 | TR-1 + TR-3 | Option 1 |
|
||||
|
||||
**Recommandation Qwen**: Option 1 pour POC (2h, minimal risk, reuse infrastructure existante). Migration Option 2 post-POC si scaling multi-coords est confirmé (search, dossier).
|
||||
|
||||
**GO requis**: Dom + Claude (décision D1).
|
||||
|
||||
---
|
||||
|
||||
*Qwen — plan implémentation D1 déposé, awaiting GO.*
|
||||
98
docs/PLAN_DEPLOY_NAVIGATION_WIRING_2026-07-01.md
Normal file
98
docs/PLAN_DEPLOY_NAVIGATION_WIRING_2026-07-01.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: plan-deploiement-navigation-2026-07-01
|
||||
description: Plan déploiement + vérification pour wiring navigate — diff, smoke, rollback. Commit = GO Dom supervisé. v2 corrigée après revue croisée Claude.
|
||||
type: project
|
||||
---
|
||||
|
||||
# Plan de déploiement — Wiring navigate (brique navigation serveur)
|
||||
|
||||
- Date : 2026-07-01 23:50 → **v2 2026-07-02 11:00** (corrections après revue croisée Claude)
|
||||
- Branche : `feat/push-log-dgx`
|
||||
- Commit = **GO Dom supervisé** (serveur DGX clinique live)
|
||||
|
||||
## Fichiers modifiés (3 hotspots + 4 modules navigation + 6 fichiers tests)
|
||||
|
||||
| Fichier | Changement | Lignes |
|
||||
|---------|-----------|--------|
|
||||
| `agent_v0/server_v1/api_stream.py` | +1 import `from core.navigation import _handle_navigate_action` + +1 dispatch `elif type_ == "navigate"` (asyncio.wait_for, timeout=180s) | +10 |
|
||||
| `agent_v0/server_v1/replay_engine.py` | +1 `"navigate"` dans `_ALLOWED_ACTION_TYPES` + +1 `"navigate"` dans `_SERVER_SIDE_ACTION_TYPES` | +2 |
|
||||
| `core/navigation/__init__.py` | Nouveau : handler `_handle_navigate_action` + exports `__all__` | +115 |
|
||||
| `core/navigation/visual_verifier.py` | Nouveau : OCR-ancré verify_before/after, fuzzy match | +408 |
|
||||
| `core/navigation/grounding.py` | Nouveau : OCR-anchor-first + VLM fallback + coords cache | +375 |
|
||||
| `core/navigation/visual_login.py` | Nouveau : DPI urgences login, verify + resolve | +227 |
|
||||
| `core/navigation/action_resolver.py` | Nouveau : coords normalisés, OCR adapters | +205 |
|
||||
|
||||
**Tests ajoutés (6 fichiers, 131 tests) :**
|
||||
|
||||
| Fichier | Tests | Rôle |
|
||||
|---------|-------|------|
|
||||
| `tests/unit/test_visual_verifier.py` | 48 | OCR-ancré, fuzzy match, verify_before/after |
|
||||
| `tests/unit/test_grounding.py` | 39 | OCR-anchor, VLM fallback, coords cache |
|
||||
| `tests/unit/test_visual_login.py` | 17 | DPI urgences login, verify + resolve |
|
||||
| `tests/unit/test_action_resolver.py` | 14 | coords normalisés, OCR adapters |
|
||||
| `tests/unit/test_navigate_wiring.py` | 7 | Boot non-régression (import, allowed types, handler callable) |
|
||||
| `tests/unit/test_navigate_handler_e2e.py` | 6 | E2e mocké (nominal, OCR miss, no screenshot, never-fail) |
|
||||
|
||||
## Smoke commands post-commit (à exécuter sur DGX après deploy)
|
||||
|
||||
```bash
|
||||
# 1. Boot serveur streaming — pas d'ImportError
|
||||
RPA_AUTH_DISABLED=true python -c "from agent_v0.server_v1 import api_stream; print('api_stream OK')"
|
||||
|
||||
# 2. Types autorisés — navigate présent
|
||||
RPA_AUTH_DISABLED=true python -c "from agent_v0.server_v1.replay_engine import _ALLOWED_ACTION_TYPES, _SERVER_SIDE_ACTION_TYPES; print('navigate in ALLOWED:', 'navigate' in _ALLOWED_ACTION_TYPES); print('navigate in SERVER_SIDE:', 'navigate' in _SERVER_SIDE_ACTION_TYPES)"
|
||||
|
||||
# 3. Handler callable
|
||||
RPA_AUTH_DISABLED=true python -c "from core.navigation import _handle_navigate_action; print('handler callable:', callable(_handle_navigate_action))"
|
||||
|
||||
# 4. Tests non-regression (navigation + wiring)
|
||||
RPA_AUTH_DISABLED=true python -m pytest tests/unit/ -k "navigat or visual_verifier or grounding or visual_login or action_resolver or wiring or e2e" -v --tb=short
|
||||
|
||||
# 5. Service rpa-streaming actif + health endpoint
|
||||
systemctl is-active rpa-streaming.service
|
||||
curl -s http://localhost:5005/health | python3 -m json.tool
|
||||
```
|
||||
|
||||
## Critères de rollback (si smoke échoue)
|
||||
|
||||
| Critère | Action |
|
||||
|---------|--------|
|
||||
| `ImportError` sur `api_stream` | Rollback git — `git revert --no-edit <SHA_PRE_DEPLOY>..HEAD` |
|
||||
| `"navigate"` absent des `_ALLOWED`/`_SERVER_SIDE` | Rollback — `git revert --no-edit <SHA_PRE_DEPLOY>..HEAD` |
|
||||
| Handler non callable | Rollback — `git revert --no-edit <SHA_PRE_DEPLOY>..HEAD` |
|
||||
| Tests wiring/e2e FAIL | Ne pas deploy — investiguer avant |
|
||||
| Serveur 5005 ne boote pas | `systemctl restart rpa-streaming` + vérifier logs |
|
||||
|
||||
## Données à préserver sur DGX (ne pas écraser)
|
||||
|
||||
- `visual_workflow_builder/backend/instance/workflows.db` — **TRACKÉ par git** (modifié dans working tree). `git reset --hard` l'écraserait → **INTERDIT**. Backup obligatoire avant deploy.
|
||||
- `.env.local` — creds clinique (DASHBOARD_PASSWORD, RPA_VLM_MODEL)
|
||||
|
||||
## Procédure de rollback rapide
|
||||
|
||||
```bash
|
||||
# Étape 0 : NOTER SHA_PRE_DEPLOY avant le merge
|
||||
cd /home/aivanov/ai/rpa_vision_v3
|
||||
SHA_PRE_DEPLOY=$(git rev-parse HEAD) # ← noter ce SHA avant git pull/merge
|
||||
|
||||
# Backup workflows.db (tracké par git — ne jamais reset --hard)
|
||||
cp visual_workflow_builder/backend/instance/workflows.db /tmp/workflows.db.backup
|
||||
|
||||
# Rollback : revert vers SHA pré-deploy
|
||||
git revert --no-edit <SHA_PRE_DEPLOY>..HEAD # annule tout ce qui est arrivé APRÈS le SHA noté (pas HEAD~1 — core/navigation/__init__.py est nouveau, checkout échouerait)
|
||||
systemctl restart rpa-streaming rpa-vision-v3-api
|
||||
|
||||
# Restaurer workflows.db runtime si besoin
|
||||
cp /tmp/workflows.db.backup visual_workflow_builder/backend/instance/workflows.db
|
||||
```
|
||||
|
||||
> **⚠️ INTERDICTION** : `git reset --hard` est **INTERDIT** sur DGX — `workflows.db` est tracké par git et modifié dans le working tree. Un reset hard écraserait les données runtime.
|
||||
|
||||
## Statut
|
||||
|
||||
- Build+TDD : **FAIT** (131 tests verts, 0 régression)
|
||||
- Plan déploiement : **FAIT v2** (4 corrections revue croisée Claude appliquées : service name, health URL, DGX path, rollback command + workflows.db tracking)
|
||||
- Commit : **EN ATTENTE GO Dom** (demain matin)
|
||||
- Deploy DGX : **EN ATTENTE GO Dom** (supervisé, serveur clinique live)
|
||||
|
||||
— Qwen
|
||||
263
docs/PLAN_MENAGE_CODE_MORT_2026-06-23.md
Normal file
263
docs/PLAN_MENAGE_CODE_MORT_2026-06-23.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Plan de ménage code mort — 2026-06-23
|
||||
|
||||
- `Auteur`: Qwen (audit read-only)
|
||||
- `Date`: 2026-06-23
|
||||
- `Statut`: plan — **aucune exécution sans GO Dom**
|
||||
- `Méthode`: existing-first (graphify-out/ + grep imports), vérification wiring runtime
|
||||
|
||||
---
|
||||
|
||||
## Synthèse
|
||||
|
||||
| Zone | Fichiers Python | A (WIRED) | B (ORPHELIN) | C (MORT) | Lignes C estimées |
|
||||
|------|----------------|-----------|-------------|----------|-------------------|
|
||||
| core/ | 226 | ~70 (31%) | ~75 (33%) | ~22 (10%) | ~800 |
|
||||
| agent_v0/server_v1/ | 26 | 21 | 1 | 4 | ~510 |
|
||||
| agent_v0/agent_v1/ | 44 | 36 | 2 | 6 | ~290 |
|
||||
| server/ | 8 | 4 | 2 | 2 | ~300 |
|
||||
| scripts/ | ~40 | 3 | 0 | ~37 | ~2500 |
|
||||
| deploy/ | 10+ | 3 | 5 | 2 | ~100 |
|
||||
| root | 4 | 1 | 2 | 1 | ~80 |
|
||||
| **Total** | ~360 | ~138 | ~84 | ~74 | **~4580** |
|
||||
|
||||
**~20% du codebase est MORT confirmé (catégorie C), ~23% est ORPHELIN/projection (B).**
|
||||
Les zones les plus chargées : `core/analytics/` (13/17 orphelins), `core/cognition/` (4/5 morts), `core/extraction/` (4/5 morts), `scripts/` (37/40 morts).
|
||||
|
||||
---
|
||||
|
||||
## NE PAS TOUCHER (runtime démo + systemd DGX + installateur)
|
||||
|
||||
- `agent_v0/server_v1/api_stream.py` (7747 lignes) — serveur principal runtime
|
||||
- `agent_v0/server_v1/stream_processor.py` (6085 lignes) — orchestrateur central
|
||||
- `agent_v0/server_v1/resolve_engine.py` — résolution anchors OCR/VLM
|
||||
- `agent_v0/server_v1/replay_engine.py` — replay actions
|
||||
- `agent_v0/agent_v1/core/executor.py` — exécuteur agent Windows
|
||||
- `visual_workflow_builder/backend/app.py` — VWB backend
|
||||
- `web_dashboard/app.py` — dashboard
|
||||
- `server/api_upload.py` — upload API (loopback DGX)
|
||||
- Tous les services systemd DGX (rpa-vision-v3-*.service, ollama, rpa-vllm-grounder)
|
||||
- `deploy/installer/` (Lea.iss, config_template.txt, build_installer.sh)
|
||||
- `deploy/lea_package/` (config.txt, requirements_agent.txt)
|
||||
- Chemin démo `Urgence_aiva_demo` end-to-end
|
||||
|
||||
---
|
||||
|
||||
## Inventaire par zone — catégorie C (MORT confirmé)
|
||||
|
||||
### core/ — MORT (22 fichiers, ~800 lignes)
|
||||
|
||||
| Fichier | Lignes | Preuve mort | Doublon de ? |
|
||||
|---------|--------|-------------|-------------|
|
||||
| `cognition/precondition.py` | ~50 | Zero imports | — |
|
||||
| `cognition/scene_expected.py` | ~50 | Zero imports | — |
|
||||
| `cognition/trace.py` | ~80 | Zero imports | — |
|
||||
| `cognition/vram_orchestrator.py` | ~100 | Zero imports (sauf __init__ test) | gpu/device_policy.py |
|
||||
| `extraction/data_store.py` | ~80 | Zero imports | — |
|
||||
| `extraction/extraction_engine.py` | ~120 | Zero imports (sauf __init__ try/except) | field_extractor.py |
|
||||
| `extraction/iteration_controller.py` | ~60 | Zero imports | — |
|
||||
| `extraction/schema.py` | ~40 | Zero imports | — |
|
||||
| `execution/spatial_index.py` | ~60 | Zero imports | — |
|
||||
| `execution/target_memory.py` | ~80 | Zero imports | learning/target_memory_store.py |
|
||||
| `execution/workflow_runner.py` | ~200 | Zero imports (sauf __init__.py) | dag_executor.py |
|
||||
| `interfaces/action_executor_interface.py` | ~40 | Tests only | — |
|
||||
| `interfaces/error_handler_interface.py` | ~30 | Tests only | — |
|
||||
| `interfaces/target_resolver_interface.py` | ~30 | Tests only | — |
|
||||
| `graph/simple_state.py` | ~40 | Zero imports | — |
|
||||
| `grounding/server.py` | ~500 | **SUPPRIMÉ** (n'existe plus) | — |
|
||||
| `detection/seeclick_adapter.py` | ~300 | Zero runtime imports, __init__ fallback mort | — |
|
||||
| `supervision/circuit_breaker.py` | ~40 | Zero imports, doublon system/circuit_breaker | system/circuit_breaker |
|
||||
| `supervision/supervisor.py` | ~200 | Zero imports, docstrings mentionnent modules jamais wired | — |
|
||||
| `gpu/clip_manager.py` | ~60 | Zero imports | embedding/clip_embedder |
|
||||
| `gpu/ollama_manager.py` | ~80 | Zero imports | detection/ollama_client |
|
||||
| `auth/manage_vault.py` | ~40 | Zero imports | auth/credential_vault |
|
||||
|
||||
### agent_v0/ — MORT (10 fichiers, ~800 lignes)
|
||||
|
||||
| Fichier | Lignes | Preuve mort | Doublon de ? |
|
||||
|---------|--------|-------------|-------------|
|
||||
| `server_v1/vm_controller.py` | 143 | Importé uniquement par visual_wait (mort) | — |
|
||||
| `server_v1/visual_wait.py` | 54 | Importé uniquement par vm_controller (mort) | — |
|
||||
| `server_v1/workflow_replay.py` | 256 | Zero imports, projection jamais intégrée | replay_engine.py |
|
||||
| `agent_v1/window_info.py` | 55 | Zero imports | window_info_crossplatform.py |
|
||||
| `agent_v1/tools/test_lea_pause_flow.py` | ~60 | Script debug standalone | — |
|
||||
| `agent_v1/tools/test_lea_toast.py` | ~80 | Script debug standalone | — |
|
||||
| `agent_v1/ui/_test_paused_toast.py` | ~40 | Script debug standalone | — |
|
||||
| `agent_v1/monitoring/__init__.py` | 0 | Stub vide jamais développé | — |
|
||||
| `agent_v0/config.py` | 58 | Zero imports | agent_v1/config.py |
|
||||
| `agent_v0/setup_v1.sh` | 30 | Vestige, requirements.txt absent | — |
|
||||
|
||||
### server/ — MORT (2 fichiers, ~300 lignes)
|
||||
|
||||
| Fichier | Lignes | Preuve mort | Doublon de ? |
|
||||
|---------|--------|-------------|-------------|
|
||||
| `server/api_upload_dev_8001.py` | ~150 | Pas dans services.conf, dev-only | api_upload.py |
|
||||
| `server/api_upload_dev_8002.py` | ~150 | Pas dans services.conf, dev-only | api_upload.py |
|
||||
|
||||
### scripts/ — MORT (~37 fichiers, ~2500 lignes)
|
||||
|
||||
| Fichier pattern | Count | Preuve mort | Notes |
|
||||
|----------------|-------|-------------|-------|
|
||||
| `demo_*_*_vwb_*_*.py` (dated) | ~12 | Scripts debug Jan 2026, jamais appelés par runtime | Vestiges développement VWB propriétés |
|
||||
| `diagnostic_*_*.py` (dated) | ~6 | Scripts debug Jan 2026 | Diagnostic palette/catalogue |
|
||||
| `test_*_vwb_*_*.py` (dated) | ~10 | Scripts test standalone Jan 2026 | Tests ad-hoc palette/propriétés |
|
||||
| `implementer_*.py`, `implementation_*.py` | ~2 | Scripts debug Jan 2026 | Implémentation propriétés |
|
||||
| `creer_sauvegarde_vwb_*.py` | 1 | Script debug | Sauvegarde ad-hoc |
|
||||
| `analyse_cas_undefined_*.py` | 1 | Script debug | Analyse ad-hoc |
|
||||
| `start_vwb_backend_*.py` (4 variants) | 4 | Doublons de `start_vwb_backend.py` | Versions ultra_stable, thread_safe, final, catalogue_complet |
|
||||
| `start_system_complet_*.sh` | 1 | Script dated | Remplacé par systemd |
|
||||
| `start_vwb_complete_*.sh` (2) | 2 | Scripts dated | Remplacé par systemd |
|
||||
|
||||
**scripts/ WIRED (3)** : `record_and_build.py` (A), `bench_t2a_dryrun.py` (A), `backup_vwb_and_audit.sh` (A)
|
||||
|
||||
### deploy/ — MORT (2 fichiers)
|
||||
|
||||
| Fichier | Preuve mort | Doublon de ? |
|
||||
|---------|-------------|-------------|
|
||||
| `deploy/configs/config_dev_windows.txt` | Config dev, pas utilisé par installateur | config_template.txt |
|
||||
| `agent_v0/setup_v1.sh` | Vestige, requirements.txt absent | — |
|
||||
|
||||
### root — MORT (1 fichier)
|
||||
|
||||
| Fichier | Preuve mort |
|
||||
|---------|-------------|
|
||||
| `mcp_rpa_vision.py` | MCP server jamais importé/callé par runtime ou systemd |
|
||||
|
||||
---
|
||||
|
||||
## Inventaire par zone — catégorie B (ORPHELIN/projection)
|
||||
|
||||
### core/ — ORPHELIN (~45+30 borderline, principalement analytics/ et healing/strategies/)
|
||||
|
||||
| Zone | Fichiers | Preuve orphelin | Projection plausible ? |
|
||||
|------|---------|-----------------|----------------------|
|
||||
| `analytics/` (13 orphelins) | metrics_collector, anomaly_detector, insight_generator, performance_analyzer, success_rate_calculator, archive_storage, timeseries_store, query_engine, report_generator, analytics_api, dashboard_manager, realtime_analytics, resource_collector | Test-only ou zero imports | ✅ Analytics = fonctionnalité produit future (dashboard insights) |
|
||||
| `healing/strategies/` (5) | base_strategy, format_transformation, semantic_variants, spatial_fallback, timing_adaptation | Test-only via `test_self_healing.py` | ✅ Self-healing = produit prévu |
|
||||
| `cognition/working_memory.py` (1) | Intra-core lazy import seulement | ✅ Cognition = produit futur |
|
||||
| `detection/owl_detector.py` (1) | Importé par agent_chat/autonomous_planner seulement | ✅ OWL = grounder alternatif |
|
||||
| `detection/roi_optimizer.py`, `spatial_analyzer.py` | Test-only | ✅ Optimisation ROI future |
|
||||
| `grounding/smart_resize.py` | Test-only, jamais importé runtime | ✅ Resize intelligent utile |
|
||||
| `gpu/vram_monitor.py` | Zero imports | ✅ GPU monitoring utile |
|
||||
| `monitoring/automation_scheduler.py` | __init__ try/except | ✅ Automation scheduling |
|
||||
| `security/flask_security.py`, `input_validator.py`, `ip_allowlist.py`, `rate_limiter.py`, `audit_log.py` | __init__ exports mais jamais importés directement | ✅ Sécurité = P0 produit |
|
||||
| `precision/` (5 fichiers) | Test-only | ✅ Metrics/precision utile |
|
||||
| `system/artifact_retention.py` | __init__ export, jamais utilisé runtime | ✅ Rétention utile |
|
||||
|
||||
### agent_v0/ — ORPHELIN
|
||||
|
||||
| Fichier | Preuve | Projection ? |
|
||||
|---------|--------|-------------|
|
||||
| `server_v1/session_worker.py` | Commentaires seulement, pas importé | ✅ Background session processing |
|
||||
| `agent_v1/core/anchor_catalog.py` + `anchor_relative.py` | Importé par tests seulement | ✅ Anchor resolution future |
|
||||
| `deploy_windows.py` | Script packaging manuel | ✅ Packaging Windows |
|
||||
| `deploy/windows_client/` (23 fichiers) | Copie déployée sur Windows, pas importée depuis repo Linux | ✅ = package agent Windows |
|
||||
|
||||
### server/ — ORPHELIN
|
||||
|
||||
| Fichier | Preuve | Projection ? |
|
||||
|---------|--------|-------------|
|
||||
| `server/processing_pipeline.py` | Importé par api_upload seulement | ✅ Pipeline processing |
|
||||
| `server/processing_queue.py` | Importé par api_upload seulement | ✅ Queue processing |
|
||||
| `server/storage_encrypted.py` | Importé par api_upload seulement | ✅ Encrypted storage |
|
||||
| `server/worker_daemon.py` | Importé par api_upload seulement | ✅ Worker daemon |
|
||||
|
||||
### root — ORPHELIN
|
||||
|
||||
| Fichier | Preuve | Projection ? |
|
||||
|---------|--------|-------------|
|
||||
| `monitoring_server.py` | Optional dans services.conf (5003), pas déployé DGX | ✅ Monitoring utile |
|
||||
| `run_gui.py` | Script standalone GUI launcher | ✅ GUI dev tool |
|
||||
| `cli.py` | CLI entry point, importé par core modules | ✅ CLI utile |
|
||||
|
||||
### deploy/ — ORPHELIN
|
||||
|
||||
| Fichier | Preuve | Projection ? |
|
||||
|---------|--------|-------------|
|
||||
| `deploy/dgx/vm_launch.sh` + `vm_stop.sh` | Scripts QEMU VM DGX | ✅ VM management |
|
||||
| `deploy/configs/config_pc_fixe_lan.txt` + `config_tim_pauline.txt` + `config_vm_lan.txt` | Configs déployées manuellement | ✅ Déploiement multi-site |
|
||||
| `deploy/windows-rdp-launcher/` | Scripts RDP | ✅ Remote access |
|
||||
| `deploy/hyperv_glpi_ubuntu24/` | Script HyperV | ✅ VM creation |
|
||||
|
||||
---
|
||||
|
||||
## Quick wins sûrs (C évident, zéro risque)
|
||||
|
||||
| # | Fichier | Lignes | Risque | Action |
|
||||
|---|---------|--------|--------|--------|
|
||||
| 1 | `core/interfaces/` (3 fichiers) | ~100 | Zéro | Supprimer — jamais importé runtime |
|
||||
| 2 | `core/cognition/` (4 morts: precondition, scene_expected, trace, vram_orchestrator) | ~280 | Zéro | Supprimer — zero imports |
|
||||
| 3 | `core/extraction/` (4 morts: data_store, engine, controller, schema) | ~300 | Zéro | Supprimer — field_extractor seul wired |
|
||||
| 4 | `core/supervision/` (2: circuit_breaker doublon, supervisor jamais wired) | ~240 | Zéro | Supprimer — doublons |
|
||||
| 5 | `core/detection/seeclick_adapter.py` | ~300 | Zéro | Supprimer — fallback mort |
|
||||
| 6 | `server_v1/vm_controller.py` + `visual_wait.py` | ~200 | Zéro | Supprimer pair — jamais importé |
|
||||
| 7 | `agent_v1/window_info.py` | 55 | Zéro | Supprimer doublon |
|
||||
| 8 | `agent_v0/config.py` | 58 | Zéro | Supprimer doublon |
|
||||
| 9 | `scripts/ dated Jan 2026` (~37) | ~2500 | Zéro | Supprimer — vestiges dev |
|
||||
| 10 | `server/api_upload_dev_8001.py` + `api_upload_dev_8002.py` | ~300 | Faible | Supprimer dev-only |
|
||||
|
||||
**Total quick wins** : ~10 actions, ~4133 lignes supprimées, zéro risque runtime
|
||||
|
||||
---
|
||||
|
||||
## Zones à risque (à instruire avant GO)
|
||||
|
||||
| Zone | Risque | Pourquoi |
|
||||
|------|--------|----------|
|
||||
| `core/analytics/` (13 orphelins) | Moyen | Projection produit plausible — supprimer = perdre design analytics |
|
||||
| `core/healing/strategies/` (5) | Moyen | Self-healing = produit prévu — test-only mais intention future |
|
||||
| `core/grounding/infigui_server.py` + `infigui_worker.py` | Moyen | Server jamais lancé mais worker existe — dépendance circulaire mort |
|
||||
| `core/security/` (5 orphelins) | Moyen | Sécurité P0 produit — supprimer = perdre auth/rate-limiting design |
|
||||
| `core/gpu/` (3 morts: vram_monitor, clip_manager, ollama_manager) | Faible | GPU monitoring plausible mais doublon vérifié |
|
||||
|
||||
---
|
||||
|
||||
## Proposition de réorganisation
|
||||
|
||||
**Pas de réorganisation proposée à ce stade.** Le ménage C suffira à dégraisser ~4500 lignes. Les B (projections) resteront en place avec documentation `# PROJECTION: <raison>` pour les distinguer du code wired.
|
||||
|
||||
**Après ménage C**, réorganisation possible :
|
||||
- `scripts/` dated → archiver dans `scripts/archive/` ou supprimer
|
||||
- `core/interfaces/` mort → supprimer (remplacé par protocols Python si besoin)
|
||||
- `deploy/configs/` → consolider dans `deploy/installer/config_template.txt`
|
||||
|
||||
---
|
||||
|
||||
## Plan d'exécution par lots (chirurgie itérative supervisée)
|
||||
|
||||
**Chaque lot = petit, testable (≤ 2 min), QG entre chaque, GO Dom requis.**
|
||||
|
||||
| Lot | Action | Lignes | Test QG | Risque |
|
||||
|-----|--------|--------|---------|--------|
|
||||
| **L1** | Supprimer `core/interfaces/` (3 fichiers) | ~100 | `pytest tests/ -k "not e2e"` | Zéro |
|
||||
| **L2** | Supprimer `core/cognition/` morts (4 fichiers) | ~280 | pytest | Zéro |
|
||||
| **L3** | Supprimer `core/extraction/` morts (4 fichiers) | ~300 | pytest | Zéro |
|
||||
| **L4** | Supprimer `core/supervision/` (2 fichiers) | ~240 | pytest | Zéro |
|
||||
| **L5** | Supprimer `core/detection/seeclick_adapter.py` | ~300 | pytest + vérifier __init__.py fallback | Zéro |
|
||||
| **L6** | Supprimer `server_v1/vm_controller.py` + `visual_wait.py` | ~200 | pytest | Zéro |
|
||||
| **L7** | Supprimer `agent_v1/window_info.py` + `agent_v0/config.py` + `setup_v1.sh` | ~140 | pytest | Zéro |
|
||||
| **L8** | Nettoyer __init__.py exports morts dans core/ | ~50 lignes | pytest + import check | Faible |
|
||||
| **L9** | Supprimer `scripts/` dated Jan 2026 (~37 fichiers) | ~2500 | pytest (aucun script dans test path) | Zéro |
|
||||
| **L10** | Supprimer `server/api_upload_dev_8001/8002.py` | ~300 | pytest + vérifier services.conf | Faible |
|
||||
| **L11** | Supprimer `core/gpu/` morts (3 fichiers) | ~200 | pytest | Faible |
|
||||
| **L12** | Supprimer `core/auth/manage_vault.py` + `execution/workflow_runner.py` | ~240 | pytest | Faible |
|
||||
|
||||
**Total** : 12 lots, ~4580 lignes, risque zéro à faible. Chaque lot précédé de GO Dom + QG post-lot.
|
||||
|
||||
---
|
||||
|
||||
## Liste NE PAS TOUCHER
|
||||
|
||||
- Runtime démo `Urgence_aiva_demo` (VWB → backend → agent_v1 → Léa → Easily)
|
||||
- Services systemd DGX (10 services actifs)
|
||||
- Installateur clinique (`deploy/installer/`, `deploy/lea_package/`)
|
||||
- `api_stream.py` (7747 lignes, serveur principal)
|
||||
- `stream_processor.py` (6085 lignes, orchestrateur)
|
||||
- `resolve_engine.py` (résolution anchors)
|
||||
- `replay_engine.py` (replay actions)
|
||||
- `learned_workflow_bridge.py` (pont VWB ↔ core)
|
||||
- `core/competences/` (verdicts, promotions, persist, catalog — wired)
|
||||
- `core/federation/` (GlobalFAISSIndex, LearningPack — wired)
|
||||
- `core/embedding/` (clip, faiss, state, fusion — wired)
|
||||
- `core/execution/` (observe_reason_act, target_resolver, input_handler — wired)
|
||||
- `core/healing/healing_engine.py` + `execution_integration.py` — wired
|
||||
- `core/analytics/analytics_system.py` + `screen_change_detector.py` — wired
|
||||
- `core/workflow/` (semantic_matcher, variable_manager, execution_plan, shadow_observer — wired)
|
||||
46
docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md
Normal file
46
docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Plan de migration — persistance workflows (JSON → SQLAlchemy)
|
||||
|
||||
- `Date`: 2026-06-09
|
||||
- `Auteur`: Claude (proposition, validation Dom requise)
|
||||
- `Statut`: PLANIFIÉ — **post-POC, aucun refactor engagé**
|
||||
- `Réf dette`: DETTE-015 (docs/DETTE_TECHNIQUE.md)
|
||||
- `Priorité`: P2 (fragilité réelle, contournée proprement pour le POC par symlink)
|
||||
|
||||
## Constat
|
||||
|
||||
Trois stockages de workflows coexistent, sans source de vérité unique :
|
||||
|
||||
| Store | Emplacement | Utilisé par | État |
|
||||
|-------|-------------|-------------|------|
|
||||
| Fichiers JSON | `visual_workflow_builder/backend/data/workflows/*.json` | route API VWB `/api/workflows/` (`api/workflows.py:53`, **relatif au cwd**) | **source réelle** (42 fichiers) |
|
||||
| DB SQLAlchemy | `…/backend/instance/workflows.db`, table `workflows` + Alembic | composants SQLAlchemy (`db.models`) | propre mais **pas lue par la route** (23 lignes) |
|
||||
| JSON legacy | `data/training/workflows/` | dashboard `web_dashboard` (`app.py:187-189`) | vide partout |
|
||||
|
||||
### Problèmes
|
||||
1. **Pas de source unique** → divergences (les 42 JSON ≠ 23 lignes DB).
|
||||
2. **Résolution par cwd** → le bug P0-1 du 2026-06-09 (dev cwd=backend OK ; DGX cwd=racine = 0 workflows).
|
||||
3. Pas d'écriture atomique ni de validation de schéma côté JSON.
|
||||
4. Confusion : 3 emplacements, dont un legacy mort.
|
||||
|
||||
## Cible
|
||||
|
||||
**Unifier sur la DB SQLAlchemy déjà présente** (infra à moitié en place : table + Alembic). La route API lit/écrit la DB ; le store fichier JSON et le legacy sont retirés. Bénéfices : source unique, transactions, intégrité, requêtes, fin de la dépendance au cwd.
|
||||
|
||||
> Note : un store fichier n'est pas disqualifiant en soi ; c'est la **coexistence non synchronisée** + la dépendance au cwd qui posent problème. On choisit SQLAlchemy car l'infra existe déjà (vs fiabiliser le JSON, qui laisserait le double store).
|
||||
|
||||
## Plan (TDD, post-POC, validation Dom entre étapes)
|
||||
|
||||
1. **Audit d'usage** : recenser tous les appels à `WorkflowDatabase` (lecture **et** écriture) — API, moteur d'exécution, agent, frontend VWB. Cartographier le contrat (méthodes `list/load/save/delete`).
|
||||
2. **Repository SQLAlchemy** : implémenter un `WorkflowRepository` exposant le **même contrat** que `WorkflowDatabase`, adossé à la table `workflows` (réutiliser `db.models`). Tests unitaires CRUD.
|
||||
3. **Script de migration** : importer les 42 JSON → table `workflows` (idempotent, backup DB avant). Vérifier parité (42 JSON ↔ N lignes, diff de contenu).
|
||||
4. **Bascule de la route** derrière un flag (`RPA_WORKFLOWS_BACKEND=sqlalchemy|json`, défaut `json`) → tests d'équivalence API (mêmes réponses qu'avant).
|
||||
5. **Bascule par défaut** sur SQLAlchemy une fois la parité prouvée ; retrait du symlink (DETTE-015).
|
||||
6. **Nettoyage** : retirer le store legacy `data/training/workflows` (dashboard) ou le rebrancher sur le repository ; supprimer `WorkflowDatabase` quand plus aucun appelant.
|
||||
|
||||
## Préconditions / risques
|
||||
- **Ne pas engager avant la fin du POC** (refactor de persistance = risque pour la démo).
|
||||
- Touche frontend VWB + agent + moteur d'exécution → bascule progressive sous flag obligatoire.
|
||||
- Le symlink (DETTE-015) reste le contournement stable jusqu'à la migration.
|
||||
|
||||
## Effort estimé
|
||||
~1,5–2,5 j en TDD (audit + repository + migration + tests d'équivalence + bascule).
|
||||
103
docs/PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md
Normal file
103
docs/PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Plan de remise au carré — chaîne d'apprentissage & rejeu de Léa
|
||||
|
||||
- `Date`: 2026-06-27
|
||||
- `Auteur`: Claude (mandat Dom)
|
||||
- `Statut`: actif — formalise l'analyse du 27/06 ; **n'invente rien**, chapeaute `PLAN_ACTION_SUITE_2026-06-23` (axe « Rejeu intelligent ») et `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16`.
|
||||
- `Contrainte cardinale (Dom 27/06)`: **câbler la chaîne ET rendre Léa correcte AVANT la dernière manip manuelle de Dom** (= avant la perte de l'accès hands-on / on-site). Cette contrainte est la **barre d'acceptation** de ce plan. *(À préciser : quoi exactement, et quelle date.)*
|
||||
|
||||
---
|
||||
|
||||
## 1. Pourquoi ça ne marche pas (vérifié le 27/06)
|
||||
|
||||
**En une phrase : la chaîne a été diagnostiquée (16/06), décidée (23/06) et planifiée (R1→R7), puis la livraison clinique a absorbé les 2 semaines — le câblage end-to-end n'a jamais démarré. Le blocage est d'EXÉCUTION, pas de décision.**
|
||||
|
||||
Faits vérifiés (grep/fichier, ce jour) — la chaîne n'est pas câblée au runtime :
|
||||
|
||||
| Maillon | Preuve vérifiée 27/06 | Effet |
|
||||
|---|---|---|
|
||||
| Worker d'enrichissement | `data/training/_worker_queue.txt` = **0 octet, mtime 11/06** | enrichissement **à l'arrêt depuis 16 j** |
|
||||
| Import auto session→workflow | `ShadowLearningHook()` instancié **uniquement dans son propre fichier**, jamais dans `finalize`/runtime | l'appris **n'est pas** rendu rejouable sans geste manuel |
|
||||
| Lecture du fonds au rejeu | `GlobalFAISSIndex.search()` → **grep = 0 appel** ; `TargetResolver.lookup()` jamais appelé par `replay-session` | rejeu = **coords figées**, pas cible apprise |
|
||||
| Dé-silo | filtre `machine_id` à **5 points** de `stream_processor.py` (3199, 3285, 4499, 4530, 5062) | apprentissage **siloté par poste** |
|
||||
|
||||
Causes racines (au-delà du « c'est débranché ») :
|
||||
1. **Les 2 semaines = infra de survie** (portage DGX ARM, installateur EXE, réseau/firewall/VPN, reprise panne secteur + reboot, watchdog OVMF, enrôlement, streaming, push-log). Indispensable pour livrer — mais **0 h sur le câblage de la boucle**.
|
||||
2. **Découpage par composant, jamais par boucle.** Hook, FAISS, TargetMemory bâtis et validés isolément (sessions/agents différents). **Personne n'a possédé ni testé la boucle entière sur une vraie session** → trous accumulés à chaque soudure, invisibles.
|
||||
3. **Le raccourci démo est devenu permanent.** Le rejeu `Urgence_aiva` marchait en coords figées + templating `{{var}}` (seule pièce câblée). « Ça marchait » → le chemin intelligent n'a **jamais été allumé**.
|
||||
|
||||
⚠️ **À confirmer en amont (audit runtime Qwen, NON vérifié indépendamment)** : « **11/15 postes heartbeat-only, 0 % résolution vision/OCR/anchors** ». Si avéré, le premier problème n'est pas l'apprentissage mais que **les postes ne font pas encore le geste du POC** (pas de vraie capture, cascade vision non exercée). → **à prouver, chiffré, AVANT tout recâblage** (Phase 0).
|
||||
|
||||
---
|
||||
|
||||
## 1bis. CARTE DE CÂBLAGE VÉRIFIÉE (28/06 — 3 agents read-only, sourcé code)
|
||||
|
||||
> Cette section **corrige** le §1 sur deux points (diagnostic affiné, moins grave qu'annoncé).
|
||||
|
||||
**Deux corrections vérifiées (mes affirmations antérieures étaient fausses) :**
|
||||
1. **« sessions → squelettes sans action » = FAUX.** Les actions (clics/saisies) sont attachées au workflow sur les **edges** (`WorkflowEdge.action`, `graph_builder.py:1457`), pas sur `node.variants` (qui = variantes *visuelles* d'écran, champ non peuplé au runtime). 48/71 workflows auto-appris portent leurs actions ; 23 sont vides (sessions trop courtes).
|
||||
2. **« rejeu = coords figées » = FAUX.** Léa **résout chaque cible par la vue** à chaque rejeu (cascade OCR→template→YOLO→VLM sur anchors, `resolve_engine.py:1804`). Coords = fallback ultime seulement. Conforme 100 % vision.
|
||||
|
||||
**Ce qui MARCHE** : capture→workflow avec actions ; worker traite ; 36/71 atteignent `AUTO_CANDIDATE` ; rejeu visuel (VWB-DB 226 `click_anchor` + JSON auto-appris non vides) ; **R2 à moitié branché** (`TargetMemoryStore` consulté en tête de `_resolve_target_sync`, `resolve_engine.py:1862`).
|
||||
|
||||
**Les 4 vrais trous (sourcés) :**
|
||||
|
||||
| # | Trou | Preuve | Type |
|
||||
|---|---|---|---|
|
||||
| **1 (P0)** | **11/15 postes n'enregistrent rien** | démarrage capture 100 % manuel (clic TIM « Apprenez-moi », `smart_tray.py:349` / `chat_window.py:1716`) ; heartbeats auto (`main.py:378`). Risque : dialogue consentement `Tk()` (`smart_tray.py:54`) invisible en RDP/Citrix `pythonw` | **amont / UX (à confirmer bug vs usage par logs client)** |
|
||||
| 2 | **Apprentissage incrémental débranché** | `LearningManager` non instancié serveur ; mute `WorkflowStats` mémoire non re-persisté ; `record_observation` (`learning_manager.py:54`) **0 appelant**. Seul `GraphBuilder` écrit `learning_state`, fige sur OBSERVATION si qualité faible (`graph_builder.py:400`) | promotion jamais déclenchée |
|
||||
| 3 | **2 mondes disjoints** : JSON auto-appris ≠ DB VWB rejouable | stores/loaders/matchers séparés ; une session apprise ne devient pas un workflow DB rejouable | = **R1** (pont JSON→DB) |
|
||||
| 4 | **Fonds commun jamais lu au rejeu** | `GlobalFAISSIndex.search()` = 0 appel (seul `add_pack` écrit) | = **R3** (FAISS au rejeu) |
|
||||
|
||||
**Points d'insertion confirmés** : R1 = worker `_process_session` après `_persist_workflow` (réutiliser `import_learned_workflow`/`learned_workflow_bridge`, idempotence par `workflow_trajectory_signature` existante). R2/R3 = `resolve_engine.py:1862-1878` (élargir `memory_lookup` + insérer `GlobalFAISSIndex.search()`).
|
||||
|
||||
**Priorité (contrainte « Léa correcte avant dernière manip ») : #1 (amont) d'abord** — si 11/15 postes ne capturent pas, l'aval est sans objet. Test décisif = grep logs client `"Session … en cours"` vs `"Session finalisée"` (Qwen).
|
||||
|
||||
---
|
||||
|
||||
## 2. État des décisions (rappel — la plupart sont déjà prises)
|
||||
|
||||
Tranchées le 23/06 (`DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23.md`) → **on exécute, on ne re-décide pas** :
|
||||
- **F2-1/F14-1** : rejeu intelligent = **OUI, prérequis** (consulter le fonds appris, pas de coords figées).
|
||||
- **F1-1** : critère de fusion = **signature de trajectoire**. ~~create-or-update~~ → **create-or-skip** (révisé Dom 2026-07-02 : un ré-apprentissage ne doit PAS écraser un workflow validé par revue humaine ; la 1ʳᵉ version importée fait foi. Refresh explicite = chantier séparé si besoin. Implémenté ainsi dans `learned_workflow_bridge.py`, cf. `CARTO_CODE_NON_BRANCHE_2026-07-02.md`).
|
||||
- **F9-1** : **DB = vérité**, JSON = échange ; métrique = workflows rejouables validés.
|
||||
- **F6-1** : mutualisation **cross + intra-clinique** (fédération anonymisée dans le périmètre + lever silo `machine_id`).
|
||||
|
||||
**Reste ouvert (1 seule, vraie décision) :**
|
||||
- **Q-F2-2 — Provider Léa au runtime** : quel modèle/route sert la **résolution** au rejeu. ⚠️ Gap tracé 23/06 : le point d'entrée actif = **agent_chat 5004 → `SemanticMatcher.find_workflow()` sur fichiers JSON**, pas la DB → **contredit F9-1**. Se résout **en chemin** à la Phase 2 (où la résolution est recâblée). Reco modèle : Qwen3-VL-4B grounder + gemma4 (bench 13/06).
|
||||
|
||||
**Décisions potentiellement induites par la Phase 0** : si « 0 % vision » confirmé, une décision « comment forcer/garantir la capture vision réelle sur poste » surgira (priorité absolue, avant R1).
|
||||
|
||||
---
|
||||
|
||||
## 3. Plan d'exécution (séquencé, chirurgie supervisée)
|
||||
|
||||
> Les chantiers R1→R7 détaillés sont dans `PLAN_ACTION_SUITE_2026-06-23` (§ Axe central) — **non dupliqués ici**. Ce plan ajoute la **Phase 0 de mesure** (nouvelle) et l'**ordre/critères**.
|
||||
|
||||
**Phase 0 — MESURER (avant tout recâblage).** Établir la vérité terrain : par poste, nb de **vraies sessions**, la **cascade vision est-elle déclenchée** (compteur de résolutions par méthode), captures reçues, état queue. **C'est ce que push-log + une télémétrie vision apportent** (lien direct avec briques 1-4 livrées). → *Mission Qwen (accès runtime DGX).* **Critère de sortie : on sait, chiffré, ce que font les 15 postes.**
|
||||
|
||||
**Phase 1 — RECONNECTER L'AMONT (R1).** Import auto session→workflow post-`finalize` + **relancer le worker** (queue morte 11/06). *Critère : une session TIM réelle devient un workflow rejouable sans geste manuel.*
|
||||
|
||||
**Phase 2 — RECONNECTER L'AVAL (R2+R3) + résoudre Q-F2-2.** Câbler `TargetResolver.lookup()` + lecture FAISS au rejeu, **fallback obligatoire sur coords figées** (non négociable Qwen — enrichir, pas casser) ; aligner le point d'entrée résolution sur la DB (F9-1). *Critère : Léa résout par cible apprise, retombe sur coords si échec.*
|
||||
|
||||
**Phase 3 — BOUCLE + DÉ-SILO (R4/R5/R6).** verify post-condition (échec → pause supervisée), réécriture du fonds, lever silo `machine_id` + brancher fédération (`GlobalFAISSIndex.search()`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Gouvernance (corrige la cause racine #2)
|
||||
|
||||
- **Un seul propriétaire de la boucle entière** (pas un découpage par composant).
|
||||
- **Critère d'acceptation = un test end-to-end sur une session réelle**, pas une validation par brique.
|
||||
- Chirurgie itérative supervisée : un maillon = un test ≤ 2 min = GO Dom ; démo `Urgence_aiva` intacte à chaque étape ; reconfirmer le wiring runtime avant chaque modif (imports lazy = verdicts « orphelin » non fiables).
|
||||
- **Merge prod supervisé Dom.**
|
||||
|
||||
## 5. Lien avec la « dernière manip manuelle » (deadline)
|
||||
|
||||
La contrainte de Dom fait de ce plan un **chemin critique** : tant que Léa n'est pas correcte (au moins Phase 0 + Phase 1-2 sur 1 poste pilote), **la dernière manip manuelle ne doit pas avoir lieu** — sinon plus d'accès hands-on pour réparer. → **Définir avec Dom : quelle est cette manip, et sa date butoir**, pour caler le séquencement.
|
||||
|
||||
## 6. Première action concrète
|
||||
|
||||
**Phase 0 confiée à Qwen** (chiffres runtime). Doc + page décisions = ce fichier. Reste : GO Dom sur le séquencement + définition de la deadline « dernière manip ».
|
||||
|
||||
---
|
||||
|
||||
*Plans sources (ne pas dupliquer) : `PLAN_ACTION_SUITE_2026-06-23`, `CARTO_APPRENTISSAGE_FONDS_COMMUN_2026-06-16`, `DECISIONS_PRODUIT_EN_ATTENTE_2026-06-23`, `PLAN_CHANTIER_UNIFICATION_LEA_VWB_2026-06-17`.*
|
||||
74
docs/POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20.md
Normal file
74
docs/POSTMORTEM_PANNE_SECTEUR_DGX_2026-06-20.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Post-mortem — Panne secteur DGX 2026-06-20 (test reboot non planifié)
|
||||
|
||||
- `Auteur`: Claude (infra)
|
||||
- `Date`: 2026-06-20 ~03:00 CEST
|
||||
- `Scope`: La coupure électrique réelle de 02:07 traitée comme **exécution non planifiée du test de stabilité reboot** (`CHECKLIST_DGX_PRE_CLINIQUE.md` §6). Objectif : mesurer ce qui se rétablit **seul** vs ce qui exige une **intervention** — c'est le critère clé pour une clinique sans personnel technique sur place.
|
||||
- `Méthode`: diagnostic read-only multi-agents (Claude infra, Qwen appli/guest, Codex consolidation). Aucun reset Git, aucun changement réseau hors IP statique arbitrée par Dom.
|
||||
|
||||
---
|
||||
|
||||
## 1. Timeline
|
||||
|
||||
| Heure | Événement |
|
||||
|---|---|
|
||||
| 02:07 | Coupure secteur. DGX reboot. Poste Dom (Linux) reboot. Laptop Windows `.11` (sur batterie) jamais coupé. |
|
||||
| 02:07:42 | `win11-arm-lea.service` (user) auto-démarre la VM. |
|
||||
| ~02:09 | Watcher coordination (systemd user) revenu seul. Services rpa système remontés. |
|
||||
| 02:07→02:18 | VM bloquée en boucle TianoCore (QEMU 99% CPU, guest agent/SSH absents). |
|
||||
| 02:18 | Codex diagnostique : `OVMF_VARS.fd` corrompu par coupure brutale. |
|
||||
| 02:21 | Codex restaure OVMF connu-bon (18/06) + TPM frais → VM boot prouvé (écran verrouillage Windows). |
|
||||
| ~02:28 | Claude : DGX revenu en DHCP sur `.46` (au lieu de `.45`) → IP statique `.45` appliquée (décision Dom). |
|
||||
| ~02:35 | Accès VM Dom rétabli (tunnel + VNC, mot de passe OK). |
|
||||
| ~02:55 | Crash-loop `dashboard-user` éteint (Qwen). Revue infra/appli consolidée. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Ce qui s'est rétabli SEUL (✅ socle solide)
|
||||
|
||||
| Domaine | Constat | Réf checklist |
|
||||
|---|---|---|
|
||||
| Boot DGX + services rpa | Tous `active+enabled` (dashboard, streaming, agent-chat, vwb back/front, api, worker, stream-worker, **vllm-grounder**, firewall) | §1 PASS |
|
||||
| Firewall | Réappliqué : 5900/5902/3389/22220/8000/11434 **filtrés LAN**, seuls 5001/5002/5004/5005 ouverts | §2 PASS (fort) |
|
||||
| Auth | Dashboard 401, VWB 401 (basic auth), streaming Bearer | §3 majoritaire PASS |
|
||||
| Auto-start VM | `win11-arm-lea.service` a bien démarré la VM (linger=yes) | §4.1 — prouvé (était « à implémenter ») |
|
||||
| Coordination | Watcher couche-1 (systemd user) revenu seul | — |
|
||||
|
||||
→ **Le socle infra/services/sécurité survit à une coupure brutale sans intervention.**
|
||||
|
||||
## 3. Ce qui a EXIGÉ une intervention (⚠️ gaps reprise non-assistée)
|
||||
|
||||
| # | Problème | Cause | Correctif (qui) | Risque clinique |
|
||||
|---|---|---|---|---|
|
||||
| G1 | **DGX IP a dérivé `.45`→`.46`** | bail DHCP après reboot | IP statique `.45` (Claude/Dom) | **Élevé** — casse tous clients/tunnels pointant `.45`. DHCP non fiable. |
|
||||
| G2 | **VM bloquée TianoCore** | `OVMF_VARS.fd` corrompu (coupure brutale) | restore OVMF connu-bon + TPM frais (Codex) | **Élevé** — sans agent, VM morte jusqu'à intervention manuelle. |
|
||||
| G3 | **`dashboard-user` crash-loop** (244 restarts) | fallback user clash port 5001 (service système le sert déjà) | stop + mask session (Qwen) | Moyen — bruit/ressources ; `disabled` mais relancé. |
|
||||
| G4 | **Léa guest non reconnectée** | `config.txt` = `CONFIGURE_ME` + login Windows requis | à renseigner `.45`+token (Qwen) | **Élevé** — VM redémarre mais Léa ne reprend pas le travail seule. |
|
||||
| G5 | **Mot de passe VNC** | `-vnc password=on` sans `set_password` dans les scripts | rétabli de fait (tunnel) | Faible/Moyen — fragile si relaunch sans repose mdp. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommandations de durcissement — reprise CLINIQUE non-assistée
|
||||
|
||||
> Toutes **modifiantes → à valider par Dom** (mises en file, non appliquées cette nuit).
|
||||
|
||||
1. **BIOS DGX = « Power On » au retour AC** (physique, Dom) — sinon une coupure laisse le DGX éteint.
|
||||
2. **IP statique** : fait au labo (`.45`) ; cible clinique = Ethernet statique `.178` (DHCP = point faible démontré par G1).
|
||||
3. **Auto-réparation OVMF** dans `win11-arm-lea.service` : au boot Windows réussi, snapshot `OVMF_VARS.fd` sain ; `ExecStartPre` : si boucle TianoCore détectée (CPU 99% + guest agent absent N s), restaurer le snapshot sain automatiquement. → neutralise G2 sans agent.
|
||||
4. **`rpa-vision-v3-dashboard-user`** : `mask` **persistant** (pas seulement session) — G3.
|
||||
5. **Léa reprise auto** (G4) : `config.txt` persistant vers IP DGX + token ; auto-login Windows + Léa auto-start (`pythonw`) + reconnexion fleet sans geste humain.
|
||||
6. **Mot de passe VNC** (G5) : poser le mot de passe au lancement via le monitor (script), ou documenter la procédure de repose.
|
||||
|
||||
---
|
||||
|
||||
## 5. Propositions de MAJ pour `CHECKLIST_DGX_PRE_CLINIQUE.md` (Qwen, propriétaire)
|
||||
|
||||
- §4.1 « auto-start VM » : passer **À IMPLÉMENTER → VALIDÉ** (prouvé par la panne, 02:07:42).
|
||||
- §1.10 / Items à fixer #1 : dashboard service **système actif** confirmé ; le fallback user est l'orphelin → masquer.
|
||||
- §6 « Test reboot » : **exécuté en réel le 2026-06-20** → renseigner les résultats (col. Résultat) à partir des sections 2 et 3 ci-dessus.
|
||||
- Ajouter une ligne **G1 dérive IP DHCP** et **G2 corruption OVMF** comme items de durcissement explicites.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verdict test
|
||||
|
||||
Le **socle technique tient** (services, firewall, auth, auto-start VM). Les **deux points durs** pour une clinique sans technicien sur place sont **G1 (dérive IP DHCP)** et **G2 (corruption OVMF VM non auto-réparée)** : tous deux ont nécessité un agent cette nuit. La cible clinique doit les **automatiser** (IP statique Ethernet + auto-réparation OVMF). G4 (Léa ne reprend pas seule) est le troisième chantier reprise.
|
||||
135
docs/QG_REVIEW_D1_NAVIGATE_COORDS_2026-07-02.md
Normal file
135
docs/QG_REVIEW_D1_NAVIGATE_COORDS_2026-07-02.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# QG Review Framework — D1 NavigateCoords Patch
|
||||
|
||||
**Auteur**: Qwen
|
||||
**Date**: 2026-07-02
|
||||
**Statut**: EN ATTENTE patch Codex
|
||||
**Scope**: Review du patch D1 (Option 1 — Compiler Injection) produit par Codex
|
||||
|
||||
---
|
||||
|
||||
## Baseline test coverage (pré-patch)
|
||||
|
||||
| Fichier | Classes | Tests | Rôle |
|
||||
|---------|---------|-------|------|
|
||||
| `test_navigate_handler_e2e.py` | 4 | 8 | Handler mock — nominal, OCR miss, no screenshot, never-fail |
|
||||
| `test_navigate_wiring.py` | 4 | 5 | Import/wiring non-regression |
|
||||
| `test_action_resolver.py` | 6 | 10 | NavigateCoords, NavigateResult, grounded_to_coords, navigate_login |
|
||||
| `test_coords_consumption_gap.py` | 3 | 10 | **GAP DOCUMENTATION** — résolution viable, compiler gap, navigate→[] |
|
||||
| **Total** | **17** | **33** | |
|
||||
|
||||
**Tests critiques à mettre à jour après D1 patch**:
|
||||
- `test_coords_consumption_gap.py::test_navigate_action_type_unknown` — affirme actuellement `actions == []`; doit affirmer `len(actions) >= 1` et `actions[0]["type"] == "navigate"` après D1
|
||||
- `test_coords_consumption_gap.py::TestCompilerGapLiteralFloats` — 4 tests documentant le gap literal-floats; après D1, les tests coords_var doivent affirmer templates strings ARE produites quand coords_var présent
|
||||
|
||||
**Point d'insertion exact D1**:
|
||||
- Fichier: `replay_engine.py`
|
||||
- Entre `elif action_type == "llm_generate"` (retourne `[normalized]` ~L1949) et `else:` clause (~L1953)
|
||||
- Navigate branch: `elif action_type == "navigate"` → `normalized["type"] = "navigate"` + parameters dict → `return [normalized]`
|
||||
|
||||
**P1-C root cause**:
|
||||
- `_resolve_runtime_vars_in_str` (L2025): `return str(value)` — tout {{var.field}} résolu devient string "0.35" pas float 0.35
|
||||
- Coercion helper `_coerce_action_coords` doit agir APRÈS `_resolve_runtime_vars` (L4335), AVANT `type_ = action.get("type")` (L4337)
|
||||
|
||||
---
|
||||
|
||||
## Critères de review — Checklist
|
||||
|
||||
### C1 : Branche navigate dans `_edge_to_normalized_actions` (Gap C)
|
||||
|
||||
| # | Critère | GO | NOGO |
|
||||
|---|---------|----|------|
|
||||
| C1-1 | Branche `elif action_type == "navigate"` ajoutée entre `llm_generate` (L1949) et `else` (L1951) | Present, position correcte | Absente ou mal positionnée |
|
||||
| C1-2 | `normalized["type"] = "navigate"` | Oui | Type incorrect |
|
||||
| C1-3 | Parameters dict avec `login_coords_var`, `password_coords_var`, `submit_coords_var` | Noms exacts, valeurs default | Noms divergent ou absents |
|
||||
| C1-4 | Retourne `[normalized]` (1 action serveur-side) | `[normalized]` | `[]` ou autre |
|
||||
| C1-5 | Test TR-1 : `test_navigate_action_type_unknown` mis à jour — affirme `len(result) >= 1` et `result[0]["type"] == "navigate"` | Test updated + passes | Test non mis à jour ou fails |
|
||||
|
||||
### C2 : coords_var dans branches mouse_click / text_input (Gap A)
|
||||
|
||||
| # | Critère | GO | NOGO |
|
||||
|---|---------|----|------|
|
||||
| C2-1 | `coords_var = action_params.get("coords_var")` check dans mouse_click | Present | Absent |
|
||||
| C2-2 | Si coords_var → `x_pct = f"{{{{{coords_var}.x_pct}}}"` et `y_pct = f"{{{{{coords_var}.y_pct}}}"` | Template strings correctes | Syntaxe template incorrecte ou .y_pct pour x_pct |
|
||||
| C2-3 | Si coords_var absent → literal floats comme avant (fallback existant) | Branch else intacte | Branch else modifiée ou supprimée |
|
||||
| C2-4 | `normalized["coords_var"] = coords_var` ajouté pour traçabilité | Oui | Absent |
|
||||
| C2-5 | Même mécanisme dans text_input branch | Identique à mouse_click | Absent ou divergent |
|
||||
| C2-6 | BUG vérifié : text_input x_pct template = `{{coords_var.x_pct}}` (pas `.y_pct` deux fois) | Correct | y_pct en double |
|
||||
|
||||
### C3 : `_coerce_action_coords()` helper (Gap B / P1-C)
|
||||
|
||||
| # | Critère | GO | NOGO |
|
||||
|---|---------|----|------|
|
||||
| C3-1 | Helper défini dans api_stream.py (pas replay_engine.py) | api_stream.py | Autre fichier |
|
||||
| C3-2 | Appel APRÈS `_resolve_runtime_vars` (L4335), AVANT `type_ = action.get("type")` (L4337) | Position correcte | Avant resolver ou après type_ check |
|
||||
| C3-3 | float pass-through : `isinstance(val, float) → continue` | Idempotent sur actions existantes | Pas de float check → conversion inutile |
|
||||
| C3-4 | string→float : `try: action[key] = float(val)` | Conversion correcte | Pas de try/except → crash possible |
|
||||
| C3-5 | Template non résolu → pause_for_human (pas fallback 0.0/0.0) | `val.startswith("{{") and val.endswith("}}")` → pause_for_human | Fallback 0.0/0.0 ou autre coords dangereux |
|
||||
| C3-6 | Conversion impossible → pause_for_human | ValueError/TypeError → pause_for_human | Exception non catchée |
|
||||
| C3-7 | `_skip_reason` documenté pour debug | Oui | Absent |
|
||||
| C3-8 | `safety_level = "high"` pour pause_for_human | Oui | Absent ou autre valeur |
|
||||
| C3-9 | Retourne action mutée (pas de new dict) | Mutation in-place | Copie → risque race |
|
||||
| C3-10 | Keys itérées = ("x_pct", "y_pct") uniquement | Pas de sur-itération | Autres keys modifiées |
|
||||
|
||||
### C4 : Never-fail contract
|
||||
|
||||
| # | Critère | GO | NOGO |
|
||||
|---|---------|----|------|
|
||||
| C4-1 | `_handle_navigate_action` ne lance jamais d'exception non catchée | Contract preserved | Nouvelle exception possible |
|
||||
| C4-2 | `_coerce_action_coords` ne lance jamais — tout cas couvert par try/except ou pause_for_human | Contract preserved | Exception possible |
|
||||
|
||||
### C5 : Limites de scope POC
|
||||
|
||||
| # | Critère | GO | NOGO |
|
||||
|---|---------|----|------|
|
||||
| C5-1 | Maximum 4 fichiers modifiés | ≤ 4 | > 4 |
|
||||
| C5-2 | Pas de changement schema VWB dans POC patch | Pas de modification VWB code | VWB code modifié |
|
||||
| C5-3 | Pas de nouvelle dépendance pip | 0 nouvelles deps | Nouvelle dep |
|
||||
| C5-4 | Pas de modification OmniParser wiring | `_omniparser_available = False` intact | Modifié |
|
||||
|
||||
### C6 : Test coverage
|
||||
|
||||
| # | Critère | GO | NOGO |
|
||||
|---|---------|----|------|
|
||||
| C6-1 | TR-1 : navigate compile à 1 action (pas []) | Passes | Fails |
|
||||
| C6-2 | TR-2 : coords_var template resolution + float conversion | Passes | Fails |
|
||||
| C6-3 | Test `_coerce_action_coords` : float pass-through | Passes | Absent |
|
||||
| C6-4 | Test `_coerce_action_coords` : string→float conversion | Passes | Absent |
|
||||
| C6-5 | Test `_coerce_action_coords` : template non résolu → pause_for_human | Passes | Absent |
|
||||
| C6-6 | Test `_coerce_action_coords` : conversion impossible → pause_for_human | Passes | Absent |
|
||||
| C6-7 | Test idempotence : action existante float non modifiée | Passes | Absent |
|
||||
| C6-8 | `pytest tests/unit/` passe en intégralité | 0 failures | ≥1 failure |
|
||||
|
||||
### C7 : Risques additionnels (3 identifiés dans PLAN_D1)
|
||||
|
||||
| # | Risque | Mitigation attendue | GO | NOGO |
|
||||
|---|--------|--------------------|----|------|
|
||||
| C7-1 | Résolution partielle (x_pct résolu, y_pct template) | `_coerce_action_coords` → pause_for_human si ANY key unresolved | Mitigation presente | Pas de mitigation |
|
||||
| C7-2 | Idempotence sur mouse_click existant | `isinstance(val, float) → continue` | Idempotent | Risque de double conversion |
|
||||
| C7-3 | Race condition sur variables dict partagé | BFS séquentiel garantit navigate→click ordre | Note dans code/doc | Pas de mention |
|
||||
|
||||
---
|
||||
|
||||
## Procédure de review
|
||||
|
||||
1. **Lire le patch** : `git diff` sur les fichiers modifiés par Codex
|
||||
2. **Vérifier chaque critère C1-C7** : GO/NOGO par ligne
|
||||
3. **Exécuter les tests** : `cd /home/dom/ai/rpa_vision_v3 && .venv/bin/python -m pytest tests/unit/ -x -v`
|
||||
4. **Produire le verdict** : Table GO/NOGO avec justification + verdict global
|
||||
|
||||
## Format verdict
|
||||
|
||||
```
|
||||
## QG Verdict — D1 NavigateCoords Patch
|
||||
|
||||
| Critère | GO/NOGO | Note |
|
||||
|---------|---------|------|
|
||||
| C1 | GO | Branche navigate correcte |
|
||||
| C2 | NOGO | BUG: y_pct en double dans text_input |
|
||||
| ... | ... | ... |
|
||||
|
||||
**Verdict global**: GO / NOGO (avec réserves listées)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Qwen — framework QG prêt, awaiting Codex patch pour exécution.*
|
||||
@@ -27,6 +27,9 @@ Spécification complète pour l'implémentation :
|
||||
### Autres Documents
|
||||
|
||||
- **`ROADMAP_RPA_100_VISION.md`** - Vision et roadmap du projet
|
||||
- **`INSTALLATION_MULTI_SITE.md`** - Guide installation POC/MVP/production et multi-etablissement
|
||||
- **`PLAN_ACTION_SUITE_2026-06-23.md`** - Plan d'action consolidé post-livraison clinique (chapeaute les plans existants ; axe central = rejeu intelligent des actions apprises)
|
||||
- **`PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md`** - Remise au carré de la chaîne apprentissage/rejeu : pourquoi elle n'est pas câblée (vérifié) + plan d'exécution Phase 0 (mesure) → R1-R6, contrainte « Léa correcte avant la dernière manip manuelle »
|
||||
|
||||
## 🎯 Par Où Commencer ?
|
||||
|
||||
|
||||
272
docs/README_PROPOSED_2026-07-02.md
Normal file
272
docs/README_PROPOSED_2026-07-02.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# RPA Vision V3 — Automatisation basée sur la compréhension visuelle des interfaces
|
||||
|
||||
> ⚠️ **Projet en phase POC clinique** — déployé mais **non production-ready**.
|
||||
> Voir [`docs/STATUS.md`](STATUS.md) pour l'état réel par module. Certaines
|
||||
> briques tournent en clinique de bout en bout, d'autres sont **codées mais
|
||||
> gated/dormantes** (activation supervisée par Dom uniquement).
|
||||
|
||||
*Dernière mise à jour : 2 juillet 2026 (brouillon proposé — remplace le README daté du 14 avril 2026)*
|
||||
|
||||
> **Note de relecture** : ce brouillon reflète la trajectoire mai→juillet 2026
|
||||
> (livraison DGX Spark, flotte clinique Wallerstein, PII, extraction, grounder
|
||||
> Qwen3-VL, installeur EXE, MAJ silencieuse). L'ancien README/STATUS s'arrêtait
|
||||
> au « premier replay Notepad du 13 avril 2026 » et ne reflétait plus la réalité.
|
||||
|
||||
## Intention
|
||||
|
||||
Automatiser des workflows métier hospitaliers par **compréhension sémantique
|
||||
de l'écran** plutôt que par coordonnées de clic fixes. Le système observe le
|
||||
TIM, reconstruit un graphe d'états de l'interface, et cherche à **rejouer
|
||||
intelligemment** la procédure en reconnaissant visuellement les éléments cibles
|
||||
— y compris quand l'UI change légèrement. Objectif produit : Léa **apprend** un
|
||||
parcours et le **généralise**, ce n'est **pas** du record-and-replay.
|
||||
|
||||
Terrain cible : postes hospitaliers hétérogènes (**vrais logiciels métier en
|
||||
mode web, RDP et Citrix**), TIM sur **2 écrans** → capture de la **fenêtre
|
||||
active**. C'est cette hétérogénéité qui justifie l'approche « 100 % vision ».
|
||||
|
||||
Contraintes fortes :
|
||||
- **100 % vision** : résolution de l'UI par la vue, pas par sélecteurs DOM/API.
|
||||
- **100 % local** : inférence sur GPU local (Ollama / vLLM). Aucun appel cloud
|
||||
dans le pipeline par défaut (passeport souverain santé).
|
||||
|
||||
> Historique : la maquette « Easily Assure » a servi de banc de test jusqu'à
|
||||
> mi-2026 ; elle est **abandonnée comme cible** (recadrage juin 2026). Ne plus
|
||||
> raisonner « Easily ».
|
||||
|
||||
## Architecture en couches
|
||||
|
||||
```
|
||||
RawSession (couche 0) — capture événements + screenshots (Agent V1 Windows)
|
||||
↓
|
||||
ScreenState (couche 1) — états d'écran à plusieurs niveaux d'abstraction
|
||||
↓
|
||||
UIElement (couche 2) — détection sémantique (OCR / template / VLM)
|
||||
↓
|
||||
State Embedding (couche 3) — fusion multi-modale + index FAISS
|
||||
↓
|
||||
Workflow Graph (couche 4) — nœuds, transitions, résolution de cibles par la vue
|
||||
```
|
||||
|
||||
### Topologie runtime réelle (POC clinique)
|
||||
|
||||
```
|
||||
[Léa — client Windows sur poste TIM] ~15-19 postes enrôlés
|
||||
│ (HTTPS/Bearer, sortant uniquement)
|
||||
▼
|
||||
[DGX Spark — 192.168.1.178 — serveur clinique unique]
|
||||
├─ Streaming server (5005) — sessions live, pipeline core, replay
|
||||
├─ Dashboard Fleet (5001) — enrôlement + build installeur + supervision
|
||||
├─ Agent Chat (5004)
|
||||
├─ VWB backend/frontend (5002/3002) — admin
|
||||
└─ Grounder VLM (Qwen-VL, vLLM/Ollama)
|
||||
```
|
||||
|
||||
Le serveur **n'initie jamais** de connexion vers les postes. Accès distant Dom
|
||||
via VPN clinique (Stormshield) + SSH cert ; déploiement code par scp/rsync
|
||||
(le DGX ne fait pas `git pull`).
|
||||
|
||||
## État des fonctionnalités (synthèse)
|
||||
|
||||
Le détail par module est dans [`docs/STATUS.md`](STATUS.md).
|
||||
Légende : **opérationnel** (utilisé en clinique, sans régression connue) /
|
||||
**alpha** (fonctionnel sur cas de référence, peu généralisé) /
|
||||
**gated** (codé + testé mais désactivé par défaut, activation supervisée) /
|
||||
**débranché** (code présent mais jamais appelé au runtime) /
|
||||
**en cours**.
|
||||
|
||||
**Opérationnel (tourne en clinique)**
|
||||
- Capture Windows (Agent V1) + streaming vers le DGX (JPEG + downscale, PII-safe).
|
||||
- Client Léa autonome : **installeur EXE Windows** (python-embed 3.12.8, sans
|
||||
Python système, install per-user sans UAC), enrôlement via dashboard Fleet,
|
||||
démarrage auto (raccourci Bureau optionnel à l'install). Version client
|
||||
**1.0.2** (upgrade-safe).
|
||||
- Résilience client RDP/Citrix : watchdog re-affiche le tray si Léa disparaît.
|
||||
- Streaming server FastAPI (5005). **Sessions live en mémoire**
|
||||
(`live_session_manager.py`) ; **persistances SQLite/JSONL** en place à côté :
|
||||
registre d'agents enrôlés, store de logs clients par `machine_id`, mémoire de
|
||||
résolutions (`replay_memory`), DB workflows VWB.
|
||||
- **Assainissement PII** couche 1 (regex + structurel, `pii_sanitizer.py`) :
|
||||
câblé au chokepoint `stream_event`, floute aussi les `focus_*`,
|
||||
`text_input` → token `[SAISIE]`. Déployé sur le DGX.
|
||||
- Enrôlement flotte : dashboard construit à la volée le ZIP Léa complet
|
||||
autoportant avec config (URL serveur / token / machine_id).
|
||||
- Grounding visuel : résolution **par la vue** au replay — stratégie **VLM-first**
|
||||
sur les éléments texte, ordre pré-compilé **OCR → template → VLM** (YOLO présent
|
||||
dans le code mais sans appelant au runtime). Les coords figées ne sont
|
||||
qu'ultime fallback.
|
||||
Grounder **Qwen3-VL** câblé (`RPA_GROUNDING_ENGINE=qwen3vl_vllm`) — activé en
|
||||
override runtime sur le DGX (défaut du repo = legacy Qwen2.5-VL).
|
||||
|
||||
**Alpha**
|
||||
- Construction de workflow graph depuis une session ; matching heuristique.
|
||||
- Replay E2E supervisé multi-étapes (bien au-delà du 1er succès Notepad d'avril).
|
||||
- Mode apprentissage : pause + demande d'aide humaine quand la résolution échoue
|
||||
(l'échec est un signal d'apprentissage, pas un stop en erreur).
|
||||
- Embeddings CLIP + index FAISS (construit ; **lecture au replay débranchée**).
|
||||
- **Extraction dossier patient** (`core/extraction/`) : orchestrateur
|
||||
OCR(valeurs) → VLM(rôles, ancré sur ids OCR = 0 hallucination) → qualité,
|
||||
persistance en DB VWB. Handler `extract_dossier` dispatché côté serveur.
|
||||
*Lecture écran→JSON prouvée sur vrai DPI urgences ; à confirmer en usage courant.*
|
||||
- **Navigation visuelle** (`core/navigation/`, nouveau) : login visuel, grounding
|
||||
OCR-first, `verify_before`/`verify_after` (vision = validateur). Handler
|
||||
`navigate` dispatché côté serveur. *Naissant — à éprouver.*
|
||||
- Web Dashboard (5001), Agent Chat (5004), module auth (Fernet + TOTP),
|
||||
federation (LearningPack, export/import).
|
||||
- Visual Workflow Builder (VWB) : catalogue d'actions, import d'anchors Léa,
|
||||
tests de compétence supervisés, Basic auth LAN. *Les bugs DB runtime
|
||||
historiques ont été largement corrigés ; durcissement encore en cours.*
|
||||
|
||||
**Gated (codé + testé, OFF par défaut — activation supervisée)**
|
||||
- **MAJ silencieuse client** (canary par poste, DETTE-022) : résolveur canary
|
||||
serveur + orchestrateur client implémentés et testés. `RPA_AUTO_UPDATE_ENABLED`
|
||||
OFF ; swap atomique + rollback **implémentés** (`Lea.bat`, marqueur
|
||||
`UPDATE_READY`) mais **jamais exercés en prod** — revue humaine obligatoire
|
||||
avant activation.
|
||||
- **Import auto de l'appris → DB VWB rejouable** (R1) : `RPA_R1_AUTO_IMPORT` OFF.
|
||||
- **Log shipper client** (remontée auto des logs vers le serveur) : gated OFF.
|
||||
- PII couche 2 (NER CamemBERT-bio, ONNX CPU côté DGX) : à embarquer, dormant.
|
||||
|
||||
**Débranché / en cours**
|
||||
- Chaîne apprentissage **non bouclée end-to-end** : R1 (pont JSON appris ↔ DB VWB)
|
||||
et R3 (lecture FAISS au replay) manquants ; apprentissage incrémental débranché ;
|
||||
savoir siloté par `machine_id`.
|
||||
- Self-healing / recovery global ; analytics / reporting.
|
||||
|
||||
## Limitations connues
|
||||
|
||||
- Le replay est validé sur un nombre restreint d'applications ; robustesse
|
||||
grounding encore un sujet ouvert (dette DETTE-006/010).
|
||||
- Chaîne apprentissage : capture→workflow marche, mais le **bouclage
|
||||
observation→rejeu généralisé n'est pas câblé** (R1/R3). FAISS construit mais
|
||||
jamais lu au replay.
|
||||
- **Combien de postes exercent réellement le geste complet est non vérifié**
|
||||
(Phase 0 de mesure en attente). Postes possiblement muets sous RDP/Citrix.
|
||||
- PII : couche 1 déployée ; couche 2 (NER) dormante. Historique de données
|
||||
capturées en clair — décision purge/assainissement en attente.
|
||||
- Asymétrie connue : VWB direct utilise un détecteur d'UI au recording que le
|
||||
replay sur Léa n'utilise pas (sujet ouvert post-POC, à ne pas « fixer » là).
|
||||
- 🔴 `POST /api/v3/execute/instruction` pilote l'écran X11 de l'hôte **sans
|
||||
sandbox/kill-switch** ; atteignable sur LAN clinique. Chantier sandbox (F8.4)
|
||||
identifié comme prioritaire.
|
||||
|
||||
## Démarrage
|
||||
|
||||
### Prérequis
|
||||
|
||||
- Python 3.10 à 3.12
|
||||
- Serveur : GPU NVIDIA local. Cible clinique = **DGX Spark** (GB10, mémoire
|
||||
unifiée, ARM64, headless). Alternative dev = x86 RTX 5070.
|
||||
- LLM local : **Ollama** (`:11434`) et/ou **vLLM** (grounder Qwen3-VL).
|
||||
- Windows 10/11 pour le client Agent V1.
|
||||
|
||||
### Installation (serveur / dev Linux)
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv # venv unique du repo (svc.sh + unités systemd utilisent .venv)
|
||||
source .venv/bin/activate # (le DGX historique utilise venv_v3 ; réfs venv_v3 du Makefile = périmées en local)
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Ollama local + modèle VLM
|
||||
ollama serve &
|
||||
ollama pull gemma4:latest # modèle VLM par défaut du repo (core/config.py, .env.example)
|
||||
# Le grounder Qwen3-VL est servi par vLLM, pas par un pull Ollama — voir docs/STATUS.md
|
||||
|
||||
cp .env.example .env # ajuster RPA_VLM_MODEL, VLM_ENDPOINT, ports
|
||||
```
|
||||
|
||||
### Lancer les services
|
||||
|
||||
Services pilotés par `svc.sh` (source de vérité : `services.conf`).
|
||||
|
||||
```bash
|
||||
./svc.sh status
|
||||
./svc.sh start boot # groupe nominal (variantes : full, vwb, ou un service seul)
|
||||
./svc.sh start streaming # streaming server seul (5005)
|
||||
./svc.sh stop boot
|
||||
```
|
||||
|
||||
| Port | Service |
|
||||
|---|---|
|
||||
| 8000 | API Server (upload / traitement core) |
|
||||
| 5001 | Web Dashboard / Fleet (enrôlement, supervision) |
|
||||
| 5002 | VWB Backend (Flask) |
|
||||
| 5003 | Monitoring |
|
||||
| 5004 | Agent Chat |
|
||||
| 5005 | Streaming Server (Agent V1 → pipeline core) — canal principal Léa |
|
||||
| 5006 | Session Cleaner |
|
||||
| 5099 | Worker VLM (analyse sessions → workflows JSON) — lancé avec le groupe boot |
|
||||
| 3002 | VWB Frontend (Vite/React) |
|
||||
|
||||
### Client Windows (Agent V1) — déploiement clinique
|
||||
|
||||
Le client capture souris/clavier/écran et envoie au serveur (sortant uniquement).
|
||||
Déploiement recommandé via le **dashboard Fleet** (enrôlement + ZIP autoportant),
|
||||
puis `Installer-Lea.bat` sur le poste (installeur EXE Python-embedded).
|
||||
|
||||
```bash
|
||||
# Build du ZIP complet autoportant (python-embed + source à jour)
|
||||
./deploy/build_package_full.sh
|
||||
# Build de l'installeur EXE (Inno Setup, per-user)
|
||||
./deploy/installer/build_installer.sh
|
||||
```
|
||||
|
||||
## Arborescence du dépôt
|
||||
|
||||
```
|
||||
rpa_vision_v3/
|
||||
├── agent_v0/
|
||||
│ ├── agent_v1/ # Client Windows (capture, tray, MAJ, PII-safe logs)
|
||||
│ └── server_v1/ # FastAPI streaming + pipeline (replay, resolve, PII, extraction)
|
||||
├── core/
|
||||
│ ├── detection/ # Cascade OCR / template / YOLO / VLM
|
||||
│ ├── embedding/ # CLIP + FAISS
|
||||
│ ├── graph/ # Workflow graphs
|
||||
│ ├── extraction/ # Lecture écran→JSON (OCR→VLM→qualité) [nouveau]
|
||||
│ ├── navigation/ # Login visuel, grounding, verify [nouveau]
|
||||
│ ├── execution/, learning/, auth/, federation/, gpu/
|
||||
├── server/ # API upload / traitement core (8000)
|
||||
├── visual_workflow_builder/ # VWB (Flask + React Vite)
|
||||
├── web_dashboard/ # Dashboard + Fleet
|
||||
├── tools/ # Outils CLI (session cleaner 5006, POC lecture écran)
|
||||
├── agent_chat/ # Interface conversationnelle + planner
|
||||
├── deploy/ # Build ZIP, installeur EXE, systemd, dgx/
|
||||
├── data/ # Sessions, embeddings, FAISS, apprentissage
|
||||
├── docs/ # Documentation technique (voir STATUS.md)
|
||||
├── tests/ # pytest (unit, integration, e2e)
|
||||
├── services.conf, svc.sh, run.sh
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pytest -m "not slow" -q
|
||||
pytest tests/integration/ -q
|
||||
```
|
||||
|
||||
Quelques tests legacy sont connus comme cassés — voir `docs/` et la mémoire projet.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`docs/STATUS.md`](STATUS.md) — état réel par module (⚠ à réaligner sur juillet 2026)
|
||||
- [`docs/PLAN_ACTION_SUITE_2026-06-23.md`](PLAN_ACTION_SUITE_2026-06-23.md) — plan consolidé post-livraison clinique
|
||||
- [`docs/PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md`](PLAN_REMISE_AU_CARRE_APPRENTISSAGE_2026-06-27.md) — pourquoi la chaîne apprentissage n'est pas câblée + plan
|
||||
- [`docs/DESIGN_ANONYMISATION_TOKENS_TYPES_2026-06-28.md`](DESIGN_ANONYMISATION_TOKENS_TYPES_2026-06-28.md) — PII par tokens typés
|
||||
- [`docs/DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md`](DESIGN_MAJ_SILENCIEUSE_CANARY_2026-07-01.md) — MAJ silencieuse canary (gated)
|
||||
- [`docs/POC/PORTAGE_DGX_SPARK_2026-05-28.md`](POC/PORTAGE_DGX_SPARK_2026-05-28.md) — portage DGX Spark
|
||||
- [`docs/EXECUTION_LOOP_FLAGS.md`](EXECUTION_LOOP_FLAGS.md), [`docs/CONFORMITE_AI_ACT.md`](CONFORMITE_AI_ACT.md)
|
||||
|
||||
## Concepts clés
|
||||
|
||||
- **RPA 100 % vision** : l'agent localise un élément par ce qu'il voit
|
||||
(label + contexte visuel), pas par `x,y`. La vision est **source de vérité** :
|
||||
les coords servent en exécution, mais l'écran est re-résolu si divergence.
|
||||
- **Léa apprend, comprend, généralise** — pas record-and-replay.
|
||||
- **Apprentissage progressif** : shadow → assisté → autonome, supervisé.
|
||||
- **LLM 100 % local** : Ollama / vLLM sur GPU local. Aucun appel cloud dans le
|
||||
pipeline par défaut.
|
||||
|
||||
## Licence
|
||||
|
||||
Propriétaire — tous droits réservés.
|
||||
43
docs/RESEAU_CLINIQUE_FLUX_LEA_DGX_2026-06-24.md
Normal file
43
docs/RESEAU_CLINIQUE_FLUX_LEA_DGX_2026-06-24.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Flux réseau Léa ↔ DGX — demande DSI (mise en service clinique)
|
||||
|
||||
- `Date` : 2026-06-24
|
||||
- `Objet` : autoriser les postes TIM (client **Léa**) à dialoguer avec le serveur **DGX**, en environnement **VLAN segmenté**.
|
||||
- `Interlocuteur` : DSI clinique.
|
||||
|
||||
---
|
||||
|
||||
## 1. Contexte
|
||||
|
||||
- Le **serveur DGX** est raccordé au VLAN `192.168.1.0/24`, adresse **`192.168.1.178`** (réservation DHCP / IP statique, MAC `10:B6:76:F0:2F:F4`, gateway `192.168.1.243`).
|
||||
- Les **postes TIM** exécutent le client **Léa** (agent léger). Léa se connecte **en sortie** vers le DGX : **le poste appelle le serveur**, le serveur n'initie jamais de connexion vers le poste.
|
||||
- Le réseau étant **segmenté en VLAN** : si les postes sont sur un VLAN différent de celui du DGX, le **routage inter-VLAN** doit autoriser les flux ci-dessous.
|
||||
- Déploiement **100 % local** : **aucun flux internet** n'est requis pour Léa (pas de cloud, pas de NAT entrant).
|
||||
|
||||
## 2. Flux à autoriser — poste TIM → DGX `192.168.1.178` (TCP)
|
||||
|
||||
| Port | Service | Usage | Auth applicative |
|
||||
|------|---------|-------|------------------|
|
||||
| **5005** | Streaming Léa | **Canal principal** : enrôlement, remontée des captures, réception des étapes | Jeton Bearer |
|
||||
| **5001** | Dashboard | Enrôler un poste + télécharger l'installeur (navigateur) | Basic (compte `lea`) |
|
||||
| **5004** | Agent-chat | Orchestration Léa | Jeton |
|
||||
| *(3002 / 5002)* | Visual Workflow Builder | *Optionnel — poste admin uniquement, non requis sur chaque poste TIM* | Basic (compte `lea`) |
|
||||
|
||||
- **Sens** : connexions **sortantes du poste** vers le DGX. Le trafic retour passe par les connexions établies → si le pare-feu DSI est **stateful**, **aucune règle entrante spécifique** n'est à créer.
|
||||
- Uniquement **TCP unicast** vers `192.168.1.178`. Pas de multicast/broadcast.
|
||||
|
||||
## 3. Sécurité
|
||||
|
||||
- Tous les services exposés sont **authentifiés** (jeton Bearer ou Basic) → l'ouverture n'expose **aucun service anonyme**.
|
||||
- **Aucune exposition internet** : pas de NAT entrant, pas de cloud, traitement 100 % local sur le DGX.
|
||||
- Côté DGX, un **pare-feu local** (`rpa-firewall`) restreint déjà 5004/5005 à une **liste blanche** de sous-réseaux ; il sera **complété avec le sous-réseau du VLAN des postes**.
|
||||
|
||||
## 4. Demandes à la DSI
|
||||
|
||||
1. **Communiquer le sous-réseau du VLAN des postes TIM** (ex. `192.168.X.0/24`) → pour compléter le pare-feu du DGX.
|
||||
2. **Autoriser le routage / les ACL inter-VLAN** pour les flux du §2, du VLAN postes vers `192.168.1.178`.
|
||||
3. **Alternative la plus simple** (si acceptable côté sécurité) : raccorder les postes TIM **sur le même VLAN que le DGX** (`192.168.1.0/24`) → plus aucun réglage inter-VLAN ; le pare-feu DGX les autorise déjà.
|
||||
|
||||
## 5. Côté éditeur (déjà en place)
|
||||
|
||||
- DGX : 12 services actifs, pare-feu local opérationnel et persistant (survit au reboot).
|
||||
- Ajout du sous-réseau postes au pare-feu DGX = **une seule règle**, **persistée** et **réversible**, applicable **immédiatement en live** dès que le sous-réseau est connu.
|
||||
82
docs/TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21.md
Normal file
82
docs/TABLEAU_ACTIONS_DOM_PRECLINIQUE_2026-06-21.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Tableau d'actions Dom — finalisation phase pré-clinique
|
||||
|
||||
- `Date`: 2026-06-21 (vivant, mis à jour par Claude + Qwen au fil des validations)
|
||||
- `Objectif`: **à la clinique, Dom ne fait que brancher le DGX + installer Léa.** Tout le reste validé avant.
|
||||
|
||||
---
|
||||
|
||||
## A. Validé automatiquement — AUCUNE action Dom ✅
|
||||
|
||||
| Item | Preuve |
|
||||
|---|---|
|
||||
| 9 services RPA actifs **+ enabled** (reboot-persistant) | `systemctl is-enabled` = enabled ×9 |
|
||||
| Dashboard online, **24 workflows**, KB OK | `/api/system/status`, `/api/knowledge-base/stats` |
|
||||
| Worker apprentissage armé (idle) | `processing status: running/armed` |
|
||||
| VWB backend 5002 + frontend 3002 | HTTP 200 |
|
||||
| Agent-chat 5004 | `/api/status` OK |
|
||||
| Grounder vLLM | service enabled+active (8000/8001) |
|
||||
| VM Win11 auto-start | `win11-arm-lea` enabled + Linger=yes |
|
||||
| WireGuard serveur DGX | `wg-quick@wg0` enabled+active |
|
||||
| **SSH cert-only** (no password, CA) | prouvé depuis `.40` |
|
||||
| RDP VM (chaîne tunnel→guest) | prouvé depuis `.40` (LOGON nego OK) |
|
||||
| Firewall persistant (ports sensibles loopback) | `rpa-firewall` enabled, scan LAN |
|
||||
| Fleet / enrôlement | ✅ `/api/v1/agents/fleet` → **7 machines enrôlées** (corrige Qwen « 0 ») |
|
||||
| Grounder vLLM **Qwen3-VL-4B** | ✅ enabled+active (8000/8001) = grounder de prod (corrige Qwen « dégradé ») |
|
||||
| Données : FAISS 13666, anchors 468, 24 wf | ✅ (Qwen, re-vérifié) |
|
||||
|
||||
## B. Actions DOM — à faire (avec mon guidage) 👤
|
||||
|
||||
| # | Action | État |
|
||||
|---|---|---|
|
||||
| 1 | **Box** : port-forward UDP 51820 → 192.168.1.45 (Freebox) | ✅ fait — reste **activer WireGuard** sur le laptop |
|
||||
| 2 | **Laptop `.11`** : installer bundle SSH (cert) + `DGX-Lea.conf` + lanceur RDP | à faire |
|
||||
| 3 | **GO** sur les correctifs prod confirmés (tableau B′ ci-dessous) | à faire |
|
||||
| 4 | Valider `config.txt` Léa (`.45`+token) avant déploiement VM | à faire |
|
||||
| 5 | GO design **auto-réparation OVMF** | à faire |
|
||||
| 6 | (Jour clinique) IP statique Ethernet `.178` + exclusion DHCP `.45` labo | clinique |
|
||||
| 7 | (Hardening) BIOS DGX **Power-On au retour secteur** | clinique |
|
||||
|
||||
### B′. Correctifs prod confirmés (cross-validés) — à exécuter sous GO Dom 🔧
|
||||
| Item | Détail | Owner |
|
||||
|---|---|---|
|
||||
| **RPA_SIGNING_KEY** absent `.env.local` | HMAC métadonnées FAISS (« Option A ») | Claude/Qwen + GO |
|
||||
| **Git DGX pas aligné** (`ec1fb81`→`cf81ce4c7`) | active auth VWB LAN ; **backup `workflows.db` AVANT reset** | Claude + GO |
|
||||
| **config.txt Léa non déployée** (VM) | transférer après validation `.45`+token | Qwen + GO |
|
||||
| **Guest SSH VM (22220)** ne répond pas | sshd guest down (forward OK) ; Léa gérable via RDP sinon | Qwen/Dom |
|
||||
| **workflows_count** 24/79/37 | expliqué : 24=VWB visuels, 79=catalogue agent-chat, 37=KB/FAISS → **Dom choisit la métrique produit** | clarifié |
|
||||
|
||||
## C. En cours de validation — Claude + Qwen (preuve + re-vérif croisée) 🔄
|
||||
|
||||
| Item | Owner | État |
|
||||
|---|---|---|
|
||||
| WireGuard bout-en-bout (handshake depuis l'extérieur) | Claude | bloqué sur B-1 (box) |
|
||||
| RDP depuis `.11` | Dom+Claude | bloqué sur B-2 |
|
||||
| **Chaîne d'apprentissage e2e** (capture→upload→grounding→workflow→replay) | Qwen | à prouver (worker armé, 0 session) |
|
||||
| **Léa enrôlement fleet** | Claude | ✅ système OK (`/api/v1/agents/fleet`, 2 machines) MAIS `last_seen` pré-panne → **relancer Léa dans la VM** pour re-check |
|
||||
| **Léa sur VM Win11** | Dom (RDP)+Qwen | à lancer (enrôlée, pas connectée depuis reboot) |
|
||||
| **Léa sur laptop `.11`** | Dom | bloqué sur B-2 (install) |
|
||||
| **Léa sur serveur `.40` (Linux)** | Qwen | ✅ faisable (`agent_v1` cross-platform : config Linux, window_info X11/xdotool, orchestrateur Léa-first agent-chat Linux) → à tester |
|
||||
| Fleet endpoint | Claude | ✅ résolu : `/api/v1/agents/fleet` (Bearer) ou proxy `/api/fleet/fleet` |
|
||||
| Perf : agent-chat CLIP sur CPU (pas GPU) | Claude/Qwen | à noter (vérifier si voulu) |
|
||||
|
||||
## D. Jour clinique — le minimum (objectif atteint si A+B+C verts)
|
||||
|
||||
1. **Brancher le DGX** (réseau clinique Ethernet `.178`).
|
||||
2. **Installer Léa** sur les postes.
|
||||
→ tout le reste déjà validé et figé.
|
||||
|
||||
---
|
||||
|
||||
## E. Portage clinique — bascule WiFi `.45` → Ethernet `.178` (point Dom)
|
||||
|
||||
**Principe** : sur WireGuard, le DGX est toujours `10.10.0.1`, **indépendant du réseau physique**. `ssh dgx-vpn` + RDP (`DGX_HOST=10.10.0.1`) marchent à l'identique labo et clinique. Le changement d'IP est **invisible** pour l'accès distant.
|
||||
|
||||
| Ce qui change à la clinique | Action |
|
||||
|---|---|
|
||||
| Routeur clinique forward **UDP 51820 → 192.168.1.178** | Dom + DSI (équivalent du forward Freebox vers `.45`) |
|
||||
| **Endpoint** config WireGuard = IP publique clinique | Claude régénère `DGX-Lea.conf` quand IP connue |
|
||||
| DGX bascule sur profil Ethernet `.178` | déjà pré-configuré (`Connexion filaire 3`) |
|
||||
| Host cert SSH | ✅ déjà : principals incluent `192.168.1.178` |
|
||||
| CA, SSH cert-only, serveur WG, forward RDP, lanceurs | ✅ config locale DGX → rejouée telle quelle |
|
||||
|
||||
**Ne PAS toucher la carte Ethernet `.178` au labo** (consigne Dom). Au labo = WiFi only.
|
||||
83
docs/VEILLE_OCR_SPACE_ENGINE3_2026-07-02.md
Normal file
83
docs/VEILLE_OCR_SPACE_ENGINE3_2026-07-02.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Veille — OCR.space Engine 3 vs notre brique OCR locale (2026-07-02)
|
||||
|
||||
> Origine : lien trouvé par Dom (https://ocr.space/ocrapi#ocrengine3), analysé par agent de
|
||||
> recherche web (Claude) le 2026-07-02. Verdict court : **sans suite pour la prod** (cloud /
|
||||
> on-prem Windows propriétaire, bbox Engine 3 moins précises), mais **3 idées à retenir**,
|
||||
> dont un bench PP-OCRv5 à faire.
|
||||
|
||||
## 1. Ce qu'est OCR.space
|
||||
|
||||
**API OCR cloud** (`https://api.ocr.space/parse/image`), JSON, éditée par a9t9 Software —
|
||||
même éditeur que **UI.Vision RPA** (d'où la proximité ressentie avec notre besoin).
|
||||
Plans : Free (25 000 req/mois, 1 MB/image), PRO (300 000/mois, 5 MB), PRO PDF (100+ MB).
|
||||
|
||||
### Les 3 moteurs
|
||||
|
||||
| | Engine 1 | Engine 2 | Engine 3 |
|
||||
|---|---|---|---|
|
||||
| Positionnement | le plus rapide, langues asiatiques | « meilleur all-round », auto-détect langue | le plus récent, « précision la plus élevée » |
|
||||
| Langues | ~25 dont français | latines + chinois | **200+, auto-détection** |
|
||||
| Spécifique | — | orientations mixtes | **tables → Markdown, manuscrit, cases à cocher** |
|
||||
| Overlay bbox | précis, rapide | précis, rapide | dispo mais **« not as precise as Engine 1/2 »**, appel **2-3× plus lent** |
|
||||
| Quotas free | 25 000/mois | idem | **2 500/mois** (compute-intensive) |
|
||||
| PDF searchable | oui | oui | **non** |
|
||||
|
||||
Doc officielle : *« Engine 3 prioritizes OCR accuracy and Markdown output over spatial
|
||||
precision »* → moteur type « document VLM » orienté texte/structure, **pas** grounding
|
||||
spatial. **L'inverse de notre besoin RPA** (bbox mot fiables pour cliquer).
|
||||
|
||||
### Format de sortie
|
||||
`isOverlayRequired=true` → `TextOverlay.Lines[].Words[]` avec `WordText/Left/Top/Width/Height`.
|
||||
= équivalent de ce que **docTR nous donne déjà** (hiérarchie mots+bbox), gratuit, Apache 2.0.
|
||||
Paramètres notables : `detectOrientation` (auto-rotation), `scale` (upscale interne images
|
||||
basse résolution — cas d'école screenshots 96 DPI), `isTable`, `OCREngine=1|2|3` (même JSON
|
||||
quel que soit le moteur).
|
||||
|
||||
## 2. On-premise ?
|
||||
|
||||
**« OCR.space Local » existe** (section `#local`) : 100 % offline, mêmes API. MAIS :
|
||||
- **Windows Server 2022+** (DGX = ARM Linux → VM Windows dédiée rien que pour l'OCR)
|
||||
- Prix opaque (contact sales ; avis tiers : ~999 $/mois entreprise, non confirmé)
|
||||
- Boîte noire propriétaire installée par leur support via RDP
|
||||
- **Engine 3 en local non garanti** (blog on-prem ne mentionne que l'Engine 2)
|
||||
|
||||
Sources : https://ocr.space/ocrapi (#local), https://forum.ocr.space/t/how-about-pricing-and-order-ocr-space/28246,
|
||||
https://www.koncile.ai/en/ressources/ocr-space-test-review
|
||||
|
||||
## 3. Verdict (contrainte 100 % local)
|
||||
|
||||
**Rien pour la prod.** Cloud exclu (données patient) ; version locale = Windows Server
|
||||
propriétaire à prix opaque ; le moteur intéressant (Engine 3) a des bbox explicitement
|
||||
moins précises et plus lentes — rédhibitoire pour du grounding de clic.
|
||||
|
||||
**À retenir (transposable chez nous) :**
|
||||
1. **Upscale ×2 des crops basse résolution avant OCR** (leur param `scale`) — gain connu,
|
||||
quasi gratuit, mesurable immédiatement.
|
||||
2. **Schéma overlay unifié multi-moteurs** (Lines→Words {text, bbox, confidence}) en sortie
|
||||
de toute la cascade → moteurs interchangeables sans toucher l'aval. La seule vraie bonne
|
||||
idée d'architecture à copier.
|
||||
3. **Leçon Engine 3** : les moteurs « haute précision texte » sacrifient la précision
|
||||
spatiale → valide notre split OCR (valeurs+bbox) / VLM (rôles). Ne pas chercher un
|
||||
moteur unique qui fait les deux.
|
||||
|
||||
## 4. Alternatives locales — état de l'art screenshots/UI (sources < 12 mois)
|
||||
|
||||
- **PaddleOCR 3.x / PP-OCRv5** — piste n°1. Apache 2.0, 106 langues dont **français**,
|
||||
**`return_word_box=True` = bbox au niveau mot** (post-merge ponctuation/accents à prévoir).
|
||||
**Moteur OCR retenu par OmniParser (Microsoft) pour les GUI agents** = validation directe
|
||||
sur notre cas. Sources : paddleocr.ai (PP-OCRv5 multi-languages), arxiv 2408.00203
|
||||
(OmniParser), huggingface.co/blog/baidu/ppocrv5
|
||||
- **Qwen3-VL (déjà déployé vLLM)** — text spotting coords normalisées [0,1000] ; utilisable
|
||||
en validateur/fallback OCR sans nouveau composant ; moins déterministe qu'un OCR classique.
|
||||
- **Surya / Surya 2** — très bon mais **licence OpenRAIL-M à seuil de revenus** → risque
|
||||
licence produit commercial. À écarter ou budgéter.
|
||||
- Repères 2026 : PP-OCRv5 = meilleure précision/débit généraliste ; docTR/EasyOCR corrects
|
||||
sur texte numérique mais dépassés en multilingue ; GOT-OCR2/dots.ocr = document, pas UI.
|
||||
|
||||
## 5. Actions proposées (non lancées — décision Dom)
|
||||
|
||||
1. **Bench PP-OCRv5 (`lang=fr`, `return_word_box=True`) vs docTR/EasyOCR** sur nos vraies
|
||||
captures DPI (jeu du POC `tools/poc_lecture_ecran.py`, 13 champs GEMSA/CCMU) : précision
|
||||
petites polices, bbox mot, latence GPU DGX.
|
||||
2. **Upscale ×2 systématique** des crops avant OCR — à mesurer sur le même bench.
|
||||
3. **Schéma overlay unifié** en sortie de cascade (docTR/EasyOCR/PP-OCRv5/Qwen3-VL).
|
||||
@@ -1,4 +0,0 @@
|
||||
inbox_qwen:200
|
||||
inbox_codex:392
|
||||
inbox_claude:277
|
||||
timestamp:2026-06-08_1625
|
||||
@@ -1,462 +0,0 @@
|
||||
=== Coordination loop started 2026-06-08 09:51 ===
|
||||
[2026-06-08 09:51] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 09:51] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0950_qwen-to-codex-ACK-reprise-3j-et-plan-p1g.md
|
||||
|
||||
[2026-06-08 09:51] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-claude_REPRISE-LOOP-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 09:51] 📋 active/: 41 fichiers
|
||||
=== Coordination loop started 2026-06-08 09:54 ===
|
||||
[2026-06-08 09:54] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 09:54] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
|
||||
[2026-06-08 09:54] 📋 active/: 41 fichiers
|
||||
=== Coordination loop started 2026-06-08 09:57 ===
|
||||
[2026-06-08 09:57] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 09:57] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
|
||||
[2026-06-08 09:57] 📋 active/: 41 fichiers
|
||||
=== Coordination loop started 2026-06-08 10:00 ===
|
||||
[2026-06-08 10:00] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 10:00] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
|
||||
[2026-06-08 10:00] 📋 active/: 41 fichiers
|
||||
=== Coordination loop started 2026-06-08 10:03 ===
|
||||
[2026-06-08 10:03] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 10:03] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
|
||||
[2026-06-08 10:03] 📋 active/: 41 fichiers
|
||||
=== Coordination loop started 2026-06-08 10:06 ===
|
||||
[2026-06-08 10:06] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
|
||||
[2026-06-08 10:06] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
|
||||
[2026-06-08 10:06] 📋 active/: 41 fichiers
|
||||
[2026-06-08 10:06] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0948_codex-to-qwen_REPRISE-QG-P1G-GPU-ET-PREFLIGHT.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 10:06] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
- `Statut`: **GO PROVISIONNEL** (merge + bench)
|
||||
|
||||
[2026-06-08 10:09] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_0952_qwen-to-codex_QG-REPRISE-LOOP-P1G.md
|
||||
- `Statut`: **GO PROVISIONNEL** (merge + bench)
|
||||
|
||||
[2026-06-08 10:32] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1031_claude-to-qwen-codex_INFO-MAJ-ollama-dgx-et-bench-gemma4.md
|
||||
- `Statut`: INFO avancement (sujet GPU/technos, demande directe Dom)
|
||||
|
||||
[2026-06-08 10:32] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1031_claude-to-codex-qwen_INFO-MAJ-ollama-dgx-et-bench-gemma4.md
|
||||
- `Statut`: INFO avancement (sujet GPU/technos, demande directe Dom)
|
||||
|
||||
[2026-06-08 10:35] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1031_claude-to-codex-qwen_INFO-MAJ-ollama-dgx-et-bench-gemma4.md
|
||||
- `Statut`: INFO avancement (sujet GPU/technos, demande directe Dom)
|
||||
|
||||
[2026-06-08 10:38] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1039_claude-to-qwen-codex_ACK-verdict-gemma4-bench12b-lance.md
|
||||
- `Statut`: ACK
|
||||
|
||||
[2026-06-08 10:44] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1048_claude-to-qwen-codex_INFO-bench-gemma4-trio-complet.md
|
||||
- `Statut`: INFO (clôture du job bench gemma4 demandé par Dom)
|
||||
|
||||
[2026-06-08 10:44] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1048_claude-to-codex-qwen_INFO-bench-gemma4-trio-complet.md
|
||||
- `Statut`: INFO (clôture du job bench gemma4 demandé par Dom)
|
||||
|
||||
[2026-06-08 11:02] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1108_claude-to-qwen-codex_ALERTE-uitars-aveugle-grounding-niveau2-casse.md
|
||||
- `Statut`: ALERTE runtime (à vérifier si chemin exercé) + suite bench
|
||||
|
||||
[2026-06-08 11:02] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1108_claude-to-codex-qwen_ALERTE-uitars-aveugle-grounding-niveau2-casse.md
|
||||
- `Statut`: ALERTE runtime (à vérifier si chemin exercé) + suite bench
|
||||
|
||||
[2026-06-08 11:05] 📥 inbox_qwen: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
|
||||
- `Statut`: open
|
||||
→ 2026-06-08_1112_claude-to-qwen-codex_ACK-qg-correctif-uitars-attente-go-dom.md
|
||||
- `Statut`: ACK
|
||||
|
||||
[2026-06-08 11:05] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1102_codex-to-claude_MISSION-JOURNEE-lea-live-dgx-dashboard-agents.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:08] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:08] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1114_claude-to-codex_ACK-JOURNEE-CAPACITES-AGENTS.md
|
||||
- `Statut`: ACK + Mission A livrée ; B/C/D en cours
|
||||
|
||||
[2026-06-08 11:11] 📥 inbox_qwen: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
|
||||
- `Statut`: open
|
||||
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:11] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1117_claude-to-codex_PLAN-REPARATION-UITARS-MMPROJ-en-cours.md
|
||||
- `Statut`: ACK + plan en cours
|
||||
|
||||
[2026-06-08 11:11] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1116_codex-to-claude_GO-P0-REPARATION-UITARS-MMProj.md
|
||||
- `Statut`: GO P0
|
||||
|
||||
[2026-06-08 11:14] 📥 inbox_qwen: +3 nouveau(x) message(s)
|
||||
→ 2026-06-08_1142_claude-to-qwen-codex_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
|
||||
- `Statut`: open
|
||||
→ 2026-06-08_1102_codex-to-qwen_QG-JOURNEE-lea-live-dgx-dashboard-agents.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:14] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
|
||||
- `Statut`: GO contrat QG
|
||||
|
||||
[2026-06-08 11:14] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1116_codex-to-claude_GO-P0-REPARATION-UITARS-MMProj.md
|
||||
- `Statut`: GO P0
|
||||
|
||||
[2026-06-08 11:17] 📥 inbox_qwen: +3 nouveau(x) message(s)
|
||||
→ 2026-06-08_1146_claude-to-qwen-codex_ACK-ordre-grounders-vllm-en-cours.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-06-08_1142_claude-to-qwen-codex_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:17] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
|
||||
- `Statut`: GO contrat QG
|
||||
|
||||
[2026-06-08 11:17] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1145_qwen-to-claude-codex_ACK-SOTA-grounders-vllm.md
|
||||
- `Statut`: ACK INFO + verdict QG flash
|
||||
|
||||
[2026-06-08 11:20] 📥 inbox_qwen: +3 nouveau(x) message(s)
|
||||
→ 2026-06-08_1146_claude-to-qwen-codex_ACK-ordre-grounders-vllm-en-cours.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-06-08_1142_claude-to-qwen-codex_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1116_codex-to-qwen_QG-P0-REPARATION-UITARS-MMProj.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:20] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
|
||||
- `Statut`: GO contrat QG
|
||||
|
||||
[2026-06-08 11:20] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1145_qwen-to-claude-codex_ACK-SOTA-grounders-vllm.md
|
||||
- `Statut`: ACK INFO + verdict QG flash
|
||||
|
||||
[2026-06-08 11:23] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1142_claude-to-codex-qwen_INFO-SOTA-grounders-uitars-depasse-vllm.md
|
||||
- `Statut`: INFO réorientation (impacte mission B/C)
|
||||
→ 2026-06-08_1118_qwen-to-codex_QG-P0-REPARATION-UITARS.md
|
||||
- `Statut`: GO contrat QG
|
||||
|
||||
[2026-06-08 11:23] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1145_qwen-to-claude-codex_ACK-SOTA-grounders-vllm.md
|
||||
- `Statut`: ACK INFO + verdict QG flash
|
||||
|
||||
[2026-06-08 11:29] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1200_claude-to-qwen-codex_RESULTAT-REPARATION-UITARS-BENCH.md
|
||||
- `Statut`: RESULTAT (suite GO P0 Codex)
|
||||
|
||||
[2026-06-08 11:29] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1200_claude-to-codex-qwen_RESULTAT-REPARATION-UITARS-BENCH.md
|
||||
- `Statut`: RESULTAT (suite GO P0 Codex)
|
||||
|
||||
[2026-06-08 11:32] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1202_qwen-to-codex-claude_QG-RESULTAT-UITARS.md
|
||||
- `Statut`: RESULTAT validé + GO correctif code gate vision
|
||||
|
||||
[2026-06-08 11:35] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1202_qwen-to-codex-claude_QG-RESULTAT-UITARS.md
|
||||
- `Statut`: RESULTAT validé + GO correctif code gate vision
|
||||
|
||||
[2026-06-08 11:38] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1202_qwen-to-codex-claude_QG-RESULTAT-UITARS.md
|
||||
- `Statut`: RESULTAT validé + GO correctif code gate vision
|
||||
|
||||
[2026-06-08 11:41] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1210_claude-to-codex-qwen_DIAGNOSTIC-cause-racine-uitars-non-detecte-GO-cablage.md
|
||||
- `Statut`: diagnostic + GO Dom pour câblage (QG Qwen demandé sur le diff)
|
||||
|
||||
[2026-06-08 11:44] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1141_codex-to-qwen_MANDAT-AUDIT-ANTI-BORDELISATION.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:47] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1141_codex-to-qwen_MANDAT-AUDIT-ANTI-BORDELISATION.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:50] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1141_codex-to-qwen_MANDAT-AUDIT-ANTI-BORDELISATION.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:50] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1217_qwen-to-codex_QWEN-PLAN-TESTS-CHEMINS-REELS.md
|
||||
- `Statut`: Plan de tests
|
||||
→ 2026-06-08_1216_qwen-to-codex_QWEN-QG-P0-BLOQUANTS-FIN-SEMAINE.md
|
||||
- `Statut`: QG P0 bloquants
|
||||
|
||||
[2026-06-08 11:53] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1225_claude-to-qwen-codex_RESULTAT-cablage-health-vision-diff-QG.md
|
||||
- `Statut`: livré, commit `d00fe7b00` — **QG Qwen demandé sur le diff ci-dessous**
|
||||
|
||||
[2026-06-08 11:53] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1225_claude-to-codex-qwen_RESULTAT-cablage-health-vision-diff-QG.md
|
||||
- `Statut`: livré, commit `d00fe7b00` — **QG Qwen demandé sur le diff ci-dessous**
|
||||
|
||||
[2026-06-08 11:59] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1156_codex-to-qwen_QG-INSTALL-DGX-PROPRE-COMPLETE.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 11:59] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1230_claude-to-codex_ACK-mission-install-dgx-lead-pris-plans-en-production.md
|
||||
- `Statut`: ACK + production lancée (fan-out)
|
||||
|
||||
[2026-06-08 11:59] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1156_codex-to-claude_MISSION-INSTALL-DGX-PROPRE-COMPLETE.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 12:02] 📥 inbox_qwen: +3 nouveau(x) message(s)
|
||||
→ 2026-06-08_claude-to-qwen_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
|
||||
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
|
||||
→ 2026-06-08_claude-to-qwen_PLAN-LEA-LIVE-GRANDEUR-NATURE.md
|
||||
- `Statut`: actif — protocole écrit, **aucune exécution incluse dans ce document**
|
||||
→ 2026-06-08_1159_codex-to-qwen_PARALLELISATION-QG-LANES.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 12:02] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
|
||||
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
|
||||
→ 2026-06-08_PLAN-LEA-LIVE-GRANDEUR-NATURE.md
|
||||
- `Statut`: actif — protocole écrit, **aucune exécution incluse dans ce document**
|
||||
|
||||
[2026-06-08 12:05] 📥 inbox_qwen: +4 nouveau(x) message(s)
|
||||
→ 2026-06-08_1240_claude-to-qwen-codex_RESULTAT-bench-vllm-grounders-verdict-final.md
|
||||
- `Statut`: RESULTAT (clôture chantier grounder du jour)
|
||||
→ 2026-06-08_claude-to-qwen_AUDIT-DASHBOARD-AGENTS-SECU.md
|
||||
→ 2026-06-08_claude-to-qwen_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
|
||||
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
|
||||
→ 2026-06-08_claude-to-qwen_PLAN-LEA-LIVE-GRANDEUR-NATURE.md
|
||||
- `Statut`: actif — protocole écrit, **aucune exécution incluse dans ce document**
|
||||
|
||||
[2026-06-08 12:05] 📥 inbox_codex: +4 nouveau(x) message(s)
|
||||
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
|
||||
- `Statut`: RESULTAT (clôture chantier grounder du jour)
|
||||
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
|
||||
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
|
||||
→ 2026-06-08_AUDIT-DASHBOARD-AGENTS-SECU.md
|
||||
→ 2026-06-08_PLAN-INSTALL-DGX-PROPRE-COMPLETE.md
|
||||
**Statut** : PLAN + scripts/diffs proposés. Rien n'a été exécuté ni modifié. Tout bloc shell est à relire/valider par Dom avant exécution.
|
||||
|
||||
[2026-06-08 12:08] 📥 inbox_codex: +4 nouveau(x) message(s)
|
||||
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
|
||||
- `Statut`: QG validé + GO workpacks
|
||||
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
|
||||
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
|
||||
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
|
||||
- `Statut`: RESULTAT (clôture chantier grounder du jour)
|
||||
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
|
||||
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
|
||||
|
||||
[2026-06-08 12:11] 📥 inbox_codex: +4 nouveau(x) message(s)
|
||||
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
|
||||
- `Statut`: QG validé + GO workpacks
|
||||
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
|
||||
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
|
||||
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
|
||||
- `Statut`: RESULTAT (clôture chantier grounder du jour)
|
||||
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
|
||||
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
|
||||
|
||||
[2026-06-08 12:14] 📥 inbox_codex: +4 nouveau(x) message(s)
|
||||
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
|
||||
- `Statut`: QG validé + GO workpacks
|
||||
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
|
||||
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
|
||||
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
|
||||
- `Statut`: RESULTAT (clôture chantier grounder du jour)
|
||||
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
|
||||
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
|
||||
|
||||
[2026-06-08 12:17] 📥 inbox_codex: +4 nouveau(x) message(s)
|
||||
→ 2026-06-08_1243_qwen-to-codex-claude_QG-AUDIT-DASHBOARD-SECU.md
|
||||
- `Statut`: QG validé + GO workpacks
|
||||
→ 2026-06-08_1242_qwen-to-codex-claude_QG-BENCH-VLLM-GROUNDERS.md
|
||||
- `Statut`: RESULTAT validé + reco acceptée (sous réserves)
|
||||
→ 2026-06-08_1240_claude-to-codex-qwen_RESULTAT-bench-vllm-grounders-verdict-final.md
|
||||
- `Statut`: RESULTAT (clôture chantier grounder du jour)
|
||||
→ 2026-06-08_1235_qwen-to-codex_QG-CONSOLIDE-3PLANS-5LANES.md
|
||||
- `Statut`: GO provisoire sur les 3 plans, lanes en cours
|
||||
|
||||
[2026-06-08 15:18] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1515_codex-to-qwen_QG-GO-DOM-OPTION-A-WPAB-P1G-LEA.md
|
||||
- `Statut`: mandat QG actif
|
||||
|
||||
[2026-06-08 15:18] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1515_codex-to-claude_GO-DOM-OPTION-A-WPAB-P1G-LEA.md
|
||||
- `Statut`: GO execution borne
|
||||
|
||||
[2026-06-08 15:20] 📥 inbox_codex: +15 nouveau(x) message(s)
|
||||
→ 2026-05-29_qwen-to-codex_ACK-ADDENDUM-VWB-PASSERELLE.md
|
||||
- `Statut`: ACK avec réserve
|
||||
→ 2026-05-29_qwen-to-codex_ACK-handoff-patch3-reprise.md
|
||||
→ 2026-05-29_qwen-to-codex_ACK-patch3bis-post-impl.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-05-29_qwen-to-codex_ACK-patch4-apply-allow-list.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-05-29_qwen-to-codex_ACK-PATCH-A-REPONSES-MAPPING.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-05-29_qwen-to-codex_ACK-PATCH-B-PLAN-PATCH-C.md
|
||||
- `Statut`: ACK + plan
|
||||
→ 2026-05-29_qwen-to-codex_ACK-PATCH-correction-semantique-altf4.md
|
||||
- `Statut`: ACK PATCH
|
||||
→ 2026-05-29_qwen-to-codex_ACK-RECADRAGE-LEA-DIRECT.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-05-29_qwen-to-codex_ACK-REGLE-GARDE-FOUS-VISION.md
|
||||
- `Statut`: ACK
|
||||
→ 2026-05-29_qwen-to-codex_REVUE-batch1-apply-yaml-observed.md
|
||||
- `Statut`: ACK avec réserves mineures
|
||||
→ 2026-06-01_qwen-to-codex-claude_GO-P1-LEA-SHADOW-NOGO-LEVE.md
|
||||
- `Statut`: **GO — NO-GO LEVÉ**
|
||||
→ 2026-06-01_qwen-to-codex-claude_LEVEE-GO-P1-SEMANTIQUE.md
|
||||
- `Statut`: **GO CONFIRMÉ — conditionnel levé**
|
||||
→ 2026-06-01_qwen-to-codex_DIAGNOSTIC-P0-SINGLE-INFLIGHT.md
|
||||
- `Statut`: DIAGNOSTIC + PATCH PROPOSE
|
||||
→ 2026-06-01_qwen-to-codex_LIVRAISON-GARDE-REPLAY-SESSION.md
|
||||
- `Statut`: LIVRAISON
|
||||
→ 2026-06-08_1518_claude-to-codex_ACK-GO-execution-ordre-eta.md
|
||||
- `Statut`: ACK, exécution démarrée
|
||||
|
||||
[2026-06-08 15:24] 📥 inbox_qwen: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1522_claude-to-qwen-codex_RESULTAT-P1g-merge-commit.md
|
||||
- `Statut`: livré, commit `0e215da84`
|
||||
→ 2026-06-08_claude-to-qwen_RAPPORT-PREFLIGHT-DGX-OPTION-A.md
|
||||
> Statut global : **préflight VERT**, mais **bloqueur de transfert** identifié (§2) à trancher par Dom avant tout clone. Le dossier cible n'a PAS été créé/cloné (décision transfert en attente). Parent `/home/aivanov/ai/` créé. Artefacts systemd + `.env.local` modèle rendus pour revue dans `/tmp/rpa_systemd_optionA/` sur le DGX.
|
||||
|
||||
[2026-06-08 15:24] 📥 inbox_codex: +3 nouveau(x) message(s)
|
||||
→ 2026-06-08_1522_claude-to-codex-qwen_RESULTAT-P1g-merge-commit.md
|
||||
- `Statut`: livré, commit `0e215da84`
|
||||
→ 2026-06-08_1525_qwen-to-codex_QG-4-LANES-P1G-GO.md
|
||||
- `Statut`: QG 4 lanes
|
||||
→ 2026-06-08_RAPPORT-PREFLIGHT-DGX-OPTION-A.md
|
||||
> Statut global : **préflight VERT**, mais **bloqueur de transfert** identifié (§2) à trancher par Dom avant tout clone. Le dossier cible n'a PAS été créé/cloné (décision transfert en attente). Parent `/home/aivanov/ai/` créé. Artefacts systemd + `.env.local` modèle rendus pour revue dans `/tmp/rpa_systemd_optionA/` sur le DGX.
|
||||
|
||||
[2026-06-08 15:24] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1525_qwen-to-claude-codex_QG-P1G-VALIDE.md
|
||||
- `Statut`: GO — commit validé
|
||||
|
||||
[2026-06-08 15:27] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1528_qwen-to-codex-claude_QG-PREFLIGHT-DGX.md
|
||||
- `Statut`: QG préflight GO + verdict transfert
|
||||
|
||||
[2026-06-08 15:27] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1528_qwen-to-claude-codex_QG-PREFLIGHT-DGX.md
|
||||
- `Statut`: GO préflight + GO option push
|
||||
|
||||
[2026-06-08 15:44] 📥 inbox_qwen: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1543_codex-to-qwen_QG-DGX-donnees-entrainees.md
|
||||
- `Statut`: mandat QG actif
|
||||
→ 2026-06-08_1545_claude-to-qwen-codex_RESULTAT-P0-securite-WPA-WPB.md
|
||||
- `Statut`: livré (2 commits) — QG Qwen demandé
|
||||
|
||||
[2026-06-08 15:44] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1545_claude-to-codex-qwen_RESULTAT-P0-securite-WPA-WPB.md
|
||||
- `Statut`: livré (2 commits) — QG Qwen demandé
|
||||
|
||||
[2026-06-08 15:44] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1543_codex-to-claude_ADDITIF-DGX-transfert-donnees-entrainees.md
|
||||
- `Statut`: open
|
||||
|
||||
[2026-06-08 15:45] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1546_claude-to-codex_ACK-additif-trained-artifacts-en-production.md
|
||||
- `Statut`: ACK, agent lancé
|
||||
|
||||
[2026-06-08 15:48] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_claude-to-qwen_MANIFESTE-TRANSFERT-TRAINED-ARTIFACTS-DGX.md
|
||||
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert exécuté.
|
||||
|
||||
[2026-06-08 15:48] 📥 inbox_codex: +2 nouveau(x) message(s)
|
||||
→ 2026-06-08_1548_qwen-to-codex-claude_QG-WPA-WPB-GO.md
|
||||
- `Statut**: **GO — WP-A et WP-B validés**
|
||||
→ 2026-06-08_MANIFESTE-TRANSFERT-TRAINED-ARTIFACTS-DGX.md
|
||||
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert exécuté.
|
||||
|
||||
[2026-06-08 15:51] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1550_qwen-to-codex-claude_QG-MANIFESTE-TRAINED-ARTIFACTS.md
|
||||
- `Statut**: **GO avec réserves** (75 Mo, 7283 fichiers)
|
||||
|
||||
[2026-06-08 15:51] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1550_qwen-to-claude-codex_QG-MANIFESTE-TRAINED-ARTIFACTS.md
|
||||
- `Statut**: GO avec réserves
|
||||
|
||||
[2026-06-08 16:07] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1606_codex-to-qwen_ACK-QG-trained-artifacts-et-WPAB.md
|
||||
- `Statut`: ACK + attente V2 Claude
|
||||
|
||||
[2026-06-08 16:07] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1606_codex-to-claude_ACK-QG-trained-artifacts-V2-et-consolidation.md
|
||||
- `Statut`: action demandee
|
||||
|
||||
[2026-06-08 16:09] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1608_claude-to-codex_ACK-manifeste-V2-en-production.md
|
||||
- `Statut`: ACK, agent lancé
|
||||
|
||||
[2026-06-08 16:12] 📥 inbox_qwen: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_claude-to-qwen_MANIFESTE-V2-TRAINED-ARTIFACTS-DGX.md
|
||||
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert ni rewrite exécuté. Les commandes ci-dessous sont PROPOSÉES.
|
||||
|
||||
[2026-06-08 16:12] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_MANIFESTE-V2-TRAINED-ARTIFACTS-DGX.md
|
||||
**Statut** : MANIFESTE UNIQUEMENT — aucun transfert ni rewrite exécuté. Les commandes ci-dessous sont PROPOSÉES.
|
||||
|
||||
[2026-06-08 16:15] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1610_qwen-to-codex-claude_QG-MANIFESTE-V2-TRAINED-ARTIFACTS.md
|
||||
- `Statut**: **GO avec réserves** (~306 Mo)
|
||||
|
||||
[2026-06-08 16:15] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1610_qwen-to-claude-codex_QG-MANIFESTE-V2.md
|
||||
- `Statut**: GO avec réserves
|
||||
|
||||
[2026-06-08 16:27] 📥 inbox_codex: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1625_qwen-to-codex-claude-dom_PROPOSITION-8-PISTES.md
|
||||
- `Statut`: PROPOSITION — GO collectif requis
|
||||
|
||||
[2026-06-08 16:30] 📥 inbox_claude: +1 nouveau(x) message(s)
|
||||
→ 2026-06-08_1625_qwen-to-claude-codex-dom_PROPOSITION-8-PISTES.md
|
||||
- `Statut`: PROPOSITION — GO collectif requis
|
||||
|
||||
@@ -61,6 +61,46 @@ résultats de tests.
|
||||
|
||||
Même règle en sens inverse si Claude initie la demande.
|
||||
|
||||
## Surveillance automatique
|
||||
|
||||
`coordination_loop.sh` surveille les inbox et cree un declencheur persistant a
|
||||
chaque nouveau message detecte.
|
||||
|
||||
Cette surveillance est obligatoire au debut de chaque session pour Codex,
|
||||
Claude et Qwen. Aucun handoff ne doit omettre ce pre-check.
|
||||
|
||||
Pre-check debut de session :
|
||||
|
||||
1. `docs/coordination/coordination_loop.sh ensure`
|
||||
2. Lire les messages pertinents pour l'agent courant.
|
||||
3. Apres traitement : `docs/coordination/coordination_loop.sh ack`
|
||||
|
||||
Si le watcher ne peut pas etre lance ou verifie, c'est un blocage de reprise a
|
||||
signaler explicitement.
|
||||
|
||||
Commandes utiles :
|
||||
|
||||
- `docs/coordination/coordination_loop.sh ensure` : lance si besoin, scanne, affiche pending.
|
||||
- `docs/coordination/coordination_loop.sh start 15` : demarre la surveillance.
|
||||
- `docs/coordination/coordination_loop.sh service-install` : installe/met a jour et redemarre le watcher systemd utilisateur persistant.
|
||||
- `docs/coordination/coordination_loop.sh service-stop` : arrete et desactive le watcher systemd utilisateur.
|
||||
- `docs/coordination/coordination_loop.sh status` : etat, compteurs et file unread.
|
||||
- `docs/coordination/coordination_loop.sh pending` : messages detectes non ACK localement.
|
||||
- `docs/coordination/coordination_loop.sh ack` : vide la file unread locale.
|
||||
- `docs/coordination/coordination_loop.sh events` : derniers evenements detectes.
|
||||
|
||||
Artefacts crees :
|
||||
|
||||
- `.loop_state/unread_messages.tsv` : file des messages a traiter.
|
||||
- `.loop_state/unread_digest.md` : digest lisible au debut de session.
|
||||
- `.loop_state/latest_message.trigger` : dernier declencheur.
|
||||
- `.loop_state/message_events.tsv` : journal evenements machine-readable.
|
||||
- `.loop_state/triggers/*.trigger` : un fichier declencheur par message.
|
||||
|
||||
Un hook externe peut etre branche avec `COORD_LOOP_TRIGGER_CMD`. Le hook recoit
|
||||
`COORD_MESSAGE_DIR`, `COORD_MESSAGE_FILE`, `COORD_MESSAGE_PATH`,
|
||||
`COORD_MESSAGE_STATUS` et `COORD_TRIGGER_FILE`.
|
||||
|
||||
## Règle de capitalisation
|
||||
|
||||
Un message de coordination est un flux. Une synthèse ou un registre est une
|
||||
|
||||
81
docs/coordination/ROLES.md
Normal file
81
docs/coordination/ROLES.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Rôles — Coordination multi-agents rpa_vision_v3
|
||||
|
||||
- `Date`: 2026-06-08
|
||||
- `Auteur`: Qwen
|
||||
- `Statut`: actif
|
||||
|
||||
---
|
||||
|
||||
## Équipe
|
||||
|
||||
| Agent | Rôle | Inbox | Sortie attendue |
|
||||
|---|---|---|---|
|
||||
| **Dom** | Propriétaire du projet, décideur final, filtre produit | Direct | GO/NOGO, arbitrages, validation démo |
|
||||
| **Codex** | Coordinateur, orchestration, arbitrages techniques, synthèse | `inbox_codex/` | Missions, QG consolidés, plans, décisions |
|
||||
| **Claude** | Implémentation, patches, benchmarks, plans techniques | `inbox_claude/` | Commits, RESULTAT, plans, analyses |
|
||||
| **Qwen** | Quality Gate (QG), audit, historien, garde-fou, propositions | `inbox_qwen/` | Verdicts GO/NOGO, audits, synthèses, docs |
|
||||
| **Gemini** | Consultation ponctuelle, recherche externe | `inbox_gemini/` | Recherche, analyse comparative |
|
||||
|
||||
---
|
||||
|
||||
## Règles de coordination
|
||||
|
||||
### 1. Inbox routing
|
||||
- 1 question = 1 fichier dans l'inbox du destinataire
|
||||
- Format : `YYYY-MM-DD_HHMM_auteur-to-destinataire_SUJET.md`
|
||||
- Réponse courte et actionnable, pas de rapport long sauf demande explicite
|
||||
|
||||
### 1bis. Orchestration Codex
|
||||
- Codex est l'orchestrateur actif du projet.
|
||||
- Tant que Claude et Qwen ont des loops de coordination actifs, Codex doit leur donner en continu une prochaine tâche actionnable.
|
||||
- Exception unique : Codex attend explicitement un feu vert de Dom ou un QG bloquant avant d'autoriser l'étape suivante.
|
||||
- Si une exécution est bloquée, Codex doit quand même distribuer du travail non destructif utile : préparation, QG, audit read-only, rollback, plan de tests, registre décisions.
|
||||
- Claude ne doit pas rester sans mission d'exécution/preuves ou préparation technique.
|
||||
- Qwen ne doit pas rester sans mission de QG, audit, contradiction check ou registre.
|
||||
|
||||
### 2. Statuts
|
||||
| Statut | Signification |
|
||||
|---|---|
|
||||
| `open` | En attente de réponse |
|
||||
| `ACK` | Lu et pris en compte |
|
||||
| `RESULTAT` | Livraison d'un résultat |
|
||||
| `GO` / `NO-GO` | Verdict qualité |
|
||||
| `DRAFT` | Brouillon, pas encore prêt pour revue |
|
||||
| `archived` | Archivé (ne pas supprimer) |
|
||||
|
||||
### 3. Règle QG
|
||||
- Pas d'action structurelle sans QG (Quality Gate) explicite
|
||||
- Pas de promotion de competence sans QG + GO Dom
|
||||
- Qwen signale à Dom toute contradiction entre agents
|
||||
|
||||
### 4. Règles de sécurité
|
||||
- Pas de secret en clair dans les messages de coordination
|
||||
- Pas de replay autonome sans Dom devant Windows
|
||||
- Pas de code mort supprimé sans GO Dom explicite
|
||||
- Pas d'activation modèle/grounder sans bench + QG + GO Dom
|
||||
|
||||
### 5. Capitalisation
|
||||
- Chaque avancée importante doit être capitalisée dans `active/` ou `registre/`
|
||||
- Les inboxes sont un flux, pas une mémoire
|
||||
- `syntheses/` = rapports d'inventaires et benchmarks
|
||||
- `registre/` = décisions formelles
|
||||
|
||||
### 6. Sessions
|
||||
- Qwen reste dans la session longue (historien, garde le fil complet)
|
||||
- Codex et Claude ont chacun leur session propre
|
||||
- Si Qwen voit des contradictions entre les instructions de Codex/Claude, il signale à Dom avant d'agir
|
||||
|
||||
---
|
||||
|
||||
## Flux de travail typique
|
||||
|
||||
```
|
||||
Codex → mission → Claude/ Qwen
|
||||
Claude → implémentation → commit → RESULTAT → Codex/Qwen
|
||||
Qwen → QG → GO/NO-GO → Codex
|
||||
Codex → synthèse → Dom → GO/NOGO
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Document vivant — mettre à jour quand les rôles évoluent.*
|
||||
233
docs/coordination/RUNBOOK-DGX-POST-REBOOT-CHECK.md
Normal file
233
docs/coordination/RUNBOOK-DGX-POST-REBOOT-CHECK.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# RUNBOOK DGX - Check post-deploiement et post-reboot POC
|
||||
|
||||
- Date: 2026-06-17
|
||||
- Responsable coordination: Codex
|
||||
- Scope: POC DGX uniquement
|
||||
- Cible labo actuelle: DGX `192.168.1.45`
|
||||
- Cible clinique preparee: Ethernet `192.168.1.178/24`, gateway `192.168.1.243`, DNS `192.168.1.9` puis `192.168.1.8`, IPv6 inactive, profil inactif tant que Dom/Codex ne donnent pas le GO.
|
||||
|
||||
## Objectif
|
||||
|
||||
Valider qu'apres deploiement puis redemarrage du DGX, toute la chaine POC reste fonctionnelle sans dependance `localhost` cote utilisateur:
|
||||
|
||||
- dashboard;
|
||||
- VWB;
|
||||
- import workflow;
|
||||
- anchors visibles;
|
||||
- agent-chat / "Discuter avec Lea";
|
||||
- streaming/fleet;
|
||||
- worker/replay;
|
||||
- installateur Lea autonome;
|
||||
- configuration reseau DGX sans bascule clinique accidentelle.
|
||||
|
||||
## Regle de conduite
|
||||
|
||||
- Dom execute uniquement les gestes UI de la section "Check Dom".
|
||||
- Les agents techniques collectent les preuves systeme et reseau.
|
||||
- Toute divergence est notee avec: etape, symptome, impact, rollback propose.
|
||||
- Pas de changement reseau clinique actif pendant le check labo: le Wi-Fi reste le lien de test.
|
||||
|
||||
## 0. Prerequis avant reboot
|
||||
|
||||
| Point | Attendu | Statut |
|
||||
|---|---|---|
|
||||
| Branche DGX | `poc-dgx` | A confirmer par Claude |
|
||||
| Commit DGX | `9605cc9d9` ou commit ulterieur explicitement valide | A confirmer par Claude |
|
||||
| Commits POC presents | `667575c3a`, `9605cc9d9` | A confirmer par Claude/Qwen |
|
||||
| Deploy propre | pas de fichier parasite embarque dans le runtime POC | A confirmer |
|
||||
| Donnees runtime | backup `backend/instance/workflows.db` avant reset/deploy, restore apres deploy, hash/size verifies | A confirmer par Claude |
|
||||
| Donnees non trackees | `data/workflows` (symlink) + donnees VWB sous `visual_workflow_builder/backend/data/` non supprimes | A confirmer |
|
||||
| VWB build | bundle reconstruit depuis `poc-dgx` | A confirmer |
|
||||
| Installateur Lea | version `1.0.1`, Python embedded obligatoire | A confirmer |
|
||||
| Profil Ethernet clinique | prepare si besoin, `autoconnect off`, non active | A confirmer par Qwen |
|
||||
| Enrolement VM Lea | VM/poste pointe vers DGX labo `192.168.1.45` pour le test, pas vers l'URL cloud | A confirmer |
|
||||
|
||||
NO-GO reboot si le deploiement n'est pas fige, si le DGX ne pointe pas sur le bon commit, ou si `workflows.db`/anchors/workflows ne sont pas verifies apres deploy.
|
||||
|
||||
## 1. Baseline avant reboot
|
||||
|
||||
Un agent technique releve l'etat avant redemarrage:
|
||||
|
||||
| Famille | Controle | Attendu |
|
||||
|---|---|---|
|
||||
| Services principaux | dashboard, VWB backend, streaming, agent-chat, worker/fleet actifs | actifs |
|
||||
| Ports utilisateur | `5001`, `5004`, `5005` disponibles depuis le LAN autorise | OK |
|
||||
| Ports internes | `5002`, `8000`, `11434` selon architecture DGX | OK ou documente |
|
||||
| Dashboard | acces par `http://192.168.1.45:5001` | login ou 401 attendu, pas page blanche |
|
||||
| Streaming | health `:5005` | 200/healthy |
|
||||
| Auth streaming | route protegee sans token | 401 attendu |
|
||||
| Agent chat | status/health `:5004` | 200/online |
|
||||
| VWB | frontend charge depuis IP DGX | OK |
|
||||
| Bundle VWB | aucune URL active `localhost`/`127.0.0.1` | OK |
|
||||
| VM Windows Lea | Lea pointe vers DGX `192.168.1.45` pour `5004/5005` | OK |
|
||||
|
||||
Si un point est deja rouge avant reboot, on corrige avant de redemarrer. Le reboot ne doit pas servir a masquer une panne.
|
||||
|
||||
## 2. Reboot DGX
|
||||
|
||||
1. Claude annonce le debut du reboot dans la coordination.
|
||||
2. Codex garde la coordination ouverte.
|
||||
3. Attendre le retour reseau du DGX sur le Wi-Fi labo.
|
||||
4. Ne pas activer le profil Ethernet clinique pendant ce cycle.
|
||||
5. Claude poste l'heure de retour et le premier etat des services.
|
||||
|
||||
NO-GO immediat si le DGX ne revient pas sur le reseau labo ou si le dashboard reste inaccessible plus de 10 minutes apres retour reseau.
|
||||
|
||||
## 3. Check technique post-reboot
|
||||
|
||||
### 3.1 Services
|
||||
|
||||
Noms constates/attendus a verifier sur le DGX. Ne pas supposer qu'un service existe: si une unite est absente, le noter et verifier si sa fonction est couverte par un autre service.
|
||||
|
||||
- `rpa-streaming`
|
||||
- `rpa-agent-chat`
|
||||
- `rpa-vision-v3-dashboard`
|
||||
- `rpa-vision-v3-vwb-frontend`
|
||||
- `rpa-vision-v3-vwb-backend`
|
||||
- `rpa-vision-v3-worker`
|
||||
- `rpa-vision-v3-stream-worker` si installe, sinon documenter l'absence
|
||||
- `rpa-vision-v3-api`
|
||||
- `rpa-grounding` ou service grounder equivalent si present; sinon documenter le fallback
|
||||
- `rpa-vision-v3-healthcheck.timer` si actif dans le deploiement final; sinon ne pas bloquer mais documenter
|
||||
|
||||
Attendu:
|
||||
|
||||
- services POC critiques actifs apres reboot;
|
||||
- services POC critiques enabled si necessaires au demarrage automatique;
|
||||
- pas de boucle restart;
|
||||
- logs recents sans traceback bloquant;
|
||||
- healthcheck timer actif ou decision documentee si non utilise;
|
||||
- VWB frontend servi par le vrai frontend POC (`frontend_v4`), pas l'ancien `frontend/`.
|
||||
- Ne jamais utiliser `git clean -xfd` sur DGX pendant la sequence POC: cela detruirait les donnees runtime non trackees.
|
||||
|
||||
### 3.2 Reseau et ports
|
||||
|
||||
| Port | Usage | Attendu labo |
|
||||
|---|---|---|
|
||||
| `5001` | dashboard / VWB integre | accessible via `192.168.1.45` |
|
||||
| `5004` | agent-chat / ChatWindow Lea | accessible depuis VM Windows autorisee |
|
||||
| `5005` | streaming/fleet/replay | accessible depuis VM Windows autorisee |
|
||||
| `5002` | VWB backend si separe | conforme au deploiement DGX |
|
||||
| `8000` | upload/API core si actif | conforme au deploiement DGX |
|
||||
| `11434` | Ollama local DGX | local/interne sauf decision contraire |
|
||||
|
||||
Attendu:
|
||||
|
||||
- aucun appel navigateur produit vers `localhost` ou `127.0.0.1`;
|
||||
- `5004/5005` repondent depuis la VM Windows;
|
||||
- le dashboard charge toutes ses ressources depuis l'IP DGX;
|
||||
- la carte Ethernet clinique reste inactive ou non connectee pendant le check labo.
|
||||
|
||||
### 3.3 Endpoints et fonctions serveur
|
||||
|
||||
| Controle | Attendu |
|
||||
|---|---|
|
||||
| Dashboard `5001` | page login/auth visible, pas 500 |
|
||||
| Streaming `5005/health` | healthy |
|
||||
| Streaming route protegee sans token | 401 |
|
||||
| Fleet machines | VM Lea visible ou reenrolement documente |
|
||||
| Enrolement VM Lea | `config.txt`/runtime pointe vers DGX, pas vers URL cloud |
|
||||
| Replay next | repond correctement pour machine enrollee |
|
||||
| Agent chat `5004` | online et session chat possible |
|
||||
| VWB catalog/workflows | liste chargee |
|
||||
| Anchors API | upload/thumbnail/original accessibles depuis IP DGX |
|
||||
| Worker | pas de backlog bloquant, pas de crash loop |
|
||||
| Modele/grounding | service disponible ou fallback documente |
|
||||
|
||||
## 4. Check Dom UI post-reboot
|
||||
|
||||
Dom ne fait que ces gestes:
|
||||
|
||||
| Etape | Geste Dom | GO | NO-GO |
|
||||
|---|---|---|---|
|
||||
| T1 Dashboard | Ouvrir l'URL DGX fournie | page login/dashboard visible | page blanche, site inaccessible, erreur 500 |
|
||||
| T2 Chat Lea | Dans la VM, ouvrir "Discuter avec Lea", envoyer "Bonjour" | reponse en quelques secondes | pas connectee, erreur serveur, fermeture fenetre |
|
||||
| T3 Bulles action | Demander une action simple | bulles/progression visibles | aucune progression visible pendant une action lancee |
|
||||
| T4 Import VWB | Importer un workflow JSON Lea | succes, workflow visible | erreur, rien ne se passe |
|
||||
| T5 Anchors | Ouvrir une etape avec ancre | image d'ancre/crop visible | image absente, plein ecran au lieu de crop |
|
||||
| T6 Replay supervise | Lancer un replay safe/supervise | execution ou demande de confirmation | clic hors cible, blocage, absence de reaction |
|
||||
| T7 Reconnexion apres reboot | Fermer/rouvrir Lea sur VM | reconnecte DGX sans reconfig manuelle | demande Python/outils externes ou config manuelle non prevue |
|
||||
|
||||
## 5. Criteres GO/NO-GO
|
||||
|
||||
GO livraison labo si:
|
||||
|
||||
- DGX revient apres reboot;
|
||||
- services critiques actifs;
|
||||
- dashboard accessible via IP DGX;
|
||||
- VWB charge sans `localhost`;
|
||||
- chat Lea fonctionne depuis VM;
|
||||
- import workflow OK;
|
||||
- anchors visibles;
|
||||
- replay supervise OK ou comportement d'arret/confirmation conforme;
|
||||
- installateur Lea autonome confirme;
|
||||
- profil Ethernet clinique non active par erreur.
|
||||
|
||||
NO-GO livraison si un des points suivants echoue:
|
||||
|
||||
- DGX ne revient pas proprement apres reboot;
|
||||
- dashboard/VWB inaccessible;
|
||||
- `5004` ou `5005` indisponible depuis la VM;
|
||||
- "Discuter avec Lea" ne fonctionne pas;
|
||||
- installateur demande Python ou un outil externe;
|
||||
- VWB appelle encore `localhost` cote navigateur;
|
||||
- import/anchors/replay cassent;
|
||||
- configuration Ethernet clinique activee accidentellement pendant les tests labo.
|
||||
|
||||
## 6. Preuves a archiver
|
||||
|
||||
Claude fournit:
|
||||
|
||||
- commit DGX courant;
|
||||
- preuve backup/restore `workflows.db` si reset/deploy effectue;
|
||||
- confirmation que `data/workflows` et les vrais chemins `visual_workflow_builder/backend/data/anchors`, `visual_workflow_builder/backend/data/anchor_images`, `visual_workflow_builder/backend/instance/workflows.db` sont presents apres deploy/reboot;
|
||||
- liste services actifs/enabled;
|
||||
- ports et endpoints avec resultats;
|
||||
- resultat grep bundle no-localhost;
|
||||
- resultat test VM Lea chat;
|
||||
- resultat import/anchors/replay;
|
||||
- chemin de l'artefact installateur Lea 1.0.1;
|
||||
- incidents et rollback si besoin.
|
||||
|
||||
Qwen fournit:
|
||||
|
||||
- controle croise commits/fichiers parasites;
|
||||
- matrice GO/NO-GO post-reboot;
|
||||
- verification profil Ethernet clinique inactif;
|
||||
- etat VM Windows 11 ARM DGX ou decision fallback VMware clinique.
|
||||
|
||||
Codex consolide:
|
||||
|
||||
- verdict final pret/pas pret;
|
||||
- liste des anomalies restantes;
|
||||
- decision de passage au check site ou retour correction.
|
||||
|
||||
## 7. Rollback minimal
|
||||
|
||||
Si VWB regresse:
|
||||
|
||||
- revenir au dernier commit POC valide connu;
|
||||
- redeployer le bundle precedent;
|
||||
- redemarrer uniquement dashboard/VWB backend;
|
||||
- refaire T1, T4, T5.
|
||||
|
||||
Si agent-chat regresse:
|
||||
|
||||
- verifier service `rpa-agent-chat`;
|
||||
- verifier env `5004`/feedback bus;
|
||||
- redemarrer agent-chat puis streaming si necessaire;
|
||||
- refaire T2/T3.
|
||||
|
||||
Si streaming/fleet regresse:
|
||||
|
||||
- verifier service `rpa-streaming`;
|
||||
- verifier token/auth;
|
||||
- verifier reenrolement VM;
|
||||
- refaire T2/T6.
|
||||
|
||||
Si reseau clinique s'active par erreur:
|
||||
|
||||
- revenir au profil Wi-Fi labo;
|
||||
- desactiver le profil Ethernet clinique;
|
||||
- confirmer retour dashboard `192.168.1.45`;
|
||||
- ne reprendre les tests qu'apres stabilisation.
|
||||
97
docs/coordination/RUNBOOK-LEA-LIVE-DRAFT.md
Normal file
97
docs/coordination/RUNBOOK-LEA-LIVE-DRAFT.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Runbook — Test Lea live grandeur nature
|
||||
|
||||
- `Date`: 2026-06-08
|
||||
- `Auteur`: Qwen (draft)
|
||||
- `Statut`: DRAFT — en attente preflight vert + GO Dom
|
||||
- **Interdit** : ❌ Replay autonome
|
||||
|
||||
---
|
||||
|
||||
## Prérequis
|
||||
|
||||
| # | Vérification | Commande / Action | Critère GO |
|
||||
|---|---|---|---|
|
||||
| 1 | Windows cible visible | `ping DESKTOP-58D5CAC_windows` | ✅ Répond |
|
||||
| 2 | httpx OK côté Windows | `python -c "import httpx; print('OK')"` dans venv Windows | ✅ Import OK |
|
||||
| 3 | Agent V1 en cours | Vérifier processus Windows agent V1 | ✅ Running |
|
||||
| 4 | DGX Ollama accessible | `curl http://localhost:11434/api/tags` | ✅ Répond |
|
||||
| 5 | Workflows acquis retrouvés | Dashboard → workflows chargés | ✅ ≥ 1 workflow visible |
|
||||
| 6 | Dom présent physiquement | Devant le poste Windows | ✅ Confirmé |
|
||||
|
||||
---
|
||||
|
||||
## Exécution
|
||||
|
||||
### Étape 1 : Préflight (NON destructif)
|
||||
|
||||
```bash
|
||||
# Sur le poste dev Linux
|
||||
curl -X POST http://localhost:5005/api/v1/traces/stream/replay/preflight \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"machine_id": "DESKTOP-58D5CAC_windows"}'
|
||||
```
|
||||
|
||||
**Attendu** : `workflow_known: true`, aucun blocage G1-G6.
|
||||
|
||||
### Étape 2 : Capture Shadow supervisée
|
||||
|
||||
1. Ouvrir le dashboard Lea
|
||||
2. Démarrer un apprentissage supervisé
|
||||
3. Scénario safe : Notepad → Enregistrer → Explorateur → Easily Assure
|
||||
4. **Ne pas** déclencher de replay autonome
|
||||
5. Observer et collecter les preuves en temps réel
|
||||
|
||||
### Étape 3 : Collecte des preuves
|
||||
|
||||
| Preuve | Où la trouver | Critère |
|
||||
|---|---|---|
|
||||
| `live_events.jsonl` | `data/training/live_events.jsonl` | Non vide, timestamps cohérents |
|
||||
| `learn_*.json` | `agent_chat/state/learn_*.json` | Présent, state non vide |
|
||||
| Shadow understanding | `agent_chat/state/` | Compréhension non vide |
|
||||
| Screenshots avant/après | Captures manuelles | Chaque action documentée |
|
||||
| Rapports preflight | Sortie curl étape 1 | `workflow_known: true` |
|
||||
|
||||
### Étape 4 : Arrêt propre
|
||||
|
||||
1. Stopper l'apprentissage via le dashboard
|
||||
2. Vérifier que les fichiers de state sont cohérents
|
||||
3. Ne pas lancer de replay automatique
|
||||
|
||||
---
|
||||
|
||||
## Critères GO/NOGO
|
||||
|
||||
### GO (tous doivent être verts)
|
||||
|
||||
| ID | Critère |
|
||||
|---|---|
|
||||
| G1 | Préflight `workflow_known: true` |
|
||||
| G2 | Windows cible joignable |
|
||||
| G3 | `httpx` import OK côté Windows |
|
||||
| G4 | DGX Ollama accessible (`localhost:11434`) |
|
||||
| G5 | Workflows acquis retrouvés (≥ 1) |
|
||||
| G6 | Dom confirme présent physiquement |
|
||||
|
||||
### NOGO (un seul suffit)
|
||||
|
||||
| ID | Critère |
|
||||
|---|---|
|
||||
| N1 | Préflight KO |
|
||||
| N2 | Windows injoignable |
|
||||
| N3 | `httpx` absent |
|
||||
| N4 | DGX Ollama inaccessible |
|
||||
| N5 | Dom absent devant Windows |
|
||||
| N6 | Replay autonome demandé |
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
1. Arrêter proprement l'apprentissage
|
||||
2. Ne pas supprimer les fichiers de state
|
||||
3. Documenter le point d'arrêt
|
||||
4. Rapporter à Codex/Claude
|
||||
|
||||
---
|
||||
|
||||
*Ce runbook est un draft. Il sera validé après preflight vert et GO Dom.*
|
||||
@@ -1,54 +1,592 @@
|
||||
#!/bin/bash
|
||||
# Coordination inbox loop v3 — compare par nom de fichiers
|
||||
#!/usr/bin/env bash
|
||||
# Coordination inbox loop v4.
|
||||
#
|
||||
# One-shot by default:
|
||||
# docs/coordination/coordination_loop.sh once
|
||||
#
|
||||
# Long-running foreground loop:
|
||||
# docs/coordination/coordination_loop.sh watch 15
|
||||
#
|
||||
# Background loop:
|
||||
# docs/coordination/coordination_loop.sh start 15
|
||||
#
|
||||
# Trigger files:
|
||||
# docs/coordination/.loop_state/unread_messages.tsv
|
||||
# docs/coordination/.loop_state/unread_digest.md
|
||||
# docs/coordination/.loop_state/latest_message.trigger
|
||||
# docs/coordination/.loop_state/message_events.tsv
|
||||
|
||||
COORD_DIR="/home/dom/ai/rpa_vision_v3/docs/coordination"
|
||||
LOG="/home/dom/ai/rpa_vision_v3/docs/coordination/.loop_log.txt"
|
||||
TMP="/tmp/coord_loop"
|
||||
mkdir -p "$TMP"
|
||||
set -euo pipefail
|
||||
|
||||
NEW_FOUND=0
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT_PATH="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[0]}")"
|
||||
|
||||
check_inbox() {
|
||||
local inbox_name="$1"
|
||||
local baseline_file="$TMP/baseline_${inbox_name}.txt"
|
||||
local inbox_path="${COORD_DIR}/${inbox_name}"
|
||||
local current_file="$TMP/current_${inbox_name}.txt"
|
||||
COORD_DIR="${COORD_DIR:-$SCRIPT_DIR}"
|
||||
LOG="${COORD_LOOP_LOG:-$COORD_DIR/.loop_log.txt}"
|
||||
SUMMARY="${COORD_LOOP_BASELINE:-$COORD_DIR/.inbox_baseline.txt}"
|
||||
STATE_DIR="${COORD_LOOP_STATE_DIR:-$COORD_DIR/.loop_state}"
|
||||
PID_FILE="${COORD_LOOP_PID_FILE:-$STATE_DIR/coordination_loop.pid}"
|
||||
OUT_FILE="${COORD_LOOP_OUT:-$STATE_DIR/coordination_loop.out}"
|
||||
DEFAULT_INTERVAL="${COORD_LOOP_INTERVAL:-15}"
|
||||
EVENTS_FILE="${COORD_LOOP_EVENTS_FILE:-$STATE_DIR/message_events.tsv}"
|
||||
PENDING_FILE="${COORD_LOOP_PENDING_FILE:-$STATE_DIR/unread_messages.tsv}"
|
||||
DIGEST_FILE="${COORD_LOOP_DIGEST_FILE:-$STATE_DIR/unread_digest.md}"
|
||||
LATEST_TRIGGER="${COORD_LOOP_LATEST_TRIGGER:-$STATE_DIR/latest_message.trigger}"
|
||||
TRIGGER_DIR="${COORD_LOOP_TRIGGER_DIR:-$STATE_DIR/triggers}"
|
||||
TRIGGER_CMD="${COORD_LOOP_TRIGGER_CMD:-}"
|
||||
DESKTOP_NOTIFY="${COORD_LOOP_DESKTOP_NOTIFY:-1}"
|
||||
SYSTEMD_UNIT_NAME="${COORD_LOOP_SYSTEMD_UNIT:-rpa-coordination-watcher.service}"
|
||||
|
||||
ls "$inbox_path" 2>/dev/null | sort > "$current_file"
|
||||
if [[ -n "${COORD_LOOP_DIRS:-}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
WATCH_DIRS=($COORD_LOOP_DIRS)
|
||||
else
|
||||
WATCH_DIRS=(inbox_qwen inbox_codex inbox_claude active)
|
||||
fi
|
||||
|
||||
if [ ! -f "$baseline_file" ]; then
|
||||
cp "$current_file" "$baseline_file"
|
||||
DRY_RUN=0
|
||||
ARGS=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
-h|--help) ARGS+=("help") ;;
|
||||
*) ARGS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
set -- "${ARGS[@]}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [command] [interval_seconds] [--dry-run]
|
||||
|
||||
Commands:
|
||||
once Scan once and update the persistent baseline (default).
|
||||
watch Scan forever in the foreground.
|
||||
start Start watch mode in the background.
|
||||
ensure Start if needed, scan once, then show pending messages.
|
||||
stop Stop the background loop.
|
||||
status Show background loop status and current counters.
|
||||
pending Show unread coordination messages detected by the loop.
|
||||
ack Mark detected coordination messages as read locally.
|
||||
events Show recent message trigger events.
|
||||
service-install Install/update and restart the user systemd watcher service.
|
||||
service-stop Stop and disable the user systemd watcher service.
|
||||
service-status Show the user systemd watcher service status.
|
||||
baseline Reset the persistent baseline to the current files.
|
||||
tail Tail the loop log.
|
||||
|
||||
Environment:
|
||||
COORD_LOOP_DIRS="inbox_qwen inbox_codex inbox_claude active"
|
||||
COORD_LOOP_INTERVAL=15
|
||||
COORD_LOOP_TRIGGER_CMD='command to run for each new message'
|
||||
COORD_LOOP_DESKTOP_NOTIFY=1
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_state_dir() {
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
mkdir -p "$STATE_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
timestamp_human() {
|
||||
date '+%Y-%m-%d %H:%M'
|
||||
}
|
||||
|
||||
timestamp_file() {
|
||||
date '+%Y-%m-%d_%H%M'
|
||||
}
|
||||
|
||||
state_file_for() {
|
||||
local dir_name="$1"
|
||||
printf '%s/%s.files' "$STATE_DIR" "$dir_name"
|
||||
}
|
||||
|
||||
current_file_for() {
|
||||
local dir_name="$1"
|
||||
printf '%s/%s.current' "$STATE_DIR" "$dir_name"
|
||||
}
|
||||
|
||||
list_files() {
|
||||
local dir_name="$1"
|
||||
local dir_path="$COORD_DIR/$dir_name"
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
find "$dir_path" -maxdepth 1 -type f ! -name '.*' -printf '%f\n' | LC_ALL=C sort -u
|
||||
}
|
||||
|
||||
summary_epoch() {
|
||||
if [[ ! -f "$SUMMARY" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local ts
|
||||
ts="$(awk -F: '$1 == "timestamp" {print $2}' "$SUMMARY" | tail -n 1)"
|
||||
if [[ -z "$ts" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
date -d "${ts/_/ }" '+%s' 2>/dev/null
|
||||
}
|
||||
|
||||
bootstrap_baseline_from_summary() {
|
||||
local dir_name="$1"
|
||||
local baseline_file="$2"
|
||||
local dir_path="$COORD_DIR/$dir_name"
|
||||
local epoch
|
||||
|
||||
epoch="$(summary_epoch)" || return 1
|
||||
[[ -d "$dir_path" ]] || return 1
|
||||
|
||||
find "$dir_path" -maxdepth 1 -type f ! -name '.*' -printf '%T@ %f\n' \
|
||||
| awk -v cutoff="$epoch" '$1 <= cutoff {sub(/^[^ ]+ /, ""); print}' \
|
||||
| LC_ALL=C sort -u > "$baseline_file"
|
||||
}
|
||||
|
||||
count_files() {
|
||||
local dir_name="$1"
|
||||
list_files "$dir_name" | wc -l | tr -d ' '
|
||||
}
|
||||
|
||||
extract_status() {
|
||||
local file_path="$1"
|
||||
grep -m1 -E '(^[[:space:]-]*`?Statut`?[[:space:]]*:|^\*\*Statut[^*]*\*\*[[:space:]]*:)' "$file_path" 2>/dev/null \
|
||||
| sed 's/[[:space:]]*$//' || true
|
||||
}
|
||||
|
||||
pending_count() {
|
||||
if [[ -f "$PENDING_FILE" ]]; then
|
||||
wc -l < "$PENDING_FILE" | tr -d ' '
|
||||
else
|
||||
printf '0'
|
||||
fi
|
||||
}
|
||||
|
||||
write_pending_digest() {
|
||||
[[ "$DRY_RUN" -eq 1 ]] && return 0
|
||||
ensure_state_dir
|
||||
|
||||
local count
|
||||
count="$(pending_count)"
|
||||
{
|
||||
printf '# Coordination unread digest\n\n'
|
||||
printf -- '- `Updated`: %s\n' "$(date --iso-8601=seconds)"
|
||||
printf -- '- `Pending`: %s\n\n' "$count"
|
||||
|
||||
if [[ "$count" == "0" || ! -s "$PENDING_FILE" ]]; then
|
||||
printf 'No pending coordination messages.\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '## Pending messages\n\n'
|
||||
while IFS=$'\t' read -r ts dir_name file_name file_path _rest; do
|
||||
[[ -z "${file_path:-}" ]] && continue
|
||||
printf -- '- `%s` `%s` `%s`\n' "$ts" "$dir_name" "$file_name"
|
||||
printf ' - path: `%s`\n' "$file_path"
|
||||
if [[ -f "$file_path" ]]; then
|
||||
local title
|
||||
local status_line
|
||||
title="$(sed -n '1p' "$file_path" | sed 's/[[:space:]]*$//')"
|
||||
status_line="$(extract_status "$file_path")"
|
||||
[[ -n "$title" ]] && printf ' - title: %s\n' "$title"
|
||||
[[ -n "$status_line" ]] && printf ' - status: %s\n' "$status_line"
|
||||
fi
|
||||
done < "$PENDING_FILE"
|
||||
|
||||
printf '\n## Commands\n\n'
|
||||
printf -- '- Read pending: `docs/coordination/coordination_loop.sh pending`\n'
|
||||
printf -- '- Ack after processing: `docs/coordination/coordination_loop.sh ack`\n'
|
||||
} > "$DIGEST_FILE"
|
||||
}
|
||||
|
||||
safe_fragment() {
|
||||
printf '%s' "$1" | tr -c 'A-Za-z0-9._=-' '_' | cut -c 1-180
|
||||
}
|
||||
|
||||
record_message_event() {
|
||||
local dir_name="$1"
|
||||
local dir_path="$2"
|
||||
local file_name="$3"
|
||||
local status_line="$4"
|
||||
|
||||
[[ "$DRY_RUN" -eq 1 ]] && return 0
|
||||
|
||||
mkdir -p "$TRIGGER_DIR"
|
||||
|
||||
local ts_iso
|
||||
local ts_file
|
||||
local safe_file
|
||||
local file_path
|
||||
local trigger_file
|
||||
local status_clean
|
||||
|
||||
ts_iso="$(date --iso-8601=seconds)"
|
||||
ts_file="$(date '+%Y%m%dT%H%M%S')"
|
||||
safe_file="$(safe_fragment "$file_name")"
|
||||
file_path="$dir_path/$file_name"
|
||||
trigger_file="$TRIGGER_DIR/${ts_file}_${dir_name}_${safe_file}.trigger"
|
||||
status_clean="${status_line//$'\t'/ }"
|
||||
status_clean="${status_clean//$'\n'/ }"
|
||||
|
||||
{
|
||||
printf 'timestamp=%s\n' "$ts_iso"
|
||||
printf 'dir=%s\n' "$dir_name"
|
||||
printf 'file=%s\n' "$file_name"
|
||||
printf 'path=%s\n' "$file_path"
|
||||
printf 'status=%s\n' "$status_clean"
|
||||
} > "$trigger_file"
|
||||
|
||||
cp "$trigger_file" "$LATEST_TRIGGER"
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$ts_iso" "$dir_name" "$file_name" "$file_path" "$status_clean" >> "$EVENTS_FILE"
|
||||
printf '%s\t%s\t%s\t%s\n' "$ts_iso" "$dir_name" "$file_name" "$file_path" >> "$PENDING_FILE"
|
||||
write_pending_digest
|
||||
|
||||
if [[ "$DESKTOP_NOTIFY" == "1" ]] && command -v notify-send >/dev/null 2>&1; then
|
||||
notify-send "Coordination: nouveau message" "${dir_name}/${file_name}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ -n "$TRIGGER_CMD" ]]; then
|
||||
(
|
||||
export COORD_MESSAGE_TIMESTAMP="$ts_iso"
|
||||
export COORD_MESSAGE_DIR="$dir_name"
|
||||
export COORD_MESSAGE_FILE="$file_name"
|
||||
export COORD_MESSAGE_PATH="$file_path"
|
||||
export COORD_MESSAGE_STATUS="$status_clean"
|
||||
export COORD_TRIGGER_FILE="$trigger_file"
|
||||
bash -lc "$TRIGGER_CMD"
|
||||
) >> "$OUT_FILE" 2>&1 || true &
|
||||
fi
|
||||
}
|
||||
|
||||
write_summary() {
|
||||
local tmp_summary="$STATE_DIR/inbox_baseline.tmp"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
for dir_name in "${WATCH_DIRS[@]}"; do
|
||||
printf '%s:%s\n' "$dir_name" "$(count_files "$dir_name")"
|
||||
done
|
||||
printf 'timestamp:%s\n' "$(timestamp_file)"
|
||||
return
|
||||
fi
|
||||
|
||||
local new_files
|
||||
new_files=$(grep -Fxvf "$baseline_file" "$current_file" 2>/dev/null)
|
||||
|
||||
if [ -n "$new_files" ]; then
|
||||
NEW_FOUND=1
|
||||
local count
|
||||
count=$(echo "$new_files" | wc -l)
|
||||
echo "[$(date '+%Y-%m-%d %H:%M')] 📥 ${inbox_name}: +${count} nouveau(x) message(s)" >> "$LOG"
|
||||
echo "$new_files" | while read -r f; do
|
||||
echo " → $f" >> "$LOG"
|
||||
local statut
|
||||
statut=$(grep -m1 'Statut' "${inbox_path}/${f}" 2>/dev/null || echo "")
|
||||
if [ -n "$statut" ]; then
|
||||
echo " ${statut}" >> "$LOG"
|
||||
fi
|
||||
done
|
||||
echo "" >> "$LOG"
|
||||
fi
|
||||
|
||||
cp "$current_file" "$baseline_file"
|
||||
: > "$tmp_summary"
|
||||
for dir_name in "${WATCH_DIRS[@]}"; do
|
||||
printf '%s:%s\n' "$dir_name" "$(count_files "$dir_name")" >> "$tmp_summary"
|
||||
done
|
||||
printf 'timestamp:%s\n' "$(timestamp_file)" >> "$tmp_summary"
|
||||
mv "$tmp_summary" "$SUMMARY"
|
||||
}
|
||||
|
||||
check_inbox "inbox_qwen"
|
||||
check_inbox "inbox_codex"
|
||||
check_inbox "inbox_claude"
|
||||
log_line() {
|
||||
local line="$1"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
printf '%s\n' "$line"
|
||||
else
|
||||
printf '%s\n' "$line" >> "$LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$NEW_FOUND" -eq 1 ]; then
|
||||
echo "📥 Nouveau message coordination détecté — voir $LOG"
|
||||
else
|
||||
echo "❤️ loop OK $(date '+%H:%M')"
|
||||
fi
|
||||
reset_baseline() {
|
||||
ensure_state_dir
|
||||
local scan_lock_fd
|
||||
exec {scan_lock_fd}>"$STATE_DIR/scan.lock"
|
||||
flock "$scan_lock_fd"
|
||||
|
||||
for dir_name in "${WATCH_DIRS[@]}"; do
|
||||
list_files "$dir_name" > "$(state_file_for "$dir_name")"
|
||||
done
|
||||
write_summary
|
||||
write_pending_digest
|
||||
log_line "[$(timestamp_human)] coordination loop baseline reset"
|
||||
|
||||
flock -u "$scan_lock_fd"
|
||||
exec {scan_lock_fd}>&-
|
||||
printf 'Baseline coordination initialisee: %s\n' "$SUMMARY"
|
||||
}
|
||||
|
||||
scan_once() {
|
||||
ensure_state_dir
|
||||
local scan_lock_fd
|
||||
exec {scan_lock_fd}>"$STATE_DIR/scan.lock"
|
||||
flock "$scan_lock_fd"
|
||||
|
||||
local new_found=0
|
||||
local initialized=0
|
||||
|
||||
for dir_name in "${WATCH_DIRS[@]}"; do
|
||||
local dir_path="$COORD_DIR/$dir_name"
|
||||
local baseline_file
|
||||
local current_file
|
||||
local temp_baseline=0
|
||||
baseline_file="$(state_file_for "$dir_name")"
|
||||
current_file="$(current_file_for "$dir_name")"
|
||||
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
current_file="$(mktemp)"
|
||||
list_files "$dir_name" > "$current_file"
|
||||
else
|
||||
list_files "$dir_name" > "$current_file"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$baseline_file" ]]; then
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
baseline_file="$(mktemp)"
|
||||
temp_baseline=1
|
||||
if ! bootstrap_baseline_from_summary "$dir_name" "$baseline_file"; then
|
||||
initialized=1
|
||||
cp "$current_file" "$baseline_file"
|
||||
fi
|
||||
else
|
||||
if ! bootstrap_baseline_from_summary "$dir_name" "$baseline_file"; then
|
||||
initialized=1
|
||||
cp "$current_file" "$baseline_file"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
LC_ALL=C sort -u "$baseline_file" -o "$baseline_file"
|
||||
|
||||
local new_files
|
||||
new_files="$(LC_ALL=C comm -13 "$baseline_file" "$current_file" || true)"
|
||||
|
||||
if [[ -n "$new_files" ]]; then
|
||||
new_found=1
|
||||
local count
|
||||
count="$(printf '%s\n' "$new_files" | wc -l | tr -d ' ')"
|
||||
log_line "[$(timestamp_human)] 📥 ${dir_name}: +${count} nouveau(x) message(s)"
|
||||
|
||||
while IFS= read -r file_name; do
|
||||
[[ -z "$file_name" ]] && continue
|
||||
log_line " → $file_name"
|
||||
local status_line
|
||||
status_line="$(extract_status "$dir_path/$file_name")"
|
||||
if [[ -n "$status_line" ]]; then
|
||||
log_line " ${status_line}"
|
||||
fi
|
||||
record_message_event "$dir_name" "$dir_path" "$file_name" "$status_line"
|
||||
done <<< "$new_files"
|
||||
log_line ""
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
cp "$current_file" "$baseline_file"
|
||||
else
|
||||
rm -f "$current_file"
|
||||
fi
|
||||
[[ "$temp_baseline" -eq 1 ]] && rm -f "$baseline_file"
|
||||
done
|
||||
|
||||
write_summary
|
||||
|
||||
local rc=0
|
||||
if [[ "$new_found" -eq 1 ]]; then
|
||||
printf 'Nouveau message coordination detecte - voir %s\n' "$LOG"
|
||||
rc=2
|
||||
elif [[ "$initialized" -eq 1 ]]; then
|
||||
printf 'Baseline coordination initialisee - aucun ancien message rejoue\n'
|
||||
else
|
||||
printf 'loop OK %s\n' "$(date '+%H:%M')"
|
||||
fi
|
||||
|
||||
flock -u "$scan_lock_fd"
|
||||
exec {scan_lock_fd}>&-
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
watch_loop() {
|
||||
local interval="${1:-$DEFAULT_INTERVAL}"
|
||||
ensure_state_dir
|
||||
printf '%s\n' "$$" > "$PID_FILE"
|
||||
trap 'if [[ -f "'"$PID_FILE"'" ]] && [[ "$(cat "'"$PID_FILE"'")" == "'"$$"'" ]]; then rm -f "'"$PID_FILE"'"; fi' EXIT INT TERM
|
||||
log_line "=== Coordination loop started $(timestamp_human), interval=${interval}s ==="
|
||||
while true; do
|
||||
scan_once || true
|
||||
sleep "$interval"
|
||||
done
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
|
||||
}
|
||||
|
||||
start_loop() {
|
||||
local interval="${1:-$DEFAULT_INTERVAL}"
|
||||
ensure_state_dir
|
||||
if is_running; then
|
||||
printf 'Coordination loop deja actif: pid=%s\n' "$(cat "$PID_FILE")"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
if command -v setsid >/dev/null 2>&1; then
|
||||
setsid bash -c '
|
||||
pid_file="$1"
|
||||
script_path="$2"
|
||||
interval="$3"
|
||||
out_file="$4"
|
||||
printf "%s\n" "$$" > "$pid_file"
|
||||
exec "$script_path" watch "$interval" >> "$out_file" 2>&1 < /dev/null
|
||||
' _ "$PID_FILE" "$SCRIPT_PATH" "$interval" "$OUT_FILE" &
|
||||
else
|
||||
nohup bash -c '
|
||||
pid_file="$1"
|
||||
script_path="$2"
|
||||
interval="$3"
|
||||
out_file="$4"
|
||||
printf "%s\n" "$$" > "$pid_file"
|
||||
exec "$script_path" watch "$interval" >> "$out_file" 2>&1 < /dev/null
|
||||
' _ "$PID_FILE" "$SCRIPT_PATH" "$interval" "$OUT_FILE" >/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
local launcher_pid=$!
|
||||
local pid=""
|
||||
for _ in 1 2 3 4 5; do
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
pid="$(cat "$PID_FILE")"
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ -z "$pid" ]]; then
|
||||
pid="$launcher_pid"
|
||||
printf '%s\n' "$pid" > "$PID_FILE"
|
||||
fi
|
||||
printf 'Coordination loop demarre: pid=%s interval=%ss\n' "$pid" "$interval"
|
||||
printf 'Log: %s\n' "$LOG"
|
||||
}
|
||||
|
||||
ensure_loop() {
|
||||
local interval="${1:-$DEFAULT_INTERVAL}"
|
||||
if ! is_running; then
|
||||
start_loop "$interval"
|
||||
fi
|
||||
scan_once || true
|
||||
show_status
|
||||
show_pending
|
||||
}
|
||||
|
||||
stop_loop() {
|
||||
if command -v systemctl >/dev/null 2>&1 \
|
||||
&& systemctl --user is-active --quiet "$SYSTEMD_UNIT_NAME" 2>/dev/null; then
|
||||
systemctl --user stop "$SYSTEMD_UNIT_NAME" || true
|
||||
rm -f "$PID_FILE"
|
||||
printf 'Service watcher arrete: %s\n' "$SYSTEMD_UNIT_NAME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! is_running; then
|
||||
printf 'Coordination loop inactif\n'
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
local pid
|
||||
pid="$(cat "$PID_FILE")"
|
||||
kill "$pid"
|
||||
rm -f "$PID_FILE"
|
||||
printf 'Coordination loop arrete: pid=%s\n' "$pid"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
if is_running; then
|
||||
printf 'Coordination loop: actif pid=%s\n' "$(cat "$PID_FILE")"
|
||||
else
|
||||
printf 'Coordination loop: inactif\n'
|
||||
fi
|
||||
printf 'Dirs: %s\n' "${WATCH_DIRS[*]}"
|
||||
for dir_name in "${WATCH_DIRS[@]}"; do
|
||||
printf '%s:%s\n' "$dir_name" "$(count_files "$dir_name")"
|
||||
done
|
||||
[[ -f "$SUMMARY" ]] && printf 'Baseline: %s\n' "$SUMMARY"
|
||||
[[ -f "$LOG" ]] && printf 'Log: %s\n' "$LOG"
|
||||
printf 'Unread trigger queue: %s (%s pending)\n' "$PENDING_FILE" "$(pending_count)"
|
||||
printf 'Unread digest: %s\n' "$DIGEST_FILE"
|
||||
[[ -f "$LATEST_TRIGGER" ]] && printf 'Latest trigger: %s\n' "$LATEST_TRIGGER"
|
||||
[[ -n "$TRIGGER_CMD" ]] && printf 'Trigger cmd: configured\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
show_pending() {
|
||||
if [[ ! -s "$PENDING_FILE" ]]; then
|
||||
printf 'Aucun message coordination en attente dans %s\n' "$PENDING_FILE"
|
||||
return 0
|
||||
fi
|
||||
cat "$PENDING_FILE"
|
||||
}
|
||||
|
||||
ack_pending() {
|
||||
ensure_state_dir
|
||||
local scan_lock_fd
|
||||
exec {scan_lock_fd}>"$STATE_DIR/scan.lock"
|
||||
flock "$scan_lock_fd"
|
||||
|
||||
: > "$PENDING_FILE"
|
||||
write_pending_digest
|
||||
log_line "[$(timestamp_human)] unread coordination trigger queue acked"
|
||||
|
||||
flock -u "$scan_lock_fd"
|
||||
exec {scan_lock_fd}>&-
|
||||
printf 'Messages coordination marques lus localement: %s\n' "$PENDING_FILE"
|
||||
}
|
||||
|
||||
show_events() {
|
||||
if [[ ! -s "$EVENTS_FILE" ]]; then
|
||||
printf 'Aucun evenement coordination dans %s\n' "$EVENTS_FILE"
|
||||
return 0
|
||||
fi
|
||||
tail -n "${1:-40}" "$EVENTS_FILE"
|
||||
}
|
||||
|
||||
install_user_service() {
|
||||
local user_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
local unit_path="$user_dir/$SYSTEMD_UNIT_NAME"
|
||||
local template_path="$COORD_DIR/systemd/$SYSTEMD_UNIT_NAME"
|
||||
|
||||
if [[ ! -f "$template_path" ]]; then
|
||||
printf 'Template systemd introuvable: %s\n' "$template_path" >&2
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "$user_dir"
|
||||
install -m 0644 "$template_path" "$unit_path"
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable "$SYSTEMD_UNIT_NAME"
|
||||
systemctl --user restart "$SYSTEMD_UNIT_NAME"
|
||||
printf 'Service watcher installe/mis a jour et redemarre: %s\n' "$unit_path"
|
||||
systemctl --user --no-pager --full status "$SYSTEMD_UNIT_NAME" || true
|
||||
}
|
||||
|
||||
stop_user_service() {
|
||||
systemctl --user disable --now "$SYSTEMD_UNIT_NAME" || true
|
||||
rm -f "$PID_FILE"
|
||||
printf 'Service watcher desactive et arrete: %s\n' "$SYSTEMD_UNIT_NAME"
|
||||
}
|
||||
|
||||
show_service_status() {
|
||||
systemctl --user --no-pager --full status "$SYSTEMD_UNIT_NAME" || true
|
||||
}
|
||||
|
||||
cmd="${1:-once}"
|
||||
case "$cmd" in
|
||||
once) scan_once ;;
|
||||
watch) watch_loop "${2:-$DEFAULT_INTERVAL}" ;;
|
||||
start) start_loop "${2:-$DEFAULT_INTERVAL}" ;;
|
||||
ensure) ensure_loop "${2:-$DEFAULT_INTERVAL}" ;;
|
||||
stop) stop_loop ;;
|
||||
status) show_status ;;
|
||||
pending) show_pending ;;
|
||||
ack) ack_pending ;;
|
||||
events) show_events "${2:-40}" ;;
|
||||
service-install) install_user_service ;;
|
||||
service-stop) stop_user_service ;;
|
||||
service-status) show_service_status ;;
|
||||
baseline) reset_baseline ;;
|
||||
tail) tail -n "${2:-80}" "$LOG" ;;
|
||||
help) usage ;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
208
docs/coordination/patches/2026-06-18_api_workflows_fix.patch
Normal file
208
docs/coordination/patches/2026-06-18_api_workflows_fix.patch
Normal file
@@ -0,0 +1,208 @@
|
||||
diff --git a/tests/unit/test_dashboard_routes.py b/tests/unit/test_dashboard_routes.py
|
||||
index 3f8f0528c..69cc1b2fb 100644
|
||||
--- a/tests/unit/test_dashboard_routes.py
|
||||
+++ b/tests/unit/test_dashboard_routes.py
|
||||
@@ -212,6 +212,58 @@ class TestDashboardRoutes:
|
||||
data = resp.get_json()
|
||||
assert 'workflows' in data
|
||||
|
||||
+ def test_workflows_list_reads_vwb_db(self, client, monkeypatch, tmp_path):
|
||||
+ """Régression red-gate : /api/workflows reflète la base VWB v3, pas 0.
|
||||
+
|
||||
+ Avant correctif l'endpoint globait un store JSON vide et renvoyait
|
||||
+ toujours total:0. On construit une DB VWB minimale (schéma canonique
|
||||
+ workflows + steps) et on vérifie que l'endpoint expose le compte réel.
|
||||
+ """
|
||||
+ import sqlite3
|
||||
+ from pathlib import Path
|
||||
+
|
||||
+ db_path = tmp_path / "instance" / "workflows.db"
|
||||
+ db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
+ conn = sqlite3.connect(str(db_path))
|
||||
+ conn.execute(
|
||||
+ "CREATE TABLE workflows (id VARCHAR(64) PRIMARY KEY, name VARCHAR(255), "
|
||||
+ "description TEXT, created_at DATETIME, updated_at DATETIME, "
|
||||
+ "is_active BOOLEAN, source VARCHAR(64), review_status VARCHAR(32))"
|
||||
+ )
|
||||
+ conn.execute(
|
||||
+ "CREATE TABLE steps (id VARCHAR(64) PRIMARY KEY, workflow_id VARCHAR(64), "
|
||||
+ "action_type VARCHAR(64))"
|
||||
+ )
|
||||
+ conn.execute(
|
||||
+ "INSERT INTO workflows VALUES (?,?,?,?,?,?,?,?)",
|
||||
+ ("wf_aiva", "Urgence_aiva_demo", "demo", "2026-06-01", "2026-06-18",
|
||||
+ 1, "manual", ""),
|
||||
+ )
|
||||
+ conn.execute(
|
||||
+ "INSERT INTO workflows VALUES (?,?,?,?,?,?,?,?)",
|
||||
+ ("wf_learned", "Learned_flow", "", "2026-06-02", "2026-06-17",
|
||||
+ 1, "learned_import", "pending"),
|
||||
+ )
|
||||
+ # 3 steps pour wf_aiva → nodes_count attendu = 3
|
||||
+ for i in range(3):
|
||||
+ conn.execute(
|
||||
+ "INSERT INTO steps VALUES (?,?,?)", (f"s{i}", "wf_aiva", "click")
|
||||
+ )
|
||||
+ conn.commit()
|
||||
+ conn.close()
|
||||
+
|
||||
+ monkeypatch.setattr(dashboard_app, "VWB_DB_PATH", Path(db_path))
|
||||
+
|
||||
+ resp = client.get('/api/workflows')
|
||||
+ assert resp.status_code == 200
|
||||
+ data = resp.get_json()
|
||||
+ assert data['total'] == 2, f"attendu 2 workflows, obtenu {data['total']}"
|
||||
+ names = {w['name'] for w in data['workflows']}
|
||||
+ assert 'Urgence_aiva_demo' in names
|
||||
+ aiva = next(w for w in data['workflows'] if w['name'] == 'Urgence_aiva_demo')
|
||||
+ assert aiva['nodes_count'] == 3
|
||||
+ assert aiva['source'] == 'manual'
|
||||
+
|
||||
def test_sessions_list(self, client):
|
||||
"""L'API sessions retourne la liste."""
|
||||
resp = client.get('/api/agent/sessions')
|
||||
diff --git a/web_dashboard/app.py b/web_dashboard/app.py
|
||||
index 7ee00c811..aec1edaf9 100644
|
||||
--- a/web_dashboard/app.py
|
||||
+++ b/web_dashboard/app.py
|
||||
@@ -189,6 +189,20 @@ SESSIONS_PATH = DATA_PATH / "sessions"
|
||||
WORKFLOWS_PATH = DATA_PATH / "workflows"
|
||||
LOGS_PATH = BASE_PATH / "logs"
|
||||
|
||||
+# Source canonique des workflows (décision produit D3) : la base VWB v3
|
||||
+# (SQLAlchemy/SQLite) que Léa lit déjà au runtime. Chemin absolu robuste (PAS la
|
||||
+# DB fantôme vide à la racine du repo `instance/workflows.db`, schéma obsolète,
|
||||
+# ni l'ancien store JSON `data/training/workflows/` créé vide sur DGX).
|
||||
+# Surchargeable via RPA_VWB_DB_PATH pour les déploiements atypiques.
|
||||
+def _resolve_vwb_db_path() -> Path:
|
||||
+ override = os.getenv("RPA_VWB_DB_PATH", "").strip()
|
||||
+ if override:
|
||||
+ return Path(override).expanduser()
|
||||
+ return BASE_PATH / "visual_workflow_builder" / "backend" / "instance" / "workflows.db"
|
||||
+
|
||||
+
|
||||
+VWB_DB_PATH = _resolve_vwb_db_path()
|
||||
+
|
||||
# StorageManager
|
||||
storage = StorageManager(base_path=str(DATA_PATH))
|
||||
|
||||
@@ -261,7 +275,9 @@ def system_status():
|
||||
"""Statut du système."""
|
||||
try:
|
||||
sessions_count = len(list(SESSIONS_PATH.glob('*'))) if SESSIONS_PATH.exists() else 0
|
||||
- workflows_count = len(list(WORKFLOWS_PATH.glob('*.json'))) if WORKFLOWS_PATH.exists() else 0
|
||||
+ # Source canonique D3 : base VWB v3 (même comptage que /api/workflows),
|
||||
+ # pas l'ancien store JSON `data/training/workflows/` créé vide sur DGX.
|
||||
+ workflows_count = len(_load_workflows_from_vwb_db())
|
||||
|
||||
dependencies_ok = True
|
||||
try:
|
||||
@@ -785,36 +801,83 @@ def rename_session_workflow(session_id):
|
||||
# API Workflows
|
||||
# =============================================================================
|
||||
|
||||
+def _load_workflows_from_vwb_db() -> list:
|
||||
+ """Charge les workflows depuis la base VWB v3 (source canonique D3).
|
||||
+
|
||||
+ Lit directement le SQLite que Léa interroge au runtime (cf.
|
||||
+ `agent_chat/app.py` → `GET /api/v3/session/state`). On compte les `steps`
|
||||
+ par workflow pour `nodes_count` (pas de notion d'`edges` en DAG linéaire :
|
||||
+ `edges_count` = max(steps-1, 0)). Robuste à l'absence de la DB ou des
|
||||
+ colonnes `source`/`review_status` (DB ancienne) : retourne [] sans planter.
|
||||
+ """
|
||||
+ import sqlite3
|
||||
+
|
||||
+ if not VWB_DB_PATH.exists():
|
||||
+ return []
|
||||
+
|
||||
+ workflows = []
|
||||
+ conn = sqlite3.connect(str(VWB_DB_PATH))
|
||||
+ try:
|
||||
+ conn.row_factory = sqlite3.Row
|
||||
+ # Colonnes disponibles (la DB fantôme/ancienne n'a pas source/review_status)
|
||||
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(workflows)")}
|
||||
+ has_source = 'source' in cols
|
||||
+ has_review = 'review_status' in cols
|
||||
+
|
||||
+ select_cols = ['id', 'name', 'description', 'created_at', 'updated_at']
|
||||
+ if has_source:
|
||||
+ select_cols.append('source')
|
||||
+ if has_review:
|
||||
+ select_cols.append('review_status')
|
||||
+
|
||||
+ # Nombre de steps par workflow (= nodes du DAG)
|
||||
+ step_counts = {
|
||||
+ row[0]: row[1]
|
||||
+ for row in conn.execute(
|
||||
+ "SELECT workflow_id, COUNT(*) FROM steps GROUP BY workflow_id"
|
||||
+ )
|
||||
+ }
|
||||
+
|
||||
+ rows = conn.execute(
|
||||
+ f"SELECT {', '.join(select_cols)} FROM workflows ORDER BY updated_at DESC"
|
||||
+ ).fetchall()
|
||||
+
|
||||
+ for row in rows:
|
||||
+ wf_id = row['id']
|
||||
+ nodes_count = step_counts.get(wf_id, 0)
|
||||
+ workflows.append({
|
||||
+ 'workflow_id': wf_id,
|
||||
+ 'name': row['name'] or wf_id,
|
||||
+ 'description': row['description'] or '',
|
||||
+ 'nodes_count': nodes_count,
|
||||
+ 'edges_count': max(nodes_count - 1, 0),
|
||||
+ 'learning_state': 'OBSERVATION',
|
||||
+ 'created_at': str(row['created_at'] or ''),
|
||||
+ 'updated_at': str(row['updated_at'] or ''),
|
||||
+ 'execution_count': 0,
|
||||
+ 'source': row['source'] if has_source else 'manual',
|
||||
+ 'review_status': row['review_status'] if has_review else '',
|
||||
+ 'file_path': f"vwb_db://{wf_id}",
|
||||
+ })
|
||||
+ finally:
|
||||
+ conn.close()
|
||||
+
|
||||
+ return workflows
|
||||
+
|
||||
+
|
||||
@app.route('/api/workflows')
|
||||
def list_workflows():
|
||||
- """Liste tous les workflows."""
|
||||
+ """Liste tous les workflows depuis la base VWB v3 (source canonique D3).
|
||||
+
|
||||
+ Avant ce correctif, l'endpoint globait `data/training/workflows/*.json`
|
||||
+ (ancien store JSON, créé vide sur DGX) et renvoyait toujours `total: 0`,
|
||||
+ rendant la surface « ce que Léa sait » faussement vide. On lit désormais la
|
||||
+ même base SQLite que Léa au runtime.
|
||||
+ """
|
||||
try:
|
||||
- workflows = []
|
||||
hide_unnamed = request.args.get('hide_unnamed', 'true').lower() == 'true'
|
||||
|
||||
- if not WORKFLOWS_PATH.exists():
|
||||
- WORKFLOWS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
- return jsonify({'workflows': [], 'total': 0, 'hidden_unnamed': 0})
|
||||
-
|
||||
- for wf_file in WORKFLOWS_PATH.glob('*.json'):
|
||||
- try:
|
||||
- with open(wf_file, 'r') as f:
|
||||
- wf_data = json.load(f)
|
||||
-
|
||||
- workflows.append({
|
||||
- 'workflow_id': wf_data.get('workflow_id', wf_file.stem),
|
||||
- 'name': wf_data.get('name', wf_file.stem),
|
||||
- 'description': wf_data.get('description', ''),
|
||||
- 'nodes_count': len(wf_data.get('nodes', [])),
|
||||
- 'edges_count': len(wf_data.get('edges', [])),
|
||||
- 'learning_state': wf_data.get('learning_state', 'OBSERVATION'),
|
||||
- 'created_at': wf_data.get('created_at', ''),
|
||||
- 'updated_at': wf_data.get('updated_at', ''),
|
||||
- 'execution_count': wf_data.get('execution_count', 0),
|
||||
- 'file_path': str(wf_file)
|
||||
- })
|
||||
- except Exception as e:
|
||||
- print(f"Erreur lecture workflow {wf_file}: {e}")
|
||||
+ workflows = _load_workflows_from_vwb_db()
|
||||
|
||||
# Filtrer les workflows "Unnamed" si demandé
|
||||
if hide_unnamed:
|
||||
@@ -0,0 +1,310 @@
|
||||
diff --git a/agent_v0/server_v1/api_stream.py b/agent_v0/server_v1/api_stream.py
|
||||
index 547aeb299..aa620853b 100644
|
||||
--- a/agent_v0/server_v1/api_stream.py
|
||||
+++ b/agent_v0/server_v1/api_stream.py
|
||||
@@ -835,15 +835,56 @@ def _get_worker_queue_status() -> Dict[str, Any]:
|
||||
components_ready = bool(components) and all(bool(v) for v in components.values())
|
||||
health_status = (health or {}).get("status")
|
||||
running = bool(health) and not health_stale and health_status != "stopped"
|
||||
+
|
||||
+ # Distinction VEILLE (armé, lazy) vs DÉGRADÉ (vrai échec).
|
||||
+ #
|
||||
+ # Les composants lourds (ScreenAnalyzer/CLIP/FAISS/StateEmbedding) sont
|
||||
+ # chargés en lazy par run_worker : le processor n'est instancié qu'au
|
||||
+ # premier _process_session (cf. run_worker._get_processor / _process_session).
|
||||
+ # Un worker neuf qui n'a jamais reçu de session écrit donc status="healthy"
|
||||
+ # avec tous les composants à false — c'est l'état NORMAL « en veille », pas
|
||||
+ # une panne. L'étiqueter "degraded" fait lire une panne là où il n'y en a pas.
|
||||
+ #
|
||||
+ # Signal retenu pour « init jamais tentée » : TOUS les composants à false ET
|
||||
+ # sessions_processed == 0 ET sessions_failed == 0. Justification : run_worker
|
||||
+ # n'appelle _get_processor() (donc l'init lazy) que dans _process_session, qui
|
||||
+ # incrémente toujours exactement un compteur (processed / failed / skipped).
|
||||
+ # Tant que processed == 0 ET failed == 0, aucune session n'a déclenché une
|
||||
+ # init suivie d'un traitement — le worker est armé en attente. Un simple skip
|
||||
+ # (dossier/shots absents) passe quand même par _get_processor() : les
|
||||
+ # composants se chargent, donc tous-à-false devient faux et on n'entre pas ici.
|
||||
+ # run_worker._health_components() écrit toujours les 4 clés (jamais un dict
|
||||
+ # vide), d'où le test sur les VALEURS et non sur la présence des clés.
|
||||
+ # Si run_worker a lui-même forcé status="degraded" (VLM + ScreenAnalyzer
|
||||
+ # absent, cf. run_worker._write_health), c'est un VRAI échec : on le conserve.
|
||||
+ stats = (health or {}).get("stats") or {}
|
||||
+ init_attempted = bool(stats.get("sessions_processed", 0)) or bool(
|
||||
+ stats.get("sessions_failed", 0)
|
||||
+ )
|
||||
+ components_all_false = bool(components) and not any(
|
||||
+ bool(v) for v in components.values()
|
||||
+ )
|
||||
+ armed = (
|
||||
+ running
|
||||
+ and not components_ready
|
||||
+ and health_status == "healthy"
|
||||
+ and components_all_false # aucun composant lourd encore chargé
|
||||
+ and not init_attempted
|
||||
+ )
|
||||
+
|
||||
status = health_status or "unknown"
|
||||
if not running:
|
||||
status = "stale" if health else "unknown"
|
||||
+ elif armed:
|
||||
+ # En veille : worker sain, composants chargés à la 1re session.
|
||||
+ status = "idle"
|
||||
elif not components_ready:
|
||||
status = "degraded"
|
||||
|
||||
return {
|
||||
"running": running,
|
||||
"status": status,
|
||||
+ "armed": armed,
|
||||
"queue_length": len(queue),
|
||||
"queue": queue,
|
||||
"replay_lock_active": REPLAY_LOCK_FILE.exists(),
|
||||
@@ -858,11 +899,29 @@ def _get_worker_queue_status() -> Dict[str, Any]:
|
||||
"components": components,
|
||||
"components_ready": components_ready,
|
||||
"processing_ready": running and not REPLAY_LOCK_FILE.exists() and components_ready,
|
||||
- "stats": (health or {}).get("stats") or {},
|
||||
+ "status_hint": _worker_status_hint(status, armed),
|
||||
+ "stats": stats,
|
||||
"note": "Le worker VLM tourne dans un process séparé (agent_v0.server_v1.run_worker).",
|
||||
}
|
||||
|
||||
|
||||
+def _worker_status_hint(status: str, armed: bool) -> str:
|
||||
+ """Message humain pour le statut worker (consommé par le dashboard)."""
|
||||
+ if armed or status == "idle":
|
||||
+ return "En veille — composants chargés à la 1re session."
|
||||
+ if status == "degraded":
|
||||
+ return "Worker apprentissage dégradé — init des composants en échec."
|
||||
+ if status == "stale":
|
||||
+ return "Health file périmé (> 180s) — worker peut-être arrêté."
|
||||
+ if status == "stopped":
|
||||
+ return "Worker arrêté."
|
||||
+ if status == "busy":
|
||||
+ return "Traitement d'une session en cours."
|
||||
+ if status == "healthy":
|
||||
+ return "Worker prêt — composants chargés."
|
||||
+ return "État worker inconnu."
|
||||
+
|
||||
+
|
||||
# =========================================================================
|
||||
# Compteur d'analyses en cours par session (pour attendre avant finalize)
|
||||
# =========================================================================
|
||||
diff --git a/tests/integration/test_stream_processor.py b/tests/integration/test_stream_processor.py
|
||||
index 660187901..344e614cb 100644
|
||||
--- a/tests/integration/test_stream_processor.py
|
||||
+++ b/tests/integration/test_stream_processor.py
|
||||
@@ -1289,3 +1289,158 @@ class TestAPIEndpoints:
|
||||
assert len(workflows) == 1
|
||||
assert workflows[0]["workflow_id"] == "wf_api_001"
|
||||
assert workflows[0]["nodes"] == 2
|
||||
+
|
||||
+
|
||||
+class TestWorkerStatusTruthfulness:
|
||||
+ """Truthfulness du statut worker exposé par _get_worker_queue_status.
|
||||
+
|
||||
+ Distingue VEILLE (armé, lazy : worker neuf qui n'a jamais traité de
|
||||
+ session, composants chargés à la 1re session) de DÉGRADÉ (init tentée
|
||||
+ et en échec). Un worker en veille ne doit JAMAIS être étiqueté 'degraded'.
|
||||
+ """
|
||||
+
|
||||
+ # Même contrainte que TestAPIEndpoints : api_stream fail-closed à l'import
|
||||
+ # si RPA_API_TOKEN absent.
|
||||
+ _TEST_API_TOKEN = "test_token_for_worker_status_0123456789abcdef"
|
||||
+
|
||||
+ @pytest.fixture(autouse=True)
|
||||
+ def _ensure_api_token(self, monkeypatch):
|
||||
+ monkeypatch.setenv("RPA_API_TOKEN", self._TEST_API_TOKEN)
|
||||
+
|
||||
+ @pytest.fixture
|
||||
+ def status_env(self, tmp_path, monkeypatch):
|
||||
+ """Isole les fichiers worker (health/queue/lock) sur tmp_path."""
|
||||
+ from agent_v0.server_v1 import api_stream
|
||||
+
|
||||
+ health_file = tmp_path / "_worker_health.json"
|
||||
+ queue_file = tmp_path / "_worker_queue.txt"
|
||||
+ lock_file = tmp_path / "_replay_active.lock"
|
||||
+ monkeypatch.setattr(api_stream, "WORKER_HEALTH_FILE", health_file)
|
||||
+ monkeypatch.setattr(api_stream, "WORKER_QUEUE_FILE", queue_file)
|
||||
+ monkeypatch.setattr(api_stream, "REPLAY_LOCK_FILE", lock_file)
|
||||
+ return api_stream, health_file
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def _write_health(health_file, **overrides):
|
||||
+ """Écrit un health file frais (mtime récent => non stale)."""
|
||||
+ payload = {
|
||||
+ "pid": 1234,
|
||||
+ "started_at": "2026-06-18T10:00:00",
|
||||
+ "last_cycle": "2026-06-18T10:00:30",
|
||||
+ "current_session": None,
|
||||
+ "queue_length": 0,
|
||||
+ "components": {
|
||||
+ "screen_analyzer": False,
|
||||
+ "clip_embedder": False,
|
||||
+ "faiss_manager": False,
|
||||
+ "state_embedding_builder": False,
|
||||
+ },
|
||||
+ "stats": {
|
||||
+ "sessions_processed": 0,
|
||||
+ "sessions_failed": 0,
|
||||
+ "sessions_skipped": 0,
|
||||
+ "total_screenshots_analyzed": 0,
|
||||
+ },
|
||||
+ "status": "healthy",
|
||||
+ }
|
||||
+ payload.update(overrides)
|
||||
+ health_file.write_text(json.dumps(payload), encoding="utf-8")
|
||||
+
|
||||
+ def test_fresh_worker_is_idle_not_degraded(self, status_env):
|
||||
+ """Worker neuf : healthy, 0 session, tous composants false
|
||||
+ => statut 'idle' (en veille / armé), PAS 'degraded'."""
|
||||
+ api_stream, health_file = status_env
|
||||
+ self._write_health(health_file) # défaut = état neuf
|
||||
+
|
||||
+ status = api_stream._get_worker_queue_status()
|
||||
+
|
||||
+ assert status["running"] is True
|
||||
+ assert status["status"] == "idle", status
|
||||
+ assert status["armed"] is True
|
||||
+ assert status["components_ready"] is False
|
||||
+ # processing_ready reste False tant que les composants ne sont pas chargés
|
||||
+ assert status["processing_ready"] is False
|
||||
+ assert "veille" in status["status_hint"].lower()
|
||||
+
|
||||
+ def test_worker_init_failed_is_degraded(self, status_env):
|
||||
+ """Init tentée et en échec : run_worker force status='degraded'
|
||||
+ (VLM + ScreenAnalyzer absent) => on conserve 'degraded'."""
|
||||
+ api_stream, health_file = status_env
|
||||
+ self._write_health(
|
||||
+ health_file,
|
||||
+ status="degraded", # forcé par run_worker._write_health
|
||||
+ components={
|
||||
+ "screen_analyzer": False,
|
||||
+ "clip_embedder": True,
|
||||
+ "faiss_manager": True,
|
||||
+ "state_embedding_builder": False,
|
||||
+ },
|
||||
+ stats={
|
||||
+ "sessions_processed": 0,
|
||||
+ "sessions_failed": 1, # une session a tenté l'init et échoué
|
||||
+ "sessions_skipped": 0,
|
||||
+ "total_screenshots_analyzed": 0,
|
||||
+ },
|
||||
+ )
|
||||
+
|
||||
+ status = api_stream._get_worker_queue_status()
|
||||
+
|
||||
+ assert status["running"] is True
|
||||
+ assert status["status"] == "degraded", status
|
||||
+ assert status["armed"] is False
|
||||
+ assert status["processing_ready"] is False
|
||||
+ assert "dégradé" in status["status_hint"].lower()
|
||||
+
|
||||
+ def test_worker_partial_components_after_attempt_is_degraded(self, status_env):
|
||||
+ """Composants partiels après tentative de traitement (sessions_failed>0),
|
||||
+ sans status forcé par le worker => 'degraded' (pas 'idle')."""
|
||||
+ api_stream, health_file = status_env
|
||||
+ self._write_health(
|
||||
+ health_file,
|
||||
+ status="healthy",
|
||||
+ components={
|
||||
+ "screen_analyzer": True,
|
||||
+ "clip_embedder": True,
|
||||
+ "faiss_manager": False, # un composant manquant
|
||||
+ "state_embedding_builder": True,
|
||||
+ },
|
||||
+ stats={
|
||||
+ "sessions_processed": 0,
|
||||
+ "sessions_failed": 2,
|
||||
+ "sessions_skipped": 0,
|
||||
+ "total_screenshots_analyzed": 0,
|
||||
+ },
|
||||
+ )
|
||||
+
|
||||
+ status = api_stream._get_worker_queue_status()
|
||||
+
|
||||
+ assert status["status"] == "degraded", status
|
||||
+ assert status["armed"] is False
|
||||
+
|
||||
+ def test_worker_ready_after_processing_is_healthy(self, status_env):
|
||||
+ """Worker ayant traité au moins une session, tous composants chargés
|
||||
+ => 'healthy' et processing_ready=True."""
|
||||
+ api_stream, health_file = status_env
|
||||
+ self._write_health(
|
||||
+ health_file,
|
||||
+ status="healthy",
|
||||
+ components={
|
||||
+ "screen_analyzer": True,
|
||||
+ "clip_embedder": True,
|
||||
+ "faiss_manager": True,
|
||||
+ "state_embedding_builder": True,
|
||||
+ },
|
||||
+ stats={
|
||||
+ "sessions_processed": 3,
|
||||
+ "sessions_failed": 0,
|
||||
+ "sessions_skipped": 0,
|
||||
+ "total_screenshots_analyzed": 42,
|
||||
+ },
|
||||
+ )
|
||||
+
|
||||
+ status = api_stream._get_worker_queue_status()
|
||||
+
|
||||
+ assert status["status"] == "healthy", status
|
||||
+ assert status["armed"] is False
|
||||
+ assert status["components_ready"] is True
|
||||
+ assert status["processing_ready"] is True
|
||||
diff --git a/web_dashboard/templates/index.html b/web_dashboard/templates/index.html
|
||||
index c96cc8bf4..aeb0e7fa8 100644
|
||||
--- a/web_dashboard/templates/index.html
|
||||
+++ b/web_dashboard/templates/index.html
|
||||
@@ -2838,13 +2838,23 @@
|
||||
]);
|
||||
|
||||
const processingReady = processing && processing.processing_ready === true;
|
||||
- const processingDegraded = processing && !processing.error && !processingReady;
|
||||
+ // « En veille » (armé/lazy) ≠ « dégradé » : un worker neuf sans
|
||||
+ // session a tous ses composants à false par design (chargement à la
|
||||
+ // 1re session), ce n'est PAS une panne. Seul status==='degraded'
|
||||
+ // (init tentée et en échec) est une vraie alerte.
|
||||
+ const processingArmed = processing && (processing.armed === true || processing.status === 'idle');
|
||||
+ const processingDegraded = processing && !processing.error && processing.status === 'degraded';
|
||||
+ const statusHint = (processing && processing.status_hint) || '';
|
||||
statusEl.innerHTML = processingDegraded
|
||||
? '<span style="color:#f59e0b;">⚠️</span>'
|
||||
- : '<span style="color:#22c55e;">✅</span>';
|
||||
+ : processingArmed
|
||||
+ ? '<span style="color:#3b82f6;">⏸️</span>'
|
||||
+ : '<span style="color:#22c55e;">✅</span>';
|
||||
statusEl.title = processingDegraded
|
||||
- ? 'Streaming en ligne, worker apprentissage dégradé'
|
||||
- : 'Serveur streaming en ligne';
|
||||
+ ? `Streaming en ligne, worker apprentissage dégradé${statusHint ? ' — ' + statusHint : ''}`
|
||||
+ : processingArmed
|
||||
+ ? `Streaming en ligne, worker en veille${statusHint ? ' — ' + statusHint : ''}`
|
||||
+ : 'Serveur streaming en ligne';
|
||||
|
||||
document.getElementById('streamActiveSessions').textContent = data.active_sessions || 0;
|
||||
document.getElementById('streamTotalEvents').textContent = data.total_events || 0;
|
||||
@@ -2862,14 +2872,20 @@
|
||||
if (data.server_version) rows.push({label: 'Version serveur', value: data.server_version});
|
||||
if (processing && !processing.error) {
|
||||
const status = processing.status || 'unknown';
|
||||
+ const workerIcon = processingReady ? '✅' : (processingArmed ? '⏸️' : '⚠️');
|
||||
rows.push({
|
||||
label: 'Worker apprentissage',
|
||||
- value: processingReady ? `✅ ${status}` : `⚠️ ${status}`
|
||||
+ value: `${workerIcon} ${status}`
|
||||
});
|
||||
rows.push({
|
||||
label: 'Composants intelligence',
|
||||
- value: processing.components_ready ? 'prêts' : 'non prêts'
|
||||
+ value: processing.components_ready
|
||||
+ ? 'prêts'
|
||||
+ : (processingArmed ? 'en veille (chargés à la 1re session)' : 'non prêts')
|
||||
});
|
||||
+ if (statusHint) {
|
||||
+ rows.push({label: 'Détail worker', value: statusHint});
|
||||
+ }
|
||||
if (processing.queue_length !== undefined) {
|
||||
rows.push({label: 'Queue apprentissage', value: processing.queue_length});
|
||||
}
|
||||
318
docs/coordination/patches/2026-06-19_vwb_basic_auth.patch
Normal file
318
docs/coordination/patches/2026-06-19_vwb_basic_auth.patch
Normal file
@@ -0,0 +1,318 @@
|
||||
diff --git a/visual_workflow_builder/backend/app.py b/visual_workflow_builder/backend/app.py
|
||||
index 7bdae57b0..c3a285cc0 100644
|
||||
--- a/visual_workflow_builder/backend/app.py
|
||||
+++ b/visual_workflow_builder/backend/app.py
|
||||
@@ -28,6 +28,109 @@ load_dotenv() # fallback .env dans cwd (n'écrase pas les vars déjà définies
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
|
||||
+# ============================================================
|
||||
+# HTTP Basic Auth LAN (cohérent avec le dashboard 5001)
|
||||
+# ============================================================
|
||||
+# Le VWB (backend 5002) était exposé au LAN SANS authentification. On ajoute
|
||||
+# un middleware before_request qui exige un header Authorization: Basic <b64>
|
||||
+# pour toute requête NON-loopback (LAN), avec les MÊMES credentials que le
|
||||
+# dashboard : DASHBOARD_USER / DASHBOARD_PASSWORD (dans .env.local).
|
||||
+#
|
||||
+# GARDE-FOU CRITIQUE — exemption loopback :
|
||||
+# Le dashboard (agent_chat/app.py `_fetch_vwb_workflows`) et les healthchecks
|
||||
+# appellent ce backend en boucle locale (http://localhost:5002 → 127.0.0.1).
|
||||
+# Exiger l'auth en loopback CASSERAIT l'intégration dashboard↔VWB. On exempte
|
||||
+# donc 127.0.0.1 / ::1 (et ::ffff:127.0.0.1) de toute auth.
|
||||
+#
|
||||
+# Différence assumée avec le dashboard (fail-closed) : ici on NE crashe PAS si
|
||||
+# DASHBOARD_PASSWORD est absent. On log un warning et on laisse passer le LAN
|
||||
+# (mode POC dev/dégradé). En clinique, DASHBOARD_PASSWORD est défini dans
|
||||
+# .env.local (chargé ci-dessus, lignes 24-26) → l'auth LAN est effective.
|
||||
+import base64 as _base64
|
||||
+import hmac as _hmac
|
||||
+
|
||||
+_VWB_AUTH_USER = os.getenv("DASHBOARD_USER", "lea").strip()
|
||||
+_VWB_AUTH_PASSWORD = os.getenv("DASHBOARD_PASSWORD", "").strip()
|
||||
+# Désactivation explicite (dev/tests, parité avec le dashboard).
|
||||
+_VWB_AUTH_DISABLED = os.getenv("DASHBOARD_AUTH_DISABLED", "").lower() in (
|
||||
+ "1", "true", "yes",
|
||||
+)
|
||||
+
|
||||
+# Adresses considérées comme loopback (server-to-server, jamais challengées).
|
||||
+_VWB_LOOPBACK_ADDRS = {"127.0.0.1", "::1", "::ffff:127.0.0.1"}
|
||||
+
|
||||
+# Paths publics (pas d'auth) — healthchecks systemd / NPM / smokes.
|
||||
+_VWB_PUBLIC_PATHS = {"/health", "/api/health"}
|
||||
+
|
||||
+if not _VWB_AUTH_PASSWORD and not _VWB_AUTH_DISABLED:
|
||||
+ logging.getLogger("vwb.auth").warning(
|
||||
+ "[SECURITE] DASHBOARD_PASSWORD non defini : l'auth Basic LAN du VWB "
|
||||
+ "(5002) est INACTIVE (le LAN passe sans credentials). Definir "
|
||||
+ "DASHBOARD_PASSWORD dans .env.local pour l'activer (cible clinique)."
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+def _vwb_auth_ok(header_value: str) -> bool:
|
||||
+ """Valide le header Authorization Basic. Comparaison constant-time.
|
||||
+
|
||||
+ Logique identique au dashboard (`web_dashboard/app.py::_dashboard_auth_ok`).
|
||||
+ """
|
||||
+ if not header_value or not header_value.lower().startswith("basic "):
|
||||
+ return False
|
||||
+ try:
|
||||
+ decoded = _base64.b64decode(header_value[6:].strip()).decode("utf-8")
|
||||
+ except (ValueError, UnicodeDecodeError):
|
||||
+ return False
|
||||
+ if ":" not in decoded:
|
||||
+ return False
|
||||
+ user, _, password = decoded.partition(":")
|
||||
+ user_ok = _hmac.compare_digest(user, _VWB_AUTH_USER)
|
||||
+ pwd_ok = _hmac.compare_digest(password, _VWB_AUTH_PASSWORD)
|
||||
+ return user_ok and pwd_ok
|
||||
+
|
||||
+
|
||||
+@app.before_request
|
||||
+def _vwb_basic_auth_middleware():
|
||||
+ """Middleware d'auth HTTP Basic LAN sur le backend VWB (port 5002).
|
||||
+
|
||||
+ - Bypass total si DASHBOARD_AUTH_DISABLED=true (dev/tests).
|
||||
+ - Bypass total si DASHBOARD_PASSWORD absent (mode POC degrade, warning emis
|
||||
+ au demarrage) — on ne casse pas le service faute de secret.
|
||||
+ - Loopback (127.0.0.1 / ::1) : JAMAIS challenge (proxy dashboard, healthcheck).
|
||||
+ - Preflight CORS (OPTIONS) : laisse passer (le navigateur n'envoie pas
|
||||
+ l'en-tete Authorization au preflight).
|
||||
+ - Paths publics (_VWB_PUBLIC_PATHS) : healthchecks externes.
|
||||
+ - Sinon (requete LAN) : header Authorization: Basic <b64> obligatoire, sinon 401.
|
||||
+ """
|
||||
+ from flask import request, Response
|
||||
+
|
||||
+ # Dev / tests / mode degrade sans secret : bypass total
|
||||
+ if _VWB_AUTH_DISABLED or not _VWB_AUTH_PASSWORD:
|
||||
+ return None
|
||||
+
|
||||
+ # Preflight CORS : pas d'auth (le navigateur n'envoie pas les credentials)
|
||||
+ if request.method == "OPTIONS":
|
||||
+ return None
|
||||
+
|
||||
+ # Exemption loopback (server-to-server : dashboard, healthcheck)
|
||||
+ if (request.remote_addr or "") in _VWB_LOOPBACK_ADDRS:
|
||||
+ return None
|
||||
+
|
||||
+ # Paths publics (healthchecks externes)
|
||||
+ if (request.path or "/") in _VWB_PUBLIC_PATHS:
|
||||
+ return None
|
||||
+
|
||||
+ if _vwb_auth_ok(request.headers.get("Authorization", "")):
|
||||
+ return None
|
||||
+
|
||||
+ # Pas authentifie — challenge 401 avec WWW-Authenticate
|
||||
+ return Response(
|
||||
+ '{"error": "authentication required"}',
|
||||
+ status=401,
|
||||
+ mimetype="application/json",
|
||||
+ headers={"WWW-Authenticate": 'Basic realm="RPA Vision V3 VWB"'},
|
||||
+ )
|
||||
+
|
||||
# ============================================================
|
||||
# Logging — fichier rotatif + console (idempotent)
|
||||
# ============================================================
|
||||
diff --git a/visual_workflow_builder/backend/instance/workflows.db b/visual_workflow_builder/backend/instance/workflows.db
|
||||
index db6eabd62..b7e181cbe 100644
|
||||
Binary files a/visual_workflow_builder/backend/instance/workflows.db and b/visual_workflow_builder/backend/instance/workflows.db differ
|
||||
diff --git a/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py b/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py
|
||||
new file mode 100644
|
||||
index 000000000..f4bff4d9d
|
||||
--- /dev/null
|
||||
+++ b/visual_workflow_builder/backend/tests/test_vwb_basic_auth.py
|
||||
@@ -0,0 +1,195 @@
|
||||
+"""
|
||||
+Tests de l'auth HTTP Basic LAN du backend VWB (port 5002).
|
||||
+
|
||||
+Le VWB etait expose au LAN SANS authentification. Le middleware
|
||||
+`_vwb_basic_auth_middleware` ajoute un challenge 401 sur toute requete
|
||||
+NON-loopback, avec les MEMES credentials que le dashboard
|
||||
+(DASHBOARD_USER / DASHBOARD_PASSWORD).
|
||||
+
|
||||
+Controles cles :
|
||||
+- Loopback (127.0.0.1) sans credentials -> 200 (proxy dashboard / healthcheck).
|
||||
+- LAN (REMOTE_ADDR non loopback) sans credentials -> 401 + WWW-Authenticate.
|
||||
+- LAN avec mauvais mot de passe -> 401.
|
||||
+- LAN avec bons credentials -> passage (pas de 401).
|
||||
+- /health public meme en LAN.
|
||||
+- DASHBOARD_AUTH_DISABLED=true -> bypass total.
|
||||
+- DASHBOARD_PASSWORD absent -> auth inactive (mode POC degrade, pas de crash).
|
||||
+"""
|
||||
+from __future__ import annotations
|
||||
+
|
||||
+import base64
|
||||
+import importlib
|
||||
+import os
|
||||
+import sys
|
||||
+from pathlib import Path
|
||||
+
|
||||
+import pytest
|
||||
+
|
||||
+# Le backend VWB s'importe en tant que module top-level `app`
|
||||
+# (cf. tests/conftest.py : `from app import app, db`). On ajoute le repertoire
|
||||
+# backend au path pour pouvoir le recharger avec les variables d'env voulues.
|
||||
+_BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||||
+if str(_BACKEND_DIR) not in sys.path:
|
||||
+ sys.path.insert(0, str(_BACKEND_DIR))
|
||||
+
|
||||
+# Adresse LAN simulee (non loopback)
|
||||
+_LAN_ADDR = "192.168.1.50"
|
||||
+_LAN_ENV = {"REMOTE_ADDR": _LAN_ADDR}
|
||||
+
|
||||
+
|
||||
+def _basic_auth_header(user: str, password: str) -> str:
|
||||
+ token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
||||
+ return f"Basic {token}"
|
||||
+
|
||||
+
|
||||
+def _reload_app():
|
||||
+ """Recharge le module `app` pour relire les constantes d'auth depuis l'env."""
|
||||
+ if "app" in sys.modules:
|
||||
+ return importlib.reload(sys.modules["app"])
|
||||
+ return importlib.import_module("app")
|
||||
+
|
||||
+
|
||||
+@pytest.fixture
|
||||
+def auth_enabled_client(monkeypatch):
|
||||
+ """Client VWB avec auth LAN active (DASHBOARD_USER/PASSWORD definis)."""
|
||||
+ monkeypatch.setenv("DASHBOARD_USER", "lea")
|
||||
+ monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
|
||||
+ monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
|
||||
+ mod = _reload_app()
|
||||
+ mod.app.config["TESTING"] = True
|
||||
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
+ with mod.app.test_client() as c:
|
||||
+ with mod.app.app_context():
|
||||
+ mod.db.create_all()
|
||||
+ yield c
|
||||
+ mod.db.drop_all()
|
||||
+
|
||||
+
|
||||
+@pytest.fixture
|
||||
+def auth_disabled_client(monkeypatch):
|
||||
+ """Client VWB avec auth desactivee (DASHBOARD_AUTH_DISABLED=true)."""
|
||||
+ monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
|
||||
+ monkeypatch.setenv("DASHBOARD_PASSWORD", "secret-test-pwd")
|
||||
+ mod = _reload_app()
|
||||
+ mod.app.config["TESTING"] = True
|
||||
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
+ with mod.app.test_client() as c:
|
||||
+ with mod.app.app_context():
|
||||
+ mod.db.create_all()
|
||||
+ yield c
|
||||
+ mod.db.drop_all()
|
||||
+
|
||||
+
|
||||
+@pytest.fixture
|
||||
+def no_password_client(monkeypatch):
|
||||
+ """Client VWB sans DASHBOARD_PASSWORD (mode POC degrade : auth inactive)."""
|
||||
+ monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
|
||||
+ monkeypatch.delenv("DASHBOARD_AUTH_DISABLED", raising=False)
|
||||
+ mod = _reload_app()
|
||||
+ mod.app.config["TESTING"] = True
|
||||
+ mod.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
+ with mod.app.test_client() as c:
|
||||
+ with mod.app.app_context():
|
||||
+ mod.db.create_all()
|
||||
+ yield c
|
||||
+ mod.db.drop_all()
|
||||
+
|
||||
+
|
||||
+class TestVwbBasicAuth:
|
||||
+ """Auth HTTP Basic LAN sur le backend VWB (5002)."""
|
||||
+
|
||||
+ def test_loopback_no_creds_passes(self, auth_enabled_client):
|
||||
+ """Requete loopback (127.0.0.1) sans creds -> PAS de 401.
|
||||
+
|
||||
+ Garde-fou critique : le dashboard proxifie en loopback. La requete
|
||||
+ ne doit jamais etre challengee (200, ou autre code applicatif != 401).
|
||||
+ """
|
||||
+ resp = auth_enabled_client.get("/api/v3/session/state")
|
||||
+ assert resp.status_code != 401, (
|
||||
+ f"Loopback ne doit jamais etre challenge (got {resp.status_code})"
|
||||
+ )
|
||||
+
|
||||
+ def test_lan_no_creds_returns_401(self, auth_enabled_client):
|
||||
+ """Requete LAN (non loopback) sans creds -> 401 + WWW-Authenticate."""
|
||||
+ resp = auth_enabled_client.get(
|
||||
+ "/api/v3/session/state", environ_base=_LAN_ENV
|
||||
+ )
|
||||
+ assert resp.status_code == 401
|
||||
+ assert "WWW-Authenticate" in resp.headers
|
||||
+ assert "Basic" in resp.headers["WWW-Authenticate"]
|
||||
+
|
||||
+ def test_lan_wrong_password_returns_401(self, auth_enabled_client):
|
||||
+ """Requete LAN avec mauvais mot de passe -> 401."""
|
||||
+ resp = auth_enabled_client.get(
|
||||
+ "/api/v3/session/state",
|
||||
+ environ_base=_LAN_ENV,
|
||||
+ headers={"Authorization": _basic_auth_header("lea", "wrong")},
|
||||
+ )
|
||||
+ assert resp.status_code == 401
|
||||
+
|
||||
+ def test_lan_wrong_user_returns_401(self, auth_enabled_client):
|
||||
+ """Requete LAN avec mauvais utilisateur -> 401."""
|
||||
+ resp = auth_enabled_client.get(
|
||||
+ "/api/v3/session/state",
|
||||
+ environ_base=_LAN_ENV,
|
||||
+ headers={"Authorization": _basic_auth_header("intruder", "secret-test-pwd")},
|
||||
+ )
|
||||
+ assert resp.status_code == 401
|
||||
+
|
||||
+ def test_lan_valid_credentials_pass(self, auth_enabled_client):
|
||||
+ """Requete LAN avec bons creds -> PAS de 401 (auth franchie)."""
|
||||
+ resp = auth_enabled_client.get(
|
||||
+ "/api/v3/session/state",
|
||||
+ environ_base=_LAN_ENV,
|
||||
+ headers={"Authorization": _basic_auth_header("lea", "secret-test-pwd")},
|
||||
+ )
|
||||
+ assert resp.status_code != 401, (
|
||||
+ f"Bons creds doivent franchir l'auth (got {resp.status_code})"
|
||||
+ )
|
||||
+
|
||||
+ def test_lan_malformed_header_returns_401(self, auth_enabled_client):
|
||||
+ """Requete LAN avec header mal forme (Bearer) -> 401."""
|
||||
+ resp = auth_enabled_client.get(
|
||||
+ "/api/v3/session/state",
|
||||
+ environ_base=_LAN_ENV,
|
||||
+ headers={"Authorization": "Bearer tototoken"},
|
||||
+ )
|
||||
+ assert resp.status_code == 401
|
||||
+
|
||||
+ def test_lan_health_is_public(self, auth_enabled_client):
|
||||
+ """/health reste public meme en LAN (healthcheck externe)."""
|
||||
+ resp = auth_enabled_client.get("/health", environ_base=_LAN_ENV)
|
||||
+ assert resp.status_code == 200
|
||||
+
|
||||
+ def test_lan_options_preflight_not_blocked(self, auth_enabled_client):
|
||||
+ """Preflight CORS (OPTIONS) en LAN -> pas de 401 (CORS preserve)."""
|
||||
+ resp = auth_enabled_client.open(
|
||||
+ "/api/v3/session/state", method="OPTIONS", environ_base=_LAN_ENV
|
||||
+ )
|
||||
+ assert resp.status_code != 401
|
||||
+
|
||||
+ def test_auth_disabled_bypass_lan(self, auth_disabled_client):
|
||||
+ """DASHBOARD_AUTH_DISABLED=true -> LAN passe sans creds."""
|
||||
+ resp = auth_disabled_client.get(
|
||||
+ "/api/v3/session/state", environ_base=_LAN_ENV
|
||||
+ )
|
||||
+ assert resp.status_code != 401
|
||||
+
|
||||
+ def test_no_password_degraded_lan_passes(self, no_password_client):
|
||||
+ """DASHBOARD_PASSWORD absent -> mode POC degrade : LAN passe (pas de crash)."""
|
||||
+ resp = no_password_client.get(
|
||||
+ "/api/v3/session/state", environ_base=_LAN_ENV
|
||||
+ )
|
||||
+ assert resp.status_code != 401
|
||||
+
|
||||
+
|
||||
+@pytest.fixture(autouse=True)
|
||||
+def _restore_module(monkeypatch):
|
||||
+ """Restaure le module `app` en mode auth desactivee apres chaque test,
|
||||
+ pour ne pas contaminer les autres tests VWB (qui importent `app`)."""
|
||||
+ yield
|
||||
+ monkeypatch.setenv("DASHBOARD_AUTH_DISABLED", "true")
|
||||
+ monkeypatch.delenv("DASHBOARD_PASSWORD", raising=False)
|
||||
+ monkeypatch.delenv("DASHBOARD_USER", raising=False)
|
||||
+ if "app" in sys.modules:
|
||||
+ importlib.reload(sys.modules["app"])
|
||||
83
docs/coordination/registre/2026-06-08_decisions.md
Normal file
83
docs/coordination/registre/2026-06-08_decisions.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Registre des décisions — 2026-06-08
|
||||
|
||||
- `Date`: 2026-06-08
|
||||
- `Auteur`: Qwen (compilation)
|
||||
- `Statut`: actif, source de vérité jusqu'à contre-ordre Dom
|
||||
|
||||
---
|
||||
|
||||
## Décisions tranchées
|
||||
|
||||
### D-01 : DGX = Option A (court terme)
|
||||
- **Cible** : `/home/aivanov/ai/rpa_vision_v3`, user `aivanov`
|
||||
- **Tranché par** : Dom (15:15)
|
||||
- **Référence** : `active/2026-06-08_1515_decisions-dom-go-operationnels.md`
|
||||
- **Branch** : `poc-dgx` poussée sur Gitea (`6d34b3cb6`)
|
||||
- **Option B** (`/opt/rpa_vision_v3`, user `rpa`) : reportée post-POC
|
||||
|
||||
### D-02 : WP-A dashboard fail-closed — GO
|
||||
- **Ce que ça fait** : Dashboard refuse de démarrer si `DASHBOARD_PASSWORD` absent ET auth non désactivée. Mot de passe par défaut supprimé.
|
||||
- **Commit** : `549ea0631`
|
||||
- **Tests** : 11/11 passés
|
||||
- **Tranché par** : Dom (15:15) + QG Qwen (15:48)
|
||||
|
||||
### D-03 : WP-B verrou enrôlement — GO
|
||||
- **Ce que ça fait** : `RPA_FLEET_ENROLL_LOCKED=true` bloque enrôlement nouveau `machine_id`. Ferme contournement "poste révoqué + nouveau ID + token global".
|
||||
- **Commit** : `f18de016d`
|
||||
- **Tests** : 6/6 passés
|
||||
- **Tranché par** : Dom (15:15) + QG Qwen (15:48)
|
||||
|
||||
### D-04 : P1.g GPU merge — GO
|
||||
- **Ce que ça fait** : `resolve_device(auto/cuda/cpu)` avec garde-fou VRAM (`min_free_gb=2`, `max_total_gb=6`), fallback CPU.
|
||||
- **Commit** : `0e215da84`
|
||||
- **Tests** : 15/15 passés, smoke OK (`auto → cuda`, `cpu → cpu`)
|
||||
- **Tranché par** : Dom (15:15) + QG Qwen (15:25)
|
||||
- **Rollback** : `RPA_VISION_DEVICE=cpu`, `RPA_EASYOCR_GPU=0`
|
||||
|
||||
### D-05 : Grounder candidat = Qwen3-VL-4B-Instruct via vLLM
|
||||
- **Bench** : 87.5% accuracy, 1.1s latence, 1 dangereux/16, Apache-2.0
|
||||
- **Tranché par** : Claude (15:45) + QG Qwen (16:10)
|
||||
- **Note** : Pas d'activation runtime sans GO Dom + bench sur écrans réels
|
||||
|
||||
### D-06 : Grounding gemma4:26b supervisé
|
||||
- **Bench** : 69% accuracy, **0 dangereux**, 14.4s OCR
|
||||
- **Tranché par** : Claude (15:45) + QG Qwen (16:10)
|
||||
- **Usage** : Grounding supervisé (pas autonome)
|
||||
|
||||
### D-07 : UI-TARS gate vision
|
||||
- **Ce que ça fait** : `has_vision_capability()` via `/api/show` — skip propre si modèle aveugle + `logger.warning`
|
||||
- **Commit** : `d00fe7b00`
|
||||
- **Tranché par** : Claude (12:25) + QG Qwen (12:28)
|
||||
- **Contexte** : UI-TARS cassé sur DGX (aveugle, mmproj non chargé) → 500 silencieux corrigé
|
||||
|
||||
### D-08 : Trained artifacts V2 — GO avec réserves
|
||||
- **Paquet** : ~306 Mo, 6107 fichiers (anchors ajoutés, screen_states retirés)
|
||||
- **Anti-secret** : CLEAN (0 token, 0 identité patient, 0 machine_id réel)
|
||||
- **Rewrite** : 2 colonnes (`image_path` + `thumbnail_path`) `/home/dom/` → `/home/aivanov/`
|
||||
- **Revue visuelle** : Dom confirme captures 100% fictives (mockup `urgence.labs`)
|
||||
- **Tranché par** : Claude (16:08) + Codex (16:06) + QG Qwen (16:10, 16:50)
|
||||
- **Transfert** : Bloqué en attente GO Dom effectif
|
||||
|
||||
### D-09 : Lea live — GO quand preflight vert
|
||||
- **Condition** : Dom devant Windows + preflight G1-G6 verts
|
||||
- **Scope** : Capture Shadow supervisée, Notepad/Explorateur/Easily lecture seule
|
||||
- **Interdit** : ❌ Replay autonome
|
||||
- **Tranché par** : Dom (15:15)
|
||||
|
||||
### D-10 : Code mort — NO-GO suppression
|
||||
- **Ce qui est interdit** : Suppression, déplacement, archive, marquage `DEPRECATED` massif
|
||||
- **Autorisé** : Inventaire read-only, documentation
|
||||
- **Tranché par** : Dom (16:37) + Codex (16:37) + Claude (16:35)
|
||||
- **Référence** : `active/2026-06-08_1637_decision-piste2-no-go-code-mort.md`
|
||||
|
||||
---
|
||||
|
||||
## Décisions en attente
|
||||
|
||||
| Sujet | En attente de | Impact |
|
||||
|---|---|---|
|
||||
| Unification `.service` files | Conception Codex | Migration DGX propre |
|
||||
| Transfert trained artifacts DGX | GO Dom effectif + tar créé | Clone DGX complet |
|
||||
| Node.js sur DGX | Dom/Claude | VWB frontend 3002 |
|
||||
| Benchmark GPU P1.g | Claude (en cours) | Activation large P1.g |
|
||||
| Scan anti-code-mort | Attente (read-only) | Détection automatique "UI-TARS bis" |
|
||||
145
docs/coordination/registre/2026-06-09_decisions.md
Normal file
145
docs/coordination/registre/2026-06-09_decisions.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Registre des decisions — 2026-06-09
|
||||
|
||||
- `Date`: 2026-06-09
|
||||
- `Auteur`: Codex
|
||||
- `Statut`: actif
|
||||
|
||||
---
|
||||
|
||||
## Decisions tranchees
|
||||
|
||||
### D-2026-06-09-01 : Codex orchestre activement Claude et Qwen
|
||||
|
||||
- **Tranche par** : Dom, 2026-06-09
|
||||
- **Decision** : Codex est l'orchestrateur actif du projet. Tant que Claude et Qwen ont des loops de coordination actifs, Codex doit leur fournir une prochaine tache actionnable.
|
||||
- **Exception** : Codex peut ne pas distribuer une execution seulement s'il attend explicitement un feu vert de Dom ou un QG bloquant.
|
||||
- **Implication** : en cas de blocage sur execution, Codex distribue quand meme des taches non destructives : preparation, audit read-only, QG, rollback, plan de tests, registre decisions.
|
||||
- **Persistance** : regle ajoutee a `docs/coordination/ROLES.md`.
|
||||
|
||||
### D-2026-06-09-02 : Dashboard DGX — GO partiel, produit incomplet
|
||||
|
||||
- **Tranche par** : QG Qwen + Codex
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1120_claude-to-codex_RESULTAT-DASHBOARD-DGX-E2E.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1130_qwen-to-codex-claude-dom_QG-DASHBOARD-DGX-E2E-GO-PARTIEL.md`
|
||||
- **Decision** : securite dashboard DGX validee ; dashboard produit non valide tant que P0 workflows non servis et ZIP agent absent ne sont pas corriges.
|
||||
- **Suite autorisee** : preparation Lea live read-only ; preparation corrections P0 sans execution.
|
||||
- **Bloquant multi-TIM** : WP-C token par poste.
|
||||
|
||||
### D-2026-06-09-03 : Branching stable = `poc-prod`
|
||||
|
||||
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
|
||||
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
|
||||
- **Decision** : branche stable POC/NVP/prod = `poc-prod`.
|
||||
- **Schema retenu** : `main` racine, `dev` integration quotidienne, `poc-prod` stable protegee promue depuis `dev` apres QG.
|
||||
- **Conservation** : `poc-dgx` conserve comme snapshot DGX.
|
||||
- **Reste ouvert** : chantier blobs ~15 G, sort de `master`, droits de push/protection sur `poc-prod`.
|
||||
|
||||
### D-2026-06-09-04 : Docker — rester clone + venv + systemd pour le POC
|
||||
|
||||
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1410_claude-to-codex_AUDIT-DOCKER-VS-CLONE-SYSTEMD.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
|
||||
- **Decision court terme** : ne pas dockeriser pour le POC ; conserver clone + venv + systemd.
|
||||
- **Decision moyen terme** : Docker hybride progressif post-POC, avec Ollama et agent RPA hors conteneur.
|
||||
|
||||
### D-2026-06-09-05 : GO execution P0-1/P0-2 dashboard DGX
|
||||
|
||||
- **Tranche par** : Dom, relaye par Claude a 14:12 CEST.
|
||||
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
|
||||
- **P0-1** : appliquer `DATABASE_URL` absolu pour VWB, restart service, verifier `/api/workflows/` = 23.
|
||||
- **P0-2** : build `deploy/Lea_v1.0.0.zip` sur DGX pour usage POC interne.
|
||||
- **Garde-fou** : paquet agent non distribuable multi-TIM tant que WP-C token par poste n'est pas implemente et QG.
|
||||
|
||||
### D-2026-06-09-06 : M2 cible dev Linux, Ollama via tunnel DGX
|
||||
|
||||
- **Tranche par** : QG Qwen corrige a 15:12 CEST.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1420_claude-to-codex_RESULTAT-M2-VERIF-SERVEUR-DEV-LINUX-READONLY.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1425_claude-to-codex_RESULTAT-M2-OLLAMA-ENV-READONLY.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1512_qwen-to-codex-claude-dom_QG-M2-CORRECTION-OLLAMA-TUNNEL-GO.md`
|
||||
- **Decision** : M2 cible le serveur dev Linux `192.168.1.40`, pas le DGX directement.
|
||||
- **Ollama** : acces via tunnel SSH local `127.0.0.1:11434` vers DGX ; Qwen indique tunnel relance et modeles disponibles.
|
||||
- **Garde-fou** : execution live Shadow uniquement avec Dom devant Windows + GO Codex. Tunnel non persistant = P1 a stabiliser avant demo multi-machine.
|
||||
|
||||
### D-2026-06-09-07 : WP-C peut commencer apres P0 dashboard
|
||||
|
||||
- **Tranche par** : QG Qwen a 15:05 et 15:06 CEST.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1505_qwen-to-codex-claude-dom_QG-PREP-P0-DASHBOARD-WPC-GO.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1506_qwen-to-codex-claude-dom_QG-POST-P0-BRANCHES-DOCKER.md`
|
||||
- **Decision QG** : WP-C token par poste peut commencer en parallele M2 apres P0 dashboard.
|
||||
- **Garde-fou** : WP-C reste bloquant multi-TIM ; tests P0 requis : build ZIP sans token global + enroll poste unique.
|
||||
|
||||
### D-2026-06-09-08 : P0 dashboard corrige par symlink workflow store
|
||||
|
||||
- **Tranche par** : Dom, relaye et execute par Claude a 20:25 CEST.
|
||||
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_2025_claude-to-codex-qwen_RESULTAT-P0-DASHBOARD-CORRECTIONS.md`
|
||||
- **Rectificatif** : le diagnostic `DATABASE_URL` etait incomplet/errone ; la route VWB `/api/workflows/` lit les JSON via `WorkflowDatabase("data/workflows")`, pas SQLAlchemy.
|
||||
- **Cause racine** : divergence de `WorkingDirectory` dev vs DGX, donc chemin relatif `data/workflows` resolu au mauvais endroit sur DGX.
|
||||
- **Correction POC** : symlink `data/workflows` -> `visual_workflow_builder/backend/data/workflows`.
|
||||
- **Verification** : VWB 5002 `/api/workflows/` = 39 workflows.
|
||||
- **Rollback** : `rm data/workflows && mkdir data/workflows`.
|
||||
- **Note** : `DATABASE_URL` laisse en place, neutre pour cette route, utile aux composants SQLAlchemy.
|
||||
|
||||
### D-2026-06-09-09 : DETTE-015 double store workflows
|
||||
|
||||
- **Ref** : `docs/coordination/inbox_codex/2026-06-09_2035_claude-to-codex-qwen_INFO-DETTE-015-store-workflows.md`
|
||||
- **Decision** : enregistrer la dette double store workflows en P2 post-POC.
|
||||
- **Docs** :
|
||||
- `docs/DETTE_TECHNIQUE.md`
|
||||
- `docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md`
|
||||
- **Cible post-POC** : unifier sur la DB SQLAlchemy existante sous flag, TDD, sans refactor avant la demo.
|
||||
|
||||
### D-2026-06-09-10 : Qwen indisponible, Codex orchestre en mode degrade
|
||||
|
||||
- **Tranche par** : Dom, 2026-06-09.
|
||||
- **Decision** : Qwen est considere temporairement indisponible. Codex ne lui route plus de nouvelles taches tant que la panne n'est pas levee.
|
||||
- **Implication** : Codex assure l'interim de coordination et demande a Claude des self-checks/read-only/handoffs, sans executer les actions qui exigent un QG formel si elles sont structurelles ou risquées.
|
||||
- **Garde-fou** : pas de code WP-C, pas de live M2, pas de multi-TIM sans GO explicite Dom/Codex et, si possible, QG a posteriori quand Qwen revient.
|
||||
|
||||
### D-2026-06-09-11 : Qwen revenu, reprise QG normale
|
||||
|
||||
- **Tranche par** : Dom/Codex, 2026-06-09 20:47 CEST.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2025_qwen-to-codex-claude-dom_QG-TUNNEL-PERSISTANT-M2-READINESS-GO.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2026_qwen-to-codex-claude-dom_QG-WPC-CARTO-GO.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2027_qwen-to-codex-claude-dom_ACK-MESSAGES-FIN-JOURNEE.md`
|
||||
- **Decision** : Qwen est revenu ; Codex reprend le mode normal avec QG Qwen.
|
||||
- **Action** : Qwen doit rendre le QG post-P0 dashboard sur le resultat Claude 20:25 ; Claude peut avancer WP-C Patch 1 local/TDD uniquement.
|
||||
|
||||
### D-2026-06-09-12 : Dashboard post-P0 — GO final
|
||||
|
||||
- **Tranche par** : QG Qwen a 20:50 CEST.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_QG-POST-P0-DASHBOARD-GO.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_ACK-POST-P0.md`
|
||||
- **Decision** : P0-1 et P0-2 resolus ; dashboard produit debloque pour M2.
|
||||
- **Verification Qwen** : VWB 5002 `/api/workflows/` = 39, symlink OK, tunnel Ollama actif.
|
||||
- **Garde-fou** : ZIP agent POC interne uniquement ; multi-TIM reste NOGO avant WP-C.
|
||||
- **Point P1** : verifier et retirer `DATABASE_URL` si present dans env systemd/service pour eviter dette inutile avant GO M2.
|
||||
|
||||
### D-2026-06-09-13 : WP-C Patch 1 GO, Patch 2 autorise local/TDD
|
||||
|
||||
- **Tranche par** : QG Qwen a 21:10 CEST + Codex.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2058_claude-to-codex_RESULTAT-WPC-PATCH1-MIGRATION-TDD.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2104_claude-to-codex_INFO-WPC-PATCH1-COMMITE.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2110_qwen-to-codex-claude-dom_QG-WPC-PATCH1-GO.md`
|
||||
- **Patch 1** : GO, commit local `f7f692641`, non pousse, non deploye DGX.
|
||||
- **DATABASE_URL** : garder ; P1 post-POC, pas de cleanup avant M2.
|
||||
- **Patch 2 autorise** : generation token a l'enroll, local/TDD uniquement, pas de branchement auth, pas de ZIP, pas de deploiement DGX.
|
||||
|
||||
---
|
||||
|
||||
## Decisions en attente
|
||||
|
||||
| Sujet | En attente de | Impact |
|
||||
|---|---|---|
|
||||
| WP-C Patch 2 local/TDD | Resultat Claude + QG Qwen | Generation token enroll seulement |
|
||||
| Branching details : blobs/master/protection | QG Qwen + GO Dom | Nettoyage et protection stable |
|
||||
| Execution M2 Lea live | Dom devant Windows + GO Codex | Capture Shadow supervisee |
|
||||
| Push/deploiement WP-C Patch 1+ | GO Dom/Codex separe | Integration distante |
|
||||
|
||||
— Codex
|
||||
62
docs/coordination/registre/2026-06-10_decisions.md
Normal file
62
docs/coordination/registre/2026-06-10_decisions.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Registre des decisions — 2026-06-10
|
||||
|
||||
- `Date`: 2026-06-10
|
||||
- `Auteur`: Codex
|
||||
- `Statut`: actif
|
||||
|
||||
---
|
||||
|
||||
## Decisions tranchees
|
||||
|
||||
### D-2026-06-10-01 : Plan de continuite si contexte Codex bas/reset
|
||||
|
||||
- **Tranche par** : Dom, relaye par Codex a 09:18 CEST.
|
||||
- **Contexte** : Dom signale qu'il reste tres peu de contexte Codex et qu'un reset est prevu vers 14:12.
|
||||
- **Decision** : Codex doit poser un plan de relais pour permettre a Claude et Qwen d'avancer sans dependance forte a son contexte.
|
||||
- **Claude** : peut avancer WP-C Patch 2 local/TDD seulement et produire preuves/resultat.
|
||||
- **Qwen** : garde le fil, rend QG, signale contradictions, tient active/registre si necessaire.
|
||||
- **Dom** : reste arbitre pour live, push, deploiement, branches et extension de scope.
|
||||
- **Refs** :
|
||||
- `docs/handoffs/2026-06-10_handoff_codex_context-low-reset-1412.md`
|
||||
- `docs/coordination/active/2026-06-10_0918_continuite-codex-context-reset.md`
|
||||
- `docs/coordination/inbox_claude/2026-06-10_0918_codex-to-claude_RELANCE-WPC-PATCH2-CONTINUITE.md`
|
||||
- `docs/coordination/inbox_qwen/2026-06-10_0918_codex-to-qwen_QG-CONTINUITE-CONTEXT-RESET.md`
|
||||
|
||||
### D-2026-06-10-02 : WP-C Patch 1-3 GO, arret avant Patch 4 runtime
|
||||
|
||||
- **Tranche par** : QG Qwen + Codex, 2026-06-10.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-10_1425_qwen-to-codex-claude-dom_QG-WPC-PATCH2-GO.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-10_1435_qwen-to-codex-claude-dom_QG-WPC-PATCH3-GO.md`
|
||||
- `docs/coordination/active/2026-06-10_1437_wpc-patch1-3-go-stop-patch4.md`
|
||||
- **Decision** : WP-C Patch 1, Patch 2 et Patch 3 sont GO, locaux, non pousses, non deployes.
|
||||
- **Etat commits** :
|
||||
- Patch 1 : `f7f692641`.
|
||||
- Patch 2 : `9fb2c7bfe`.
|
||||
- Patch 3 : `b20d17882`.
|
||||
- **Garde-fou** : Patch 4 est le premier impact runtime (`api_stream.py::_verify_token` derriere flag `RPA_FLEET_PER_AGENT_TOKEN`) et reste interdit sans decision Dom explicite.
|
||||
- **Autorise** : preparation read-only, plan TDD, plan rollback, audit risques.
|
||||
- **Interdit** : Patch 4 code, push, deploiement, ZIP/package, M2 live, multi-TIM, secret en clair.
|
||||
|
||||
### D-2026-06-10-03 : WP-C arrete pour POC, dette auth globale acceptee
|
||||
|
||||
- **Tranche par** : Dom, relaye/QG par Qwen a 14:50 CEST.
|
||||
- **Refs** :
|
||||
- `docs/coordination/inbox_codex/2026-06-10_1450_qwen-to-codex-claude-dom_DECISION-WPC-ABANDON-DETTE.md`
|
||||
- `docs/coordination/active/2026-06-10_1440_anti-doublon-wpc-verdict.md`
|
||||
- `docs/DETTE_TECHNIQUE.md` (`DETTE-016`)
|
||||
- **Decision** : le token global + `machine_id` auto-declare est suffisant pour le POC controle.
|
||||
- **Consequence** : WP-C token par poste est arrete pour le POC ; Patch 4 runtime est annule.
|
||||
- **Etat commits** : Patch 1-3 restent locaux, inertes, non pousses, non deployes.
|
||||
- **Dette** : `DETTE-016` creee en P2/ACCEPTED pour le gap "auth token global sans verification cryptographique par poste".
|
||||
- **Garde-fou** : reouvrir WP-C ou une solution equivalente uniquement avant distribution multi-TIM elargie, exposition reseau non maitrisee ou besoin de revocation par poste non contournable.
|
||||
|
||||
## Decisions en attente
|
||||
|
||||
| Sujet | En attente de | Impact |
|
||||
|---|---|---|
|
||||
| M2 Lea live | Dom devant Windows + GO Codex | Execution supervisee |
|
||||
| Branches/blobs/protection | QG + GO Dom | Hygiene repo post-POC |
|
||||
| Reouverture WP-C post-POC | Decision Dom explicite | Seulement si multi-TIM elargi / exposition reseau / revocation forte |
|
||||
|
||||
— Codex
|
||||
@@ -0,0 +1,105 @@
|
||||
# Revue globale post-panne DGX — 2026-06-20
|
||||
|
||||
- `Pilote`: Codex
|
||||
- `Contributeurs`: Claude, Qwen
|
||||
- `Contexte`: coupure électrique à 02:07 CEST
|
||||
- `Verdict`: **GO labo supervisé / NO-GO fonctionnement autonome et clinique**
|
||||
|
||||
## Résumé
|
||||
|
||||
Le DGX, les services, les données et la VM Windows ont survécu ou ont été récupérés. L'accès Dom à la VM est rétabli. La panne révèle cependant que l'accès graphique et le démarrage complet de Léa ne sont pas encore autonomes: le tunnel SSH et le mot de passe VNC sont volatils, et aucun agent Windows n'a encore prouvé une reconnexion après la panne.
|
||||
|
||||
## Checklist consolidée
|
||||
|
||||
| Domaine | Contrôle | État | Preuve / observation |
|
||||
|---|---|---:|---|
|
||||
| Coordination | Watcher Codex/Claude/Qwen | OK | service utilisateur persistant `enabled+active` |
|
||||
| Réseau | DGX joignable sur `192.168.1.45` | OK | IP WiFi statique, SSH et services accessibles |
|
||||
| Réseau | Exclusion/réservation `.45` dans le DHCP box | À FAIRE | risque de conflit futur |
|
||||
| Réseau | Ethernet clinique inactif | OK | interface sans carrier |
|
||||
| Réseau | Ethernet clinique `autoconnect=off` | KO | profil encore `autoconnect=yes` |
|
||||
| Sécurité | 5900/5902/3389/22220/8000 fermés depuis le LAN | OK | nmap: filtered/closed; loopback conservé |
|
||||
| Services | Services système RPA | OK | 10 units `enabled`; services critiques actifs, zéro erreur/restart |
|
||||
| Dashboard | API et UI | OK | auth 401 sans creds; UI réelle rendue; Socket.IO/API 200 |
|
||||
| Dashboard | Fallback user concurrent | KO | service user disabled mais actif en crash-loop, 167+ restarts, conflit 5001 |
|
||||
| Dashboard | Monitoring 5003 / Session Cleaner 5006 | DÉGRADÉ | affichés arrêtés; non bloquants POC mais classification à clarifier |
|
||||
| Dashboard | Liens navigateur vers `localhost` | DÉGRADÉ | liens 8000/5002/3002/5004/5005 incorrects depuis un poste distant |
|
||||
| Dashboard | Backups/restore points | DÉGRADÉ | dashboard annonce 0 backup et aucun point de restauration |
|
||||
| VWB | Backend/frontend | OK | health 200; UI réelle rendue; catalogue/session APIs 200 |
|
||||
| VWB | Cohérence catalogue | DÉGRADÉ | DB=24 workflows; sélecteur UI=22; review=8 |
|
||||
| VWB | Workflows appris | DÉGRADÉ | plusieurs entrées importables à 0 nœud/0 transition |
|
||||
| Données | `workflows.db` | OK | SQLite `integrity_check=ok`, 24 workflows |
|
||||
| Données | Workflows fichiers | OK | symlink runtime valide, 42 fichiers |
|
||||
| Données | Anchors | OK | 468 fichiers anchors + 47 anchor images |
|
||||
| FAISS | Index et métadonnées | OK | 13 666 vecteurs, test HTTP 200 `success=true`, index sain |
|
||||
| Worker | Daemon et queue | OK | heartbeat post-panne; queue total/pending/failed=0 |
|
||||
| Agent chat | Service | OK | status online, 79 workflows |
|
||||
| Streaming | Service | OK | `/health=200`, version 1.0.0 |
|
||||
| Upload API | Service 8000 | OK | `rpa-vision-v3-api` actif+enabled, 401 loopback attendu |
|
||||
| Ollama/grounder | Services | OK/DÉGRADÉ | process/services présents; fonction modèle non exercée dans ce smoke |
|
||||
| VM | Autostart QEMU | OK après recovery | user service active+enabled, linger, 0 restart |
|
||||
| VM | Boot Windows | OK | framebuffer Windows lock screen, 8,6 Go RSS |
|
||||
| VM | VNC serveur loopback | OK | RFB 3.8 sur `127.0.0.1:5902` |
|
||||
| VM | Tunnel poste Dom | RÉTABLI MAIS VOLATIL | tunnel local actif; Remmina connecté; perdu au reboot poste |
|
||||
| VM | Mot de passe VNC | RÉTABLI MAIS VOLATIL | auth de bout en bout validée; absent du script d'autostart |
|
||||
| VM | SSH guest 22220 | KO | hostfwd présent mais aucune bannière guest |
|
||||
| VM | Guest agent QEMU | KO/NON PROUVÉ | socket présent, aucune réponse `guest-ping` |
|
||||
| Léa Windows | Reconnexion agent post-panne | KO/NON PROUVÉ | aucun `last_seen` fleet postérieur à la panne |
|
||||
| Léa Windows | `config.txt` live vers `.45` | NON TESTÉ | le template build ne prouve pas la configuration installée |
|
||||
| Git DGX | Branche | OK | `poc-dgx` |
|
||||
| Git DGX | Reproductibilité déploiement | DÉGRADÉ | HEAD `ec1fb81`, patch auth `app.py` + DB modifiés, cible `cf81ce4c7` non intégrée |
|
||||
| Installateur | Artefact autonome 1.0.1 | DÉGRADÉ | seul `deploy/Lea_v1.0.0.zip` visible dans ce checkout |
|
||||
| Frontend | VWB servi par Vite dev | POC SEULEMENT | port 3002 LAN; à remplacer par build/proxy pour clinique |
|
||||
|
||||
## Cause de l'absence d'accès VM
|
||||
|
||||
1. Le poste Dom a redémarré: le tunnel SSH local a disparu.
|
||||
2. Le VNC QEMU est volontairement loopback-only et filtré depuis le LAN.
|
||||
3. QEMU redémarre avec `password=on`, mais aucun script ne réinjecte le mot de passe.
|
||||
|
||||
Correctif runtime appliqué: tunnel `localhost:5902` recréé, mot de passe VNC reposé sans exposition, authentification validée, Remmina connecté.
|
||||
|
||||
## Tests fonctionnels nécessitant Dom dans Windows
|
||||
|
||||
- [ ] Se connecter à la session Windows.
|
||||
- [ ] Vérifier que Léa démarre automatiquement; sinon la lancer.
|
||||
- [ ] Vérifier le `config.txt` live vers le DGX `.45`.
|
||||
- [ ] Confirmer un nouveau `last_seen` de la machine dans Fleet.
|
||||
- [ ] Ouvrir « Discuter avec Léa » et envoyer `Bonjour`.
|
||||
- [ ] Vérifier les bulles/progressions d'une action simple.
|
||||
- [ ] Importer un workflow JSON dans VWB.
|
||||
- [ ] Ouvrir une étape avec anchor et confirmer le crop.
|
||||
- [ ] Exécuter un replay supervisé sans action destructive.
|
||||
|
||||
## Actions prioritaires
|
||||
|
||||
### P0 — autonomie après reboot
|
||||
|
||||
1. Stopper proprement le fallback `rpa-vision-v3-dashboard-user` en crash-loop; garder le service système.
|
||||
2. Rendre le tunnel VM persistant sur le poste Dom (`systemd --user` ou `autossh`).
|
||||
3. Rendre l'auth VNC persistante via un secret local protégé, ou retirer le password QEMU et s'appuyer sur SSH+loopback.
|
||||
4. Après login Windows, prouver le démarrage/re-enrôlement Léa et corriger le `config.txt` live si nécessaire.
|
||||
|
||||
### P1 — installation fiable
|
||||
|
||||
5. Exclure/réserver `.45` dans le DHCP de la box.
|
||||
6. Passer le profil Ethernet clinique à `autoconnect=no` tant que non autorisé.
|
||||
7. Réconcilier Git DGX avec backup de `workflows.db`; ne pas utiliser `reset --hard` sans procédure validée.
|
||||
8. Produire/identifier l'installateur Léa 1.0.1 attendu.
|
||||
9. Corriger les liens dashboard `localhost` et distinguer clairement services critiques/optionnels.
|
||||
10. Mettre en place un vrai backup/restore point post-déploiement.
|
||||
|
||||
### P2 — dette produit
|
||||
|
||||
11. Diagnostiquer guest SSH/QEMU guest agent.
|
||||
12. Réconcilier les 24 workflows DB avec les 22 affichés et traiter les 8 reviews/entrées à 0 nœud.
|
||||
13. Remplacer Vite dev par un build statique/proxy avant clinique.
|
||||
|
||||
## Preuves visuelles
|
||||
|
||||
- `output/playwright/dgx-post-panne-dashboard-20260620-0241.png`
|
||||
- `output/playwright/dgx-post-panne-vwb-20260620-0241.png`
|
||||
|
||||
## Divergences de revue résolues
|
||||
|
||||
La première matrice Qwen confondait `5006` avec l'upload API, inspectait les unités user au lieu des unités système et utilisait des chemins de données non runtime. Les contrôles Codex/Claude ont confirmé: upload API sur 8000, services système persistants, 42 fichiers workflows et 468+47 fichiers anchors. Les constats Qwen conservés sont le guest SSH KO, le fallback dashboard en crash-loop et le worktree DGX non reproductible.
|
||||
14
docs/coordination/systemd/rpa-coordination-watcher.service
Normal file
14
docs/coordination/systemd/rpa-coordination-watcher.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=RPA Vision coordination inbox watcher
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/dom/ai/rpa_vision_v3
|
||||
Environment=COORD_LOOP_INTERVAL=15
|
||||
ExecStart=/home/dom/ai/rpa_vision_v3/docs/coordination/coordination_loop.sh watch 15
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=RPA Vision V3 - Web Dashboard (Flask) user fallback
|
||||
After=default.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/aivanov/ai/rpa_vision_v3
|
||||
EnvironmentFile=/home/aivanov/ai/rpa_vision_v3/.env.local
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment=ENVIRONMENT=production
|
||||
Environment=RPA_SERVICE_NAME=rpa-vision-v3-dashboard-user
|
||||
Environment=RPA_GROUNDING_SOCKET=/run/rpa/grounding.sock
|
||||
Environment=RPA_GROUNDING_IMG_DIR=/run/rpa
|
||||
ExecStart=/home/aivanov/ai/rpa_vision_v3/venv_v3/bin/python3 web_dashboard/app.py
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
TimeoutStopSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
69
docs/coordination/templates/TEMPLATE-QG.md
Normal file
69
docs/coordination/templates/TEMPLATE-QG.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Template — Quality Gate (QG)
|
||||
|
||||
- `De`: [Agent QG]
|
||||
- `A`: [Destinataire]
|
||||
- `Copie`: [CC]
|
||||
- `Date`: [YYYY-MM-DD HH:MM Europe/Paris]
|
||||
- `Statut`: QG
|
||||
- `Répond à`: [Fichier de référence]
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
[1-2 lignes décrivant ce qui est évalué]
|
||||
|
||||
---
|
||||
|
||||
## Critères d'acceptation
|
||||
|
||||
| # | Critère | Requis | Résultat | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| 1 | [Description du critère] | ✅ | [Résultat observé] | GO/NO-GO |
|
||||
| 2 | ... | | | |
|
||||
| 3 | ... | | | |
|
||||
|
||||
---
|
||||
|
||||
## Vérifications directes
|
||||
|
||||
| Test | Commande / Méthode | Résultat attendu | Résultat observé | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| Tests unitaires | `pytest ...` | X/Y passés | [Résultat] | ✅/❌ |
|
||||
| Smoke test | [Commande] | [Attendu] | [Observé] | ✅/❌ |
|
||||
| Code review | [Fichiers] | [Critère] | [Observé] | ✅/❌ |
|
||||
|
||||
---
|
||||
|
||||
## Réserves
|
||||
|
||||
| # | Réserve | Impact | Action requise |
|
||||
|---|---|---|---|
|
||||
| R1 | [Description] | [Faible/Moyen/Haut] | [Action] |
|
||||
|
||||
---
|
||||
|
||||
## Verdict global
|
||||
|
||||
**[GO / GO avec réserves / NO-GO]**
|
||||
|
||||
[1-2 lignes de justification]
|
||||
|
||||
---
|
||||
|
||||
## Prochaines étapes
|
||||
|
||||
| Étape | Owner | Deadline |
|
||||
|---|---|---|
|
||||
| [Action] | [Qui] | [Quand] |
|
||||
|
||||
---
|
||||
|
||||
## Stop conditions
|
||||
|
||||
- [Condition qui bloquerait le GO]
|
||||
- [Autre condition]
|
||||
|
||||
---
|
||||
|
||||
*Template réutilisable — copier et adapter pour chaque QG.*
|
||||
@@ -0,0 +1,228 @@
|
||||
# Handoff Codex — fin journee 2026-06-08 / reprise 2026-06-09
|
||||
|
||||
- `Date`: 2026-06-08 18:17 CEST
|
||||
- `Auteur`: Codex
|
||||
- `Objet`: reprise demain apres cycle DGX, securite, dashboard, branches, tests reels
|
||||
- `Statut`: source de reprise operationnelle
|
||||
|
||||
## Resume executif
|
||||
|
||||
La journee a bascule le projet d'un etat fragile/local vers un socle DGX utilisable en local-only.
|
||||
|
||||
Acquis valides :
|
||||
|
||||
- DGX Option A deploye : `/home/aivanov/ai/rpa_vision_v3`, branche `poc-dgx`.
|
||||
- Venv ARM DGX OK, torch CUDA OK sur GB10 `sm_121`.
|
||||
- Donnees entrainees utiles transferees : 23 workflows, 199 ancres, FAISS 512-dim `ntotal=13666`.
|
||||
- Services DGX 6/6 UP, `disabled`, bind `127.0.0.1`, pas d'exposition externe.
|
||||
- Secrets generes sur DGX, non publies, `.env.local` chmod 600.
|
||||
- WP-A/WP-B securite valides : dashboard fail-closed + verrou reenrolement.
|
||||
- P1.g GPU valide local RTX 5070 et DGX GB10, gain environ 90 %, overlap 100 %, 0 OOM.
|
||||
- Correctif GB10 memory-unified livre : `auto -> cuda` OK.
|
||||
- Correctif bind securite livre : default `127.0.0.1` via `RPA_BIND_HOST`.
|
||||
|
||||
Non acquis / a reprendre :
|
||||
|
||||
- Dashboard fonctionnel bout-en-bout non encore teste comme workflow produit.
|
||||
- Test Lea grandeur nature non execute.
|
||||
- Replay controle multi-machine non prouve.
|
||||
- Token par poste WP-C non implemente.
|
||||
- Branching Gitea propre dev vs POC/NVP/prod non encore tranche.
|
||||
- Dockerisation non tranchee : clone+venv+systemd fonctionne, Docker a etudier.
|
||||
|
||||
## Sources de verite a lire demain
|
||||
|
||||
### Etat DGX final
|
||||
|
||||
- `docs/coordination/inbox_codex/2026-06-08_1745_claude-to-codex-qwen_RECAP-FINAL-dgx-operationnel.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-08_1752_qwen-to-codex-claude_QG-RECAP-FINAL-DGX.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-DGX-CLONE-VENV-ARM.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-ARTIFACTS-V2-TRANSFERT-DGX.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-DGX-COMPAT-RUNTIME.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-08_RESULTAT-BENCH-GPU-DGX-GB10.md`
|
||||
|
||||
### Decisions et roles
|
||||
|
||||
- `docs/coordination/registre/2026-06-08_decisions.md`
|
||||
- `docs/coordination/ROLES.md`
|
||||
- `docs/coordination/active/2026-06-08_1637_decision-piste2-no-go-code-mort.md`
|
||||
- `docs/coordination/active/2026-06-08_1729_relance-missions-dgx-systemd-gb10.md`
|
||||
|
||||
### Lea live / QG
|
||||
|
||||
- `docs/coordination/RUNBOOK-LEA-LIVE-DRAFT.md`
|
||||
- `docs/coordination/templates/TEMPLATE-QG.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-08_PLAN-LEA-LIVE-GRANDEUR-NATURE.md`
|
||||
|
||||
## Etat technique detaille
|
||||
|
||||
### DGX
|
||||
|
||||
- Host : `aivanov@192.168.1.45`.
|
||||
- Repo : `/home/aivanov/ai/rpa_vision_v3`.
|
||||
- Branche : `poc-dgx`.
|
||||
- Commit final recape : `09f65cecb` cote local/Gitea `poc-dgx`.
|
||||
- Services UP local-only : `rpa-streaming` 5005, `dashboard` 5001, `api` 8000, `vwb-backend` 5002, `stream-worker`, `worker`.
|
||||
- Services `disabled` : oui.
|
||||
- Exposition externe : non, bind `127.0.0.1`.
|
||||
- VWB frontend 3002 : non inclus, Node absent / a trancher.
|
||||
- Grounding service dedie : non finalise.
|
||||
|
||||
### Securite
|
||||
|
||||
- WP-A : dashboard refuse demarrage sans `DASHBOARD_PASSWORD`.
|
||||
- WP-B : verrou reenrolement via `RPA_FLEET_ENROLL_LOCKED`.
|
||||
- `.env.local` DGX genere sur place, valeurs non consignees.
|
||||
- Auth disabled interdit en contexte DGX.
|
||||
- Reste : WP-C token par poste, a mettre en P0 apres tests reels.
|
||||
|
||||
### Donnees entrainees
|
||||
|
||||
- Archive V2 transferee et verifiee.
|
||||
- `workflows.db` : 23 workflows.
|
||||
- `visual_anchors` : 199.
|
||||
- Anchors : 468 PNG, 398 references effectives OK.
|
||||
- FAISS : dim 512, `ntotal=13666`.
|
||||
- Exclusions confirmees : `live_sessions`, `sessions`, `uploads`, `screenshots`, `.env*`, secrets.
|
||||
|
||||
### GPU / modeles
|
||||
|
||||
- P1.g device policy validee.
|
||||
- RTX 5070 local : GO Qwen.
|
||||
- DGX GB10 : GO avec correction auto, puis QG final GO.
|
||||
- `RPA_VISION_DEVICE=auto` OK apres correctif memory-unified.
|
||||
- EasyOCR + ultralytics + poids YOLO icon_detect installes sur DGX.
|
||||
- UI-TARS reste non active pour sante/replay : bench dangereux.
|
||||
- Qwen3-VL-4B via vLLM reste candidat grounder, pas cable runtime large.
|
||||
- `gemma4:26b` reste candidat supervise/judge.
|
||||
|
||||
## Branching Gitea a trancher demain
|
||||
|
||||
Constat actuel :
|
||||
|
||||
- `poc-dgx` existe et contient le snapshot operationnel DGX.
|
||||
- `main` est en retard/ambigu pour le POC.
|
||||
- Plusieurs branches historiques existent.
|
||||
|
||||
Proposition Codex :
|
||||
|
||||
- `dev` : branche de developpement active, integration quotidienne.
|
||||
- `poc/nvp/prod` ou `poc-prod` : branche stable POC/NVP/prod, protegee par QG.
|
||||
- `poc-dgx` : conserver comme snapshot DGX du 2026-06-08, puis soit merger dans `poc/nvp/prod`, soit le garder comme tag/branche de release.
|
||||
|
||||
Decision requise :
|
||||
|
||||
- nom exact de la branche stable : `poc/nvp/prod`, `poc-prod`, ou autre ;
|
||||
- regle de promotion : dev -> QG -> stable ;
|
||||
- qui peut pousser sur stable.
|
||||
|
||||
## Docker vs clone
|
||||
|
||||
Etat :
|
||||
|
||||
- Clone + venv + systemd fonctionne maintenant et doit rester le chemin POC court terme.
|
||||
- Docker n'est pas tranche.
|
||||
- Un document existe : `docs/ROADMAP_DOCKERISATION.md`.
|
||||
|
||||
Position Codex :
|
||||
|
||||
- Court terme : garder clone/venv/systemd pour ne pas casser le POC.
|
||||
- Moyen terme : etudier Docker pour services applicatifs seulement, avec volumes externes pour data/secrets/modeles.
|
||||
- Ollama/GPU/DGX ARM doivent etre traites explicitement, pas caches dans un Dockerfile premature.
|
||||
|
||||
Decision demain :
|
||||
|
||||
- lancer un audit Docker read-only ;
|
||||
- produire deux options : `clone+systemd durci` vs `docker compose applicatif`.
|
||||
|
||||
## Dashboard — priorite demain
|
||||
|
||||
Le dashboard est critique et doit etre teste comme produit, pas seulement comme process UP.
|
||||
|
||||
Checklist minimale :
|
||||
|
||||
- login Basic avec `DASHBOARD_PASSWORD` ;
|
||||
- fail-closed si secret absent ;
|
||||
- onglet Fleet visible ;
|
||||
- liste agents ;
|
||||
- creation/enrolement agent ;
|
||||
- revoke ;
|
||||
- `RPA_FLEET_ENROLL_LOCKED` ;
|
||||
- generation paquet agent ;
|
||||
- proxy dashboard -> streaming ;
|
||||
- pas de fuite token dans UI/logs/ZIP ;
|
||||
- VWB backend accessible local-only ;
|
||||
- actions dashboard realistes : ouvrir workflow, voir donnees entrainees, lancer preflight.
|
||||
|
||||
QG attendu :
|
||||
|
||||
- Qwen doit verifier securite et parcours ;
|
||||
- Claude execute les tests avec captures/logs ;
|
||||
- Codex tranche les corrections.
|
||||
|
||||
## Tests reels — priorite produit
|
||||
|
||||
Objectif demain :
|
||||
|
||||
1. Verifier dashboard DGX local-only.
|
||||
2. Preflight Lea live.
|
||||
3. Dom devant Windows.
|
||||
4. Capture supervisee safe Notepad/Explorateur/Easily lecture seule.
|
||||
5. Preuves attendues :
|
||||
- `live_events.jsonl`;
|
||||
- `agent_chat/state/learn_*.json`;
|
||||
- shadow understanding non vide ;
|
||||
- workflow genere ;
|
||||
- logs/captures coherents ;
|
||||
- preflight replay non destructif.
|
||||
6. Replay controle uniquement, pas autonome.
|
||||
7. Ensuite seulement, multi-machine.
|
||||
|
||||
Stop conditions :
|
||||
|
||||
- pas de replay autonome ;
|
||||
- pas d'action metier destructive ;
|
||||
- pas d'exposition externe ;
|
||||
- pas de service `enable` sans decision ;
|
||||
- pas de secret affiche ;
|
||||
- pas de suppression code mort.
|
||||
|
||||
## Missions demain proposees
|
||||
|
||||
### Codex
|
||||
|
||||
- Reprendre ce handoff.
|
||||
- Valider branches Gitea avec Dom.
|
||||
- Orchestrer dashboard tests.
|
||||
- Trancher Docker vs clone : lancer audit read-only.
|
||||
- Maintenir QG strict avant promotion stable.
|
||||
|
||||
### Claude
|
||||
|
||||
- Executer test dashboard DGX bout-en-bout.
|
||||
- Preparer puis executer preflight Lea live.
|
||||
- Produire rapport preuves.
|
||||
- Bench grounder Qwen3-VL-4B sur Easily reel si preconditions OK.
|
||||
- Ne pas toucher code mort.
|
||||
|
||||
### Qwen
|
||||
|
||||
- QG dashboard securite/parcours.
|
||||
- QG Lea live preflight.
|
||||
- QG branches/procedure promotion.
|
||||
- QG Docker vs clone.
|
||||
- Maintenir registre decisions.
|
||||
|
||||
## Etat git local a ne pas oublier
|
||||
|
||||
Le repo local reste charge en fichiers non suivis de coordination, graphify et artefacts d'agents.
|
||||
|
||||
Important :
|
||||
|
||||
- ne pas faire de cleanup destructif ;
|
||||
- ne pas commit les secrets ;
|
||||
- ne pas commit `data/` massif ;
|
||||
- `poc-dgx` est la branche operationnelle Gitea ;
|
||||
- le handoff present est une source de reprise, pas un commit applicatif.
|
||||
|
||||
— Codex
|
||||
@@ -0,0 +1,157 @@
|
||||
# Handoff Codex — fin journee 2026-06-09 / reprise 2026-06-10
|
||||
|
||||
- `Date`: 2026-06-09 21:18 CEST
|
||||
- `Auteur`: Codex
|
||||
- `Statut`: source de reprise operationnelle
|
||||
- `Objet`: fin de cycle dashboard DGX, M2 Lea live, WP-C, branches, Docker
|
||||
|
||||
## Resume executif
|
||||
|
||||
La journee a transforme le socle DGX/local-only en POC produit beaucoup plus proche du live.
|
||||
|
||||
Acquis valides :
|
||||
|
||||
- Dashboard DGX securite : GO.
|
||||
- Dashboard produit : GO post-P0 Qwen.
|
||||
- P0-1 workflows : corrige par symlink `data/workflows` -> `visual_workflow_builder/backend/data/workflows`, VWB 5002 sert 39 workflows.
|
||||
- P0-2 paquet agent : ZIP genere, download HTTP 200, usage POC interne seulement.
|
||||
- M2 Lea live : cible technique validee = serveur dev Linux `192.168.1.40:5005`, agent Windows pointe deja dessus.
|
||||
- Tunnel Ollama DGX : rendu persistant via `systemd --user`, QG GO, 10 modeles accessibles via `127.0.0.1:11434`.
|
||||
- Branching : branche stable decidee = `poc-prod`.
|
||||
- Deploiement POC court terme : clone + venv + systemd, pas Docker.
|
||||
- WP-C : Patch 1 local/TDD fait, commit local `f7f692641`, QG GO, non pousse, non deploye.
|
||||
|
||||
Non acquis / a reprendre :
|
||||
|
||||
- M2 Lea live non execute : attend Dom devant Windows + GO Codex au moment T.
|
||||
- WP-C Patch 2 autorise a 21:06 local/TDD seulement ; pas encore de resultat lu au moment du handoff.
|
||||
- WP-C complet non implemente ; multi-TIM reste NOGO.
|
||||
- Push/deploiement WP-C non decide.
|
||||
- Branches `main/dev/poc-prod` non creees/poussees ; blobs ~15 G non traites ; `master` non tranche.
|
||||
- DETTE-015 workflow store : documentee, migration post-POC seulement.
|
||||
|
||||
## Sources de verite recentes
|
||||
|
||||
### Dashboard / P0
|
||||
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2025_claude-to-codex-qwen_RESULTAT-P0-DASHBOARD-CORRECTIONS.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2050_qwen-to-codex-claude-dom_QG-POST-P0-DASHBOARD-GO.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2035_claude-to-codex-qwen_INFO-DETTE-015-store-workflows.md`
|
||||
- `docs/DETTE_TECHNIQUE.md`
|
||||
- `docs/PLAN_MIGRATION_WORKFLOWS_STORE_2026-06-09.md`
|
||||
|
||||
### M2 Lea live
|
||||
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1546_claude-to-codex_PREP-M2-EXECUTION-CHECKLIST.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1605_claude-to-codex-qwen_TUNNEL-OLLAMA-PERSISTANT-FAIT.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1710_claude-to-codex_RESULTAT-TUNNEL-M2-STABILITE.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2025_qwen-to-codex-claude-dom_QG-TUNNEL-PERSISTANT-M2-READINESS-GO.md`
|
||||
|
||||
### WP-C
|
||||
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1715_claude-to-codex_CARTO-WPC-TOKEN-PAR-POSTE-READONLY.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2040_claude-to-codex_PLAN-WPC-TDD-EXECUTABLE.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2058_claude-to-codex_RESULTAT-WPC-PATCH1-MIGRATION-TDD.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2104_claude-to-codex_INFO-WPC-PATCH1-COMMITE.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_2110_qwen-to-codex-claude-dom_QG-WPC-PATCH1-GO.md`
|
||||
- `docs/coordination/inbox_claude/2026-06-09_2106_codex-to-claude_GO-WPC-PATCH2-local-TDD-only.md`
|
||||
- `docs/coordination/inbox_qwen/2026-06-09_2106_codex-to-qwen_ATTENTE-QG-WPC-patch2.md`
|
||||
|
||||
### Branches / Docker
|
||||
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1405_claude-to-codex_AUDIT-M3-branches-gitea-readonly.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1410_claude-to-codex_AUDIT-DOCKER-VS-CLONE-SYSTEMD.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-09_1412_claude-to-codex-qwen_DECISIONS-DOM-poc-prod-m4-clone-p0-go.md`
|
||||
|
||||
## Decisions actees
|
||||
|
||||
### Dashboard DGX
|
||||
|
||||
- Securite dashboard : GO.
|
||||
- Produit dashboard : GO post-P0.
|
||||
- P0-1 vrai fix : symlink workflows, pas `DATABASE_URL`.
|
||||
- `DATABASE_URL` : Qwen recommande de le garder ; il aide SQLAlchemy a lire la bonne DB, meme s'il ne corrige pas `/api/workflows/`.
|
||||
- DETTE-015 : double store workflows documente, migration post-POC uniquement.
|
||||
|
||||
### M2 Lea live
|
||||
|
||||
- Cible M2 : serveur dev Linux `192.168.1.40:5005`, pas DGX directement.
|
||||
- Ollama : tunnel local `127.0.0.1:11434` vers DGX, service `ollama-tunnel.service` user, GO Qwen.
|
||||
- M2 live : techniquement pret, mais execution uniquement avec Dom devant Windows + GO Codex au moment T.
|
||||
|
||||
### Branches / Docker
|
||||
|
||||
- Stable = `poc-prod`.
|
||||
- `poc-dgx` conserve comme snapshot/deploy DGX.
|
||||
- Court terme POC = clone + venv + systemd.
|
||||
- Docker = post-POC, hybride/progressif.
|
||||
|
||||
### WP-C
|
||||
|
||||
- WP-C reste bloquant multi-TIM.
|
||||
- Patch 1 : commit local `f7f692641`, GO Qwen, non pousse, non deploye.
|
||||
- Patch 2 : GO Codex donne a Claude a 21:06, local/TDD seulement.
|
||||
|
||||
## Etat technique detaille
|
||||
|
||||
### DGX
|
||||
|
||||
- DGX reste local-only.
|
||||
- Services DGX operationnels selon les tests du jour.
|
||||
- VWB 5002 `/api/workflows/` = 39 apres symlink.
|
||||
- Paquet agent `deploy/Lea_v1.0.0.zip` existe et sert HTTP 200, mais contient encore le token global.
|
||||
- Pas de distribution multi-TIM.
|
||||
|
||||
### M2
|
||||
|
||||
- Agent Windows gele pointe `http://192.168.1.40:5005/api/v1`.
|
||||
- Serveur dev Linux expose 5005 LAN, workflows visibles.
|
||||
- Tunnel Ollama persistant local sur le dev.
|
||||
- Checklist M2 prete.
|
||||
- Live Shadow non execute.
|
||||
|
||||
### WP-C
|
||||
|
||||
- Patch 1 modifie :
|
||||
- `agent_v0/server_v1/agent_registry.py`
|
||||
- `tests/unit/test_wpc_migration.py`
|
||||
- Tests annonces par Claude :
|
||||
- `tests/unit/test_wpc_migration.py` : 3 passed
|
||||
- `test_fleet_enroll_lock_wpb.py` + migration : 9 passed
|
||||
- Commit local : `f7f692641 feat(wp-c): migration colonnes token par poste (patch 1, inerte)`
|
||||
- Prochaine etape autorisee au moment du handoff : Patch 2 local/TDD seulement.
|
||||
|
||||
## Stop conditions maintenues
|
||||
|
||||
- Pas de live Shadow sans Dom devant Windows.
|
||||
- Pas de replay autonome.
|
||||
- Pas de multi-TIM avant WP-C.
|
||||
- Pas de Patch 3+ sans QG.
|
||||
- Pas de deploiement DGX WP-C sans decision.
|
||||
- Pas de push Gitea sans decision.
|
||||
- Pas de suppression code mort.
|
||||
- Pas de secret en clair.
|
||||
|
||||
## Reprise demain
|
||||
|
||||
1. Lire ce handoff.
|
||||
2. Lire les nouveaux messages `inbox_codex` apres 2026-06-09 21:18 CEST.
|
||||
3. Si Claude a livre `RESULTAT-WPC-PATCH2-ENROLL-TOKEN-TDD`, demander/attendre QG Qwen.
|
||||
4. Si Dom veut M2 live :
|
||||
- verifier tunnel Ollama ;
|
||||
- verifier dashboard/workflows ;
|
||||
- verifier agent Windows running ;
|
||||
- confirmer Dom devant Windows ;
|
||||
- donner GO Codex uniquement au moment T.
|
||||
5. Ne pas lancer multi-TIM.
|
||||
6. Ne pas pousser `f7f692641` ni autres commits sans decision.
|
||||
7. Reprendre branches `poc-prod` seulement apres arbitrage blobs/protection.
|
||||
|
||||
## Etat git a retenir
|
||||
|
||||
- Branche locale : `poc-dgx`.
|
||||
- HEAD local : `f7f692641`, en avance sur `gitea/poc-dgx` (`09f65cecb`) d'au moins le Patch 1 WP-C.
|
||||
- Commit `f7f692641` non pousse et non deploye DGX.
|
||||
- Worktree charge en fichiers de coordination, graphify, docs et artefacts non suivis : ne pas nettoyer destructivement.
|
||||
|
||||
— Codex
|
||||
@@ -0,0 +1,66 @@
|
||||
# Handoff Codex — contexte bas / relais avant reset 14:12
|
||||
|
||||
- `Date`: 2026-06-10 09:18 CEST
|
||||
- `Auteur`: Codex
|
||||
- `Statut`: source de reprise operationnelle
|
||||
- `Declencheur`: Dom signale contexte Codex tres bas et reset prevu vers 14:12
|
||||
|
||||
## Etat courant confirme
|
||||
|
||||
- Branche locale : `poc-dgx`.
|
||||
- HEAD local : `f7f692641 feat(wp-c): migration colonnes token par poste (patch 1, inerte)`.
|
||||
- Worktree charge en docs/coordination, graphify, artefacts non suivis : ne pas nettoyer.
|
||||
- Dashboard DGX securite + produit : GO.
|
||||
- P0 workflows + ZIP : resolus.
|
||||
- M2 Lea live : techniquement pret, cible `192.168.1.40:5005`, tunnel Ollama persistant GO, execution uniquement avec Dom devant Windows + GO Codex au moment T.
|
||||
- WP-C Patch 1 : GO Qwen, commit local, non pousse, non deploye.
|
||||
- WP-C Patch 2 : autorise local/TDD depuis 2026-06-09 21:06, non livre au moment de ce handoff.
|
||||
- Multi-TIM : NOGO avant WP-C complet.
|
||||
|
||||
## Objectif de continuite
|
||||
|
||||
Permettre a Claude et Qwen d'avancer sans dependance forte au contexte Codex :
|
||||
|
||||
- Claude avance sur l'execution locale/TDD strictement bornee.
|
||||
- Qwen garde le fil, rend les QG, signale les contradictions a Dom.
|
||||
- Dom reste l'arbitre pour live Windows, push, deploiement, branches et toute extension de scope.
|
||||
|
||||
## Planning propose jusqu'au reset 14:12
|
||||
|
||||
| Fenetre | Owner | Action | Sortie attendue |
|
||||
|---|---|---|---|
|
||||
| Maintenant -> 10:30 | Claude | Relancer/executer WP-C Patch 2 local/TDD seulement | `RESULTAT-WPC-PATCH2-ENROLL-TOKEN-TDD` vers `inbox_codex/` |
|
||||
| Des reception RESULTAT | Qwen | QG Patch 2 | `QG-WPC-PATCH2-GO` ou `NOGO` vers `inbox_codex/` |
|
||||
| En parallele | Qwen | Tenir l'etat actif et verifier contradictions | Maj active/registre si necessaire |
|
||||
| En parallele | Claude | Preparer read-only M2/checklist/rollback, sans live | Note courte si utile, aucune execution Windows |
|
||||
| Si Codex indisponible | Qwen + Dom | Maintenir gates et arbitrer la suite | Pas de Patch 3/push/deploiement sans decision explicite |
|
||||
|
||||
## Autorisations pendant indisponibilite Codex
|
||||
|
||||
Autorise sans nouveau Codex :
|
||||
|
||||
- Claude : Patch 2 WP-C local/TDD uniquement, selon message `docs/coordination/inbox_claude/2026-06-09_2106_codex-to-claude_GO-WPC-PATCH2-local-TDD-only.md`.
|
||||
- Claude : audits read-only, plans de rollback, plans de test, preparation M2 sans execution.
|
||||
- Qwen : QG, audit, contradiction check, synthese, registre, mise a jour de pointeurs actifs.
|
||||
|
||||
Non autorise sans decision explicite Dom/Codex :
|
||||
|
||||
- Patch 3+ WP-C.
|
||||
- Branchement auth runtime.
|
||||
- Build/package ZIP modifie.
|
||||
- Push Gitea ou creation/protection branches.
|
||||
- Deploiement DGX.
|
||||
- Live M2 / replay / Shadow sans Dom devant Windows.
|
||||
- Multi-TIM.
|
||||
- Suppression de code mort ou nettoyage destructif.
|
||||
|
||||
## Reprise Codex apres reset
|
||||
|
||||
1. Lire ce handoff.
|
||||
2. Lire `docs/coordination/active/2026-06-10_0918_continuite-codex-context-reset.md`.
|
||||
3. Lire les nouveaux `inbox_codex/` apres 2026-06-10 09:18 CEST.
|
||||
4. Si Claude a livre Patch 2 : attendre/lire QG Qwen avant toute suite.
|
||||
5. Si Qwen a rendu GO Patch 2 : demander decision Dom avant Patch 3, push ou deploiement.
|
||||
6. Si Dom veut M2 live : verifier chaine complete et confirmer Dom devant Windows avant GO.
|
||||
|
||||
— Codex
|
||||
@@ -0,0 +1,113 @@
|
||||
# Handoff Codex — fin journee 2026-06-10 / reprise 2026-06-11
|
||||
|
||||
- `Auteur`: Codex
|
||||
- `Date cloture`: 2026-06-10 23:37 CEST
|
||||
- `Reprise`: 2026-06-11
|
||||
- `Mode demande par Dom`: bi-turbo, focus POC DGX
|
||||
- `Statut`: pret reprise
|
||||
|
||||
## Cap demain
|
||||
|
||||
Priorite unique : **POC DGX a fond**, sans dispersion.
|
||||
|
||||
Deux pistes en parallele :
|
||||
|
||||
1. **Operationnel M2/DGX**
|
||||
- smoke DGX apres nuit / eventuel reboot ;
|
||||
- verification acces dashboard/Fleet depuis Windows ;
|
||||
- M2 live supervise avec Dom devant Windows ;
|
||||
- collecte preuves record -> replay -> apprentissage si disponible.
|
||||
|
||||
2. **Cadre QG / qualite**
|
||||
- Claude finalise runbook M2 + Git safety readonly + smoke DGX ;
|
||||
- Qwen relit runbook Claude et rend GO/NOGO ;
|
||||
- Codex orchestre, verifie, tranche avec Dom.
|
||||
|
||||
## Etat valide au depart
|
||||
|
||||
### DGX
|
||||
|
||||
- 6 services systemd DGX : `enabled` + `active`.
|
||||
- DGX = cible POC.
|
||||
- DEV = machine de code/test, Ollama via tunnel vers DGX.
|
||||
- `ollama-tunnel.service` actif/persistant cote DEV.
|
||||
- VLM cible actee par Qwen : `Qwen3-VL-4B-Instruct` pour POC, avec tunnel Ollama.
|
||||
- Dashboard expose temporairement en HTTP direct pour test : decision acceptee, reversible, pas cible clinique.
|
||||
- Point a verifier demain : activation effective cote DGX depuis Windows (`restart dashboard` / `ufw allow 5001/tcp` si necessaire avec Dom).
|
||||
|
||||
### WP-C / securite agents
|
||||
|
||||
- WP-C token par poste arrete pour POC.
|
||||
- `DETTE-016` creee en P2/ACCEPTED.
|
||||
- Patch 4 runtime annule.
|
||||
- Patch 1-3 locaux/inertes/non deployes.
|
||||
- Multi-TIM POC controle : GO Dom via Fleet/dashboard existant.
|
||||
- Multi-TIM elargi/post-POC : NOGO sans WP-C ou equivalent.
|
||||
|
||||
### Git
|
||||
|
||||
- Branche courante locale : `poc-dgx`.
|
||||
- `poc-dgx` local est 3 commits devant `gitea/poc-dgx`.
|
||||
- Les 3 commits devant sont WP-C inertes :
|
||||
- `f7f692641` Patch 1 migration colonnes token ;
|
||||
- `9fb2c7bfe` Patch 2 generation token enroll ;
|
||||
- `b20d17882` Patch 3 verify_token registre.
|
||||
- Branche archive locale creee : `archive/wpc-local-inerte-2026-06-10` -> `b20d17882`.
|
||||
- **Interdit demain matin** : push `poc-dgx` tel quel.
|
||||
|
||||
## Messages / docs a relire en premier demain
|
||||
|
||||
1. `docs/coordination/active/2026-06-11_0000_pointeur-handoff-reprise-2026-06-11.md`
|
||||
2. `docs/coordination/inbox_codex/2026-06-10_2345_qwen-to-codex-claude-dom_ACK-CADRE-TRAVAIL-POC-DGX.md`
|
||||
3. `docs/coordination/inbox_codex/2026-06-10_2315_qwen-to-codex-claude-dom_QG-GATES-POST-WPC-ABANDON.md`
|
||||
4. `docs/coordination/inbox_codex/2026-06-10_2316_qwen-to-codex-claude-dom_QG-M2-LIVE-READINESS.md`
|
||||
5. `docs/AUDIT_GAPS_APPLI_100PCT_2026-06-10.md`
|
||||
6. `docs/coordination/inbox_claude/2026-06-10_2330_qwen-to-claude_AVIS-GAPS-APPLI-100PCT.md`
|
||||
7. `docs/coordination/active/2026-06-10_1540_repartition-post-wpc-dgx-m2.md`
|
||||
|
||||
## Actions immediates demain
|
||||
|
||||
1. Lire les nouveaux messages Claude/Qwen.
|
||||
2. Verifier si Claude a depose :
|
||||
- `RESULTAT-GIT-SAFETY-WPC-ARCHIVE-READONLY`
|
||||
- `RUNBOOK-M2-LIVE-POC`
|
||||
- `SMOKE-DGX-DEMAIN-MATIN`
|
||||
3. Faire smoke DGX readonly :
|
||||
- `systemctl is-enabled/is-active` des 6 services ;
|
||||
- health `5005/5002` ;
|
||||
- dashboard `5001` ;
|
||||
- Ollama tags ;
|
||||
- VWB workflows ;
|
||||
- Fleet API sans afficher de token.
|
||||
4. Verifier accessibilite Windows -> dashboard DGX.
|
||||
5. Si Dom est devant Windows : GO/NOGO Codex pour M2 live.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
- Secret en clair dans logs/docs/reponses.
|
||||
- `RPA_AUTH_DISABLED=true`.
|
||||
- Agent rouge / crash dashboard.
|
||||
- Tentative Patch 4 runtime.
|
||||
- Push/deploiement sans Dom.
|
||||
- Rebase/reset/cherry-pick sans decision Dom/Codex.
|
||||
|
||||
## Risques techniques prioritaires connus
|
||||
|
||||
Depuis `AUDIT_GAPS_APPLI_100PCT_2026-06-10.md` et avis Qwen :
|
||||
|
||||
- `A1`: timeout HTTP client 5s pouvant perdre une action longue.
|
||||
- `A2`: watchdog `_retry_pending` serveur absent.
|
||||
- `P1`: DETTE-006/010 grounding Qwen3-VL/smart_resize a trancher.
|
||||
- `A3`: ecran Windows verrouille non detecte.
|
||||
|
||||
Pour demain : ne pas coder ces points sans revalidation existing-first et decision Dom/Codex. Le premier objectif reste M2 live supervise.
|
||||
|
||||
## Definition de sortie demain matin
|
||||
|
||||
- DGX smoke OK.
|
||||
- Runbook M2 valide QG.
|
||||
- Dom devant Windows.
|
||||
- Agent Windows visible actif dans Fleet.
|
||||
- Un scenario record/replay documente avec preuves.
|
||||
|
||||
— Codex
|
||||
@@ -0,0 +1,72 @@
|
||||
# Handoff - 2026-06-13 - Codex watcher + reprise POC VLM
|
||||
|
||||
- `Date`: 2026-06-13 08:45 CEST (DEV local)
|
||||
- `Auteur`: Codex
|
||||
- `Statut`: source de reprise operationnelle
|
||||
|
||||
## Pre-check watcher obligatoire
|
||||
|
||||
Au debut de la prochaine session, avant toute action :
|
||||
|
||||
1. `docs/coordination/coordination_loop.sh ensure`
|
||||
2. Lire les messages pertinents pour l'agent courant dans `inbox_codex/`, `inbox_claude/`, `inbox_qwen/` et `active/`.
|
||||
3. Apres traitement : `docs/coordination/coordination_loop.sh ack`
|
||||
|
||||
Si le watcher ne peut pas etre lance ou verifie, signaler le blocage avant de continuer.
|
||||
|
||||
Etat valide au handoff :
|
||||
|
||||
- service utilisateur `rpa-coordination-watcher.service` installe, active et enabled ;
|
||||
- commande service : `docs/coordination/coordination_loop.sh watch 15` ;
|
||||
- PID observe apres redemarrage : `1229002` ;
|
||||
- file locale `.loop_state/unread_messages.tsv` nettoyee ;
|
||||
- dernier `ensure` : `loop OK`, `0 pending`.
|
||||
|
||||
Commandes utiles :
|
||||
|
||||
- `docs/coordination/coordination_loop.sh service-status`
|
||||
- `docs/coordination/coordination_loop.sh pending`
|
||||
- `docs/coordination/coordination_loop.sh events`
|
||||
- `docs/coordination/coordination_loop.sh service-install` si le service systemd utilisateur manque
|
||||
- `docs/coordination/coordination_loop.sh service-stop` pour arreter/desactiver explicitement le service
|
||||
|
||||
## Etat courant
|
||||
|
||||
- Le watcher/loop a ete revu et consolide : queue unread persistante, digest lisible, trigger par message, dernier trigger, journal events, lock `flock` sur scan/baseline/ack, service systemd utilisateur persistant.
|
||||
- La baseline coordination a ete reinitialisee apres nettoyage des faux positifs anciens.
|
||||
- Le tri des baselines a ete durci en `LC_ALL=C sort -u` avant `comm`, pour eviter les faux nouveaux messages.
|
||||
- `docs/coordination/README.md`, `docs/handoffs/README.md`, `docs/handoffs/TEMPLATE_HANDOFF.md` et `AGENTS.md` imposent maintenant `ensure` en debut de session.
|
||||
|
||||
## POC VLM
|
||||
|
||||
- Commit local courant : `5c5ce747b feat(grounding): cablage Qwen3-VL-4B/vLLM (RPA_GROUNDING_ENGINE, defaut off)`.
|
||||
- Commit non pousse.
|
||||
- Cablage par env `RPA_GROUNDING_ENGINE=qwen3vl_vllm`.
|
||||
- Defaut OFF : aucun impact runtime si l'env n'est pas posee.
|
||||
- Cross-review terminee : Codex + Qwen OK gates/securite, Claude a committe localement.
|
||||
|
||||
## Prochaines actions
|
||||
|
||||
1. Reprise session : lancer `docs/coordination/coordination_loop.sh ensure`.
|
||||
2. Lire toute nouvelle coordination, puis `ack`.
|
||||
3. Validation E2E DGX en one-shot du mode `RPA_GROUNDING_ENGINE=qwen3vl_vllm` contre `rpa-vllm-grounder` local-only `127.0.0.1:8001`.
|
||||
4. Ne pas rendre l'activation permanente dans les services POC tant que Dom n'a pas donne GO.
|
||||
5. Documenter les resultats E2E dans `docs/coordination/active/` et prevenir Claude/Qwen.
|
||||
|
||||
## Garde-fous
|
||||
|
||||
- DGX POC prioritaire ; DEV sert a valider avant passage DGX.
|
||||
- Pas de push, pas de deploy, pas de changement auth/reseau, pas d'activation runtime permanente sans GO Dom.
|
||||
- Pas de secret dans les handoffs/messages.
|
||||
- Decisions importantes a faire remonter a Dom via Claude, canal remote.
|
||||
- Donnees cliniques interdites.
|
||||
|
||||
## References
|
||||
|
||||
- `docs/coordination/coordination_loop.sh`
|
||||
- `docs/coordination/systemd/rpa-coordination-watcher.service`
|
||||
- `docs/coordination/README.md`
|
||||
- `docs/handoffs/README.md`
|
||||
- `.remember/remember.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-13_1006_claude-to-codex-qwen_COMMIT-CABLAGE-ET-HANDOFF-SESSION2.md`
|
||||
- `docs/coordination/inbox_codex/2026-06-13_1000_qwen-to-codex-claude_ACK-CABLAGE-GATES-SECURITE.md`
|
||||
@@ -0,0 +1,111 @@
|
||||
# Handoff Codex avant reboot serveur — Lea chat / multi-utilisateur / DETTE-019
|
||||
|
||||
Date: 2026-06-15 09:51 Europe/Paris
|
||||
Auteur: Codex
|
||||
Contexte: Dom va redemarrer la machine. Reprise attendue apres reboot.
|
||||
|
||||
## Consigne obligatoire a la reprise
|
||||
|
||||
Avant toute coordination:
|
||||
|
||||
```bash
|
||||
docs/coordination/coordination_loop.sh ensure
|
||||
```
|
||||
|
||||
Puis lire tous les messages pertinents pour Codex et terminer par:
|
||||
|
||||
```bash
|
||||
docs/coordination/coordination_loop.sh ack
|
||||
```
|
||||
|
||||
Le watcher doit etre operationnel pour Codex, Claude et Qwen avant de continuer.
|
||||
|
||||
## Etat coordination
|
||||
|
||||
- Watcher OK avant handoff.
|
||||
- Dernier message lu: `docs/coordination/inbox_codex/2026-06-15_0947_claude-to-qwen-codex_DETTE019-DEPLOYEE-PREUVE-RUNTIME.md`.
|
||||
- Ack coordination a faire juste apres creation de ce handoff.
|
||||
|
||||
## Etat Lea chat diagnostique par Codex
|
||||
|
||||
Constats non destructifs:
|
||||
|
||||
- `agent_chat` local 5004 repond:
|
||||
- `/api/status` => `online`
|
||||
- 130 workflows detectes
|
||||
- `POST /api/chat` => 200 pour `statut`
|
||||
- `POST /api/chat` => 200 pour `qu'est-ce que tu sais faire ?`
|
||||
- `streaming` local 5005 repond `/health` => `healthy`.
|
||||
- `POST /api/learn/start` fonctionne. Une session diagnostic a ete creee puis annulee proprement.
|
||||
- `tests/unit/test_chat_interface.py -q` => 34 passed.
|
||||
- `virsh list --all` montrait `win11` en `shut off`.
|
||||
- SocketIO:
|
||||
- sans Origin: OK
|
||||
- `Origin: http://localhost:5004`: OK
|
||||
- `Origin: http://192.168.1.40:5004`: OK
|
||||
- `Origin: http://127.0.0.1:5004`: FAIL HTTP 400
|
||||
|
||||
Hypothese principale:
|
||||
|
||||
- Le backend texte fonctionne, mais les echanges d'actions visibles dans la ChatWindow sont probablement coupes car le process `agent_chat` actuel n'a pas `LEA_FEEDBACK_BUS` dans son environnement.
|
||||
- Dans `agent_chat/app.py`, `_emit_lea()` est no-op si `LEA_FEEDBACK_BUS` est false/absent.
|
||||
- La ChatWindow native attend ces events `lea:*` pour afficher les bulles d'action: `action_started`, `action_progress`, `done`, `paused`, `resumed`.
|
||||
|
||||
## Travail donne a Claude et Qwen
|
||||
|
||||
Claude:
|
||||
|
||||
- Fichier: `docs/coordination/inbox_claude/2026-06-15_0942_codex-to-claude-qwen_PRIORITES-LEA-CHAT-ACTIONS-MULTIUSER.md`
|
||||
- Mission: diagnostic runtime VM/Lea, config `RPA_SERVER_URL`, `RPA_AGENT_CHAT_URL`, `RPA_MACHINE_ID`, `LEA_FEEDBACK_BUS`, tests ChatWindow et bus action apres reboot VM.
|
||||
|
||||
Qwen:
|
||||
|
||||
- Fichier: `docs/coordination/inbox_qwen/2026-06-15_0942_codex-to-qwen-claude_QG-LEA-CHAT-ACTIONS-MULTIUSER.md`
|
||||
- Mission: gates GO/NOGO chat/action/multi-utilisateur.
|
||||
|
||||
Trace active:
|
||||
|
||||
- `docs/coordination/active/2026-06-15_0942_priorites-lea-chat-interface-multiuser.md`
|
||||
|
||||
## Priorites proposees a la reprise
|
||||
|
||||
P0 - Depuis la VM Windows apres redemarrage:
|
||||
|
||||
- Verifier que `RPA_SERVER_URL` pointe vers `http://<serveur>:5005/api/v1`.
|
||||
- Verifier que `RPA_AGENT_CHAT_URL` pointe vers `http://<serveur>:5004`.
|
||||
- Verifier que `RPA_MACHINE_ID` est unique.
|
||||
- Tester `/health` 5005, `/api/status` 5004, puis `POST /api/chat`.
|
||||
- Lancer Lea et verifier le log `LeaServerClient initialise : chat=... stream_url=...`.
|
||||
|
||||
P1 - Echanges d'actions:
|
||||
|
||||
- Decider si `LEA_FEEDBACK_BUS=1` est requis pour la session.
|
||||
- Si oui, activer/verifier cote `agent_chat` et cote Windows.
|
||||
- Preuve minimale: voir `lea:action_started`, `lea:action_progress`, `lea:done` dans la ChatWindow pour un replay safe.
|
||||
|
||||
P2 - Multi-utilisateur:
|
||||
|
||||
- Deux agents avec `RPA_MACHINE_ID` distincts.
|
||||
- Deux sessions chat distinctes.
|
||||
- Un ordre/replay cible la bonne machine.
|
||||
- Pas de transcript/action visible sur l'autre machine.
|
||||
|
||||
## DETTE-019
|
||||
|
||||
Dernier message Claude lu:
|
||||
|
||||
- `DETTE-019` est deployee runtime POC DGX.
|
||||
- Push `33c1e2e0d` fait.
|
||||
- DGX HEAD `33c1e2e`.
|
||||
- `workflows.db` preservee, backup `.bak-predeploy-dette019-20260615`.
|
||||
- `rpa-streaming` DGX redemarre, service actif, health 200.
|
||||
- Preuve runtime: 5 cibles texte score 0.90, cas douteux `0013` rejete par `rejected_low_score_grounding`.
|
||||
- `DETTE-018` reste ouverte.
|
||||
|
||||
## Ce que Codex n'a pas fait
|
||||
|
||||
- Pas de modification code applicatif.
|
||||
- Pas de restart service.
|
||||
- Pas de modification VM.
|
||||
- Pas de token expose dans ce handoff.
|
||||
|
||||
23
docs/handoffs/README.md
Normal file
23
docs/handoffs/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Handoffs - regle de reprise par defaut
|
||||
|
||||
Tout handoff ou prompt de reprise doit commencer par le pre-check coordination.
|
||||
|
||||
## Pre-check watcher obligatoire
|
||||
|
||||
Avant de reprendre le travail :
|
||||
|
||||
1. Verifier/lancer/scanner le watcher :
|
||||
|
||||
`docs/coordination/coordination_loop.sh ensure`
|
||||
|
||||
2. Lire les messages pertinents pour l'agent courant dans `inbox_codex/`,
|
||||
`inbox_claude/`, `inbox_qwen/` et `active/`.
|
||||
|
||||
3. Apres traitement, vider la file locale :
|
||||
|
||||
`docs/coordination/coordination_loop.sh ack`
|
||||
|
||||
Si le watcher ne peut pas etre lance ou verifie, le handoff de reprise doit le
|
||||
signaler comme blocage avant toute autre action.
|
||||
|
||||
Cette regle vaut pour Codex, Claude et Qwen.
|
||||
25
docs/handoffs/TEMPLATE_HANDOFF.md
Normal file
25
docs/handoffs/TEMPLATE_HANDOFF.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Handoff - YYYY-MM-DD
|
||||
|
||||
- `Date`:
|
||||
- `Auteur`:
|
||||
- `Statut`: source de reprise operationnelle
|
||||
|
||||
## Pre-check watcher obligatoire
|
||||
|
||||
Au debut de la prochaine session, avant toute action :
|
||||
|
||||
1. `docs/coordination/coordination_loop.sh ensure`
|
||||
2. Lire les messages pertinents pour l'agent courant.
|
||||
3. Apres traitement : `docs/coordination/coordination_loop.sh ack`
|
||||
|
||||
Si le watcher ne peut pas etre lance ou verifie, signaler le blocage.
|
||||
|
||||
## Etat courant
|
||||
|
||||
## Decisions actives
|
||||
|
||||
## Prochaines actions
|
||||
|
||||
## Garde-fous
|
||||
|
||||
## References
|
||||
263
scripts/bench_ppocrv5_cpu.py
Normal file
263
scripts/bench_ppocrv5_cpu.py
Normal file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PP-OCRv5 CPU baseline bench — dry-run 1 capture.
|
||||
|
||||
Compare docTR vs EasyOCR vs PP-OCRv5 (CPU-only paddlepaddle).
|
||||
|
||||
Label obligatoire : baseline CPU, non verdict GPU.
|
||||
|
||||
Metrics:
|
||||
- text accuracy (field-level exact match)
|
||||
- word bbox center error (px) vs docTR reference
|
||||
- latency cold/warm (s)
|
||||
- peak memory (MB)
|
||||
"""
|
||||
|
||||
import time
|
||||
import tracemalloc
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# ── Config ──
|
||||
TEST_IMAGE = Path("/home/dom/ai/rpa_vision_v3/data/training/live_sessions/DESKTOP-58D5CAC_windows/sess_20260318T010719_62a058/shots/shot_0172_full.png")
|
||||
EASILY_IMAGE = Path("/home/dom/ai/rpa_vision_v3/output/playwright/easily_dryrun_2026-05-26/landing_wide.png")
|
||||
RESULTS_JSON = Path("/home/dom/ai/rpa_vision_v3/scripts/bench_ppocrv5_results.json")
|
||||
|
||||
ENGINES = ["ppocrv5_cpu", "doctr", "easyocr"]
|
||||
|
||||
|
||||
def bench_ppocrv5_cpu(img_path: Path) -> dict:
|
||||
"""Run PP-OCRv5 CPU on image, return results dict."""
|
||||
from paddleocr import PaddleOCR
|
||||
|
||||
tracemalloc.start()
|
||||
ocr = PaddleOCR(
|
||||
use_textline_orientation=True,
|
||||
lang="fr",
|
||||
return_word_box=True,
|
||||
)
|
||||
mem_init = tracemalloc.get_traced_memory()[1] / 1024 / 1024
|
||||
|
||||
# Cold run
|
||||
t0 = time.perf_counter()
|
||||
result_cold = ocr.ocr(str(img_path))
|
||||
t_cold = time.perf_counter() - t0
|
||||
|
||||
# Warm run
|
||||
t0 = time.perf_counter()
|
||||
result_warm = ocr.ocr(str(img_path))
|
||||
t_warm = time.perf_counter() - t0
|
||||
|
||||
mem_peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
|
||||
tracemalloc.stop()
|
||||
|
||||
# Parse results — PaddleOCR v3.4 returns list of pages
|
||||
texts = []
|
||||
bboxes = []
|
||||
if result_cold and result_cold[0]:
|
||||
for line in result_cold[0]:
|
||||
if line is None:
|
||||
continue
|
||||
bbox_raw = line[0] # [[x1,y1],[x2,y2],[x3,y3],[x4,y4]]
|
||||
text = line[1][0] # recognized text
|
||||
confidence = line[1][1]
|
||||
# Compute center
|
||||
xs = [pt[0] for pt in bbox_raw]
|
||||
ys = [pt[1] for pt in bbox_raw]
|
||||
cx = sum(xs) / len(xs)
|
||||
cy = sum(ys) / len(ys)
|
||||
texts.append({"text": text, "confidence": confidence})
|
||||
bboxes.append({"bbox": bbox_raw, "center": (cx, cy), "text": text})
|
||||
|
||||
return {
|
||||
"engine": "ppocrv5_cpu",
|
||||
"image": str(img_path),
|
||||
"cold_latency_s": round(t_cold, 3),
|
||||
"warm_latency_s": round(t_warm, 3),
|
||||
"mem_init_MB": round(mem_init, 1),
|
||||
"mem_peak_MB": round(mem_peak, 1),
|
||||
"num_detections": len(texts),
|
||||
"texts": texts,
|
||||
"bboxes": bboxes,
|
||||
"paddle_version": "3.4.0",
|
||||
"paddlepaddle_version": "3.3.1",
|
||||
"device": "cpu",
|
||||
"cuda_available_driver": True,
|
||||
"cuda_compiled_paddle": False,
|
||||
"label": "baseline CPU, non verdict GPU",
|
||||
}
|
||||
|
||||
|
||||
def bench_doctr(img_path: Path) -> dict:
|
||||
"""Run docTR CPU on image."""
|
||||
from doctr.models import ocr_predictor
|
||||
|
||||
tracemalloc.start()
|
||||
predictor = ocr_predictor(pretrained=True)
|
||||
mem_init = tracemalloc.get_traced_memory()[1] / 1024 / 1024
|
||||
|
||||
from doctr.io import DocumentFile
|
||||
doc = DocumentFile.from_images(str(img_path))
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result = predictor(doc)
|
||||
t_cold = time.perf_counter() - t0
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result2 = predictor(doc)
|
||||
t_warm = time.perf_counter() - t0
|
||||
|
||||
mem_peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
|
||||
tracemalloc.stop()
|
||||
|
||||
texts = []
|
||||
bboxes = []
|
||||
for page in result.pages:
|
||||
for block in page.blocks:
|
||||
for line in block.lines:
|
||||
for word in line.words:
|
||||
texts.append({"text": word.value, "confidence": word.confidence})
|
||||
# docTR bbox in relative coords (0-1)
|
||||
bbox = word.geometry
|
||||
# Convert relative to pixel
|
||||
import PIL.Image
|
||||
with PIL.Image.open(img_path) as im:
|
||||
w, h = im.size
|
||||
cx = (bbox[0][0] + bbox[1][0]) / 2 * w
|
||||
cy = (bbox[0][1] + bbox[1][1]) / 2 * h
|
||||
bboxes.append({
|
||||
"bbox_relative": [(bbox[0][0], bbox[0][1]), (bbox[1][0], bbox[1][1])],
|
||||
"center_px": (round(cx, 1), round(cy, 1)),
|
||||
"text": word.value,
|
||||
})
|
||||
|
||||
return {
|
||||
"engine": "doctr",
|
||||
"image": str(img_path),
|
||||
"cold_latency_s": round(t_cold, 3),
|
||||
"warm_latency_s": round(t_warm, 3),
|
||||
"mem_init_MB": round(mem_init, 1),
|
||||
"mem_peak_MB": round(mem_peak, 1),
|
||||
"num_detections": len(texts),
|
||||
"texts": texts,
|
||||
"bboxes": bboxes,
|
||||
"version": "1.0.1",
|
||||
"device": "cpu",
|
||||
"label": "baseline CPU",
|
||||
}
|
||||
|
||||
|
||||
def bench_easyocr(img_path: Path) -> dict:
|
||||
"""Run EasyOCR CPU on image."""
|
||||
import easyocr
|
||||
|
||||
tracemalloc.start()
|
||||
reader = easyocr.Reader(["fr"], gpu=False)
|
||||
mem_init = tracemalloc.get_traced_memory()[1] / 1024 / 1024
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result = reader.readtext(str(img_path))
|
||||
t_cold = time.perf_counter() - t0
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result2 = reader.readtext(str(img_path))
|
||||
t_warm = time.perf_counter() - t0
|
||||
|
||||
mem_peak = tracemalloc.get_traced_memory()[1] / 1024 / 1024
|
||||
tracemalloc.stop()
|
||||
|
||||
texts = []
|
||||
bboxes = []
|
||||
for detection in result:
|
||||
bbox_raw = detection[0] # list of [x,y] points
|
||||
text = detection[1]
|
||||
confidence = detection[2]
|
||||
xs = [pt[0] for pt in bbox_raw]
|
||||
ys = [pt[1] for pt in bbox_raw]
|
||||
cx = sum(xs) / len(xs)
|
||||
cy = sum(ys) / len(ys)
|
||||
texts.append({"text": text, "confidence": confidence})
|
||||
bboxes.append({"bbox": bbox_raw, "center_px": (round(cx, 1), round(cy, 1)), "text": text})
|
||||
|
||||
return {
|
||||
"engine": "easyocr",
|
||||
"image": str(img_path),
|
||||
"cold_latency_s": round(t_cold, 3),
|
||||
"warm_latency_s": round(t_warm, 3),
|
||||
"mem_init_MB": round(mem_init, 1),
|
||||
"mem_peak_MB": round(mem_peak, 1),
|
||||
"num_detections": len(texts),
|
||||
"texts": texts,
|
||||
"bboxes": bboxes,
|
||||
"version": "1.7.2",
|
||||
"device": "cpu",
|
||||
"label": "baseline CPU",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
# Check image exists
|
||||
img = TEST_IMAGE if TEST_IMAGE.exists() else EASILY_IMAGE
|
||||
if not img.exists():
|
||||
print(f"ERROR: No test image found. Tried {TEST_IMAGE} and {EASILY_IMAGE}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Bench image: {img}")
|
||||
print(f"Image size: ...")
|
||||
import PIL.Image
|
||||
with PIL.Image.open(img) as im:
|
||||
w, h = im.size
|
||||
print(f" {w}x{h}, mode={im.mode}")
|
||||
|
||||
all_results = {}
|
||||
|
||||
# ── PP-OCRv5 CPU ──
|
||||
print("\n=== PP-OCRv5 CPU ===")
|
||||
try:
|
||||
r = bench_ppocrv5_cpu(img)
|
||||
all_results["ppocrv5_cpu"] = r
|
||||
print(f" Cold: {r['cold_latency_s']}s | Warm: {r['warm_latency_s']}s | Detections: {r['num_detections']}")
|
||||
print(f" Memory: init {r['mem_init_MB']}MB | peak {r['mem_peak_MB']}MB")
|
||||
except Exception as e:
|
||||
print(f" FAILED: {e}")
|
||||
all_results["ppocrv5_cpu"] = {"error": str(e)}
|
||||
|
||||
# ── docTR ──
|
||||
print("\n=== docTR CPU ===")
|
||||
try:
|
||||
r = bench_doctr(img)
|
||||
all_results["doctr"] = r
|
||||
print(f" Cold: {r['cold_latency_s']}s | Warm: {r['warm_latency_s']}s | Detections: {r['num_detections']}")
|
||||
print(f" Memory: init {r['mem_init_MB']}MB | peak {r['mem_peak_MB']}MB")
|
||||
except Exception as e:
|
||||
print(f" FAILED: {e}")
|
||||
all_results["doctr"] = {"error": str(e)}
|
||||
|
||||
# ── EasyOCR ──
|
||||
print("\n=== EasyOCR CPU ===")
|
||||
try:
|
||||
r = bench_easyocr(img)
|
||||
all_results["easyocr"] = r
|
||||
print(f" Cold: {r['cold_latency_s']}s | Warm: {r['warm_latency_s']}s | Detections: {r['num_detections']}")
|
||||
print(f" Memory: init {r['mem_init_MB']}MB | peak {r['mem_peak_MB']}MB")
|
||||
except Exception as e:
|
||||
print(f" FAILED: {e}")
|
||||
all_results["easyocr"] = {"error": str(e)}
|
||||
|
||||
# Save JSON
|
||||
with open(RESULTS_JSON, "w") as f:
|
||||
json.dump(all_results, f, indent=2, default=str)
|
||||
print(f"\nResults saved to {RESULTS_JSON}")
|
||||
|
||||
# ── Synthesis table ──
|
||||
print("\n=== Synthesis ===")
|
||||
print(f"{'Engine':<15} {'Cold(s)':<10} {'Warm(s)':<10} {'Det':<6} {'Mem(MB)':<10} {'Label'}")
|
||||
for eng, r in all_results.items():
|
||||
if "error" in r:
|
||||
print(f"{eng:<15} FAILED")
|
||||
continue
|
||||
print(f"{eng:<15} {r['cold_latency_s']:<10} {r['warm_latency_s']:<10} {r['num_detections']:<6} {r['mem_peak_MB']:<10} {r.get('label', '')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
4522
scripts/bench_ppocrv5_results.json
Normal file
4522
scripts/bench_ppocrv5_results.json
Normal file
File diff suppressed because it is too large
Load Diff
104
scripts/ops/dgx_m2_r1_enable_agent_chat.sh
Normal file
104
scripts/ops/dgx_m2_r1_enable_agent_chat.sh
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/home/aivanov/ai/rpa_vision_v3"
|
||||
VENV="$ROOT/venv_v3"
|
||||
WIN_IP="${WIN_IP:-192.168.1.11}"
|
||||
ENV_FILE="$ROOT/.env.local"
|
||||
SERVICE_FILE="/etc/systemd/system/rpa-agent-chat.service"
|
||||
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
echo "ERROR: run as root with sudo" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ROOT" || ! -x "$VENV/bin/python3" || ! -f "$ROOT/agent_chat/app.py" ]]; then
|
||||
echo "ERROR: DGX RPA tree or venv missing under $ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "ERROR: missing $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ts="$(date +%Y%m%d_%H%M%S)"
|
||||
cp -a "$ENV_FILE" "$ENV_FILE.bak_m2_r1_$ts"
|
||||
|
||||
python3 - "$ENV_FILE" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
lines = path.read_text().splitlines()
|
||||
seen = False
|
||||
out = []
|
||||
for line in lines:
|
||||
if line.startswith("RPA_BIND_HOST="):
|
||||
out.append("RPA_BIND_HOST=0.0.0.0")
|
||||
seen = True
|
||||
else:
|
||||
out.append(line)
|
||||
if not seen:
|
||||
out.append("RPA_BIND_HOST=0.0.0.0")
|
||||
path.write_text("\n".join(out) + "\n")
|
||||
PY
|
||||
|
||||
install -m 0644 /dev/stdin "$SERVICE_FILE" <<UNIT
|
||||
[Unit]
|
||||
Description=RPA Vision V3 - Agent Chat (Flask/SocketIO, port 5004)
|
||||
After=network-online.target rpa-streaming.service
|
||||
Wants=network-online.target
|
||||
Requires=rpa-streaming.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aivanov
|
||||
Group=aivanov
|
||||
WorkingDirectory=$ROOT
|
||||
EnvironmentFile=$ENV_FILE
|
||||
Environment="PYTHONUNBUFFERED=1"
|
||||
Environment="ENVIRONMENT=production"
|
||||
Environment="RPA_SERVICE_NAME=rpa-agent-chat"
|
||||
Environment="PYTHONPATH=$ROOT"
|
||||
Environment="AGENT_CHAT_ENABLE_OWL=0"
|
||||
Environment="AGENT_CHAT_ENABLE_UI_DETECTION=0"
|
||||
ExecStart=$VENV/bin/python3 -m agent_chat.app
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
KillMode=mixed
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=rpa-agent-chat
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now rpa-agent-chat.service
|
||||
systemctl restart rpa-streaming.service
|
||||
|
||||
ensure_iptables_rule() {
|
||||
if ! iptables -C INPUT "$@" 2>/dev/null; then
|
||||
iptables -A INPUT "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_iptables_rule -i lo -p tcp -m multiport --dports 5004,5005 -j ACCEPT
|
||||
ensure_iptables_rule -p tcp -s "$WIN_IP" -m multiport --dports 5004,5005 -j ACCEPT
|
||||
ensure_iptables_rule -p tcp -m multiport --dports 5004,5005 -j DROP
|
||||
|
||||
echo "--- services ---"
|
||||
systemctl is-active rpa-agent-chat.service rpa-streaming.service
|
||||
echo "--- ports ---"
|
||||
ss -ltnp | grep -E ':(5004|5005)\b' || true
|
||||
echo "--- local health ---"
|
||||
curl -sS -m 5 http://127.0.0.1:5005/health
|
||||
echo
|
||||
curl -sS -m 5 -o /dev/null -w 'agent_chat_status=%{http_code}\n' http://127.0.0.1:5004/api/status
|
||||
echo "--- firewall rules ---"
|
||||
iptables -S INPUT | grep -E 'dports 5004,5005|dport (5004|5005)' || true
|
||||
125
tests/integration/test_update_check_endpoint.py
Normal file
125
tests/integration/test_update_check_endpoint.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Tests intégration HTTP de GET /api/v1/agents/update/check — DETTE-022 v2.
|
||||
|
||||
Endpoint GATED (flag RPA_AUTO_UPDATE_SERVER_ENABLED), best-effort :
|
||||
- flag OFF par défaut → 503 (anti-régression : aucun effet sur le pipeline).
|
||||
- flag ON → 200 + payload {update_available, latest_version, update_type, url}.
|
||||
- auth Bearer requise (dépendance globale _verify_token).
|
||||
|
||||
La logique PURE est testée sans serveur dans tests/unit/test_update_check_server.py
|
||||
(DETTE-013). Ici on vérifie le branchement HTTP minimal.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_ROOT = str(Path(__file__).resolve().parents[2])
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
_TEST_API_TOKEN = "test_update_check_endpoint_token"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch):
|
||||
monkeypatch.setenv("RPA_API_TOKEN", _TEST_API_TOKEN)
|
||||
from fastapi.testclient import TestClient
|
||||
from agent_v0.server_v1 import api_stream
|
||||
|
||||
monkeypatch.setattr(api_stream, "API_TOKEN", _TEST_API_TOKEN)
|
||||
return TestClient(api_stream.app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
def _auth_headers():
|
||||
return {"Authorization": f"Bearer {_TEST_API_TOKEN}"}
|
||||
|
||||
|
||||
class TestUpdateCheckEndpointFlag:
|
||||
def test_disabled_by_default_returns_503(self, client, monkeypatch):
|
||||
monkeypatch.delenv("RPA_AUTO_UPDATE_SERVER_ENABLED", raising=False)
|
||||
resp = client.get(
|
||||
"/api/v1/agents/update/check?current_version=1.0.1",
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 503
|
||||
assert "RPA_AUTO_UPDATE_SERVER_ENABLED" in resp.text
|
||||
|
||||
|
||||
class TestUpdateCheckEndpointEnabled:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_flag(self, monkeypatch):
|
||||
monkeypatch.setenv("RPA_AUTO_UPDATE_SERVER_ENABLED", "true")
|
||||
# Version cible explicite pour rendre le test déterministe.
|
||||
monkeypatch.setenv("RPA_AGENT_LATEST_VERSION", "1.0.2")
|
||||
|
||||
def test_update_available(self, client):
|
||||
resp = client.get(
|
||||
"/api/v1/agents/update/check?current_version=1.0.1&machine_id=pc-1",
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["update_available"] is True
|
||||
assert body["latest_version"] == "1.0.2"
|
||||
assert body["update_type"] == "code-only"
|
||||
assert "1.0.2" in body["url"]
|
||||
|
||||
def test_up_to_date(self, client):
|
||||
resp = client.get(
|
||||
"/api/v1/agents/update/check?current_version=1.0.2&machine_id=pc-1",
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["update_available"] is False
|
||||
|
||||
def test_requires_auth(self, client):
|
||||
resp = client.get(
|
||||
"/api/v1/agents/update/check?current_version=1.0.1",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestUpdateCheckCanary:
|
||||
"""Canary : seul le poste canary se voit proposer la nouvelle version.
|
||||
|
||||
On n'utilise PAS RPA_AGENT_LATEST_VERSION (var legacy globale) : on pilote
|
||||
la version cible via la politique canary (stable + canary + allow-list).
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_canary(self, monkeypatch):
|
||||
monkeypatch.setenv("RPA_AUTO_UPDATE_SERVER_ENABLED", "true")
|
||||
# Legacy OFF pour que la politique canary pilote la décision.
|
||||
monkeypatch.delenv("RPA_AGENT_LATEST_VERSION", raising=False)
|
||||
monkeypatch.setenv("RPA_AGENT_STABLE_VERSION", "1.0.1")
|
||||
monkeypatch.setenv("RPA_AGENT_CANARY_VERSION", "1.0.2")
|
||||
monkeypatch.setenv("RPA_AGENT_CANARY_MACHINES", "lea-4zbgwxty")
|
||||
|
||||
def test_poste_canary_recoit_la_nouvelle_version(self, client):
|
||||
resp = client.get(
|
||||
"/api/v1/agents/update/check"
|
||||
"?current_version=1.0.1&machine_id=lea-4zbgwxty",
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["update_available"] is True
|
||||
assert body["latest_version"] == "1.0.2"
|
||||
|
||||
def test_poste_hors_canary_reste_a_jour_sur_stable(self, client):
|
||||
# Poste NON canary, déjà en 1.0.1 = stable → pas de MAJ (blast radius
|
||||
# borné : la 1.0.2 ne fuite pas hors de la liste canary).
|
||||
resp = client.get(
|
||||
"/api/v1/agents/update/check"
|
||||
"?current_version=1.0.1&machine_id=un-autre-poste",
|
||||
headers=_auth_headers(),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["update_available"] is False
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user