Add system memory monitoring, Bedrock support, and Discord integrations
- 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>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import chat, memory, logs, system
|
||||
|
||||
+48
-14
@@ -30,11 +30,29 @@ class SessionResponse(BaseModel):
|
||||
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)
|
||||
@@ -68,24 +86,40 @@ async def stream_chat(req: ChatRequest, db: DBSession = Depends(get_db)):
|
||||
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 f"data: {{\"type\":\"session_id\",\"value\":\"{session.id}\"}}\n\n"
|
||||
yield sse({"type": "session_id", "value": session.id})
|
||||
|
||||
full_response = []
|
||||
async for chunk in llm.stream_chat(context, model=req.model or session.model):
|
||||
full_response.append(chunk)
|
||||
escaped = chunk.replace('"', '\\"').replace('\n', '\\n')
|
||||
yield f"data: {{\"type\":\"chunk\",\"value\":\"{escaped}\"}}\n\n"
|
||||
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()
|
||||
# アシスタントメッセージを保存
|
||||
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)
|
||||
# メモリ要約チェック
|
||||
await mem.maybe_summarize(db, session)
|
||||
|
||||
yield "data: {\"type\":\"done\"}\n\n"
|
||||
yield sse({"type": "done"})
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
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",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
from sqlalchemy import desc
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import ApiLog
|
||||
|
||||
router = APIRouter(prefix="/logs", tags=["logs"])
|
||||
|
||||
|
||||
class LogResponse(BaseModel):
|
||||
id: int
|
||||
session_id: str | None
|
||||
model: str
|
||||
request_json: str
|
||||
response_text: str | None
|
||||
error: str | None
|
||||
latency_ms: float | None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("", response_model=list[LogResponse])
|
||||
def list_logs(
|
||||
session_id: str | None = Query(None),
|
||||
status: str | None = Query(None, description="'ok' または 'error'"),
|
||||
limit: int = Query(100, le=500),
|
||||
offset: int = Query(0),
|
||||
db: DBSession = Depends(get_db),
|
||||
):
|
||||
q = db.query(ApiLog)
|
||||
if session_id:
|
||||
q = q.filter(ApiLog.session_id == session_id)
|
||||
if status == "ok":
|
||||
q = q.filter(ApiLog.error.is_(None))
|
||||
elif status == "error":
|
||||
q = q.filter(ApiLog.error.isnot(None))
|
||||
return q.order_by(desc(ApiLog.created_at)).offset(offset).limit(limit).all()
|
||||
|
||||
|
||||
@router.delete("/{log_id}", status_code=204)
|
||||
def delete_log(log_id: int, db: DBSession = Depends(get_db)):
|
||||
log = db.get(ApiLog, log_id)
|
||||
if log:
|
||||
db.delete(log)
|
||||
db.commit()
|
||||
@@ -0,0 +1,63 @@
|
||||
from fastapi import APIRouter
|
||||
import psutil
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.scheduler import send_useless_tip, send_weather_laundry_notification
|
||||
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
|
||||
class MemoryStats(BaseModel):
|
||||
cpu_percent: float
|
||||
memory_percent: float
|
||||
memory_used_mb: float
|
||||
memory_total_mb: float
|
||||
gpu_memory_used_mb: float | None = None
|
||||
gpu_memory_total_mb: float | None = None
|
||||
gpu_memory_percent: float | None = None
|
||||
|
||||
|
||||
@router.get("/memory", response_model=MemoryStats)
|
||||
async def get_memory_stats():
|
||||
"""CPU と GPU のメモリ使用状況を返す"""
|
||||
# CPU/メモリ情報
|
||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||
memory = psutil.virtual_memory()
|
||||
|
||||
stats = {
|
||||
"cpu_percent": cpu_percent,
|
||||
"memory_percent": memory.percent,
|
||||
"memory_used_mb": memory.used / (1024 * 1024),
|
||||
"memory_total_mb": memory.total / (1024 * 1024),
|
||||
}
|
||||
|
||||
# GPU 情報(オプション)
|
||||
try:
|
||||
import pynvml
|
||||
pynvml.nvmlInit()
|
||||
device_count = pynvml.nvmlDeviceGetCount()
|
||||
if device_count > 0:
|
||||
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
|
||||
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
|
||||
stats["gpu_memory_used_mb"] = mem_info.used / (1024 * 1024)
|
||||
stats["gpu_memory_total_mb"] = mem_info.total / (1024 * 1024)
|
||||
stats["gpu_memory_percent"] = (mem_info.used / mem_info.total) * 100
|
||||
pynvml.nvmlShutdown()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return MemoryStats(**stats)
|
||||
|
||||
|
||||
@router.post("/weather-notify/test")
|
||||
async def test_weather_notify():
|
||||
"""天気予報→Discord通知を手動で即時実行する(動作確認用)"""
|
||||
await send_weather_laundry_notification()
|
||||
return {"status": "sent"}
|
||||
|
||||
|
||||
@router.post("/useless-tip/test")
|
||||
async def test_useless_tip():
|
||||
"""どうでもいいtips→Discord通知を手動で即時実行する(動作確認用)"""
|
||||
await send_useless_tip()
|
||||
return {"status": "sent"}
|
||||
Reference in New Issue
Block a user