feat(dialog): R2 MVP P0 — DialogResolver + catalogue 10 entrées (flag OFF default)

- agent_v0/server_v1/core/dialog/ : catalogue compact + DialogResolver
  stateless (match titre + evidence, trichotomie stricte auto/pause/skip).
- 10 entrées P0 : confirm-save-overwrite, notepad-unsaved-changes,
  windows-file-explorer (fallback replay 4c38dbb8), easily-save/overwrite/
  confirm-action/clinical-warning, windows-uac, windows-hello-credui,
  edge-update.
- Validateur déclaratif `system_modals_cannot_be_overridden` : rejette
  toute surcharge auto/skip sur modaux SYSTÈME (windows-/defender-).
- Endpoint POST /api/v1/dialog/resolve derrière flag
  RPA_DIALOG_RESOLVER_ENABLED (OFF par défaut → 503). Aucun
  rebranchement côté agent_v1 (executor.py inchangé, P1 plus tard).
- 25 tests pytest passants (19 unit + 6 intégration HTTP).

Spec : docs/recherche/SPEC_POPUPS_CATALOGUE.md §2bis / §3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-24 17:52:38 +02:00
parent bd100bc538
commit 84d2d4a667
7 changed files with 995 additions and 0 deletions

View File

@@ -5720,6 +5720,66 @@ async def agents_fleet():
}
# =========================================================================
# R2 MVP P0 — DialogResolver (catalogue centralisé des modaux runtime)
# Flag OFF par défaut. Activer en posant RPA_DIALOG_RESOLVER_ENABLED=true.
# Le rebranchement côté agent_v1 (consommation de cet endpoint au runtime)
# vient en P1 — pour l'instant l'endpoint est exposé "prêt à consommer".
# =========================================================================
from .core.dialog import DialogResolver as _DialogResolver # noqa: E402
def _dialog_resolver_enabled() -> bool:
"""Flag d'activation R2 — lu à chaque appel pour faciliter les tests."""
return os.environ.get("RPA_DIALOG_RESOLVER_ENABLED", "").lower() in (
"1", "true", "yes", "on",
)
# Instance partagée — le résolveur est stateless (cf. resolver.py).
_dialog_resolver_singleton = _DialogResolver()
class DialogResolveRequest(BaseModel):
"""Payload de l'endpoint ``/api/v1/dialog/resolve`` (P0)."""
current_title: str
evidence_texts: List[str] = []
machine_id: Optional[str] = None
@app.post("/api/v1/dialog/resolve")
async def dialog_resolve(payload: DialogResolveRequest):
"""Renvoyer la résolution d'un modal pour un titre + evidences donnés.
Réponse :
{
"matched": bool,
"dialog_id": str,
"policy": "auto" | "pause" | "skip",
"action": {...} | None
}
Si le flag ``RPA_DIALOG_RESOLVER_ENABLED`` n'est pas positionné,
l'endpoint retourne 503 (désactivé par défaut — aucun risque de
régression sur le pipeline existant).
"""
if not _dialog_resolver_enabled():
raise HTTPException(
status_code=503,
detail=(
"DialogResolver désactivé (flag RPA_DIALOG_RESOLVER_ENABLED). "
"P0 : endpoint exposé mais OFF par défaut."
),
)
resolution = _dialog_resolver_singleton.resolve(
current_title=payload.current_title,
evidence_texts=payload.evidence_texts,
)
return resolution.to_dict()
if __name__ == "__main__":
import uvicorn