Files
rpa_vision_v3/agent_v0/agent_v1/ui/shared_state.py
Dom ae65be2555 chore: ajouter agent_v0/ au tracking git (était un repo embarqué)
Suppression du .git embarqué dans agent_v0/ — le code est maintenant
tracké normalement dans le repo principal.
Inclut : agent_v1 (client), server_v1 (streaming), lea_ui (chat client)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:12:23 +01:00

191 lines
6.3 KiB
Python

# agent_v1/ui/shared_state.py
"""
Etat partage entre le systray et le chat Lea. Thread-safe.
Point central de verite pour l'etat de l'agent :
- Enregistrement en cours (oui/non, nom de la tache)
- Replay en cours
- Compteur d'actions
Les deux composants UI (SmartTrayV1 et ChatWindow) lisent et ecrivent
dans cet objet. Chaque changement notifie tous les listeners enregistres.
"""
from __future__ import annotations
import logging
import threading
from typing import Any, Callable, List, Optional
logger = logging.getLogger(__name__)
class AgentState:
"""Etat partage entre le systray et le chat Lea. Thread-safe."""
def __init__(self) -> None:
self._lock = threading.Lock()
# Etat d'enregistrement
self._recording = False
self._recording_name = ""
self._actions_count = 0
# Etat de replay
self._replay_active = False
# Callbacks de demarrage/arret de session (relies au moteur agent)
self._on_start: Optional[Callable[[str], None]] = None
self._on_stop: Optional[Callable[[], None]] = None
# Listeners notifies a chaque changement d'etat
self._listeners: List[Callable[["AgentState"], None]] = []
# ------------------------------------------------------------------
# Proprietes en lecture seule (thread-safe)
# ------------------------------------------------------------------
@property
def is_recording(self) -> bool:
with self._lock:
return self._recording
@property
def recording_name(self) -> str:
with self._lock:
return self._recording_name
@property
def actions_count(self) -> int:
with self._lock:
return self._actions_count
@property
def is_replay_active(self) -> bool:
with self._lock:
return self._replay_active
# ------------------------------------------------------------------
# Mutations (thread-safe, notifient les listeners)
# ------------------------------------------------------------------
def start_recording(self, name: str) -> None:
"""Demarre un enregistrement (appele depuis systray OU chat).
Appelle le callback on_start si defini, puis notifie les listeners.
"""
with self._lock:
if self._recording:
logger.warning("Enregistrement deja en cours, ignore")
return
self._recording = True
self._recording_name = name
self._actions_count = 0
on_start = self._on_start
logger.info("Enregistrement demarre : %s", name)
# Appeler le callback moteur (hors du lock pour eviter deadlock)
if on_start is not None:
try:
on_start(name)
except Exception as e:
logger.error("Erreur demarrage session : %s", e)
# Annuler l'enregistrement si le moteur echoue
with self._lock:
self._recording = False
self._recording_name = ""
self._notify_listeners()
raise
self._notify_listeners()
def stop_recording(self) -> None:
"""Arrete l'enregistrement (appele depuis systray OU chat).
Appelle le callback on_stop si defini, puis notifie les listeners.
"""
with self._lock:
if not self._recording:
logger.debug("Pas d'enregistrement en cours, ignore")
return
self._recording = False
name = self._recording_name
count = self._actions_count
on_stop = self._on_stop
logger.info("Enregistrement arrete : %s (%d actions)", name, count)
# Appeler le callback moteur
if on_stop is not None:
try:
on_stop()
except Exception as e:
logger.error("Erreur arret session : %s", e)
self._notify_listeners()
def update_actions_count(self, count: int) -> None:
"""Met a jour le compteur d'actions (appele par le moteur agent)."""
with self._lock:
self._actions_count = count
self._notify_listeners()
def set_replay_active(self, active: bool) -> None:
"""Active ou desactive le mode replay."""
with self._lock:
if self._replay_active == active:
return
self._replay_active = active
logger.info("Replay %s", "actif" if active else "termine")
self._notify_listeners()
# ------------------------------------------------------------------
# Enregistrement des callbacks et listeners
# ------------------------------------------------------------------
def set_on_start(self, callback: Callable[[str], None]) -> None:
"""Definit le callback appele quand un enregistrement demarre.
Ce callback est le pont vers le moteur agent (AgentV1.start_session).
"""
with self._lock:
self._on_start = callback
def set_on_stop(self, callback: Callable[[], None]) -> None:
"""Definit le callback appele quand un enregistrement s'arrete.
Ce callback est le pont vers le moteur agent (AgentV1.stop_session).
"""
with self._lock:
self._on_stop = callback
def on_change(self, callback: Callable[["AgentState"], None]) -> None:
"""Enregistre un listener notifie a chaque changement d'etat.
Les listeners sont appeles dans un thread separe pour ne pas
bloquer l'appelant.
"""
with self._lock:
self._listeners.append(callback)
# ------------------------------------------------------------------
# Notification interne
# ------------------------------------------------------------------
def _notify_listeners(self) -> None:
"""Notifie tous les listeners enregistres du changement d'etat."""
with self._lock:
listeners = list(self._listeners)
for listener in listeners:
try:
# Appel dans un thread pour ne pas bloquer
threading.Thread(
target=listener,
args=(self,),
daemon=True,
).start()
except Exception as e:
logger.error("Erreur notification listener : %s", e)