From 2391de418d57d085f716211cedd3bcabf62bc41f Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 6 Sep 2026 13:57:03 -0400 Subject: [PATCH] feat(agent): v4.15.3 - mentions @ par espace courant + skills composes en arriere-plan - /api/agent/mentions : reponse en sections (fichiers de l'espace courant ouvert, collections, pages de collections, autres documents), workspace_id transmis par le frontend (localWsId) et prioritaire - Navigation clavier up/down : la liste defile pour garder la selection visible (menus @ et /) - Deduplication des propositions (meme type + meme titre affiches une seule fois) - Skills : plus d'injection du texte du skill dans la boite d'edition; requete composee en arriere-plan (contexte + skills + texte) a l'envoi; les skills integres injectent leur template, les enregistres partent via skill_ids - Tests : shape des mentions, dedup, scoping workspace --- CHANGELOG.md | 20 +++++ VERSION | 2 +- app/main.py | 2 +- app/routers/agent.py | 140 ++++++++++++++++++++++++--------- app/templates/agent_panel.html | 125 +++++++++++++++++++++-------- tests/test_agent.py | 33 +++++++- 6 files changed, 248 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7008f0c..5af1bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog — FlowDeck +## v4.15.3 (2026-09-06) — Agent : mentions @ par espace courant + skills composés en arrière-plan + +> La commande « @ » propose en priorité les **fichiers de l'espace de travail ouvert**, puis les +> autres contextes (collections, pages de collections, autres documents). La navigation clavier +> (↑/↓) positionne maintenant visuellement la sélection dans la fenêtre (défilement), et les +> fichiers identiques ne sont plus affichés plusieurs fois (déduplication par titre). + +### Améliorations +- **Mentions @ contextuelles** : nouvelle API `/api/agent/mentions` par **sections** : + *Fichiers de l'espace courant* (« `workspace_id` transmis par le frontend »), *Collections / bases*, + *Pages de collections*, *Autres documents*. +- **Navigation clavier corrigée** : ↑/↓ font défiler la liste ouverte pour garder la sélection + visible à l'écran (aussi appliqué au menu `/`). Entrée/Tab sélectionne toujours l'élément focalisé. +- **Déduplication** : deux fichiers du même type et même titre (ex. « Sans titre », « Aussi Vide ») + n'apparaissent qu'une seule fois dans les propositions. +- **Skills composés en arrière-plan** : sélectionner un skill n'injecte plus son texte dans la boîte + d'édition. À l'envoi la requête est assemblée : `contexte1 + contexte2 + skill-1 + skill-2 + texte`. + Les skills intégrés (templace) et enregistrés (skill_ids) sont tous deux pris en compte ; les + contextes @ continuent d'être résolus en contenu côté serveur. + ## v4.15.2 (2026-09-06) — Agent : skills multi-épinglés en bulles + chips de contexte cliquables > Les skills s'épinglent désormais au contexte **comme les mentions @** (bulles « / » purples dans diff --git a/VERSION b/VERSION index 3e2accd..7666b00 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.15.2 +4.15.3 \ No newline at end of file diff --git a/app/main.py b/app/main.py index 0083fee..eefc795 100644 --- a/app/main.py +++ b/app/main.py @@ -60,7 +60,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="4.15.2", + version="4.15.3", docs_url="/docs" if settings.log_level == "DEBUG" else None, redoc_url=None, lifespan=lifespan, diff --git a/app/routers/agent.py b/app/routers/agent.py index 033529b..5136411 100644 --- a/app/routers/agent.py +++ b/app/routers/agent.py @@ -450,58 +450,128 @@ async def apply_skill(request: Request, skill_id: int): async def list_mentions(request: Request, q: str = ""): """Éléments mentionnables dans le panneau agent (commande « @ » / bouton « + »). - Retourne un flux unique d'objets FlowDeck (documents éditeur, collections, - pages de collection) que l'utilisateur peut épingler au contexte du chat. - Chaque entrée porte un jeton ``token`` (``document:``, ``collection:``, - ``page:``) que ContextBuilder résout en contenu réel pour le LLM. + Retourne des sections d'objets FlowDeck que l'utilisateur peut épingler au + contexte du chat. La première section privilégie les **fichiers de l'espace de + travail courant ouvert** (`workspace_id`), puis viennent les collections, les + pages de collection et les autres documents. Chaque élément porte un jeton + ``token`` (``document:``, ``collection:``, ``page:``) que + ContextBuilder résout en contenu réel pour le LLM. + + Les entrées identiques (même type + même titre, ex. plusieurs pages « Sans + titre ») sont **dédupliquées** pour ne pas afficher le même fichier plusieurs fois. """ query = (q or "").strip()[:60] like = f"%{query}%" limit = 8 - items: list[dict] = [] + # L'espace courant ouvert est fourni par le frontend (localWsId) et doit primer + # sur l'espace par défaut de l'utilisateur pour cette liste contextuelle. + try: + ws = int(request.query_params.get("workspace_id", 0) or 0) or None + except (TypeError, ValueError): + ws = None + if not ws: + ws = await _workspace_id(request) + + def dedupe(items: list[dict]) -> list[dict]: + seen: set = set() + out: list[dict] = [] + for it in items: + key = (it["type"], (it.get("label") or "").strip().lower()) + if key in seen: + continue + seen.add(key) + out.append(it) + return out + + sections: list[dict] = [] + ws_name = None with get_conn() as conn: - # 1) Documents (pages éditeur) — token document: - sql_docs = ( - "SELECT p.id, p.title, p.workspace_id, w.name AS ws_name " - "FROM pages p LEFT JOIN workspaces w ON w.id=p.workspace_id " - "WHERE p.deleted_at IS NULL AND (? = '' OR p.title LIKE ?) " - "ORDER BY p.updated_at DESC LIMIT ?" - ) - for r in conn.execute(sql_docs, (query, like, limit)).fetchall(): - items.append({ - "type": "document", "icon": "📄", - "id": r["id"], "label": r["title"] or "Sans titre", - "sub": r["ws_name"] or "", "token": f"document:{r['id']}", - }) + if ws: + row = conn.execute("SELECT name FROM workspaces WHERE id=?", (ws,)).fetchone() + ws_name = row["name"] if row else None + + # 1) Fichiers de l'espace de travail courant ouvert — token document: + if ws: + sql_ws = ( + "SELECT p.id, p.title FROM pages p " + "WHERE p.workspace_id=? AND p.deleted_at IS NULL AND (? = '' OR p.title LIKE ?) " + "ORDER BY p.updated_at DESC LIMIT ?" + ) + files = [ + {"type": "document", "icon": "📄", "id": r["id"], "label": r["title"] or "Sans titre", + "sub": ws_name or "Espace courant", "token": f"document:{r['id']}"} + for r in conn.execute(sql_ws, (ws, query, like, limit)).fetchall() + ] + sections.append({"key": "files", "label": (ws_name or "Fichiers de l'espace"), + "items": dedupe(files)}) # 2) Collections (bases de données) — token collection: - sql_colls = ( - "SELECT id, name, icon FROM collections " - "WHERE (? = '' OR name LIKE ?) ORDER BY updated_at DESC LIMIT ?" - ) - for r in conn.execute(sql_colls, (query, like, limit)).fetchall(): - items.append({ - "type": "collection", "icon": r["icon"] or "🗄️", - "id": r["id"], "label": r["name"], - "sub": "Base de données", "token": f"collection:{r['id']}", - }) + colls = [] + for r in conn.execute( + "SELECT id, name, icon FROM collections WHERE (? = '' OR name LIKE ?) " + "ORDER BY updated_at DESC LIMIT ?", (query, like, limit) + ).fetchall(): + colls.append({"type": "collection", "icon": r["icon"] or "🗄️", "id": r["id"], + "label": r["name"], "sub": "Base de données", + "token": f"collection:{r['id']}"}) + if colls: + sections.append({"key": "collections", "label": "Collections / bases", + "items": dedupe(colls)}) # 3) Pages de collection — token page: - sql_pages = ( + pages = [] + for r in conn.execute( "SELECT cp.id, cp.title, cp.icon, c.name AS coll_name " "FROM collection_pages cp LEFT JOIN collections c ON c.id=cp.collection_id " - "WHERE (? = '' OR cp.title LIKE ?) ORDER BY cp.updated_at DESC LIMIT ?" - ) - for r in conn.execute(sql_pages, (query, like, limit)).fetchall(): - items.append({ - "type": "page", "icon": r["icon"] if str(r["icon"]).startswith(("📄", "📑", "🗂️", "📁")) else "📑", + "WHERE (? = '' OR cp.title LIKE ?) ORDER BY cp.updated_at DESC LIMIT ?", + (query, like, limit) + ).fetchall(): + pages.append({ + "type": "page", + "icon": r["icon"] if str(r["icon"]).startswith(("📄", "📑", "🗂️", "📁")) else "📑", "id": r["id"], "label": r["title"] or "Sans titre", "sub": f"Page · {r['coll_name']}" if r["coll_name"] else "Page", "token": f"page:{r['id']}", }) + if pages: + sections.append({"key": "pages", "label": "Pages de collections", + "items": dedupe(pages)}) - return {"mentions": items} + # 4) Autres documents éditeur hors de l'espace courant — token document: + if ws: + other = [] + for r in conn.execute( + "SELECT p.id, p.title, w.name AS ws_name " + "FROM pages p LEFT JOIN workspaces w ON w.id=p.workspace_id " + "WHERE p.deleted_at IS NULL AND (p.workspace_id IS ? OR p.workspace_id IS NULL) " + "AND (? = '' OR p.title LIKE ?) ORDER BY p.updated_at DESC LIMIT ?", + (ws, query, like, limit) + ).fetchall(): + other.append({"type": "document", "icon": "📄", "id": r["id"], + "label": r["title"] or "Sans titre", "sub": r["ws_name"] or "", + "token": f"document:{r['id']}"}) + dedup_other = dedupe(other) + if dedup_other: + sections.append({"key": "other", "label": "Autres documents", + "items": dedup_other}) + else: + # Pas de workspace courant → liste les documents éditeur (comme avant) + docs = [] + for r in conn.execute( + "SELECT p.id, p.title, w.name AS ws_name FROM pages p " + "LEFT JOIN workspaces w ON w.id=p.workspace_id " + "WHERE p.deleted_at IS NULL AND (? = '' OR p.title LIKE ?) " + "ORDER BY p.updated_at DESC LIMIT ?", (query, like, limit) + ).fetchall(): + docs.append({"type": "document", "icon": "📄", "id": r["id"], + "label": r["title"] or "Sans titre", "sub": r["ws_name"] or "", + "token": f"document:{r['id']}"}) + if docs: + sections.insert(0, {"key": "files", "label": "Documents", + "items": dedupe(docs)}) + + return {"workspace_id": ws, "sections": sections} @router.post("/feedback") diff --git a/app/templates/agent_panel.html b/app/templates/agent_panel.html index e8341b0..ca20673 100644 --- a/app/templates/agent_panel.html +++ b/app/templates/agent_panel.html @@ -136,6 +136,7 @@ .fd-mention-main{flex:1;min-width:0;display:flex;flex-direction:column} .fd-mention-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .fd-mention-sub{font-size:10.5px;color:var(--text-dim,#999);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.fd-mention-sep{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim,#888);padding:8px 14px 2px} .fd-mention-empty,.fd-mention-load{padding:11px 12px;font-size:12px;color:var(--text-dim,#aaa);text-align:center} /* Toast */ @@ -261,7 +262,7 @@ -
+
-
+
Recherche…
-