from contextlib import asynccontextmanager import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.api.middleware.rate_limit import TierRateLimitMiddleware from app.api.middleware.sanitizer import SanitizerMiddleware from app.config.settings import settings logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING) async def _memory_audit_cron_tick() -> None: """Weekly cron: contradiction scan + label canonicalization for all users (Phase 7).""" import logging # noqa: PLC0415 _log = logging.getLogger(__name__) _log.info("memory audit cron tick: starting") try: from app.db import async_session # noqa: PLC0415 from app.core.memory_maintenance import audit_memory # noqa: PLC0415 from app.models import User # noqa: PLC0415 from sqlalchemy import select # noqa: PLC0415 async with async_session() as db: result = await db.execute(select(User.id)) user_ids: list[str] = list(result.scalars().all()) for uid in user_ids: try: async with async_session() as db: await audit_memory(db, uid) except Exception as exc: _log.warning("memory audit cron tick: audit_memory failed user=%s: %s", uid, exc) _log.info("memory audit cron tick: done users=%d", len(user_ids)) except Exception as exc: _log.warning("memory audit cron tick: failed: %s", exc) async def _memory_cron_tick() -> None: """Hourly cron: drain Free-tier extraction queue + mine proactive patterns for Power+ users.""" import logging # noqa: PLC0415 _log = logging.getLogger(__name__) _log.info("memory cron tick: starting") try: from app.db import async_session # noqa: PLC0415 from app.core.memory_maintenance import drain_extraction_queue, mine_proactive_patterns # noqa: PLC0415 from app.billing.tier_manager import tier_manager # noqa: PLC0415 from app.models import User # noqa: PLC0415 from sqlalchemy import select # noqa: PLC0415 async with async_session() as db: await drain_extraction_queue(db) # mine proactive patterns for every Power+ user async with async_session() as db: result = await db.execute(select(User.id)) user_ids: list[str] = list(result.scalars().all()) for uid in user_ids: try: async with async_session() as db: tier = await tier_manager.get_tier(uid, db) if tier_manager.check_feature(tier, "proactive_mining"): await mine_proactive_patterns(db, uid) except Exception as exc: _log.warning("memory cron tick: mine_proactive_patterns failed user=%s: %s", uid, exc) _log.info("memory cron tick: done users=%d", len(user_ids)) except Exception as exc: _log.warning("memory cron tick: failed: %s", exc) @asynccontextmanager async def lifespan(app: FastAPI): # Startup: ensure agent tool modules are loaded. import app.agents # noqa: F401 scheduler = None if settings.SCHEDULER_ENABLED: from apscheduler.schedulers.asyncio import AsyncIOScheduler # noqa: PLC0415 scheduler = AsyncIOScheduler() scheduler.add_job(_memory_cron_tick, "interval", hours=1, id="memory_cron") scheduler.add_job(_memory_audit_cron_tick, "interval", weeks=1, id="memory_audit_cron") scheduler.start() logging.getLogger(__name__).info("memory cron scheduler started (interval=1h)") yield if scheduler is not None: scheduler.shutdown(wait=False) # Shutdown: dispose SQLAlchemy connection pool from app.db import engine await engine.dispose() def create_app() -> FastAPI: app = FastAPI( title="AdiuvAI Cloud API", version="0.1.0", docs_url="/docs" if settings.ENV == "dev" else None, redoc_url=None, lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Middleware stack (Starlette inserts at position 0, so last-added = outermost). # Request flow: TierRateLimit → Sanitizer → CORS → Router # Response flow: Router → CORS → Sanitizer → TierRateLimit app.add_middleware(SanitizerMiddleware) app.add_middleware(TierRateLimitMiddleware) from app.api.routes import scouts, auth, billing, chat, device_ws, memory app.include_router(auth.router, prefix="/api/v1") app.include_router(chat.router, prefix="/api/v1") app.include_router(billing.router, prefix="/api/v1") app.include_router(scouts.router, prefix="/api/v1") app.include_router(device_ws.router, prefix="/api/v1") app.include_router(memory.router, prefix="/api/v1") @app.get("/api/v1/health", tags=["health"]) async def health() -> dict: return {"status": "ok", "version": app.version} return app app = create_app()