From 10739c33faac11a442ea2001b6c24ab69a9bb114 Mon Sep 17 00:00:00 2001 From: Dom Date: Thu, 16 Apr 2026 08:42:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(vwb):=20nom=20par=20d=C3=A9faut=20explicit?= =?UTF-8?q?e=20pour=20workflows=20import=C3=A9s=20de=20L=C3=A9a=20(B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avant : tous les workflows importés s'appelaient « Unnamed Workflow » → la liste devenait illisible dès qu'il y en avait plusieurs. Après : génération d'un nom explicite par _derive_default_name : 1. Premier `template.window.title_pattern` utile dans les nodes (filtrage de "Unknown" / "unknown_window"), avec extraction de l'app derrière le séparateur Windows « – » / « - » (ex: « Sans titre – Bloc-notes » → « Bloc-notes »). 2. Premier `template.window.process_name` non-null (ex: « explorer.exe »). 3. Fallback : 8 premiers caractères du workflow_id, après nettoyage des préfixes techniques ("workflow_sess_", ...). Le nom final inclut toujours la date de l'import : « Léa Bloc-notes — 2026-04-16 08:41 » « Léa explorer.exe — 2026-04-16 08:41 » « Léa 20260404 — 2026-04-16 08:41 » (fallback) Ne se déclenche que si le nom entrant est vide, « Unnamed Workflow » ou « Workflow importé » (insensible à la casse). Le paramètre `name` explicite de la requête reste prioritaire. L'utilisateur peut renommer via le bouton éditer. Pas de modification du schema workflow (champ `name` existant). Tests manuels sur données réelles : - notepad_enriched.json (tous nodes "Unknown") → fallback id OK - Bloc-notes, Explorateur et Recherche (2) → « Léa Rechercher » - workflow construit avec title 'Sans titre – Bloc-notes' → « Léa Bloc-notes » OK Co-Authored-By: Claude Opus 4.6 (1M context) --- .../backend/api_v3/learned_workflows.py | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/visual_workflow_builder/backend/api_v3/learned_workflows.py b/visual_workflow_builder/backend/api_v3/learned_workflows.py index 85c96ac7c..8e6dd9ef4 100644 --- a/visual_workflow_builder/backend/api_v3/learned_workflows.py +++ b/visual_workflow_builder/backend/api_v3/learned_workflows.py @@ -40,6 +40,71 @@ if _ROOT not in sys.path: STREAMING_SERVER_URL = "http://localhost:5005" +# --------------------------------------------------------------------------- +# Helpers — nom par défaut à l'import +# --------------------------------------------------------------------------- + +def _derive_default_name(core_dict: Dict[str, Any]) -> str: + """ + Génère un nom par défaut explicite pour un workflow appris importé, + quand son champ `name` est vide ou vaut « Unnamed Workflow ». + + Stratégie, par ordre de priorité : + 1. Premier `template.window.title_pattern` exploitable dans les nodes + (après filtrage de "Unknown"/"unknown_window") ; on extrait le nom + de l'app derrière un séparateur « – » / « - » typique de Windows + (« Sans titre – Bloc-notes » → « Bloc-notes »). + 2. Premier `template.window.process_name` non-null. + 3. Fallback : 8 premiers caractères de `workflow_id`. + + La date de l'import (YYYY-MM-DD HH:MM) est toujours ajoutée en suffixe. + L'utilisateur peut renommer ensuite dans le VWB. + """ + from datetime import datetime as _dt + + def _extract_app(title: str) -> Optional[str]: + if not title: + return None + t = title.strip() + if not t or t.lower() in {"unknown", "unknown_window"}: + return None + # Séparateurs Windows classiques : « – » (em dash), « — », « - » + for sep in (" – ", " — ", " - "): + if sep in t: + # Le nom de l'app est généralement la partie droite + right = t.rsplit(sep, 1)[-1].strip() + if right: + return right + # Pas de séparateur → renvoyer le titre brut (ex : "Rechercher") + return t + + app_name: Optional[str] = None + for node in (core_dict.get("nodes") or []): + window = ((node.get("template") or {}).get("window") or {}) + app_name = _extract_app(window.get("title_pattern") or "") + if app_name: + break + proc = window.get("process_name") + if proc: + app_name = str(proc).strip() + break + + timestamp = _dt.now().strftime("%Y-%m-%d %H:%M") + + if app_name: + return f"Léa {app_name} — {timestamp}" + + wf_id = core_dict.get("workflow_id") or "" + # Nettoyer les préfixes techniques courants (workflow_, sess_) pour garder + # un identifiant lisible de 8 caractères. + for prefix in ("workflow_sess_", "workflow_", "sess_", "session_"): + if wf_id.startswith(prefix): + wf_id = wf_id[len(prefix):] + break + suffix = wf_id[:8] if wf_id else "?" + return f"Léa {suffix} — {timestamp}" + + # --------------------------------------------------------------------------- # GET /api/v3/learned-workflows # --------------------------------------------------------------------------- @@ -209,7 +274,14 @@ def import_learned_workflow(workflow_id: str): wf_meta, steps_list, warnings = convert_learned_to_vwb_steps(core_dict) - # Surcharger le nom si fourni + # B2 — nom par défaut explicite pour les workflows arrivant en + # "Unnamed Workflow" depuis Léa. N'affecte pas les workflows déjà + # nommés manuellement. L'humain peut renommer ensuite dans le VWB. + current_name = (wf_meta.get("name") or "").strip() + if current_name.lower() in {"", "unnamed workflow", "workflow importé"}: + wf_meta["name"] = _derive_default_name(core_dict) + + # Surcharger le nom si fourni explicitement dans la requête if data.get("name"): wf_meta["name"] = data["name"]