feat(chat): implement native hub chat with sqlite conversations store, SSE streaming and DSH 5th universe integration

This commit is contained in:
bolbol
2026-08-20 23:08:06 +01:00
parent 9580aead5d
commit c040c173df
11 changed files with 2090 additions and 572 deletions
+76
View File
@@ -0,0 +1,76 @@
from fastapi import APIRouter, HTTPException, Query, Body, Request
from fastapi.responses import StreamingResponse
from typing import Optional, Dict, Any, List
from pydantic import BaseModel
from app.chat_service import (
list_universe_conversations,
create_universe_conversation,
get_conversation_messages,
stream_chat_messages,
rename_universe_conversation,
pin_universe_conversation,
delete_universe_conversation
)
router = APIRouter(prefix="/api/chat", tags=["Chat"])
class CreateConversationRequest(BaseModel):
title: Optional[str] = "Nouvelle conversation"
class UpdateConversationRequest(BaseModel):
title: Optional[str] = None
pinned: Optional[bool] = None
class SendMessageRequest(BaseModel):
message: str
@router.get("/{universe_id}/conversations")
async def get_conversations(universe_id: str):
"""Returns the list of conversations for a universe."""
convs = await list_universe_conversations(universe_id)
return {"ok": True, "universe_id": universe_id, "conversations": convs}
@router.post("/{universe_id}/conversations")
async def create_conversation(universe_id: str, body: CreateConversationRequest = Body(default_factory=CreateConversationRequest)):
"""Creates a new conversation in a universe."""
conv = await create_universe_conversation(universe_id, title=body.title)
return {"ok": True, "conversation": conv}
@router.get("/{universe_id}/conversations/{session_key}/messages")
async def get_messages(universe_id: str, session_key: str):
"""Retrieves full message history for a conversation."""
messages = await get_conversation_messages(universe_id, session_key)
return {"ok": True, "session_key": session_key, "messages": messages}
@router.post("/{universe_id}/conversations/{session_key}/send")
async def send_message(universe_id: str, session_key: str, body: SendMessageRequest):
"""Sends a message and streams SSE chunks back."""
if not body.message or not body.message.strip():
raise HTTPException(status_code=400, detail="Message cannot be empty")
stream = stream_chat_messages(universe_id, session_key, body.message.strip())
return StreamingResponse(
stream,
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
@router.patch("/{universe_id}/conversations/{session_key}")
async def update_conv(universe_id: str, session_key: str, body: UpdateConversationRequest):
"""Updates conversation title or pinned status."""
if body.title is not None:
await rename_universe_conversation(universe_id, session_key, body.title)
if body.pinned is not None:
await pin_universe_conversation(universe_id, session_key, body.pinned)
return {"ok": True}
@router.delete("/{universe_id}/conversations/{session_key}")
async def delete_conv(universe_id: str, session_key: str):
"""Deletes a conversation."""
await delete_universe_conversation(universe_id, session_key)
return {"ok": True}