Files
ai-rpg/app/main.py

147 lines
5.0 KiB
Python
Raw Normal View History

2026-06-20 19:13:05 +03:00
"""FastAPI app factory and lifespan (DB + Qdrant init, settings seed)."""
from __future__ import annotations
import os
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select, text
from app import __version__
from app.api import admin, auth, misc, presets, sessions, worlds
from app.config import get_settings
from app.core.logging import configure_logging, get_logger
from app.core.qdrant_client import dispose_qdrant_client, init_qdrant_collections
from app.core.settings_service import (
get_admin_setup_token,
seed_default_settings,
)
from app.db import dispose_engine, get_sessionmaker
from app.models import Setting, User
_logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Application startup / shutdown lifecycle."""
cfg = get_settings()
configure_logging()
_logger.info("app_starting", version=__version__, debug=cfg.debug)
# Startup -------------------------------------------------------------
2026-06-21 01:02:25 +03:00
# Step 0: Create all DB tables (idempotent — equivalent to alembic upgrade head).
# This must run BEFORE any settings query, otherwise seed_default_settings
# crashes with "relation 'settings' does not exist".
from app.db import get_engine
from app.migrations.versions._001_initial_schema import create_all_tables
try:
engine = get_engine()
await create_all_tables(engine)
_logger.info("db_tables_ready")
except Exception as e: # noqa: BLE001
_logger.error("db_tables_create_failed", error=str(e))
# Continue anyway — /api/health will reflect the broken state
2026-06-20 19:13:05 +03:00
sm = get_sessionmaker()
try:
async with sm() as session:
# 1) seed settings (idempotent)
await seed_default_settings(session)
# 2) ensure admin setup token
token = await get_admin_setup_token(session)
# 3) check if any admin exists
from sqlalchemy import func
admins_count = (
await session.execute(
select(func.count(User.id)).where(User.is_admin.is_(True))
)
).scalar_one()
if admins_count == 0:
_logger.warning(
"no_admin_yet",
setup_url=f"/register/admin?token={token}",
)
print(f"\n=== AI-RPG Admin Setup ===")
print(f"No admin user yet. Open this URL in your browser:")
print(f" /register/admin?token={token}")
print(f"===========================\n")
else:
_logger.info("admins_present", count=admins_count)
2026-06-21 01:02:25 +03:00
# 4) seed builtin presets (idempotent)
from app.migrations.seed import seed_builtin_presets
try:
await seed_builtin_presets(session)
_logger.info("presets_seeded")
except Exception as e: # noqa: BLE001
_logger.warning("presets_seed_failed", error=str(e))
# 5) Init Qdrant collections
2026-06-20 19:13:05 +03:00
from app.core.settings_service import get_setting
dim = int(await get_setting(session, "embeddings.dimension") or 256)
try:
await init_qdrant_collections(dim)
2026-06-21 01:02:25 +03:00
_logger.info("qdrant_collections_ready", dimension=dim)
2026-06-20 19:13:05 +03:00
except Exception as e: # noqa: BLE001
_logger.warning("qdrant_init_failed", error=str(e))
except Exception as e: # noqa: BLE001
_logger.error("startup_failed", error=str(e))
# Don't crash — let /api/health reflect the broken state
pass
yield
# Shutdown ------------------------------------------------------------
_logger.info("app_stopping")
await dispose_qdrant_client()
await dispose_engine()
def create_app() -> FastAPI:
"""Build and return the FastAPI application."""
cfg = get_settings()
app = FastAPI(
title=cfg.app_name,
version=__version__,
description="AI-RPG — text RPG with an LLM Game Master.",
lifespan=lifespan,
docs_url="/api/docs",
redoc_url=None,
openapi_url="/api/openapi.json",
)
# CORS
app.add_middleware(
CORSMiddleware,
2026-06-21 00:42:29 +03:00
allow_origins=cfg.cors_origins_list,
2026-06-20 19:13:05 +03:00
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Static asset serving (icons / uploads)
assets_dir = Path(cfg.assets_dir)
assets_dir.mkdir(parents=True, exist_ok=True)
app.mount("/static/assets", StaticFiles(directory=str(assets_dir)), name="assets")
# Routers
app.include_router(misc.router)
app.include_router(auth.router)
app.include_router(worlds.router)
app.include_router(sessions.router)
app.include_router(presets.router)
app.include_router(admin.router)
return app
app = create_app()