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:
2026-06-19 00:59:38 +09:00
co-authored by Claude Sonnet 4.6
parent 903d34761c
commit 4a08ba29fa
34 changed files with 5051 additions and 160 deletions
+1
View File
@@ -0,0 +1 @@
from . import chat, memory, logs, system
+48 -14
View File
@@ -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",
},
)
+50
View File
@@ -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()
+63
View File
@@ -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"}