Files
rpa_vision_v3/core/competences/replay.py

169 lines
6.0 KiB
Python

"""Convert persisted competence YAML files into supervised replay actions."""
from __future__ import annotations
from pathlib import Path
from typing import Any, Iterable
from .catalog import DEFAULT_COMPETENCE_ROOT, CompetenceSummary, load_competences
def find_competence(
competence_id: str,
*,
root: Path | str = DEFAULT_COMPETENCE_ROOT,
states: Iterable[str] | None = None,
) -> CompetenceSummary:
"""Find one competence by id across persisted YAML states."""
for competence in load_competences(root=root, states=states):
if competence.id == competence_id:
return competence
raise KeyError(f"Competence '{competence_id}' not found")
def build_competence_replay_actions(
competence_id: str,
*,
root: Path | str = DEFAULT_COMPETENCE_ROOT,
supervised: bool = True,
) -> list[dict[str, Any]]:
"""Build Agent V1 raw replay actions for a competence.
Candidate competences are intentionally wrapped with human pauses. This
makes the first runtime pass an explicit supervised test instead of an
autonomous assertion that the competence is already stable.
"""
competence = find_competence(competence_id, root=root)
actions: list[dict[str, Any]] = []
if supervised:
actions.append(_pause_action(competence, phase="before"))
for index, method in enumerate(competence.methods, start=1):
action = _method_to_replay_action(competence, method, index)
if action:
actions.append(action)
if supervised:
actions.append(_pause_action(competence, phase="after"))
return actions
def build_competence_replay_payload(
competence_id: str,
*,
root: Path | str = DEFAULT_COMPETENCE_ROOT,
supervised: bool = True,
machine_id: str | None = None,
session_id: str | None = None,
) -> dict[str, Any]:
"""Build the payload expected by `/api/v1/traces/stream/replay/raw`."""
competence = find_competence(competence_id, root=root)
actions = build_competence_replay_actions(competence_id, root=root, supervised=supervised)
payload: dict[str, Any] = {
"actions": actions,
"task_description": f"Test compétence Léa: {competence.intent_fr}",
"params": {
"execution_mode": "supervised" if supervised else "autonomous",
"competence_id": competence.id,
"learning_state": competence.learning_state,
},
}
if machine_id:
payload["machine_id"] = machine_id
if session_id:
payload["session_id"] = session_id
return payload
def _method_to_replay_action(
competence: CompetenceSummary,
method: dict[str, Any],
index: int,
) -> dict[str, Any] | None:
kind = method.get("kind")
params = method.get("parameters") if isinstance(method.get("parameters"), dict) else {}
action_id = f"competence_{competence.id}_{index}_{kind or 'step'}"
if kind == "key_combo":
keys = params.get("keys")
if not isinstance(keys, list) or not keys:
return None
return {
"action_id": action_id,
"type": "key_combo",
"keys": [str(key) for key in keys],
"intention": competence.intent_fr,
"competence_id": competence.id,
"source_method_id": method.get("id"),
}
if kind == "wait_state":
expected = params.get("expected_state") if isinstance(params.get("expected_state"), dict) else {}
titles = expected.get("window_title_in") if isinstance(expected.get("window_title_in"), list) else []
timeout_ms = params.get("timeout_ms") if isinstance(params.get("timeout_ms"), int) else 5000
if titles:
return {
"action_id": action_id,
"type": "verify_screen",
"expected_node": f"competence:{competence.id}:wait_state",
"expected_window_title_contains": [str(title) for title in titles],
"timeout_ms": timeout_ms,
"intention": competence.intent_fr,
"competence_id": competence.id,
"source_method_id": method.get("id"),
"expected_state": expected,
}
return {
"action_id": action_id,
"type": "wait",
"duration_ms": min(timeout_ms, 5000),
"intention": competence.intent_fr,
"competence_id": competence.id,
"source_method_id": method.get("id"),
}
return None
def _pause_action(competence: CompetenceSummary, *, phase: str) -> dict[str, Any]:
failure = competence.failure_message_template
gaps = ", ".join(str(gap.get("id")) for gap in competence.t2_known_gaps if gap.get("id"))
if phase == "before":
message = (
f"Prépare le test supervisé de la compétence '{competence.id}'. "
f"Intention: {competence.intent_fr}. "
f"Attendu: {failure.get('attendu', 'état attendu non renseigné')}."
)
if gaps:
message += f" Points à surveiller: {gaps}."
else:
message = (
f"Valide le résultat de la compétence '{competence.id}'. "
f"Intention: {failure.get('intention', competence.intent_fr)}. "
f"Attendu: {failure.get('attendu', 'état attendu non renseigné')}. "
"Indique si Léa peut enregistrer ce test comme succès supervisé ou si une correction est nécessaire."
)
return {
"action_id": f"competence_{competence.id}_pause_{phase}",
"type": "pause_for_human",
"competence_id": competence.id,
"parameters": {
"message": message,
"intention": failure.get("intention", competence.intent_fr),
"attendu": failure.get("attendu", ""),
"demande": failure.get("demande", ""),
"phase": phase,
"verdict_required": phase == "after",
"verdict_endpoint": f"/api/v1/lea/competences/{competence.id}/verdict",
"competence_id": competence.id,
"write_back_enabled": False,
},
}