- Add CPU/GPU memory monitoring endpoint and frontend widget - Add Amazon Bedrock model support (API key / inference profiles) plus model selector entries - Add Discord weather/laundry notification job (JMA forecast + LLM-generated message) - Add Discord bot that replies to mentions via the configured LLM, with weather-aware context - Add scheduled "useless tips" job with separate webhook and quiet-hours skip - Switch FastAPI startup to lifespan handler, log viewer, markdown rendering, and toast UI additions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
3.7 KiB
Python
126 lines
3.7 KiB
Python
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",
|
|
},
|
|
)
|