bodhi-backend / src /api /app.py
Bodhi Deploy
Deploy Bodhi backend (API + in-container Redis)
ecb9f70
Raw
History Blame Contribute Delete
9.28 kB
"""FastAPI application β€” entry point for the Bodhi HTTP server."""
from __future__ import annotations
import os
from contextlib import asynccontextmanager
# Set TensorFlow environment variables BEFORE any imports
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2") # Suppress TF warnings
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "-1") # Force CPU usage
os.environ.setdefault("TF_ENABLE_ONEDNN_OPTS", "0") # Disable oneDNN warnings
import redis
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
load_dotenv()
# Configure logging for Bodhi debug output
import logging
import warnings
# Suppress TensorFlow and related warnings
warnings.filterwarnings('ignore', category=FutureWarning)
warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', message='.*CUDA.*')
warnings.filterwarnings('ignore', message='.*GPU.*')
warnings.filterwarnings('ignore', message='.*CuDNN.*')
logging.basicConfig(level=logging.INFO)
logging.getLogger("bodhi").setLevel(logging.DEBUG)
# Suppress TensorFlow logs
logging.getLogger("tensorflow").setLevel(logging.ERROR)
logging.getLogger("absl").setLevel(logging.ERROR)
@asynccontextmanager
async def lifespan(app: FastAPI):
from src.storage import BodhiStorage
from src.cache import BodhiCache
from src.services.llm import create_llm
from src.graph import build_interview_graph, create_durable_checkpointer
from src.api.auth import assert_auth_configured
from loguru import logger
# Fail fast if auth is neither configured nor explicitly bypassed.
assert_auth_configured()
db_url = os.getenv("DATABASE_URL", "")
if not db_url:
raise RuntimeError("DATABASE_URL is required for the API server")
storage = BodhiStorage(db_url)
storage.init_tables()
app.state.storage = storage
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
try:
logger.info(f"Attempting to connect to Redis at: {redis_url}")
cache = BodhiCache(redis_url)
logger.info("βœ“ Redis connection successful and verified")
except redis.ConnectionError as e:
logger.error(f"βœ— Redis connection failed: {e}")
logger.error(f" Redis URL: {redis_url}")
logger.error(f" Please ensure Redis server is running:")
logger.error(f" - Windows: Start Redis service or run 'redis-server'")
logger.error(f" - Check if port 6379 is accessible")
logger.error(f" - Verify firewall settings")
cache = None
except Exception as e:
logger.error(f"βœ— Redis initialization error: {type(e).__name__}: {e}")
logger.error(f" Redis URL: {redis_url}")
import traceback
logger.error(traceback.format_exc())
cache = None
app.state.cache = cache
google_key = os.getenv("GOOGLE_API_KEY", "")
llm = create_llm(api_key=google_key, model="gemini-3.1-flash-lite-preview")
app.state.llm = llm
# Durable interview state (falls back to in-memory if deps/DB unavailable).
checkpointer = create_durable_checkpointer(db_url)
app.state.graph = build_interview_graph(llm, checkpointer=checkpointer)
app.state.sarvam_key = os.getenv("SARVAM_API_KEY", "")
app.state.deepgram_key = os.getenv("DEEPGRAM_API_KEY", "")
app.state.tts_sample_rate = int(os.getenv("SARVAM_TTS_SAMPLE_RATE", "22050"))
# ── Initialize Proctoring CV Models ──────────────────────────────────────
# Each model loads independently so one failure (e.g. emotion model download)
# doesn't disable the entire proctoring pipeline. The whole block can be
# disabled with PROCTORING_ENABLED=false β€” useful when the native CV stack
# (mediapipe/tensorflow) segfaults in a given environment, which would
# otherwise crash the worker (a SIGSEGV can't be caught by try/except).
_proctoring_enabled = os.getenv("PROCTORING_ENABLED", "true").strip().lower() in (
"1", "true", "yes",
)
if not _proctoring_enabled:
logger.warning("Proctoring disabled via PROCTORING_ENABLED=false")
for _attr in ("face_detector", "gaze_analyzer", "object_detector", "emotion_analyzer"):
setattr(app.state, _attr, None)
_factories = {}
else:
logger.info("Loading proctoring CV models...")
try:
from src.proctoring_backend.services.proctoring.face_detection import FaceDetector
from src.proctoring_backend.services.proctoring.gaze_analysis import GazeAnalyzer
from src.proctoring_backend.services.proctoring.object_detection import ObjectDetector
from src.proctoring_backend.services.proctoring.emotion_analysis import EmotionAnalyzer
_factories = {
"face_detector": FaceDetector,
"gaze_analyzer": GazeAnalyzer,
"object_detector": ObjectDetector,
"emotion_analyzer": EmotionAnalyzer,
}
except Exception as e:
logger.warning(f"⚠ Proctoring dependencies unavailable: {e}")
_factories = {}
for _attr in ("face_detector", "gaze_analyzer", "object_detector", "emotion_analyzer"):
_factory = _factories.get(_attr)
if _factory is None:
setattr(app.state, _attr, None)
continue
try:
setattr(app.state, _attr, _factory())
logger.info(f"βœ“ {_attr} loaded")
except Exception as e:
logger.warning(f"⚠ {_attr} failed to load: {e}")
setattr(app.state, _attr, None)
_core_ok = all(
getattr(app.state, a, None) is not None
for a in ("face_detector", "gaze_analyzer", "object_detector")
)
logger.info(
"Proctoring core models %s; emotion %s",
"ready" if _core_ok else "UNAVAILABLE",
"ready" if getattr(app.state, "emotion_analyzer", None) else "disabled",
)
# ── Initialize Behavioral Models ─────────────────────────────────────────
if not _proctoring_enabled:
logger.warning("Behavioral models skipped (PROCTORING_ENABLED=false)")
else:
logger.info("Loading behavioral analysis models...")
try:
from src.behavioral_analysis.services.speech_service import load_models as load_speech_models
from src.behavioral_analysis.services.posture_service import load_models as load_posture_models
load_speech_models()
load_posture_models()
logger.info("βœ“ Behavioral models loaded successfully")
except Exception as e:
logger.warning(f"⚠ Behavioral models failed to load: {e}")
yield
storage.close()
app = FastAPI(
title="Bodhi API",
description="Voice-first AI Mock Interviewer β€” HTTP API",
version="1.0.0",
lifespan=lifespan,
)
# Allowed origins are read from BODHI_ALLOWED_ORIGINS (comma-separated).
# A wildcard "*" combined with allow_credentials=True is invalid per the CORS
# spec and is rejected by browsers, so we require an explicit allowlist.
_origins_env = os.getenv("BODHI_ALLOWED_ORIGINS", "").strip()
_allowed_origins = (
[o.strip() for o in _origins_env.split(",") if o.strip()]
if _origins_env
else ["http://localhost:3000", "http://localhost:5173"]
)
app.add_middleware(
CORSMiddleware,
allow_origins=_allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=[
"X-Bodhi-Text",
"X-Bodhi-Phase",
"X-Bodhi-End",
"X-Bodhi-Session",
"X-Bodhi-Transcript",
"X-Bodhi-Curriculum",
"X-Bodhi-Sentiment",
],
)
# Per-client rate limiting (no-op if slowapi is absent or disabled via env).
from src.api.ratelimit import setup_rate_limiting
setup_rate_limiting(app)
from src.api.roles import router as roles_router
from src.api.companies import router as companies_router
from src.api.documents import router as documents_router
from src.api.interviews import router as interviews_router
from src.api.audio import router as audio_router
from src.api.proctoring import router as proctoring_router
from src.api.resumes import router as resumes_router
from src.api.users import router as users_router
app.include_router(roles_router)
app.include_router(companies_router)
app.include_router(documents_router)
app.include_router(interviews_router)
app.include_router(audio_router)
app.include_router(proctoring_router)
app.include_router(resumes_router)
app.include_router(users_router)
@app.get("/health")
@app.get("/api/health")
async def health():
models = {
name: getattr(app.state, name, None) is not None
for name in ("face_detector", "gaze_analyzer", "object_detector", "emotion_analyzer")
}
# Core proctoring needs face + gaze + object; emotion is an optional enhancer.
proctoring_enabled = all(
models[m] for m in ("face_detector", "gaze_analyzer", "object_detector")
)
return {
"status": "ok",
"proctoring_enabled": proctoring_enabled,
"proctoring_models": models,
}