From 7ef98d8089d55bb4bd6d987129353b4b236265fe Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 29 May 2026 13:40:15 +0200 Subject: [PATCH] feat(lea): expose competence replay api --- agent_v0/server_v1/api_stream.py | 93 ++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/agent_v0/server_v1/api_stream.py b/agent_v0/server_v1/api_stream.py index 43d4f021a..050d3ea66 100644 --- a/agent_v0/server_v1/api_stream.py +++ b/agent_v0/server_v1/api_stream.py @@ -41,6 +41,16 @@ from .execution_plan_runner import ( inject_plan_into_queue, ) +try: + from core.competences.catalog import load_competences + from core.competences.replay import build_competence_replay_payload, find_competence + COMPETENCE_API_AVAILABLE = True +except Exception: # pragma: no cover - allows partial deployments + load_competences = None + build_competence_replay_payload = None + find_competence = None + COMPETENCE_API_AVAILABLE = False + # Pipeline d'anonymisation PII (OCR + NER côté serveur). # Import paresseux : on ne charge pas docTR tant qu'aucune image n'est reçue. try: @@ -688,6 +698,14 @@ class RawReplayRequest(BaseModel): params: Optional[Dict[str, Any]] = None +class CompetenceReplayRequest(BaseModel): + """Requête de test/replay supervisé d'une compétence Léa YAML.""" + supervised: bool = True + start_replay: bool = False + session_id: str = "" + machine_id: Optional[str] = None + + class SingleActionRequest(BaseModel): """Requête d'exécution d'une seule action (mode Copilot).""" action: Dict[str, Any] @@ -2074,6 +2092,81 @@ async def list_workflows(machine_id: Optional[str] = None): return result +def _require_competence_api() -> None: + if not COMPETENCE_API_AVAILABLE: + raise HTTPException(status_code=503, detail="Catalogue compétences Léa indisponible") + + +def _parse_competence_states(state: Optional[str]) -> Optional[List[str]]: + if not state: + return None + states = [part.strip() for part in state.split(",") if part.strip()] + return states or None + + +@app.get("/api/v1/lea/competences") +async def list_lea_competences(state: Optional[str] = None): + """Lister les compétences YAML connues par Léa, sans dépendance au VWB.""" + _require_competence_api() + states = _parse_competence_states(state) + competences = load_competences(states=states) # type: ignore[misc] + return { + "success": True, + "competences": [competence.to_dict() for competence in competences], + "total": len(competences), + "states_filter": states, + } + + +@app.get("/api/v1/lea/competences/{competence_id}") +async def get_lea_competence(competence_id: str): + """Retourner une compétence YAML et son plan de test supervisé.""" + _require_competence_api() + try: + competence = find_competence(competence_id) # type: ignore[misc] + payload = build_competence_replay_payload(competence_id, supervised=True) # type: ignore[misc] + except KeyError: + raise HTTPException(status_code=404, detail=f"Compétence '{competence_id}' introuvable") + return { + "success": True, + "competence": competence.to_dict(), + "supervised_replay_preview": payload, + } + + +@app.post("/api/v1/lea/competences/{competence_id}/replay") +async def replay_lea_competence(competence_id: str, request: CompetenceReplayRequest): + """Planifier ou lancer le replay supervisé d'une compétence Léa YAML.""" + _require_competence_api() + try: + payload = build_competence_replay_payload( # type: ignore[misc] + competence_id, + supervised=request.supervised, + machine_id=request.machine_id, + session_id=request.session_id, + ) + except KeyError: + raise HTTPException(status_code=404, detail=f"Compétence '{competence_id}' introuvable") + + if not request.start_replay: + return { + "success": True, + "status": "planned", + "competence_id": competence_id, + "raw_replay_payload": payload, + } + + replay_request = RawReplayRequest(**payload) + replay_result = await start_raw_replay(replay_request) + return { + "success": True, + "status": "started", + "competence_id": competence_id, + "raw_replay_payload": payload, + "replay": replay_result, + } + + @app.post("/api/v1/traces/stream/reload-workflows") async def reload_workflows(): """Recharger les workflows depuis le disque.