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
This commit is contained in:
+105
-35
@@ -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:<id>``, ``collection:<id>``,
|
||||
``page:<id>``) 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:<id>``, ``collection:<id>``, ``page:<id>``) 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:<id>
|
||||
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:<id>
|
||||
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:<id>
|
||||
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:<id>
|
||||
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:<id>
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user