fix(dashboard,worker): vérité produit P0 — dashboard+worker+VWB export
War-room clôture DGX 2026-06-18 (recadrage Dom : graphe/apprentissage/mémoire/dashboard = surface produit P0). Le dashboard et le statut worker affichaient des états faux ; corrige pour refléter la vérité du produit. - dashboard FAISS: distingue index brut / metadata HMAC invalide / runtime / absent (plus de faux "inactif") - dashboard process-mining: 503 explicite missing_dependency (plus de message trompeur) - dashboard /api/workflows + system/status: lecture DB VWB v3 canonique (total réel = 24, plus de 0) - worker /processing/status: véridique (lit _worker_health.json) + statut "idle/armé (lazy)" distinct de "dégradé (échec)" - VWB export: N steps -> N actions/edges (dernière action n'est plus perdue) - tests: dashboard routes, worker status truthfulness, export VWB Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -555,6 +555,7 @@ LIVE_SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_DATA_DIR = ROOT_DIR / "data" / "training"
|
||||
WORKER_QUEUE_FILE = _DATA_DIR / "_worker_queue.txt"
|
||||
REPLAY_LOCK_FILE = _DATA_DIR / "_replay_active.lock"
|
||||
WORKER_HEALTH_FILE = _DATA_DIR / "_worker_health.json"
|
||||
|
||||
# Instance globale partagée (le StreamProcessor reste dans le serveur HTTP
|
||||
# pour le CLIP, l'indexation FAISS, la gestion des sessions, le replay —
|
||||
@@ -807,7 +808,7 @@ def _memory_window_title_for_action(action_meta: Dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def _get_worker_queue_status() -> Dict[str, Any]:
|
||||
"""Retourne l'état de la queue du worker VLM (pour le monitoring)."""
|
||||
"""Retourne l'état réel de la queue et du worker VLM (pour le monitoring)."""
|
||||
queue = []
|
||||
if WORKER_QUEUE_FILE.exists():
|
||||
try:
|
||||
@@ -819,16 +820,108 @@ def _get_worker_queue_status() -> Dict[str, Any]:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
health = None
|
||||
health_error = None
|
||||
health_age_seconds = None
|
||||
if WORKER_HEALTH_FILE.exists():
|
||||
try:
|
||||
health = json.loads(WORKER_HEALTH_FILE.read_text(encoding="utf-8"))
|
||||
health_age_seconds = max(0.0, time.time() - WORKER_HEALTH_FILE.stat().st_mtime)
|
||||
except Exception as exc:
|
||||
health_error = str(exc)
|
||||
|
||||
health_stale = health_age_seconds is None or health_age_seconds > 180
|
||||
components = (health or {}).get("components") or {}
|
||||
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": True, # On ne sait pas si le worker process tourne, mais la queue existe
|
||||
"running": running,
|
||||
"status": status,
|
||||
"armed": armed,
|
||||
"queue_length": len(queue),
|
||||
"queue": queue,
|
||||
"replay_lock_active": REPLAY_LOCK_FILE.exists(),
|
||||
"queue_file": str(WORKER_QUEUE_FILE),
|
||||
"note": "Le worker VLM tourne dans un process séparé (run_worker.py)",
|
||||
"health_file": str(WORKER_HEALTH_FILE),
|
||||
"health_error": health_error,
|
||||
"health_age_seconds": health_age_seconds,
|
||||
"health_stale": health_stale,
|
||||
"worker_pid": (health or {}).get("pid"),
|
||||
"last_cycle": (health or {}).get("last_cycle"),
|
||||
"current_session": (health or {}).get("current_session"),
|
||||
"components": components,
|
||||
"components_ready": components_ready,
|
||||
"processing_ready": running and not REPLAY_LOCK_FILE.exists() and components_ready,
|
||||
"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)
|
||||
# =========================================================================
|
||||
@@ -1614,13 +1707,14 @@ async def startup():
|
||||
|
||||
threading.Thread(target=_smoke_model_health, name="model-health-smoke", daemon=True).start()
|
||||
|
||||
# Afficher le token API au démarrage pour que l'utilisateur puisse configurer l'agent
|
||||
# Ne jamais imprimer le token complet dans journald/stdout.
|
||||
_token_source = "env RPA_API_TOKEN" if os.environ.get("RPA_API_TOKEN") else "auto-généré"
|
||||
logger.info(f"API Token ({_token_source}): {API_TOKEN}")
|
||||
_token_hint = f"{API_TOKEN[:8]}…{API_TOKEN[-4:]}" if API_TOKEN else "<absent>"
|
||||
logger.info("API Token (%s): %s — auth Bearer obligatoire", _token_source, _token_hint)
|
||||
print(f"\n{'='*60}")
|
||||
print(f" API Token ({_token_source}):")
|
||||
print(f" {API_TOKEN}")
|
||||
print(f" Configurer l'agent : export RPA_API_TOKEN={API_TOKEN}")
|
||||
print(f" {_token_hint} (masqué)")
|
||||
print(" Configurer l'agent via .env.local ou l'enrollment; ne pas copier depuis les logs.")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
worker.start(blocking=False)
|
||||
|
||||
Reference in New Issue
Block a user