learn.chetana.fr

SSE : streamer la progression et les tokens

12 min readCore

Pourquoi SSE (et pas WebSocket)

Le copilot et le pipeline streament du serveur vers le client : tokens du LLM, progression node par node. Le besoin est unidirectionnel → Server-Sent Events : du HTTP simple, compatible proxys/CDN, reconnexion native — lĂ  oĂč WebSocket paie sa bidirectionnalitĂ© inutile ici. Le front envoie ses actions par des POST classiques.

Le format et le générateur

SSE = une réponse text/event-stream qui ne se termine pas, découpée en blocs event:/data: séparés par une ligne vide :

event: node_completed
data: {"node": "matcher_fast", "matched": 12}

event: text_delta
data: {"text": "Voici les produits"}

CÎté FastAPI : un générateur async branché sur StreamingResponse :

async def event_stream(run) -> AsyncIterator[str]:
    async for ev in run:                      # ← ex. graph.astream(...) (module 6)
        yield f"event: {ev.type}\ndata: {json.dumps(ev.payload)}\n\n"

@router.post("/extract/stream")
async def extract_stream(...):
    return StreamingResponse(event_stream(run), media_type="text/event-stream")

Le contrat d'événements : la vraie difficulté

Le flux SSE est une API publique : le front se cĂąble sur les noms d'Ă©vĂ©nements et la forme des payloads. Dans le projet, les Ă©vĂ©nements suivent le protocole AG-UI (module 8) et les noms des nodes du pipeline (persist_document
) sont un contrat stable — renommer un node casse la barre de progression du front. Traite tes Ă©vĂ©nements de stream comme des routes : nommĂ©s, documentĂ©s, versionnĂ©s.

Deux dĂ©tails d'ops Ă  connaĂźtre : un ping pĂ©riodique garde les connexions vivantes Ă  travers les proxys ; et si le client part, le gĂ©nĂ©rateur reçoit une annulation — le code doit y survivre proprement (fermer la gĂ©nĂ©ration LLM, pas la laisser tourner pour personne).

đŸ§© Quiz1/3

Pourquoi SSE plutĂŽt que WebSocket ici ?

🃏 Flashcards1/4