diff --git a/agent_v0/agent_v1/core/captor.py b/agent_v0/agent_v1/core/captor.py index 2e685ae0a..864890c11 100644 --- a/agent_v0/agent_v1/core/captor.py +++ b/agent_v0/agent_v1/core/captor.py @@ -32,6 +32,7 @@ from pynput.keyboard import Key, KeyCode # Importation relative pour rester dans le module v1 from ..vision.capturer import VisionCapturer from ..vision.system_info import get_screen_metadata +from .log_safe import _sanitize_metadata # from ..monitoring.system import SystemMonitor logger = logging.getLogger(__name__) @@ -676,7 +677,7 @@ class EventCaptorV1: metadata = get_screen_metadata() with self._screen_metadata_lock: self._screen_metadata = metadata - logger.debug(f"Métadonnées système rafraîchies : {metadata}") + logger.debug(f"Métadonnées système rafraîchies : {_sanitize_metadata(metadata)}") except Exception as e: logger.error(f"Erreur refresh métadonnées système : {e}") diff --git a/agent_v0/agent_v1/core/executor.py b/agent_v0/agent_v1/core/executor.py index 0bfa85c96..92fc39018 100644 --- a/agent_v0/agent_v1/core/executor.py +++ b/agent_v0/agent_v1/core/executor.py @@ -26,6 +26,7 @@ from typing import Any, Dict, Optional # DPI awareness est configure (SetProcessDpiAwareness(2) sur Windows). # Sans cela, pynput et mss utilisent des coordonnees logiques (virtualisees). from ..config import MACHINE_ID as _ # noqa: F401 — side-effect import +from .log_safe import _title_hash import mss from pynput.mouse import Button, Controller as MouseController @@ -862,7 +863,7 @@ class ActionExecutorV1: ) if handled: logger.info( - f"[RUNTIME-DIALOG] '{current_title}' gere via serveur " + f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere via serveur " f"fenetre -> bouton '{button_text}' " f"[{resolved.get('method', 'server')}]" ) @@ -890,7 +891,7 @@ class ActionExecutorV1: ) if handled: logger.info( - f"[RUNTIME-DIALOG] '{current_title}' gere localement " + f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere localement " f"fenetre -> bouton '{button_text}' [dialog_window_text_template]" ) return handled @@ -917,7 +918,7 @@ class ActionExecutorV1: ) if handled: logger.info( - f"[RUNTIME-DIALOG] '{current_title}' gere par geometrie " + f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere par geometrie " f"fenetre -> bouton '{button_text}'" ) return handled @@ -967,7 +968,7 @@ class ActionExecutorV1: if not handled: continue logger.info( - f"[RUNTIME-DIALOG] '{current_title}' gere via serveur " + f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere via serveur " f"-> bouton '{button_text}' [{resolved.get('method', 'server')}]" ) return handled @@ -992,13 +993,13 @@ class ActionExecutorV1: if not handled: continue logger.info( - f"[RUNTIME-DIALOG] '{current_title}' gere localement " + f"[RUNTIME-DIALOG] [title_hash={_title_hash(current_title)}] gere localement " f"-> bouton '{button_text}' [dialog_text_template]" ) return handled logger.info( - f"[RUNTIME-DIALOG] Aucun bouton resolu pour '{current_title}'" + f"[RUNTIME-DIALOG] Aucun bouton resolu pour [title_hash={_title_hash(current_title)}]" ) return None @@ -1258,7 +1259,7 @@ class ActionExecutorV1: if dialog_spec.get("skip_current_action_after_handle", False): logger.info( - f"[RUNTIME-DIALOG] Dialogue '{current_title}' gere -> " + f"[RUNTIME-DIALOG] Dialogue [title_hash={_title_hash(current_title)}] gere -> " f"action {action.get('action_id', 'unknown')} skippée" ) return { @@ -1587,7 +1588,7 @@ class ActionExecutorV1: ] for pattern in popup_patterns: if pattern in current_title: - logger.info(f"Observer : popup détectée par titre — '{current_title}'") + logger.info(f"Observer : popup détectée par titre — [title_hash={_title_hash(current_title)}]") # On ne peut pas résoudre les coords juste par le titre # → retourner popup sans coords, le caller fera handle_popup_vlm() return { @@ -1874,8 +1875,8 @@ class ActionExecutorV1: ) else: logger.warning( - f"[LEA] Fenêtre incorrecte : attendu '{expected_title}', " - f"actuel '{current_title}'" + f"[LEA] Fenêtre incorrecte : attendu [title_hash={_title_hash(expected_title)}], " + f"actuel [title_hash={_title_hash(current_title)}]" ) auto_result = self._maybe_handle_runtime_dialog_before_pause( action=action, @@ -1888,8 +1889,8 @@ class ActionExecutorV1: if auto_result is not None: return auto_result print( - f" [PRÉ-VÉRIF] Fenêtre '{current_title}' ≠ " - f"attendu '{expected_title}' → mode apprentissage" + f" [PRÉ-VÉRIF] Fenêtre [title_hash={_title_hash(current_title)}] ≠ " + f"attendu [title_hash={_title_hash(expected_title)}] → mode apprentissage" ) try: self.notifier.replay_learning_mode( @@ -1936,8 +1937,8 @@ class ActionExecutorV1: # des coordonnées devenues invalides. result["success"] = False result["error"] = ( - f"Fenêtre incorrecte : attendu '{expected_title}', " - f"actuel '{current_title}'" + f"Fenêtre incorrecte : attendu [title_hash={_title_hash(expected_title)}], " + f"actuel [title_hash={_title_hash(current_title)}]" ) result["warning"] = "wrong_window" result["target_description"] = expected_title @@ -1945,11 +1946,11 @@ class ActionExecutorV1: result["screenshot"] = self._capture_screenshot_b64() logger.warning( f"[LEA] Wrong window sans correction → pause " - f"(attendu '{expected_title}', actuel '{current_title}')" + f"(attendu [title_hash={_title_hash(expected_title)}], actuel [title_hash={_title_hash(current_title)}])" ) return result else: - logger.info(f"[LEA] Pré-vérif OK : '{current_title}'") + logger.info(f"[LEA] Pré-vérif OK : [title_hash={_title_hash(current_title)}]") # ── OBSERVER : pré-analyse écran avant résolution ── # Détecte popups, dialogues, états inattendus AVANT de chercher la cible. @@ -1964,8 +1965,8 @@ class ActionExecutorV1: # Popup détectée AVANT la résolution — la fermer popup_label = observation.get("popup_label", "popup") popup_coords = observation.get("popup_coords") - print(f" [OBSERVER] Popup détectée : '{popup_label}' — fermeture") - logger.info(f"Observer : popup '{popup_label}' détectée avant résolution") + print(f" [OBSERVER] Popup détectée : [title_hash={_title_hash(popup_label)}] — fermeture") + logger.info(f"Observer : popup [title_hash={_title_hash(popup_label)}] détectée avant résolution") # ── SÉCURITÉ : refuser de cliquer sur un dialogue système ── # Avant de suivre les coordonnées du serveur (VLM-based, @@ -2365,8 +2366,8 @@ class ActionExecutorV1: recheck_title = recheck_info.get("title", "") if not _matches_expected_window(recheck_title): logger.warning( - f"P0.9 transition instable : matched '{post_title}' " - f"puis '{recheck_title}' à T+0.5s ≠ '{expected_after}'" + f"P0.9 transition instable : matched [title_hash={_title_hash(post_title)}] " + f"puis [title_hash={_title_hash(recheck_title)}] à T+0.5s ≠ [title_hash={_title_hash(expected_after)}]" ) matched = False post_title = recheck_title @@ -2376,19 +2377,19 @@ class ActionExecutorV1: result["runtime_dialog"] = runtime_dialog_handled print( f" [POST-VÉRIF] Dialogue runtime géré " - f"→ retour '{post_title}'" + f"→ retour [title_hash={_title_hash(post_title)}]" ) logger.info( - "POST-VÉRIF runtime dialog géré : '%s' -> '%s'", - runtime_dialog_handled.get("dialog_title", ""), - post_title, + "POST-VÉRIF runtime dialog géré : [title_hash=%s] -> [title_hash=%s]", + _title_hash(runtime_dialog_handled.get("dialog_title", "")), + _title_hash(post_title), ) else: - print(f" [POST-VÉRIF] OK en {elapsed_wait:.1f}s — '{post_title}'") - logger.info(f"POST-VÉRIF OK en {elapsed_wait:.1f}s : '{post_title}'") + print(f" [POST-VÉRIF] OK en {elapsed_wait:.1f}s — [title_hash={_title_hash(post_title)}]") + logger.info(f"POST-VÉRIF OK en {elapsed_wait:.1f}s : [title_hash={_title_hash(post_title)}]") else: - print(f" [POST-VÉRIF] TIMEOUT {max_wait}s — '{post_title}' ≠ '{expected_after}'") - logger.warning(f"POST-VÉRIF TIMEOUT : '{post_title}' ≠ '{expected_after}'") + print(f" [POST-VÉRIF] TIMEOUT {max_wait}s — [title_hash={_title_hash(post_title)}] ≠ [title_hash={_title_hash(expected_after)}]") + logger.warning(f"POST-VÉRIF TIMEOUT : [title_hash={_title_hash(post_title)}] ≠ [title_hash={_title_hash(expected_after)}]") if runtime_dialog_handled: result["warning"] = ( f"runtime_dialog_handled_post_verify:{post_title}" @@ -2396,9 +2397,9 @@ class ActionExecutorV1: result["runtime_dialog"] = runtime_dialog_handled logger.warning( "POST-VÉRIF runtime dialog géré mais " - "fenêtre finale inattendue : '%s' ≠ '%s'", - post_title, - expected_after, + "fenêtre finale inattendue : [title_hash=%s] ≠ [title_hash=%s]", + _title_hash(post_title), + _title_hash(expected_after), ) # Contrôle strict : si success_strict, on STOP. # On durcit aussi les vrais changements de fenêtre @@ -2416,8 +2417,8 @@ class ActionExecutorV1: if bool(action.get("success_strict")) or requires_transition: result["success"] = False result["error"] = ( - f"Post-vérif échouée : fenêtre '{post_title}' " - f"au lieu de '{expected_after}'" + f"Post-vérif échouée : fenêtre [title_hash={_title_hash(post_title)}] " + f"au lieu de [title_hash={_title_hash(expected_after)}]" ) result["warning"] = "wrong_window" result["needs_human"] = True @@ -2458,7 +2459,7 @@ class ActionExecutorV1: # paste=True (opt-in via action.paste) → clipboard + Ctrl+V (non-Citrix) self._type_text(text, paste=bool(action.get("paste", False))) print(f" [TYPE] Termine.") - logger.info(f"Replay type : '{text[:30]}...' ({len(text)} chars, raw_keys={'oui' if raw_keys else 'non'})") + logger.info(f"Replay type : [{len(text)} chars] (raw_keys={'oui' if raw_keys else 'non'})") elif action_type == "key_combo": keys = action.get("keys", []) @@ -2524,12 +2525,12 @@ class ActionExecutorV1: if not self._window_title_matches_any(current_title, patterns): logger.warning( "[LEA] verify_screen garde KO : attendu un titre " - "contenant %s, actuel '%s'", - patterns, current_title, + "contenant %s, actuel [title_hash=%s]", + patterns, _title_hash(current_title), ) print( f" [VERIFY] Garde titre KO " - f"(patterns={patterns}, actuel='{current_title}') " + f"(patterns={patterns}, actuel=[title_hash={_title_hash(current_title)}]) " "→ apprentissage humain" ) try: @@ -2557,15 +2558,15 @@ class ActionExecutorV1: result["error"] = ( f"verify_screen titre fenêtre KO : attendu " f"un titre contenant {patterns}, " - f"actuel '{current_title}'" + f"actuel [title_hash={_title_hash(current_title)}]" ) result["warning"] = "setup_guard_window_mismatch" result["needs_human"] = True result["screenshot"] = self._capture_screenshot_b64() return result logger.info( - "[LEA] verify_screen garde OK : '%s' matche %s", - current_title, patterns, + "[LEA] verify_screen garde OK : [title_hash=%s] matche %s", + _title_hash(current_title), patterns, ) print(f" [VERIFY] Termine (verification deferred au serveur).") @@ -3736,8 +3737,8 @@ Example: x_pct=0.50, y_pct=0.30""" real_x = int(x_pct * sw) real_y = int(y_pct * sh) label = server_result.get("matched_element", {}).get("label", "popup") - print(f" [POPUP-SERVER] Popup détectée ! Clic sur '{label}' → ({real_x}, {real_y})") - logger.info(f"[POPUP-SERVER] Clic popup '{label}' à ({real_x}, {real_y})") + print(f" [POPUP-SERVER] Popup détectée ! Clic sur [title_hash={_title_hash(label)}] → ({real_x}, {real_y})") + logger.info(f"[POPUP-SERVER] Clic popup [title_hash={_title_hash(label)}] à ({real_x}, {real_y})") self._click((real_x, real_y), "left") time.sleep(1.0) return True @@ -3856,8 +3857,8 @@ Example: x_pct=0.50, y_pct=0.30""" raw_content = resp.json().get("message", {}).get("content", "") full_response = prefill + raw_content - print(f" [POPUP-VLM] Réponse en {elapsed:.1f}s : {full_response.strip()}") - logger.info(f"[POPUP-VLM] Réponse VLM ({elapsed:.1f}s) : {full_response.strip()}") + print(f" [POPUP-VLM] Réponse en {elapsed:.1f}s : [len={len(full_response)}, has_target={'target' in full_response}]") + logger.info(f"[POPUP-VLM] Réponse VLM ({elapsed:.1f}s) : [len={len(full_response)}, has_target={'target' in full_response}]") # Extraire le texte du bouton depuis la réponse button_text = raw_content.strip().strip('"').strip("'").strip(".") @@ -4172,7 +4173,7 @@ Example: x_pct=0.50, y_pct=0.30""" try: self.keyboard.type(char) except Exception as e: - logger.debug(f"Impossible de taper '{char}': {e}") + logger.debug(f"Impossible de taper [1 char typed]: {e}") # Délai humain entre les frappes (40-120ms) time.sleep(random.uniform(0.04, 0.12)) diff --git a/agent_v0/agent_v1/core/recovery.py b/agent_v0/agent_v1/core/recovery.py index 9ed3c9f51..cf115484a 100644 --- a/agent_v0/agent_v1/core/recovery.py +++ b/agent_v0/agent_v1/core/recovery.py @@ -24,6 +24,8 @@ from dataclasses import dataclass from enum import Enum from typing import Any, Dict, List, Optional +from .log_safe import _title_hash + logger = logging.getLogger(__name__) @@ -168,8 +170,8 @@ class RecoveryEngine: from ..window_info_crossplatform import get_active_window_info active = get_active_window_info() active_title = active.get("title", "") - logger.info(f"Recovery : Alt+F4 sur '{active_title}'") - print(f" [RECOVERY] Alt+F4 — fermeture de '{active_title}'") + logger.info(f"Recovery : Alt+F4 sur [title_hash={_title_hash(active_title)}]") + print(f" [RECOVERY] Alt+F4 — fermeture de [title_hash={_title_hash(active_title)}]") except Exception: logger.info("Recovery : Alt+F4 (fenêtre active inconnue)") print(" [RECOVERY] Alt+F4 — fermeture fenêtre indésirable") @@ -182,7 +184,7 @@ class RecoveryEngine: return RecoveryResult( action_taken=RecoveryAction.CLOSE_WINDOW, success=True, - detail=f"Alt+F4 exécuté sur '{active_title if 'active_title' in dir() else '?'}'", + detail=f"Alt+F4 exécuté sur [title_hash={_title_hash(active_title) if 'active_title' in dir() else '?'}]", ) elif strategy == RecoveryAction.CLICK_AWAY: diff --git a/agent_v0/agent_v1/main.py b/agent_v0/agent_v1/main.py index 334604277..085e2b822 100644 --- a/agent_v0/agent_v1/main.py +++ b/agent_v0/agent_v1/main.py @@ -29,6 +29,7 @@ from .ui.capture_server import CaptureServer from .session.storage import SessionStorage from .vision.capturer import VisionCapturer from .finalize_contract import dispatch_finalize_result +from .core.log_safe import _title_hash # Import optionnel du client serveur (pour le chat et les workflows) # Deux chemins : relatif (depuis agent_v0.agent_v1) ou absolu (depuis C:\rpa_vision\agent_v1) @@ -261,7 +262,7 @@ class AgentV1: # Ne PAS en relancer une ici — deux threads poll simultanés causent # une race condition où les actions sont consommées mais pas exécutées. - logger.info(f"Session {self.session_id} ({workflow_name}) sur machine {self.machine_id} en cours...") + logger.info(f"Session {self.session_id} [wf_hash={_title_hash(workflow_name)}] sur machine {self.machine_id} en cours...") def _command_watchdog_loop(self): """Surveille un fichier de commande pour executer des ordres visuels (legacy).""" diff --git a/agent_v0/agent_v1/network/streamer.py b/agent_v0/agent_v1/network/streamer.py index a05c3dd39..4f5bff80f 100644 --- a/agent_v0/agent_v1/network/streamer.py +++ b/agent_v0/agent_v1/network/streamer.py @@ -36,6 +36,7 @@ import requests from PIL import Image from ..config import API_TOKEN, BASE_DIR, STREAMING_ENDPOINT +from ..core.log_safe import _title_hash from .persistent_buffer import MAX_ATTEMPTS, PersistentBuffer @@ -138,7 +139,7 @@ class TraceStreamer: target=self._buffer_drain_loop, daemon=True ) self._drain_thread.start() - logger.info(f"Streamer pour {self.session_id} démarré") + logger.info(f"Streamer démarré") def stop(self): """Arrêter le streaming et finaliser la session côté serveur. @@ -166,7 +167,7 @@ class TraceStreamer: self._drain_thread.join(timeout=2.0) self._finalize_session() - logger.info(f"Streamer pour {self.session_id} arrêté") + logger.info(f"Streamer arrêté") def push_event(self, event_data: dict): """Enfile un événement pour envoi immédiat. @@ -632,7 +633,7 @@ class TraceStreamer: self._check_redirect(resp, url) if resp.ok: result = resp.json() - logger.info(f"Session finalisée: {result}") + logger.info(f"Session finalisée [status={result.get('status')}, wf_hash={_title_hash(result.get('workflow_name',''))}]") if self._on_finalize_result is not None: try: self._on_finalize_result(result) diff --git a/agent_v0/agent_v1/ui/activity_panel.py b/agent_v0/agent_v1/ui/activity_panel.py index a80978627..4b0e08fe1 100644 --- a/agent_v0/agent_v1/ui/activity_panel.py +++ b/agent_v0/agent_v1/ui/activity_panel.py @@ -29,6 +29,8 @@ from dataclasses import dataclass, field from enum import Enum from typing import Optional +from ..core.log_safe import _title_hash + logger = logging.getLogger(__name__) @@ -132,7 +134,7 @@ class ActivityPanel: ) self._notifier_changement() self._rafraichir_ui() - logger.info(f"[ACTIVITY] Workflow démarré : {nom} ({nb_etapes} étapes)") + logger.info(f"[ACTIVITY] Workflow démarré : [wf_hash={_title_hash(nom)}] ({nb_etapes} étapes)") def mettre_a_jour( self, diff --git a/agent_v0/agent_v1/ui/capture_server.py b/agent_v0/agent_v1/ui/capture_server.py index d65a3c0f4..45d7faba6 100644 --- a/agent_v0/agent_v1/ui/capture_server.py +++ b/agent_v0/agent_v1/ui/capture_server.py @@ -27,6 +27,8 @@ import os import time from http.server import HTTPServer, BaseHTTPRequestHandler +from ..core.log_safe import _path_ext + logger = logging.getLogger(__name__) CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006")) @@ -312,7 +314,7 @@ class _FileActionHandlerLocal: }) extensions[ext] = extensions.get(ext, 0) + 1 - logger.info(f"Liste dossier '{path_str}' : {len(files)} fichiers") + logger.info(f"Liste dossier [ext={_path_ext(path_str)}] : {len(files)} fichiers") return {"files": files, "count": len(files), "extensions": extensions, "path": path_str} def _create_dir(self, params: dict) -> dict: @@ -328,7 +330,7 @@ class _FileActionHandlerLocal: target = _Path(path_str) existed = target.exists() target.mkdir(parents=True, exist_ok=True) - logger.info(f"Dossier '{path_str}' {'existait deja' if existed else 'cree'}") + logger.info(f"Dossier [ext={_path_ext(path_str)}] {'existait deja' if existed else 'cree'}") return {"created": not existed, "path": path_str, "already_existed": existed} def _move_file(self, params: dict) -> dict: @@ -350,7 +352,7 @@ class _FileActionHandlerLocal: _Path(dst).parent.mkdir(parents=True, exist_ok=True) _shutil.move(src, dst) - logger.info(f"Fichier deplace : '{src}' -> '{dst}'") + logger.info(f"Fichier deplace : [ext={_path_ext(src)}] -> [ext={_path_ext(dst)}]") return {"moved": True, "source": src, "destination": dst} def _copy_file(self, params: dict) -> dict: @@ -376,7 +378,7 @@ class _FileActionHandlerLocal: _shutil.copytree(src, dst) else: _shutil.copy2(src, dst) - logger.info(f"Fichier copie : '{src}' -> '{dst}'") + logger.info(f"Fichier copie : [ext={_path_ext(src)}] -> [ext={_path_ext(dst)}]") return {"copied": True, "source": src, "destination": dst} def _sort_by_extension(self, params: dict) -> dict: @@ -425,7 +427,7 @@ class _FileActionHandlerLocal: extensions[ext] = extensions.get(ext, 0) + 1 logger.info( - f"Classement par extension '{source_dir_str}' : {len(moved)} fichiers" + f"Classement par extension [ext={_path_ext(source_dir_str)}] : {len(moved)} fichiers" ) return { "moved": moved, diff --git a/agent_v0/agent_v1/window_info_crossplatform.py b/agent_v0/agent_v1/window_info_crossplatform.py index 85caed8d1..8b1349f34 100644 --- a/agent_v0/agent_v1/window_info_crossplatform.py +++ b/agent_v0/agent_v1/window_info_crossplatform.py @@ -19,6 +19,8 @@ import platform import subprocess from typing import Any, Dict, Optional +from .core.log_safe import _title_hash + def _run_cmd(cmd: list[str]) -> Optional[str]: """Exécute une commande et renvoie la sortie texte (strippée), ou None en cas d'erreur.""" @@ -372,7 +374,7 @@ if __name__ == "__main__": for i in range(5): info = get_active_window_info() rect = get_active_window_rect() - print(f"[{i+1}] App: {info['app_name']:20s} | Title: {info['title']}") + print(f"[{i+1}] App: {info['app_name']:20s} | Title: [title_hash={_title_hash(info['title'])}]") if rect: print(f" Rect: {rect['rect']} | Size: {rect['size']}") else: