feat(agent_v1): brancher FeedbackBusClient dans ChatWindow tkinter

- Import fail-safe : si python-socketio manquant (ancienne install Pauline),
  _HAS_FEEDBACK_BUS=False, ChatWindow tourne normalement sans bus
- Bus démarré à la fin de _run_tk_loop si LEA_FEEDBACK_BUS=1 dans l'env
- Callback _on_lea_event → _add_lea_message (thread-safe via root.after)
- Cleanup : _bus.stop() ajouté dans _do_destroy avant la destruction tkinter

Formatage des bulles minimal pour J3.3 (texte brut "[event] key=value").
Le style mixte métier+tech viendra en J3.4. La bulle paused interactive J3.5.

Aucun crash si bus indisponible. Aucun changement de comportement si flag off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-28 09:19:41 +02:00
parent 41eba898c0
commit 6154423a91

View File

@@ -16,6 +16,15 @@ from typing import Any, Callable, Dict, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# FeedbackBus : import fail-safe (le ChatWindow doit tourner même si python-socketio
# n'est pas installé sur le poste client, par exemple ancienne installation Pauline)
try:
from ..network.feedback_bus import FeedbackBusClient
_HAS_FEEDBACK_BUS = True
except Exception:
FeedbackBusClient = None # type: ignore
_HAS_FEEDBACK_BUS = False
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Theme — palette professionnelle claire # Theme — palette professionnelle claire
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -91,6 +100,7 @@ class ChatWindow:
self._root = None self._root = None
self._ready = threading.Event() self._ready = threading.Event()
self._messages = [] # historique local self._messages = [] # historique local
self._bus: Optional[Any] = None # FeedbackBusClient (J3.3, peut rester None)
# S'abonner aux changements de l'etat partage # S'abonner aux changements de l'etat partage
if self._shared_state is not None: if self._shared_state is not None:
@@ -266,6 +276,9 @@ class ChatWindow:
# Signaler que la fenetre est prete # Signaler que la fenetre est prete
self._ready.set() self._ready.set()
# Demarrer le bus feedback Lea (events 'lea:*' temps reel)
self._start_feedback_bus()
# Boucle tkinter # Boucle tkinter
root.mainloop() root.mainloop()
@@ -608,6 +621,12 @@ class ChatWindow:
def _do_destroy(self) -> None: def _do_destroy(self) -> None:
"""Detruit la fenetre (appele dans le thread tkinter).""" """Detruit la fenetre (appele dans le thread tkinter)."""
if self._bus is not None:
try:
self._bus.stop()
except Exception:
pass
self._bus = None
if self._root is not None: if self._root is not None:
try: try:
self._root.quit() self._root.quit()
@@ -617,6 +636,39 @@ class ChatWindow:
self._root = None self._root = None
self._visible = False self._visible = False
# ======================================================================
# FeedbackBus — bulles temps reel pendant l'execution (J3.3)
# ======================================================================
def _start_feedback_bus(self) -> None:
"""Demarrer la connexion au bus 'lea:*' si flag actif et lib disponible."""
if not _HAS_FEEDBACK_BUS:
logger.debug("FeedbackBus non disponible (python-socketio manquant)")
return
flag = os.environ.get("LEA_FEEDBACK_BUS", "0").lower()
if flag not in ("1", "true", "yes", "on"):
return
try:
url = f"http://{self._server_host}:{self._chat_port}"
token = os.environ.get("RPA_API_TOKEN", "") or None
self._bus = FeedbackBusClient(url, token=token, on_event=self._on_lea_event)
self._bus.start()
logger.info("FeedbackBus demarre : %s", url)
except Exception:
logger.debug("FeedbackBus init silenced", exc_info=True)
self._bus = None
def _on_lea_event(self, event: str, payload: Dict[str, Any]) -> None:
"""Callback bus → bulle Lea. Thread-safe : _add_lea_message utilise root.after."""
short = event.removeprefix("lea:") if event.startswith("lea:") else event
parts = []
for key in ("workflow", "step", "reason", "message", "failed_action"):
v = (payload or {}).get(key)
if v not in (None, ""):
parts.append(f"{key}={v}")
suffix = "" + ", ".join(parts) if parts else ""
self._add_lea_message(f"[{short}]{suffix}")
# ====================================================================== # ======================================================================
# Ajout de messages dans la zone de chat # Ajout de messages dans la zone de chat
# ====================================================================== # ======================================================================