NeonClary
BrainForge/Security: direct vLLM (4090), HANA persona cache, Security stub
23ed0c3
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"
@asynccontextmanager
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")
@app.get("/api/health")
async def health():
return {"status": "ok"}
@app.get("/api/auth/status")
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,
}
@app.get("/api/rate-limit/status")
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
@app.get("/{full_path:path}")
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)