feat(executor): P1 DialogResolver serveur en fallback du catalog local
Léa avait déjà une infra pour les dialogs runtime (`_match_known_runtime_dialog` + `_handle_known_runtime_dialog`) mais avec un catalog local limité à 2 entrées. Le DialogResolver R2 côté serveur a 10 entrées centralisées. P1.MVP : `_try_dialog_resolver_server()` consulte l'endpoint `/api/v1/dialog/resolve` quand le catalog local n'a pas matché. La réponse `DialogResolution` est convertie en dialog_spec compatible avec `_handle_known_runtime_dialog` qui réutilise la cascade existante (serveur VLM grounding + template matching local). - Flag `RPA_DIALOG_RESOLVER_AGENT_ENABLED` (OFF par défaut) — rollback runtime - Auth Bearer via `_auth_headers()` existant - Timeout 3s, fail-safe sur exception/503/no-match → fallback humain intact - Zéro régression sur les chemins existants (le catalog local reste 1ère ligne) Tests unitaires en local (6/6 OK) : - flag OFF → None - serveur 503 → None - matched=False → None - policy=pause (UAC) → None - match auto + click_button → dialog_spec valide - exception réseau → None Tag rollback : rollback/pre-P1-2026-05-24_2105
This commit is contained in:
@@ -675,6 +675,68 @@ class ActionExecutorV1:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _try_dialog_resolver_server(
|
||||||
|
self,
|
||||||
|
current_title: str,
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""P1 : fallback DialogResolver serveur si catalog local pas de match.
|
||||||
|
|
||||||
|
Léa interroge l'endpoint ``/api/v1/dialog/resolve`` qui consulte
|
||||||
|
le catalog centralisé (10 entrées vs 2 locales). Si match auto +
|
||||||
|
action click_button, convertit le format ``DialogResolution`` en
|
||||||
|
``dialog_spec`` compatible avec ``_handle_known_runtime_dialog``
|
||||||
|
(réutilise la cascade serveur VLM + template existante).
|
||||||
|
|
||||||
|
Désactivé si ``RPA_DIALOG_RESOLVER_AGENT_ENABLED`` n'est pas
|
||||||
|
positionné (flag agent séparé du flag serveur). Retour ``None``
|
||||||
|
sur 503 (flag serveur OFF), pas de match, policy != auto, ou
|
||||||
|
action incompatible.
|
||||||
|
"""
|
||||||
|
if os.environ.get("RPA_DIALOG_RESOLVER_AGENT_ENABLED", "").lower() not in (
|
||||||
|
"1", "true", "yes", "on"
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from ..config import SERVER_URL
|
||||||
|
import requests as _requests
|
||||||
|
if not SERVER_URL:
|
||||||
|
return None
|
||||||
|
resp = _requests.post(
|
||||||
|
f"{SERVER_URL.rstrip('/')}/api/v1/dialog/resolve",
|
||||||
|
json={"current_title": current_title, "evidence_texts": []},
|
||||||
|
headers=self._auth_headers(),
|
||||||
|
timeout=3,
|
||||||
|
)
|
||||||
|
if not resp.ok:
|
||||||
|
return None
|
||||||
|
data = resp.json()
|
||||||
|
if not data.get("matched") or data.get("policy") != "auto":
|
||||||
|
return None
|
||||||
|
action = data.get("action") or {}
|
||||||
|
if action.get("type") != "click_button":
|
||||||
|
return None
|
||||||
|
button_labels = action.get("fallback_button_labels") or []
|
||||||
|
if action.get("button_label"):
|
||||||
|
button_labels = [action["button_label"]] + [
|
||||||
|
b for b in button_labels if b != action["button_label"]
|
||||||
|
]
|
||||||
|
button_labels = tuple(b for b in button_labels if b)
|
||||||
|
if not button_labels:
|
||||||
|
return None
|
||||||
|
logger.info(
|
||||||
|
f"[P1-DialogResolver] Match serveur: dialog={data.get('dialog_id')} "
|
||||||
|
f"button_texts={button_labels}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": f"server:{data.get('dialog_id')}",
|
||||||
|
"title_patterns": (),
|
||||||
|
"button_texts": button_labels,
|
||||||
|
"skip_current_action_after_handle": True,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"[P1-DialogResolver] échec: {exc}")
|
||||||
|
return None
|
||||||
|
|
||||||
def _maybe_handle_runtime_dialog_before_pause(
|
def _maybe_handle_runtime_dialog_before_pause(
|
||||||
self,
|
self,
|
||||||
action: Dict[str, Any],
|
action: Dict[str, Any],
|
||||||
@@ -691,6 +753,9 @@ class ActionExecutorV1:
|
|||||||
on applique le reflexe associe.
|
on applique le reflexe associe.
|
||||||
"""
|
"""
|
||||||
dialog_spec = self._match_known_runtime_dialog(current_title)
|
dialog_spec = self._match_known_runtime_dialog(current_title)
|
||||||
|
if not dialog_spec:
|
||||||
|
# P1 : fallback DialogResolver serveur si catalog local pas de match
|
||||||
|
dialog_spec = self._try_dialog_resolver_server(current_title)
|
||||||
if not dialog_spec:
|
if not dialog_spec:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user