Files
rpa_vision_v3/docs/AUDIT_FINALIZE_CONTRACT_INTEGRATION_2026-05-20.md

11 KiB

Audit — Point d'intégration agent pour le contrat finalize étendu

Date : 2026-05-20 Mission : Claude 4 (lecture seule) Périmètre : agent_v0/agent_v1/network/streamer.py, agent_v0/agent_v1/main.py, agent_v0/agent_v1/ui/smart_tray.py, agent_v0/agent_v1/ui/chat_window.py, agent_v0/agent_v1/ui/shared_state.py, agent_v0/lea_ui/server_client.py Contexte : le serveur va enrichir la réponse POST /api/v1/traces/stream/finalize avec un nouveau contrat { replay_ready, replay_request, launch_replay }. Objectif : identifier le plus petit point d'intégration côté agent pour consommer ce contrat proprement, sans remettre le VWB au centre.

Constat

Aujourd'hui, streamer.py:622-623 reçoit la réponse /finalize (result = resp.json()) et la jette dans un logger.info(f"Session finalisée: {result}"). Aucun consommateur ne voit ce payload. Tous les fils en aval (main.AgentV1.stop_session, smart_tray._on_stop_session, chat_window._on_quick_stop) terminent sans rien savoir du résultat serveur. La surface unique de capture du payload existe déjà — il suffit de l'exploiter.

Côté déclenchement replay, une seule surface client émet l'appel HTTP : smart_tray._launch_replay(workflow_id, workflow_name) (ligne 459-505) qui fait POST /api/v1/traces/stream/replay/start avec {"workflow_id": ...}. Tout le reste (chat_window, lea_ui/server_client) consomme un replay déjà initié (resume/abort, polling actions). Le nouveau contrat replay_request pourrait juste enrichir le payload passé à cet appel existant.

L'orchestration globale (AgentV1 dans main.py) wire déjà session start/stop via AgentState (shared_state.py). C'est le seul endroit qui voit à la fois le moment précis de la fin d'enregistrement (stop_session), l'identité de la session, et a accès à toutes les UI (tray + chat) via self._state.

Tableau — surfaces d'arrêt / relance et recommandations

Arrêt d'un enregistrement (où voir la fin)

Fichier Point d'entrée Rôle Risque branchement Recommandation
streamer.py:602-627 _finalize_session() Site unique qui reçoit resp.json() de /finalize Aucun — actuellement payload jeté 🟢 POINT D'INTÉGRATION RECOMMANDÉ — ajouter callback on_finalize_result: Callable[[dict], None] invoqué après resp.ok. Découplé, non-bloquant, contenu.
streamer.py:131-157 TraceStreamer.stop() Drain queue + appel _finalize_session Modifier le retour casse l'API publique (appelée par main.stop_session) 🟡 Acceptable mais moins propre — retourner le payload via stop() couple les appelants à un retour qui n'existait pas. Préférer le callback.
main.py:395-430 AgentV1.stop_session() Orchestre captor.stop + sleep + streamer.stop Lieu naturel pour router le payload reçu — mais doit l'obtenir d'amont 🟢 Destinataire idéal du callback — a accès à self._state pour notifier UI, et self.user_id pour reconstituer agent_<user_id> session de polling.
shared_state.AgentState.stop_recording() Méthode Source de vérité de l'arrêt UI Étendre la signature _on_stop casse rétrocompat 🟡 À éviter en première intention — l'état partagé doit rester muet sur le résultat serveur.
smart_tray._on_stop_session() (ligne 396) Bouton tray "C'est terminé" Notif "Merci ! J'ai bien mémorisé..." Faire du HTTP ici dupliquerait _launch_replay 🔴 À éviter — la couche UI ne doit pas appeler /finalize ni interpréter sa réponse.
smart_tray._on_emergency_stop() (ligne 507) Bouton ARRÊT D'URGENCE Stop immédiat sans finalize Hors scope (arrêt brutal, pas de proposition replay) Aucun branchement — un emergency stop n'attend pas le replay.
chat_window._on_quick_stop() (ligne 1458) Bouton chat "Arrêter" Appelle shared_state.stop_recording() Idem ligne au-dessus 🔴 À éviter — duplication.

Relance d'un replay (où injecter replay_request / launch_replay)

Fichier Point d'entrée Rôle Risque branchement Recommandation
smart_tray._launch_replay(workflow_id, workflow_name) (ligne 459-505) Appel HTTP POST /replay/start Seul site qui déclenche un replay neuf côté client Élargir la signature pour accepter un replay_request: dict (ou écraser workflow_id par tout le dict si fourni) 🟢 Réutiliser tel quel — passer le replay_request du serveur comme JSON body. Surface existante, mature, notification Article 50 déjà cablée ligne 469-472.
smart_tray._make_replay_callback() (ligne 326) Wrapping pour menu Fabrique callbacks pour menu "Mes tâches" Non touché par ce changement Aucun branchement
main._replay_poll_loop() (ligne 276-345) Boucle permanente Récepteur d'actions, pas déclencheur de replay Auto-déclencher ici = invisible, non conforme Article 14 🔴 À éviter formellement — le poll loop reste polymorphique sur la session, ne doit pas démarrer de session.
chat_window._on_paused_resume/_abort (ligne 1016-1080) Boutons bulle Pause Resume/abort d'un replay déjà en cours Hors scope (action sur replay existant) Aucun branchement
lea_ui/server_client.LeaServerClient.start_polling() (ligne 261) API alternative Polling parallèle non utilisé par main (qui a son propre loop) Confusion : code dormant en partie 🟡 Ne pas ajouter de logique ici — confirmer d'abord si ce client est encore actif au runtime.

Notification / proposition utilisateur post-enregistrement

Surface Mécanisme dispo Utilisation recommandée
smart_tray._notifier.notify(title, msg) (NotificationManager) Notification système non-bloquante 🟢 Notif simple "Tâche prête, voulez-vous la tester ?" si replay_ready=true mais launch_replay=false.
smart_tray._ask_consent(title, msg) (ligne 74) Dialog tkinter Yes/No 🟢 Si le serveur signale replay_ready=true, dialog post-enregistrement (réutilise le même pattern que le consentement avant enregistrement).
chat_window (bulle paused) Tkinter custom bubble 🟡 Réutiliser le pattern paused_bubble (ligne 902-1014) pour proposer un "Tester maintenant" / "Plus tard" serait propre mais plus de code. À garder pour une v2 produit.
shared_state listeners Notification générique 🟡 Émettre un nouvel événement type recording_finalized peut servir si plusieurs UI doivent réagir — mais ajoute du contrat.

Trois options d'intégration

Option A — Solution minimale (recommandée)

  1. TraceStreamer reçoit un setter optionnel set_on_finalize_result(callback: Callable[[dict], None]). Stocké en attribut.
  2. Dans _finalize_session() après resp.ok, appel non-bloquant self.on_finalize_result(result) si défini.
  3. AgentV1.__init__ wire le callback : self.streamer.set_on_finalize_result(self._on_finalize_result).
  4. AgentV1._on_finalize_result(payload) :
    • si payload.get("launch_replay") is True ET replay_request présent → invoque self.ui._launch_replay(replay_request) (avec adaptation signature pour accepter un dict)
    • sinon si payload.get("replay_ready") is Trueself.ui._notifier.notify("Léa", "J'ai compris la tâche. Voulez-vous la tester ?") + ouvre _ask_consent (en thread)
    • sinon : silencieux

Volume estimé : ~15-20 lignes de code dans 2-3 fichiers. Aucun changement de contrat dans shared_state, chat_window, lea_ui. Réutilise _launch_replay et _ask_consent existants.

Conforme : Article 14 (contrôle humain) si dialog _ask_consent ; Article 50 (transparence) si _launch_replay est utilisé (notification déjà émise ligne 469-472).

Option B — Solution produit propre (à viser ensuite)

Tout de Option A, plus :

  • Ajouter une bulle interactive dans chat_window (pattern paused_bubble), au lieu/en plus du dialog tkinter, pour proposer le test avec contexte (nom de la tâche, durée, nombre d'actions).
  • Persister la proposition non répondue dans AgentState pour que l'utilisateur la retrouve même s'il a fermé la notif.
  • Logger côté audit_trail l'événement "test post-enregistrement proposé / accepté / refusé" pour conformité Article 12.

Volume estimé : +50-80 lignes, principalement UI chat_window. Plus de couplage shared_state.

Option C — À éviter

  • Auto-déclencher le replay sans confirmation depuis le callback finalize, même si launch_replay=true. Sans dialog, non conforme Article 14 (l'utilisateur peut ne pas être devant l'écran à ce moment). Si le serveur impose vraiment launch_replay=true, l'agent doit quand même afficher un compte à rebours visible (3-5s) avec annulation possible — mais pas exécuter silencieusement.
  • Faire le HTTP /finalize depuis chat_window ou smart_tray pour lire la réponse. Duplication, l'API serveur est déjà appelée par streamer.
  • Modifier _replay_poll_loop pour intercepter une transition "finalize→start replay". Couplage faux entre poll permanent et événement ponctuel ; race condition si l'utilisateur enchaîne plusieurs enregistrements.
  • Étendre la signature de AgentState.stop_recording() pour propager le payload. La couche état partagé doit rester muette sur le contenu serveur.
  • Modifier _finalize_session() pour retourner la valeur via streamer.stop(). Casse l'API publique de stop() (utilisée par main.stop_session), oblige à modifier deux endroits, et empêche un fan-out à plusieurs listeners éventuels.

Conclusion

Un seul point d'intégration recommandé : ajouter un callback on_finalize_result à TraceStreamer, invoqué dans _finalize_session après réception de la réponse, wired par AgentV1 qui dispatche selon launch_replay / replay_ready vers les surfaces UI déjà existantes (smart_tray._launch_replay ou smart_tray._ask_consent + _notifier.notify).

Ce point :

  • Capture la réponse là où elle existe déjà (streamer.py:622) sans dupliquer d'appel HTTP.
  • Découpe la responsabilité : streamer = transport, main = orchestration, smart_tray = UI/consentement.
  • Préserve le VWB hors-jeu : aucun appel ne transite par VWB, le replay est lancé directement par l'agent via l'endpoint /replay/start déjà éprouvé.
  • Respecte Article 14 si l'auto-lancement passe par _ask_consent ou une notif validable.
  • Coût d'implémentation : ~15-20 lignes de code, 2-3 fichiers.

Hors périmètre de cette mission : la définition exacte du contenu de replay_request (qui touche au workflow_id, à la machine cible, aux paramètres execution_mode). À spécifier côté serveur avant de figer la signature client.

Méthode d'audit

  • Lecture intégrale : shared_state.py (190 lignes), smart_tray.py (781 lignes), lea_ui/server_client.py (375 lignes).
  • Lecture déjà réalisée à l'audit Léa-first (2026-05-19) : streamer.py (734 lignes), main.py (561 lignes).
  • Grep ciblé chat_window.py (1619 lignes) : stop|replay|workflow|finalize|launch|on_stop|on_replay (60 résultats analysés).
  • Aucune modification de code, aucune exploration VWB, aucune refonte UI proposée — conformément aux interdits.