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]] = []
|
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
|
# Répertoire d'upload et chemin de la base de données
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
UPLOAD_DIR = PROJECT_ROOT / "data" / "uploads"
|
UPLOAD_DIR = PROJECT_ROOT / "data" / "uploads"
|
||||||
@@ -350,23 +369,36 @@ def api_status():
|
|||||||
|
|
||||||
@app.route('/api/workflows')
|
@app.route('/api/workflows')
|
||||||
def 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:
|
if not matcher:
|
||||||
return jsonify({"workflows": [], "directories": []})
|
return jsonify({"workflows": [], "directories": []})
|
||||||
|
|
||||||
workflows = []
|
workflows = []
|
||||||
for wf in matcher.get_all_workflows():
|
for wf in matcher.get_all_workflows():
|
||||||
workflows.append({
|
wf_data = {
|
||||||
"id": wf.workflow_id,
|
"id": wf.workflow_id,
|
||||||
"name": wf.name,
|
"name": wf.name,
|
||||||
"description": wf.description,
|
"description": wf.description,
|
||||||
"tags": wf.tags,
|
"tags": wf.tags,
|
||||||
"source": wf.source_dir,
|
"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({
|
return jsonify({
|
||||||
"workflows": workflows,
|
"workflows": workflows,
|
||||||
"directories": matcher.get_directories(),
|
"directories": matcher.get_directories(),
|
||||||
|
"machines": machines,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -401,6 +433,17 @@ def api_workflows_refresh():
|
|||||||
return jsonify({"success": False, "error": str(e)})
|
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'])
|
@app.route('/api/search', methods=['POST'])
|
||||||
def api_search():
|
def api_search():
|
||||||
"""Rechercher des workflows."""
|
"""Rechercher des workflows."""
|
||||||
@@ -1377,12 +1420,21 @@ def handle_copilot_abort():
|
|||||||
# Exécution de workflow
|
# 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).
|
Tenter d'exécuter un workflow via le streaming server (Agent V1).
|
||||||
|
|
||||||
POST http://localhost:5005/api/v1/traces/stream/replay
|
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:
|
Returns:
|
||||||
dict avec le résultat si succès.
|
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).
|
None si le serveur est injoignable (connexion refusée, timeout).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
resp = http_requests.post(
|
payload = {
|
||||||
f"{STREAMING_SERVER_URL}/api/v1/traces/stream/replay",
|
|
||||||
json={
|
|
||||||
"workflow_id": workflow_id,
|
"workflow_id": workflow_id,
|
||||||
"session_id": "", # Vide = auto-détection de la session Agent V1 active
|
"session_id": "", # Vide = auto-détection de la session Agent V1 active
|
||||||
"params": params or {},
|
"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=payload,
|
||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
data = resp.json()
|
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
|
return data
|
||||||
else:
|
else:
|
||||||
# Le serveur est UP mais refuse — renvoyer l'erreur pour éviter le fallback local
|
# Le serveur est UP mais refuse — renvoyer l'erreur pour éviter le fallback local
|
||||||
|
|||||||
Reference in New Issue
Block a user