""" FastAPI application entry point. """ import os import json import time from dotenv import load_dotenv from fastapi import FastAPI load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env')) from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.sessions import SessionMiddleware from authlib.integrations.starlette_client import OAuth from .routes import router from artwork_bandit.db import database from artwork_bandit.features.nlp_encoder import NLPEncoder from artwork_bandit.features.vision_encoder import VisionEncoder from artwork_bandit.features.feature_store import FeatureStore from artwork_bandit.bandit.linucb import LinUCBBandit from artwork_bandit.bandit.thompson import ThompsonBandit APP = FastAPI() FRONTEND_URLS = os.getenv('FRONTEND_URLS', 'http://localhost:5173,http://127.0.0.1:5173').split(',') FRONTEND_URL = FRONTEND_URLS[0].strip() APP.add_middleware( SessionMiddleware, secret_key=os.getenv('SESSION_SECRET', 'replace-with-a-secure-secret'), same_site='lax', ) APP.add_middleware( CORSMiddleware, allow_origins=[url.strip() for url in FRONTEND_URLS if url.strip()], allow_methods=['*'], allow_headers=['*'], allow_credentials=True, ) SSO_DISCOVERY_URL = os.getenv('SSO_DISCOVERY_URL') SSO_CLIENT_ID = os.getenv('SSO_CLIENT_ID') SSO_CLIENT_SECRET = os.getenv('SSO_CLIENT_SECRET') SSO_SCOPES = os.getenv('SSO_SCOPES', 'openid profile email') APP.state.frontend_url = FRONTEND_URL APP.state.sso_enabled = bool(SSO_DISCOVERY_URL and SSO_CLIENT_ID and SSO_CLIENT_SECRET) print(f"[STARTUP] SSO enabled: {APP.state.sso_enabled}") APP.state.oauth = None if APP.state.sso_enabled: oauth = OAuth() oauth.register( name='sso', client_id=SSO_CLIENT_ID, client_secret=SSO_CLIENT_SECRET, server_metadata_url=SSO_DISCOVERY_URL, client_kwargs={'scope': SSO_SCOPES}, ) APP.state.oauth = oauth APP.include_router(router) DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data') USERS_F = os.path.join(DATA_DIR, 'users.json') CONTENTS_F = os.path.join(DATA_DIR, 'content.json') ARTWORK_F = os.path.join(DATA_DIR, 'artwork.json') LINUCB_ALPHA = float(os.getenv('LINUCB_ALPHA', '1.0')) @APP.on_event('startup') def startup(): try: print("[STARTUP] Loading contents...") # load data with open(CONTENTS_F, 'r', encoding='utf-8') as f: contents = json.load(f) print(f"[STARTUP] Loaded {len(contents)} contents") print("[STARTUP] Checking users...") # generate users/artworks if missing if not os.path.exists(USERS_F) or os.path.getsize(USERS_F) == 0: try: from artwork_bandit.data.generate_synthetic import main as gen_main gen_main() except Exception as e: print(f"[STARTUP] Warning: Failed to generate users: {e}") with open(USERS_F, 'w', encoding='utf-8') as f: json.dump([], f) with open(USERS_F, 'r', encoding='utf-8') as f: users = json.load(f) print(f"[STARTUP] Loaded {len(users)} users") print("[STARTUP] Checking artworks...") if not os.path.exists(ARTWORK_F) or os.path.getsize(ARTWORK_F) == 0: try: from artwork_bandit.data.generate_synthetic import main as gen_main gen_main() except Exception as e: print(f"[STARTUP] Warning: Failed to generate artworks: {e}") with open(ARTWORK_F, 'w', encoding='utf-8') as f: json.dump([], f) with open(ARTWORK_F, 'r', encoding='utf-8') as f: artworks = json.load(f) print(f"[STARTUP] Loaded {len(artworks)} artworks") print("[STARTUP] Initializing database...") # init db database.init_db() print("[STARTUP] Database initialized") print("[STARTUP] Creating encoders...") # encoders - these are now lazy-loaded nlp = NLPEncoder() vis = VisionEncoder() print("[STARTUP] Encoders created (lazy-loaded)") print("[STARTUP] Creating feature store...") fs = FeatureStore(database, nlp, vis) print("[STARTUP] Feature store created") print("[STARTUP] Precomputing embeddings...") # precompute fs.precompute_all(users, contents, artworks) print("[STARTUP] Embeddings precomputed") print("[STARTUP] Creating bandits...") # bandits # collect all artwork ids all_arts = [a['artwork_id'] for a in artworks] d = 384 + 384 + 512 linucb = LinUCBBandit(all_arts, d=d, alpha=LINUCB_ALPHA) try: linucb.load(database) except Exception as e: print(f"[STARTUP] Warning: Failed to load linucb state: {e}") thompson = ThompsonBandit(all_arts) print(f"[STARTUP] Bandits created (LinUCB with {len(all_arts)} arms, Thompson with {len(all_arts)} arms)") print("[STARTUP] Attaching state...") # attach APP.state.feature_store = fs APP.state.linucb = linucb APP.state.thompson = thompson APP.state.users = users APP.state.contents = contents APP.state.artworks = artworks print("[STARTUP] State attached - startup complete!") except Exception as e: print(f"[STARTUP ERROR] {e}") import traceback traceback.print_exc() raise def init_app(): """Idempotent initialisation run at import time to help tests and CLI use. Runs a safe subset of the startup routine and ensures DB tables exist. """ try: # create DB tables early so tests don't fail if startup() isn't invoked database.init_db() except Exception: pass try: # load minimal data and set placeholder state if startup hasn't run if not hasattr(APP.state, 'feature_store'): with open(CONTENTS_F, 'r', encoding='utf-8') as f: contents = json.load(f) # ensure users/artworks exist if not os.path.exists(USERS_F) or os.path.getsize(USERS_F) == 0: try: from artwork_bandit.data.generate_synthetic import main as gen_main gen_main() except Exception: with open(USERS_F, 'w', encoding='utf-8') as f: json.dump([], f) with open(USERS_F, 'r', encoding='utf-8') as f: users = json.load(f) if not os.path.exists(ARTWORK_F) or os.path.getsize(ARTWORK_F) == 0: try: from data.generate_synthetic import main as gen_main gen_main() except Exception: with open(ARTWORK_F, 'w', encoding='utf-8') as f: json.dump([], f) with open(ARTWORK_F, 'r', encoding='utf-8') as f: artworks = json.load(f) nlp = NLPEncoder() vis = VisionEncoder() fs = FeatureStore(database, nlp, vis) fs.precompute_all(users, contents, artworks) all_arts = [a['artwork_id'] for a in artworks] d = 384 + 384 + 512 linucb = LinUCBBandit(all_arts, d=d, alpha=LINUCB_ALPHA) thompson = ThompsonBandit(all_arts) APP.state.feature_store = fs APP.state.linucb = linucb APP.state.thompson = thompson APP.state.users = users APP.state.contents = contents APP.state.artworks = artworks except Exception as e: print(f"ERROR in init_app: {e}") import traceback traceback.print_exc() # NOTE: Do NOT call init_app() at import time - let the startup event handle initialization # This prevents encoder loading and model downloads from blocking the import @APP.get('/health') def health(): # impressions count from artwork_bandit.db.database import SessionLocal, Impression db = SessionLocal() try: cnt = db.query(Impression).count() finally: db.close() return {"status": "ok", "impressions": cnt, "features_loaded": True} @APP.on_event('shutdown') def shutdown(): try: APP.state.linucb.save(database) except Exception: pass app = APP if __name__ == '__main__': import uvicorn uvicorn.run('api.main:app', host='0.0.0.0', port=8000, reload=True)