feat(competences): plan supervised replay tests
This commit is contained in:
165
core/competences/replay.py
Normal file
165
core/competences/replay.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""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,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user