feat: unification VWB ↔ Léa — import/export bidirectionnel

- Workflows appris par Léa visibles dans le VWB ("Appris par Léa")
- Bouton "Importer" pour éditer un workflow appris
- Bouton "Exporter pour Léa" pour rendre un workflow VWB exécutable
- Conversion bidirectionnelle core ↔ VWB via learned_workflow_bridge
- Liste unifiée dans le chat Léa (merged + dédupliquée)
- reload_workflows() sur le streaming server (pas de redémarrage)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 22:41:34 +01:00
parent aa39af327f
commit 5973058f08
10 changed files with 1407 additions and 6 deletions

View File

@@ -370,15 +370,22 @@ def api_status():
@app.route('/api/workflows')
def api_workflows():
"""Liste des workflows (tous répertoires confondus).
"""Liste unifiée des workflows (appris + VWB).
Enrichit les workflows avec le machine_id source quand disponible
(attribut _machine_id ajouté par le StreamProcessor).
Sources fusionnées :
1. Workflows appris (SemanticMatcher — data/training/workflows/)
2. Workflows VWB (port 5002 — SQLite, édités par l'humain)
Dédupliqués par nom : si un workflow appris a été importé dans le VWB,
seule la version VWB est retournée (c'est la version validée/corrigée).
"""
if not matcher:
return jsonify({"workflows": [], "directories": []})
seen_ids = set()
workflows = []
# Source 1 : workflows appris (core JSON)
for wf in matcher.get_all_workflows():
wf_data = {
"id": wf.workflow_id,
@@ -386,12 +393,31 @@ def api_workflows():
"description": wf.description,
"tags": wf.tags,
"source": wf.source_dir,
"origin": "learned",
}
# 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)
seen_ids.add(wf.workflow_id)
# Source 2 : workflows VWB (édités par l'humain)
vwb_workflows = _fetch_vwb_workflows()
for vwb_wf in vwb_workflows:
vwb_id = vwb_wf.get("id", "")
vwb_wf["origin"] = "vwb"
# Si un workflow VWB a été importé depuis un appris, marquer le doublon
desc = vwb_wf.get("description", "") or ""
# Détecter les doublons par nom similaire ou description contenant l'ID core
is_duplicate = False
for core_id in seen_ids:
if core_id in desc:
is_duplicate = True
break
if vwb_id not in seen_ids and not is_duplicate:
workflows.append(vwb_wf)
seen_ids.add(vwb_id)
# Récupérer la liste des machines connectées depuis le streaming server
machines = _fetch_connected_machines()
@@ -403,6 +429,35 @@ def api_workflows():
})
def _fetch_vwb_workflows():
"""Récupère les workflows depuis le VWB backend (port 5002)."""
try:
resp = http_requests.get(
"http://localhost:5002/api/v3/session/state",
timeout=3,
)
if resp.ok:
data = resp.json()
wf_list = data.get("workflows_list", [])
result = []
for wf in wf_list:
result.append({
"id": wf.get("id", ""),
"name": wf.get("name", ""),
"description": wf.get("description", ""),
"tags": wf.get("tags", []),
"source": "vwb",
"step_count": wf.get("step_count", 0),
"review_status": wf.get("review_status"),
})
return result
except http_requests.ConnectionError:
logger.debug("VWB backend (port 5002) indisponible")
except Exception as e:
logger.warning("Erreur récupération workflows VWB: %s", e)
return []
@app.route('/api/workflows/refresh', methods=['POST'])
def api_workflows_refresh():
"""