feat: multi-machine + chat Léa Edge mode app
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user