feat: acteur gemma4 — décide PASSER/EXECUTER/STOPPER quand target_not_found
Quand le magnétoscope ne trouve pas la cible, au lieu de la pause supervisée, gemma4 (Docker port 11435, think=True) reçoit le contexte (action prévue + fenêtre active) et décide : - PASSER : le résultat est déjà atteint (onglet actif, dialog ouvert) - STOPPER : état incohérent (mauvaise app) - EXECUTER : fallback vers la pause supervisée Testé : gemma4 décide PASSER quand l'onglet est déjà actif (5s). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,70 @@ class ActionExecutorV1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Echec de l'ordre {action} : {e}")
|
logger.error(f"Echec de l'ordre {action} : {e}")
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Acteur intelligent — décision gemma4 quand le magnétoscope bloque
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def _actor_decide(self, action: dict, target_spec: dict) -> str:
|
||||||
|
"""Demander à gemma4 de décider quand le magnétoscope ne trouve pas la cible.
|
||||||
|
|
||||||
|
gemma4 reçoit le contexte (action prévue, état de l'écran) et décide :
|
||||||
|
- PASSER : l'état est déjà atteint (ex: onglet déjà actif)
|
||||||
|
- EXECUTER : l'action est nécessaire mais pas trouvable automatiquement
|
||||||
|
- STOPPER : l'état est incohérent, impossible de continuer
|
||||||
|
|
||||||
|
Appelle gemma4 en mode texte avec thinking (Docker port 11435).
|
||||||
|
Fallback : EXECUTER (pause supervisée) si gemma4 indisponible.
|
||||||
|
"""
|
||||||
|
import requests as _requests
|
||||||
|
|
||||||
|
gemma4_port = os.environ.get("GEMMA4_PORT", "11435")
|
||||||
|
by_text = target_spec.get("by_text", "")
|
||||||
|
window_title = target_spec.get("window_title", "")
|
||||||
|
|
||||||
|
# Récupérer le titre de la fenêtre ACTUELLE
|
||||||
|
try:
|
||||||
|
from ..window_info_crossplatform import get_active_window_info
|
||||||
|
current_info = get_active_window_info()
|
||||||
|
current_title = current_info.get("title", "")
|
||||||
|
except Exception:
|
||||||
|
current_title = ""
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
f"Tu es un robot RPA. L'action suivante est : cliquer sur '{by_text or 'un élément'}' "
|
||||||
|
f"dans '{window_title}'.\n"
|
||||||
|
f"La fenêtre active est \"{current_title}\".\n"
|
||||||
|
f"Dois-je faire cette action ?\n"
|
||||||
|
f"- EXECUTER : l'action est nécessaire\n"
|
||||||
|
f"- PASSER : le résultat est déjà atteint\n"
|
||||||
|
f"- STOPPER : état incohérent\n"
|
||||||
|
f"Réponds UN SEUL MOT."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = _requests.post(
|
||||||
|
f"http://localhost:{gemma4_port}/api/chat",
|
||||||
|
json={
|
||||||
|
"model": "gemma4:e4b",
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"stream": False,
|
||||||
|
"think": True,
|
||||||
|
"options": {"temperature": 0.1, "num_predict": 500},
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
content = resp.json().get("message", {}).get("content", "").strip().upper()
|
||||||
|
# Extraire le mot clé
|
||||||
|
for keyword in ("PASSER", "EXECUTER", "STOPPER"):
|
||||||
|
if keyword in content:
|
||||||
|
logger.info(f"Acteur gemma4 décide : {keyword}")
|
||||||
|
return keyword
|
||||||
|
logger.warning(f"Acteur gemma4 réponse inattendue : {content[:50]}")
|
||||||
|
return "EXECUTER"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Acteur gemma4 indisponible : {e}")
|
||||||
|
return "EXECUTER"
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Execution replay (polling serveur)
|
# Execution replay (polling serveur)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -342,21 +406,29 @@ class ActionExecutorV1:
|
|||||||
self.notifier.replay_target_not_found(target_desc)
|
self.notifier.replay_target_not_found(target_desc)
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
# Cible invisible, pas de popup — PAUSE supervisée
|
# Cible invisible — demander à l'acteur (gemma4) de décider
|
||||||
target_desc = self._describe_target(target_spec)
|
target_desc = self._describe_target(target_spec)
|
||||||
result["success"] = False
|
decision = self._actor_decide(action, target_spec)
|
||||||
result["error"] = "target_not_found"
|
|
||||||
result["target_description"] = target_desc
|
if decision == "PASSER":
|
||||||
result["target_spec"] = target_spec
|
print(f" [ACTEUR] Décision: PASSER — l'état est déjà atteint")
|
||||||
result["screenshot"] = self._capture_screenshot_b64()
|
logger.info(f"Action {action_id} : acteur décide PASSER pour '{target_desc}'")
|
||||||
result["warning"] = "visual_resolve_failed"
|
result["success"] = True
|
||||||
print(f" [ERREUR] Visual resolve échoué, pas de popup — PAUSE")
|
result["warning"] = "actor_skip"
|
||||||
logger.error(
|
elif decision == "STOPPER":
|
||||||
f"Action {action_id} : cible '{target_desc}' non trouvée, "
|
print(f" [ACTEUR] Décision: STOPPER — état incohérent")
|
||||||
f"replay en pause supervisée"
|
logger.error(f"Action {action_id} : acteur décide STOPPER pour '{target_desc}'")
|
||||||
)
|
result["success"] = False
|
||||||
# Notifier l'utilisateur via toast
|
result["error"] = f"actor_stop:{target_desc}"
|
||||||
self.notifier.replay_target_not_found(target_desc)
|
self.notifier.replay_target_not_found(target_desc)
|
||||||
|
else:
|
||||||
|
# EXECUTER ou décision inconnue → pause supervisée (fallback)
|
||||||
|
print(f" [ACTEUR] Décision: {decision} — pause supervisée")
|
||||||
|
logger.warning(f"Action {action_id} : acteur décide {decision}, pause")
|
||||||
|
result["success"] = False
|
||||||
|
result["error"] = "target_not_found"
|
||||||
|
result["warning"] = "visual_resolve_failed"
|
||||||
|
self.notifier.replay_target_not_found(target_desc)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
real_x = int(x_pct * width)
|
real_x = int(x_pct * width)
|
||||||
|
|||||||
Reference in New Issue
Block a user