From 8175b39ebac141149b3477b3aa360eac6afbc4f1 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 17 Mar 2026 20:02:45 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20multi-machine=20+=20chat=20L=C3=A9a=20E?= =?UTF-8?q?dge=20mode=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-machine : - machine_id auto (hostname_os), configurable via RPA_MACHINE_ID - Sessions/workflows isolés par machine (dossiers séparés) - Replay ciblé par machine (pas de fuite cross-machine) - Endpoint GET /machines pour lister les machines connectées - Léa affiche la machine source des workflows Chat Léa systray : - Edge en mode app (--app=URL) — fenêtre native sans barre d'adresse - Toggle via menu systray "Discuter avec Léa" - Fallback navigateur si Edge absent Co-Authored-By: Claude Opus 4.6 (1M context) --- agent_chat/app.py | 82 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/agent_chat/app.py b/agent_chat/app.py index 13e2e0a46..d0160ac00 100644 --- a/agent_chat/app.py +++ b/agent_chat/app.py @@ -110,6 +110,25 @@ execution_status = { } command_history: List[Dict[str, Any]] = [] + +def _fetch_connected_machines() -> List[Dict[str, Any]]: + """Récupérer la liste des machines connectées depuis le streaming server. + + Appel non-bloquant au endpoint /api/v1/traces/stream/machines. + Retourne une liste vide si le serveur est injoignable. + """ + try: + resp = http_requests.get( + f"{STREAMING_SERVER_URL}/api/v1/traces/stream/machines", + timeout=3, + ) + if resp.ok: + return resp.json().get("machines", []) + except Exception: + pass + return [] + + # Répertoire d'upload et chemin de la base de données PROJECT_ROOT = Path(__file__).parent.parent UPLOAD_DIR = PROJECT_ROOT / "data" / "uploads" @@ -350,23 +369,36 @@ def api_status(): @app.route('/api/workflows') def api_workflows(): - """Liste des workflows (tous répertoires confondus).""" + """Liste des workflows (tous répertoires confondus). + + Enrichit les workflows avec le machine_id source quand disponible + (attribut _machine_id ajouté par le StreamProcessor). + """ if not matcher: return jsonify({"workflows": [], "directories": []}) workflows = [] for wf in matcher.get_all_workflows(): - workflows.append({ + wf_data = { "id": wf.workflow_id, "name": wf.name, "description": wf.description, "tags": wf.tags, "source": wf.source_dir, - }) + } + # Ajouter le machine_id source si disponible (workflows appris en streaming) + machine_id = getattr(wf, '_machine_id', None) + if machine_id: + wf_data["machine_id"] = machine_id + workflows.append(wf_data) + + # Récupérer la liste des machines connectées depuis le streaming server + machines = _fetch_connected_machines() return jsonify({ "workflows": workflows, "directories": matcher.get_directories(), + "machines": machines, }) @@ -401,6 +433,17 @@ def api_workflows_refresh(): return jsonify({"success": False, "error": str(e)}) +@app.route('/api/machines') +def api_machines(): + """Liste des machines connectées au streaming server. + + Proxy vers le streaming server pour que le frontend puisse + lister les machines et cibler un replay spécifique. + """ + machines = _fetch_connected_machines() + return jsonify({"machines": machines}) + + @app.route('/api/search', methods=['POST']) def api_search(): """Rechercher des workflows.""" @@ -1377,12 +1420,21 @@ def handle_copilot_abort(): # Exécution de workflow # ============================================================================= -def _try_streaming_server_replay(workflow_id: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _try_streaming_server_replay( + workflow_id: str, + params: Dict[str, Any], + machine_id: Optional[str] = None, +) -> Optional[Dict[str, Any]]: """ Tenter d'exécuter un workflow via le streaming server (Agent V1). POST http://localhost:5005/api/v1/traces/stream/replay - avec workflow_id et params. + avec workflow_id, params et optionnellement machine_id (multi-machine). + + Args: + workflow_id: Identifiant du workflow à exécuter + params: Paramètres du workflow (variables) + machine_id: Machine cible pour le replay (None = auto-détection) Returns: dict avec le résultat si succès. @@ -1390,18 +1442,26 @@ def _try_streaming_server_replay(workflow_id: str, params: Dict[str, Any]) -> Op None si le serveur est injoignable (connexion refusée, timeout). """ try: + payload = { + "workflow_id": workflow_id, + "session_id": "", # Vide = auto-détection de la session Agent V1 active + "params": params or {}, + } + # Ajouter le machine_id si spécifié (ciblage multi-machine) + if machine_id: + payload["machine_id"] = machine_id + resp = http_requests.post( f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay", - json={ - "workflow_id": workflow_id, - "session_id": "", # Vide = auto-détection de la session Agent V1 active - "params": params or {}, - }, + json=payload, timeout=15, ) if resp.status_code == 200: data = resp.json() - logger.info(f"Workflow {workflow_id} envoyé au streaming server: {data}") + logger.info( + f"Workflow {workflow_id} envoyé au streaming server: {data}" + + (f" (machine={machine_id})" if machine_id else "") + ) return data else: # Le serveur est UP mais refuse — renvoyer l'erreur pour éviter le fallback local