Chat atelier : evenement SSE sources + affichage des pages utilisees

Le Brain emet un evenement sources (source_id, page, score des passages
retenus) AVANT le premier token ; le Core le relaie tel quel (JSON brut) ;
l UI affiche une ligne discrete sous la reponse (ex: 12, 47, 103),
prefixee du nom de fichier si plusieurs sources. Transparence pour le MJ
et diagnostic immediat quand le RAG repond a cote.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:45:56 +02:00
parent 0e4820a2f8
commit 3d1cf6e495
10 changed files with 102 additions and 10 deletions

View File

@@ -85,9 +85,14 @@ async def chat_notebook_stream(
async def event_stream() -> AsyncIterator[str]:
try:
async for token in use_case.stream(body.source_ids, messages, context=body.context, top_k=top_k):
if token:
yield sse_event("token", {"token": token})
async for ev in use_case.stream(body.source_ids, messages, context=body.context, top_k=top_k):
if ev["type"] == "token":
if ev.get("token"):
yield sse_event("token", {"token": ev["token"]})
else:
# 'sources' (et tout futur évènement typé) : relayé tel quel.
ev_type = ev.pop("type")
yield sse_event(ev_type, ev)
yield sse_event("done", {})
except (LLMProviderError, EmbeddingError) as exc:
yield sse_event("error", {"message": str(exc)})

View File

@@ -68,13 +68,25 @@ class NotebookChatUseCase:
messages: list[ChatMessage],
context: str = "",
top_k: int = 6,
) -> AsyncIterator[str]:
) -> AsyncIterator[dict]:
"""Yield des évènements : {type:'sources', sources:[…]} (une fois, avant la
réponse — transparence sur les passages utilisés), puis {type:'token', token}."""
# Question AUTONOME pour la recherche : sur une relance (« et ses
# faiblesses ? »), l'embedding du dernier message seul ne contient pas
# le sujet → on le résout depuis l'historique (best-effort, 1 appel léger,
# uniquement à partir du 2e tour). La réponse, elle, voit tout l'historique.
search_query = await standalone_question(self._llm, messages)
passages = await self._rag.retrieve(source_ids, search_query, top_k=top_k)
# Évènement 'sources' AVANT le premier token : l'UI peut afficher les
# pages utilisées (« 📖 p. 12, 47 ») dès le début de la réponse.
yield {"type": "sources", "sources": [
{
"source_id": p.get("source_id"),
"page": p.get("page"),
"score": round(float(p.get("score") or 0.0), 3),
}
for p in passages
]}
sources_block = (
"\n\n".join(self._format_passage(p) for p in passages)
if passages else "(aucun passage pertinent trouvé dans les sources)"
@@ -86,7 +98,7 @@ class NotebookChatUseCase:
system_prompt = _SYSTEM_PROMPT.format(
context_block=context_block, sources_block=sources_block)
async for token in self._llm.stream_chat(messages, system_prompt=system_prompt):
yield token
yield {"type": "token", "token": token}
@staticmethod
def _format_passage(p: dict) -> str: