feat(agent): v4.15.0 - panneau Agent style Notion AI (mentions @ inline + skills / + actions sous chaque réponse)
FlowDeck CI / test (push) Failing after 15s
FlowDeck CI / docker (push) Skipped

- Composeur contenteditable : mentions inline (pastilles icône+nom), barre de contexte groupée documents/skills, sélecteur @ (icône+titre+chemin), skills intégrés + agent_skills + commandes admin via '/'
- Actions sous chaque réponse agent : copier, insérer dans la page, feedback 👍/👎
- Backend : table agent_feedback, GET /api/agent/mentions, POST /api/agent/feedback, contexte document:<id>/page:<id>/collection:<id> résolu en contenu réel
- Inclut le travail v4.14.0 (documents/espace de travail + outils + auto-titre de conversation)
This commit is contained in:
2026-09-06 12:17:40 -04:00
parent 3025a7a44e
commit 27c9eb98db
12 changed files with 1633 additions and 86 deletions
+92
View File
@@ -442,6 +442,98 @@ async def apply_skill(request: Request, skill_id: int):
return {"conversation_id": cur.lastrowid, "skill": skill["name"], "status": "ready"}
# ── Mentions (commande @ / +) & feedback (boutons 👍 / 👎) ──
@router.get("/mentions")
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.
"""
query = (q or "").strip()[:60]
like = f"%{query}%"
limit = 8
items: list[dict] = []
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']}",
})
# 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']}",
})
# 3) Pages de collection — token page:<id>
sql_pages = (
"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 "📑",
"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']}",
})
return {"mentions": items}
@router.post("/feedback")
async def add_feedback(request: Request):
"""Enregistre le retour (👍 / 👎) porté sur une réponse de l'agent."""
user_id = await _current_user_id(request)
body = await request.json() if request.headers.get("content-type") else {}
rating = (body.get("rating") or "").strip().lower()
if rating not in ("up", "down"):
raise HTTPException(status_code=400, detail="rating doit être 'up' ou 'down'")
conversation_id = body.get("conversation_id")
message_id = body.get("message_id")
snippet = (body.get("content") or "")[:1000].strip()
comment = (body.get("comment") or "").strip()[:2000]
try:
conversation_id = int(conversation_id) if conversation_id not in (None, "") else None
except (TypeError, ValueError):
conversation_id = None
try:
message_id = int(message_id) if message_id not in (None, "") else None
except (TypeError, ValueError):
message_id = None
with get_conn() as conn:
cur = conn.execute(
"""INSERT INTO agent_feedback
(conversation_id, message_id, user_id, rating, snippet, comment)
VALUES (?,?,?,?,?,?)""",
(conversation_id, message_id, user_id, rating, snippet, comment),
)
conn.commit()
return {"id": cur.lastrowid, "rating": rating, "status": "recorded"}
# ── Triggers & tools ──