mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 07:28:13 +02:00
Phase 3 : chat streaming SSE et persistance des sessions
Backend : - db.py : SQLite (sessions + messages), schéma et CRUD - routes/sessions.py : créer/lister/ouvrir/renommer/supprimer une session - routes/chat.py : POST /api/chat, relais token par token depuis Ollama (SSE), persistance des messages, titrage auto de la session au 1er message - main.py : lifespan -> init_db, montage des nouvelles routes Frontend : - api/client.ts : APIs sessions + streamChat (parsing SSE event/data) - store : sessions, messages, état de streaming, envoi optimiste - ChatPanel : fil de conversation réel, bulles user/agent, curseur de frappe - LeftPanel : historique réel (ouvrir/supprimer, horodatage relatif) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
1 parent
3ad37ae2c7
commit
48cbcaafaf
9 files changed
+688
-50
No files matched your search
@@ -0,0 +1,84 @@
|
||||
"""Route de conversation : streaming token par token depuis Ollama (SSE).
|
||||
|
||||
Flux :
|
||||
1. on enregistre le message utilisateur ;
|
||||
2. on rejoue l'historique de la session vers Ollama en streaming ;
|
||||
3. on relaie chaque token au client (SSE) ;
|
||||
4. on enregistre la réponse complète de l'assistant.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .. import db
|
||||
from ..config import settings
|
||||
from ..ollama_client import ollama
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["chat"])
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"Tu es Loki, un assistant de développement local. Tu écris du code clair, "
|
||||
"commenté en français, et tu réponds de façon concise et utile."
|
||||
)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
session_id: str
|
||||
content: str
|
||||
model: str | None = None
|
||||
options: dict | None = None
|
||||
|
||||
|
||||
def _sse(event: str, data: dict) -> str:
|
||||
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat(req: ChatRequest) -> StreamingResponse:
|
||||
session = db.get_session(req.session_id)
|
||||
if not session:
|
||||
raise HTTPException(404, "session introuvable")
|
||||
|
||||
model = req.model or session.get("model") or settings.default_model
|
||||
|
||||
# Premier message : on titre la session avec un extrait.
|
||||
history = db.list_messages(req.session_id)
|
||||
if not history:
|
||||
title = req.content.strip().split("\n")[0][:60] or "Nouvelle session"
|
||||
db.rename_session(req.session_id, title)
|
||||
|
||||
db.add_message(req.session_id, "user", req.content, None)
|
||||
|
||||
# Contexte envoyé à Ollama : invite système + historique complet.
|
||||
convo = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
convo += [
|
||||
{"role": m["role"], "content": m["content"]}
|
||||
for m in db.list_messages(req.session_id)
|
||||
]
|
||||
|
||||
async def event_stream():
|
||||
yield _sse("start", {"model": model})
|
||||
full = ""
|
||||
try:
|
||||
async for chunk in ollama.chat(
|
||||
model, convo, options=req.options, stream=True
|
||||
):
|
||||
token = chunk.get("message", {}).get("content", "")
|
||||
if token:
|
||||
full += token
|
||||
yield _sse("token", {"content": token})
|
||||
if chunk.get("done"):
|
||||
break
|
||||
except (httpx.HTTPError, OSError) as exc:
|
||||
yield _sse("error", {"message": str(exc)})
|
||||
|
||||
if full:
|
||||
db.add_message(req.session_id, "assistant", full, model)
|
||||
yield _sse("done", {"content": full, "model": model})
|
||||
|
||||
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Routes de gestion des sessions de conversation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .. import db
|
||||
|
||||
router = APIRouter(prefix="/api/sessions", tags=["sessions"])
|
||||
|
||||
|
||||
class CreateSession(BaseModel):
|
||||
title: str = "Nouvelle session"
|
||||
model: str | None = None
|
||||
|
||||
|
||||
class RenameSession(BaseModel):
|
||||
title: str
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_sessions() -> dict:
|
||||
return {"sessions": db.list_sessions()}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def post_session(req: CreateSession) -> dict:
|
||||
return db.create_session(req.title, req.model)
|
||||
|
||||
|
||||
@router.get("/{sid}")
|
||||
async def get_one(sid: str) -> dict:
|
||||
session = db.get_session(sid)
|
||||
if not session:
|
||||
raise HTTPException(404, "session introuvable")
|
||||
return {"session": session, "messages": db.list_messages(sid)}
|
||||
|
||||
|
||||
@router.patch("/{sid}")
|
||||
async def patch_session(sid: str, req: RenameSession) -> dict:
|
||||
if not db.get_session(sid):
|
||||
raise HTTPException(404, "session introuvable")
|
||||
db.rename_session(sid, req.title)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/{sid}")
|
||||
async def remove_session(sid: str) -> dict:
|
||||
db.delete_session(sid)
|
||||
return {"ok": True}
|
||||
Reference in new issue
Block a user