Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import logging | |
| import os | |
| import secrets | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from starlette.middleware.sessions import SessionMiddleware | |
| from app.config import settings | |
| from app.clients.hana_client import hana_client | |
| from app.clients.openai_compat import close_shared_client | |
| from app.api import models, chat | |
| from app.middleware.rate_limit import ( | |
| get_oauth_username, is_org_member, get_remaining, check_rate_limit, | |
| record_conversation, | |
| ) | |
| logging.basicConfig(level=logging.INFO) | |
| LOG = logging.getLogger(__name__) | |
| STATIC_DIR = Path(__file__).resolve().parent.parent / "static" | |
| async def lifespan(app: FastAPI): | |
| LOG.info("Starting up — authenticating with HANA...") | |
| try: | |
| await hana_client.authenticate() | |
| await hana_client.get_models() | |
| LOG.info("HANA auth complete, persona cache populated.") | |
| except Exception as exc: | |
| LOG.warning("HANA auth failed (Neon models will be unavailable): %s", exc) | |
| yield | |
| LOG.info("Shutting down — closing HTTP clients...") | |
| await hana_client.close() | |
| await close_shared_client() | |
| app = FastAPI(title="AI Conversations", version="1.0.0", lifespan=lifespan) | |
| app.add_middleware( | |
| SessionMiddleware, | |
| secret_key=os.getenv("SESSION_SECRET", secrets.token_hex(32)), | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| try: | |
| from huggingface_hub import attach_huggingface_oauth | |
| attach_huggingface_oauth(app) | |
| LOG.info("HuggingFace OAuth endpoints registered.") | |
| except Exception as exc: | |
| LOG.warning("HF OAuth not available (local dev mode): %s", exc) | |
| app.include_router(models.router, prefix="/api") | |
| app.include_router(chat.router, prefix="/api") | |
| async def health(): | |
| return {"status": "ok"} | |
| async def auth_status(request: Request): | |
| username = get_oauth_username(request) | |
| if username is None: | |
| remaining = get_remaining(request) | |
| return { | |
| "logged_in": False, | |
| "username": None, | |
| "is_org_member": False, | |
| "remaining_conversations": remaining, | |
| } | |
| member = is_org_member(request) | |
| remaining = -1 if member else get_remaining(request) | |
| return { | |
| "logged_in": True, | |
| "username": username, | |
| "is_org_member": member, | |
| "remaining_conversations": remaining, | |
| } | |
| async def rate_limit_status(request: Request): | |
| remaining = get_remaining(request) | |
| return {"remaining": remaining, "daily_limit": 20} | |
| if STATIC_DIR.is_dir(): | |
| from starlette.staticfiles import StaticFiles | |
| from starlette.responses import FileResponse | |
| async def serve_spa(request: Request, full_path: str): | |
| file_path = STATIC_DIR / full_path | |
| if file_path.is_file(): | |
| return FileResponse(file_path) | |
| return FileResponse(STATIC_DIR / "index.html") | |
| LOG.info("Serving frontend from %s", STATIC_DIR) | |
| else: | |
| LOG.info("No static directory found at %s — frontend not served.", STATIC_DIR) | |