import uuid from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel from sqlalchemy.orm import Session as DBSession from app.db.database import get_db from app.db.models import Session, Message from app.core import memory as mem from app.core import llm from app.core.config import DEFAULT_MODEL router = APIRouter(prefix="/chat", tags=["chat"]) class ChatRequest(BaseModel): session_id: str | None = None message: str model: str | None = None class SessionResponse(BaseModel): id: str title: str model: str updated_at: datetime class Config: from_attributes = True class MessageResponse(BaseModel): id: int role: str content: str created_at: datetime class Config: from_attributes = True @router.get("/sessions", response_model=list[SessionResponse]) def list_sessions(db: DBSession = Depends(get_db)): return db.query(Session).order_by(Session.updated_at.desc()).all() @router.get("/sessions/{session_id}/messages", response_model=list[MessageResponse]) def get_messages(session_id: str, db: DBSession = Depends(get_db)): session = db.get(Session, session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") return session.messages @router.delete("/sessions/{session_id}", status_code=204) def delete_session(session_id: str, db: DBSession = Depends(get_db)): session = db.get(Session, session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") db.delete(session) db.commit() @router.post("/stream") async def stream_chat(req: ChatRequest, db: DBSession = Depends(get_db)): # セッション取得 or 新規作成 if req.session_id: session = db.get(Session, req.session_id) if not session: raise HTTPException(status_code=404, detail="Session not found") else: session = Session( id=str(uuid.uuid4()), title=req.message[:40], model=req.model or DEFAULT_MODEL, ) db.add(session) db.commit() # ユーザーメッセージを保存 user_msg = Message(session_id=session.id, role="user", content=req.message) db.add(user_msg) db.commit() context = mem.get_context_messages(db, session.id) async def generate(): import json as _json def sse(payload: dict) -> str: return f"data: {_json.dumps(payload, ensure_ascii=False)}\n\n" # セッションIDをまず返す(新規セッション時にフロントへ通知) yield sse({"type": "session_id", "value": session.id}) full_response = [] try: async for chunk in llm.stream_chat(context, model=req.model or session.model, session_id=session.id): full_response.append(chunk) yield sse({"type": "chunk", "value": chunk}) # アシスタントメッセージを保存 assistant_content = "".join(full_response) db.add(Message(session_id=session.id, role="assistant", content=assistant_content)) session.updated_at = datetime.now(timezone.utc) db.commit() # メモリ要約チェック await mem.maybe_summarize(db, session) yield sse({"type": "done"}) except Exception as e: yield sse({"type": "error", "value": str(e)}) return StreamingResponse( generate(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive", }, )