import asyncio import logging from contextlib import asynccontextmanager from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api import auth as auth_api from app.api import chat, knowledge_base, logs, memory, notes, system from app.api import models as models_api from app.api import settings as settings_api from app.core import settings_store from app.core.config import CORS_ORIGINS, LOG_LEVEL from app.core.site_auth import require_site_session from app.core.discord.discord_bot import start_discord_bot from app.core.discord.scheduler import start_scheduler from app.core.memory_kb.word_memory import seed_from_history from app.db.database import SessionLocal, init_db from app.db.models import Message, ModelPreset # uvicorn は自分のロガーしか設定しないため、アプリ側の logging.getLogger(__name__) は # 何もしないと logging.lastResort 送りになる。それだと WARNING 未満が消えるうえ、 # 出力にレベルもタイムスタンプもロガー名も付かず、ログから障害を追えない。 logging.basicConfig( level=LOG_LEVEL, format="%(asctime)s %(levelname)-8s %(name)s: %(message)s", ) # LiteLLM は自前のハンドラを持つので、root にも流すと同じ行が二重に出る。 logging.getLogger("LiteLLM").propagate = False # ModelSelector.vue に元々ハードコードされていたプリセット。初回起動時のみDBへシードする。 _DEFAULT_MODEL_PRESETS = [ ("GPT-5.5", "gpt-5.5"), ("GPT-5.4", "gpt-5.4"), ("GPT-5.4 mini", "gpt-5.4-mini"), ("Claude Sonnet 4.6", "claude-sonnet-4-6"), ("Claude Haiku 4.5", "claude-haiku-4-5-20251001"), ("Ollama llama3", "ollama/llama3"), ("Ollama Gemma4", "ollama/gemma4"), ("Gemma 4 12B (llama-server)", "gpt-3.5-turbo"), ("Claude Sonnet 4.6 (Bedrock)", "bedrock/jp.anthropic.claude-sonnet-4-6"), ("Llama 3 70B (Bedrock)", "bedrock/meta.llama3-70b-instruct-v1:0"), ("Nova 2 Lite (Bedrock)", "bedrock/jp.amazon.nova-2-lite-v1:0"), ] def _seed_word_memory_from_db() -> None: db = SessionLocal() try: rows = ( db.query(Message.content) .filter(Message.role.in_(["user", "assistant"])) .order_by(Message.created_at.desc()) .limit(200) .all() ) seed_from_history([content for (content,) in rows]) finally: db.close() def _load_settings_into_env() -> None: db = SessionLocal() try: settings_store.load_into_env(db) finally: db.close() def _seed_default_models() -> None: db = SessionLocal() try: if db.query(ModelPreset).first() is not None: return for label, value in _DEFAULT_MODEL_PRESETS: db.add(ModelPreset(label=label, value=value)) db.commit() finally: db.close() @asynccontextmanager async def lifespan(app: FastAPI): init_db() _load_settings_into_env() _seed_default_models() _seed_word_memory_from_db() start_scheduler() asyncio.create_task(start_discord_bot()) yield app = FastAPI(title="PlayChat API", version="0.1.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(auth_api.router) app.include_router(chat.router, dependencies=[Depends(require_site_session)]) app.include_router(memory.router, dependencies=[Depends(require_site_session)]) app.include_router(notes.router, dependencies=[Depends(require_site_session)]) app.include_router(knowledge_base.router, dependencies=[Depends(require_site_session)]) app.include_router(logs.router, dependencies=[Depends(require_site_session)]) app.include_router(system.router, dependencies=[Depends(require_site_session)]) app.include_router(settings_api.router, dependencies=[Depends(require_site_session)]) app.include_router(models_api.router, dependencies=[Depends(require_site_session)]) @app.get("/health") def health(): return {"status": "ok"}