- safety_checks_provider : tous les logger.warning d'échec LLM préfixés [BUS] lea:safety_checks_llm_failed avec une raison spécifique (exception, http_status, timeout, network, json_decode). - monitor_router : émission [BUS] lea:monitor_invalid_index si l'index explicite passé dans l'action est hors limites de monitors_geometry, et [BUS] lea:monitor_unavailable si focus actif demandé mais introuvable. Ces deux events permettent au bus de tracer chaque fallback de la cascade de routage QW1. - safety_checks_provider : import io supprimé (inutilisé). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
# agent_v0/server_v1/monitor_router.py
|
|
"""MonitorRouter — résolution de l'écran cible pour le replay (QW1).
|
|
|
|
Stratégie en cascade :
|
|
1. action.monitor_index (hérité de la session source) → cible cet écran
|
|
2. session.last_focused_monitor (focus actif vu en dernier heartbeat) → fallback
|
|
3. composite (offset 0, 0) → backward compat
|
|
|
|
Émet sur le bus lea:* l'event monitor_routed avec la source de la décision.
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class MonitorTarget:
|
|
"""Représente l'écran cible résolu pour une action de replay."""
|
|
idx: int
|
|
offset_x: int
|
|
offset_y: int
|
|
w: int
|
|
h: int
|
|
source: str # "action" | "focus" | "composite_fallback"
|
|
|
|
|
|
_COMPOSITE_FALLBACK = MonitorTarget(
|
|
idx=-1,
|
|
offset_x=0,
|
|
offset_y=0,
|
|
w=0,
|
|
h=0,
|
|
source="composite_fallback",
|
|
)
|
|
|
|
|
|
def _find_monitor(geometry: List[Dict[str, Any]], idx: int) -> Optional[Dict[str, Any]]:
|
|
"""Retourne le monitor d'index donné, ou None si absent."""
|
|
for m in geometry:
|
|
if m.get("idx") == idx:
|
|
return m
|
|
return None
|
|
|
|
|
|
def _to_target(monitor: Dict[str, Any], source: str) -> MonitorTarget:
|
|
return MonitorTarget(
|
|
idx=int(monitor["idx"]),
|
|
offset_x=int(monitor.get("x", 0)),
|
|
offset_y=int(monitor.get("y", 0)),
|
|
w=int(monitor.get("w", 0)),
|
|
h=int(monitor.get("h", 0)),
|
|
source=source,
|
|
)
|
|
|
|
|
|
def resolve_target_monitor(
|
|
action: Dict[str, Any],
|
|
session_state: Dict[str, Any],
|
|
) -> MonitorTarget:
|
|
"""Résout l'écran cible d'une action de replay.
|
|
|
|
Args:
|
|
action: Dict de l'action (peut contenir `monitor_index`).
|
|
session_state: État de la session (doit contenir `monitors_geometry`
|
|
et `last_focused_monitor`).
|
|
|
|
Returns:
|
|
MonitorTarget avec l'offset à appliquer aux coordonnées de grounding.
|
|
"""
|
|
geometry: List[Dict[str, Any]] = session_state.get("monitors_geometry") or []
|
|
|
|
# 1. Cible explicite via action
|
|
explicit_idx = action.get("monitor_index")
|
|
if explicit_idx is not None and geometry:
|
|
m = _find_monitor(geometry, int(explicit_idx))
|
|
if m is not None:
|
|
return _to_target(m, source="action")
|
|
# Index invalide → on tombe sur le fallback focus
|
|
logger.warning(
|
|
"[BUS] lea:monitor_invalid_index requested=%d available_idx=%s",
|
|
int(explicit_idx), [g.get("idx") for g in geometry],
|
|
)
|
|
|
|
# 2. Fallback focus actif
|
|
focused_idx = session_state.get("last_focused_monitor")
|
|
if focused_idx is not None and geometry:
|
|
m = _find_monitor(geometry, int(focused_idx))
|
|
if m is not None:
|
|
return _to_target(m, source="focus")
|
|
logger.warning(
|
|
"[BUS] lea:monitor_unavailable focused_idx=%d available_idx=%s",
|
|
int(focused_idx), [g.get("idx") for g in geometry],
|
|
)
|
|
|
|
# 3. Fallback composite (backward compat — comportement actuel mss.monitors[0])
|
|
return _COMPOSITE_FALLBACK
|